推荐系统中的两塔模型崛起
原文:
towardsdatascience.com/the-rise-of-two-tower-models-in-recommender-systems-be6217494831
深入探讨用于消除排序模型偏差的最新技术
·发表于 Towards Data Science ·7 分钟阅读·2023 年 10 月 29 日
–
照片由 Evgeny Smirnov 提供
推荐系统是当今世界上最普遍的机器学习应用之一。然而,底层的排序模型存在 许多偏差,这可能严重限制推荐的质量。构建无偏排序器的问题——也称为无偏排序学习(ULTR)——仍然是机器学习领域最重要的研究问题之一,且远未解决。
在这篇文章中,我们将深入探讨一种相对较新的建模方法,这种方法已经使行业能够非常有效地控制偏差,从而构建出极其优越的推荐系统:两塔模型,其中一个塔学习相关性,另一个(浅层)塔学习偏差。
虽然两塔模型可能在行业中使用了好几年,但第一篇正式向更广泛的机器学习社区介绍它们的论文是华为 2019 年的 PAL 论文。
PAL(华为,2019)——首个两塔模型
华为的论文 PAL (“位置感知排序学习”) 考虑了在华为应用商店背景下的位置偏差问题。
在整个行业的排序模型中,一再观察到位置偏差。这意味着用户更有可能点击首先展示的项目。这可能是因为他们赶时间、盲目相信排序算法,或其他原因。以下是展示华为数据中位置偏差的图表:
位置偏差。来源:华为的论文 PAL
位置偏差是一个问题,因为我们无法确定用户点击第一个项目是因为它确实最相关,还是因为它首先展示——而在推荐系统中,我们旨在解决前者的学习目标,而不是后者。
PAL 论文提出的解决方案是将学习问题分解为
p(click|x,position) = p(click|seen,x) x p(seen|position),
其中 x 是特征向量,seen
是一个二进制变量,指示用户是否已经看到该展示。在 PAL 中,seen
仅依赖于项的位置,但我们也可以添加其他变量(稍后我们会看到)。
基于这一框架,我们可以构建一个具有两个塔的模型,每个塔输出右侧的两个概率之一,然后简单地将这两个概率相乘:
来源:华为的论文 PAL
图中的云层实际上是神经网络:一个用于位置塔的浅层网络(因为它只需要处理单一特征),另一个用于 CTR 塔的深层网络(因为它需要处理大量特征并在这些特征之间创建交互)。我们也将这两个塔分别称为偏差塔和参与塔。
值得注意的是,在推理时,位置不可用时,我们仅使用参与塔进行前向传递,而不使用偏差塔。类似于 Dropout,因此模型在训练和推理时的行为有所不同。
它有效吗?是的,PAL 的效果非常好。作者建立了两个不同版本的 DeepFM 排名模型(我在 这里 写过),一个版本使用 PAL,另一个版本使用一种简单的处理项位置的方法:将其作为特征传递到参与塔中。他们的在线 A/B 测试显示,PAL 将点击率和转化率分别提高了大约 25%,这是一个巨大的提升!
PAL 显示位置本身可以作为排名模型的输入,但需要通过专门的塔进行处理,而不是主模型(这一规则也被添加为 Rule 36 到 Google 的“机器学习规则”中)。双塔模型正式诞生——尽管在 PAL 论文之前,这种方法可能已经在业界使用过。
“接下来观看”(YouTube,2019)——加性双塔模型
YouTube 的论文“Recommending What Video to Watch Next”——通常被简单称为“Watch Next”论文——发布的时间与 PAL 相近,并试图解决相同的问题:使用双塔模型去偏差推荐系统。然而,与 PAL 相比,YouTube 使用的是加性双塔模型,而不是乘性模型。
为了理解这为何有效,再次考虑学习目标的分解:
p(click|x,position) = p(click|seen,x) x p(seen|position)
由于概率是 logits 的 sigmoid 函数,sigmoid 函数本质上是加权指数函数,而指数函数遵循指数的乘法规则,因此我们也可以写作:
logit(click|x,position) = logit(click|seen,x) + logit(seen|position),
这就是,遗憾的是,加性双塔模型。
双塔加性模型。来源:“Recommending What Video to Watch Next”
相比于 PAL,另一个显著的创新是使用除了位置以外的其他特征在浅层塔中。例如,用户设备类型。这是有道理的,因为不同设备预计会表现出不同形式的位置偏差:例如,位置偏差可能在屏幕较小的手机上更为严重,因为用户可以同时看到的位置更少,并且需要更多地滑动。因此,YouTube 模型中的浅层塔不仅学习位置偏差,还学习各种选择偏差,具体取决于传入浅层塔的特征。
为了确保浅层塔实际利用了所有这些特征,作者在仅位置特征上添加了一个 10%丢弃率的 Dropout 层。没有它,模型可能会过度依赖位置特征,而无法学习其他选择偏差。
通过 A/B 测试,作者展示了添加浅层塔提高了他们的“参与度指标”(不幸的是没有给出该指标的详细信息)0.24%。
在 ULTR 中解开相关性和偏差(谷歌,2023)
到目前为止,我们假设 ULTR 中的两个塔可以在模型训练期间独立学习。不幸的是,最近的谷歌论文“Towards Disentangling Relevance and Bias in Unbiased Learning to Rank”的作者表明,这个假设并不成立。
这里有一个思想实验来说明原因:假设存在一个完美的相关性模型,即一个能够完美解释点击的模型。在这种情况下,一旦我们用新数据重新训练模型,所有的偏差塔需要做的就是学习如何将位置映射到点击。相关性塔则无需学习任何东西——它将完全无用!
换句话说,相关性对位置有干扰效应,如下图因果图中的红色箭头所示。
相关性塔对偏置塔的混淆效应。来源:Zhang et al 2023
但这不是我们想要的:我们希望相关性塔在新数据到来时继续学习。解决这个问题的一种简单方法——消除因果图中的红色边缘——是添加一个 Dropout 层到偏置对数值上,如下图所示:
在偏置塔上添加 Dropout。来源:Zhang et al 2023。
为什么使用 Dropout?关键思想是通过随机丢弃偏置对数值(bias logit),我们“推动”模型更多地依赖于相关性塔(relevance tower),而不是仅仅学习历史位置和相关性之间的映射。请注意,这一思想与 YouTube 的 Watch Next 论文非常相似,但这里我们丢弃的是整个偏置对数值,而不仅仅是位置特征。
为了使其有效,偏置对数值的 Dropout 概率需要很高:在 0.5 的 Dropout 下(即在 50% 的训练样本中随机将偏置对数值置零),作者在 Chrome 应用商店的生产数据上击败了 PAL 1% 的点击 NDGC——这是这种简单技术的有力证明。
总结
让我们回顾一下:
-
双塔模型是一种强大的方法来构建无偏排名模型:一个塔学习相关性,而另一个塔学习位置偏置(以及,选择性地,其他偏置)。
-
我们可以通过乘以两个塔的概率(如华为的 PAL 所做),或者通过加上它们的对数值(如 YouTube 的 Watch Next 所做)来组合两个塔的输出。我们将后者称为加性双塔模型(additive two-tower model)。
-
偏置塔的输入通常是项目的位置,但我们也可以添加其他引入偏置的特征,如用户设备类型。
-
Dropout(无论是在位置上还是在整个偏置对数值上)可以防止模型过度依赖历史位置,并且已被证明可以提高泛化能力。
这仅仅是冰山一角。鉴于推荐系统在今天的科技行业中的重要性,以及发现双塔建模可以显著提高预测性能,这一研究领域可能仍远未发挥其全部潜力:我们真的只是刚刚开始!
如果你喜欢这些内容并想保持对最新 ML 技术的了解,确保 订阅。
视觉变换器的崛起
ResNet 的时代是否即将结束?
·
关注 发表在 Towards Data Science · 9 分钟阅读 · 2023 年 11 月 15 日
–
图片由作者提供。
在过去的十年里,计算机视觉已经从一个有前途的研究领域发展成为现代技术革命的基石。曾经以准确分类小像素图像而引起关注的这些技术,现在能够仅凭简短的描述从虚空中生成如上图所示的高分辨率图像。这种令人惊叹的能力目前仅限于庞大的模型,但并非所有影响我们日常生活的模型都是巨大的。
在过去十年的计算机视觉领域,另一个发展方向是较小模型在性能和参数效率上的同步提升,其紧凑的内存占用和更高的帧率使得智能手机摄影、增强现实和机器人等领域的发展成为可能。像素级语义分割和深度估计,以及对象和其 3D 姿态的检测与跟踪,现在可以在越来越大的图像尺寸上实时执行,帮助自动驾驶车辆规划日常的交通操作。
本文讨论了在这些较小视觉架构中骨干网络选择的一个重要发展。正如我们将看到的,出现了一类新的模型,其性能强大且轻量级到足以引发范式的转变,并允许过去时代的坚韧老将退役。
卷积时代
自从AlexNet在 2012 年震撼了 ImageNet 挑战赛并点燃了深度学习研究的世界以来,卷积神经网络(CNNs)一直主导着计算机视觉领域。特别是,ResNet模型在 2015 年通过引入跳跃(“残差”)连接,成功解决了梯度消失问题并提高了参数效率,随后由于其强大的知识迁移能力,几乎被广泛用作各种下游任务的骨干架构,取代了VGGNet,成为当时最受欢迎的骨干架构。
而在计算机视觉研究这个快速变化的领域中,这种状态已维持近八年,这是一个史诗般的生命周期。但现在,挑战者正接近王座。继 2017 年“Attention is All You Need”在自然语言处理(NLP)领域的迅猛崛起,并发展成今天看到的大型语言模型现象之后,Vision Transformer(ViT)在 2020 年 10 月底首次展示了纯变换器架构能够在计算机视觉任务中实现最先进的性能,尽管需要更多的训练周期、资源和数据来实现这一点。
事实证明,CNN 的设计赋予它们诸如平移等变性(通过权重共享)和局部性(通过小滤波器尺寸)这些使其在图像数据结构上固有有效的归纳偏差。变换器可以通过足够的训练从零开始学习这些偏差,但这最初意味着变换器在数据效率上远不如 CNN。另一方面,尽管 CNN 标配了有助于其学习和处理图像数据的偏差,但它们缺乏变换器自注意力提供的跨层建模全局依赖的能力,而这无法通过任何数量的数据来学习。
一项名为“变换器是否比 CNN 更稳健?”的研究发现,尽管两种架构在使用相同的增强策略进行训练时对对抗攻击的鲁棒性相当,但与在任何策略下训练的大型 ResNet 模型相比,变换器在处理分布外(OOD)样本时表现出更好的泛化能力,且消融研究表明,这一优势由自注意力机制驱动。
DeiT 在 OOD 样本上始终优于参数更少的 ResNet。图形由 arxiv.org/pdf/2111.05464.pdf
提供
因此,CNN 的吸引人特性使其在学习图像特征时数据效率高,同时也预示了其衰退,因为其有限的感受野阻碍了它们对全局上下文的建模,这是一个非平凡的弱点。在 ViT 发表之前的几年中,已经尝试解决 CNN 中的长距离依赖问题:在诸如 DeepLab 的模型中探索了扩张(也称为“空洞”)卷积以增加感受野,Squeeze-and-Excitation、Non-Local Neural Networks 和 Attention Augmented Convolutional Neural Networks 展示了将注意力引入卷积操作的方法。Bottleneck Transformers (BoTNet) 通过将 ResNet 最后三层的空间卷积瓶颈块替换为多头自注意力块,在更大的图像分辨率下取得了显著的性能提升,但更大的提升尚待出现。
视觉变换器
在变换器希望取代王座之前,它们还需要克服架构上的弱点。ViT 论文在展示纯变换器架构能够在计算机视觉任务中实现最先进性能方面非常有帮助,同时也阐明了该应用所带来的挑战,包括前述的数据效率问题、自注意力相对于图像大小的不可接受的二次复杂性,以及在密集预测任务如语义分割和单目深度估计中有用的多尺度特征的缺乏。
“虽然这些初步结果令人鼓舞,但仍然存在许多挑战。其中之一是将 ViT 应用于其他计算机视觉任务,如检测和分割。”
— 来自 ViT 论文结论的行动号召。
研究界显然把 2020 年的 ViT 论文当作了待办事项清单,2021 年是繁忙的一年。首先,数据高效图像变换器 (DeiT) 通过展示增强方法和从教师模型的知识蒸馏在使用 ImageNet 数据集的更少周期内有效地训练 ViT 模型,解决了数据效率问题,从而消除了对大规模外部数据集的需求,并将无卷积的视觉模型带入了广泛采用的领域。有趣的是,使用 CNN 作为教师模型效果明显优于预训练的 DeiT,可能是因为这一过程将其结构偏差传授给了学生。视觉变换器现在已成为数据效率方面的正式威胁,但仍然缺乏多尺度特征。
“因此,考虑到我们的结果,其中图像变换器已经与卷积网络相当,我们相信,鉴于其在特定准确度下较低的内存占用,它们将迅速成为一种首选方法。”
— 来自 DeiT 论文结论的预言。
在二月末,一篇标题为 “金字塔视觉变换器:无需卷积的密集预测多功能骨干网” 的论文通过使用卷积解决了缺乏多尺度特征和自注意力复杂性的问题。Swin Transformer 注入了 CNN 的结构偏差,生成多尺度特征图,并通过在移动局部窗口中关注来减少自注意力复杂性,但像 PVT 一样,仍然依赖于补丁中的位置编码。卷积视觉变换器 (CvT) 改变了游戏规则,通过零填充 2D 卷积生成重叠的补丁嵌入,利用 CNN 的一项特性,从零填充中编码位置信息。
这张来自CvT论文的表格是对视觉变换器工作成果的有用总结,直到其发表为止。有趣的是,查看PVT 代码发现,“空间缩减”仅仅是 2D 卷积的委婉说法。
随后,PVTv2的新版本与Segformer同时发布,后者使用前者作为骨干,通过使用轻量级的全 MLP 解码器,实现了最先进的语义分割性能,并且参数效率令人印象深刻。GLPN随后使用这个骨干在单目深度估计(另一种密集预测任务)中取得了竞争力的结果。
要理解 PVTv2 作为骨干的突破性,我们可以从 Segformer 论文中的图表开始:
图表来自Segformer论文,展示了在语义分割任务上相较于同时期模型的卓越性能和效率。
上述图表清晰地展示了相比于同时期的层次化变换器和 ResNet 架构,性能和参数效率的优势(详见Segformer论文)。
接下来,我们可以回顾一个 PVTv2 支持更复杂架构的例子。Deformable DETR在 Detection Transformer(DETR)基础上有了显著改进,得益于提出的变形注意力,实现了更高的计算和数据效率,并且训练周期减少了 10 倍。Panoptic Segformer是对 Deformable DETR 的自然扩展,用于全景分割,并在 2021 年底发布,其中有一张比较图表对我们讨论特别有帮助。
图表来自Panoptic Segformer论文。请注意底部两个表现最佳的模型,分别使用了 Swin-L 和 PVTv2 骨干,其中后者在参数数目只有一半的情况下,性能相当。
遗憾的是,作者没有对 PVTv2 骨干网络的其他尺寸进行基准测试,除了最大的(B5)版本,但我们仍然可以从这个例子中获得一些重要见解。首先,我们看到尽管 Swim-L 骨干在技术上是较大模型中最具性能的,但 PVTv2-B5 在参数和 FLOPS 不到一半的情况下,性能几乎相同。此外,(虽然 PVTv2-B3 会是更好的比较对象)我们可以看到,PVTv2-B5 比 ResNet101 骨干的性能显著更优。
这些模型的成功表明,PVTv2 中的 Mix Transformer (MiT) 编码层能够用高效数量的参数生成极具描述性的特征,这要归功于其采用的“各取所长”的方法。通过将卷积操作融入变换器层,使自注意力更高效,创建多尺度特征,并注入对图像数据高效学习有利的归纳偏差,这些层次化的变换器模型可能成为计算机视觉模型开发中的下一个家喻户晓的名字。
胜者
图片由作者提供,灵感来源于这张图片。
因此,答案似乎并不是 CNN 与变换器之间的生死搏斗(参见许多过度溢美的赞词和挽歌),而是某种更浪漫的东西。层次化变换器(如 CvT 和 PVTv2)中采用 2D 卷积不仅便利地创建了多尺度特征,减少了自注意力的复杂性,并通过缓解对位置编码的需求简化了架构,这些模型还采用了残差连接,这是其祖先遗传的另一特征。变换器和 CNN 的互补优势已经在这些可行的后代中融合。
那么,ResNet 时代是否已经结束?虽然看起来确实如此,但任何论文肯定仍然需要在一段时间内包括这个不知疲倦的骨干网络进行比较。然而,重要的是要记住,这里没有失败者,只有新一代强大且可转移的特征提取器供大家享用,只要他们知道在哪里查找。像 PVTv2 这样的参数高效模型通过提供强大的特征提取,同时占用较小的内存空间,民主化了对更复杂架构的研究,值得被添加到标准骨干网络的列表中,以用于基准测试新架构。
未来的工作
本文重点讨论了卷积操作和自注意力的交叉融合如何促成了层次特征变换器的演变。这些模型在小规模下表现出了主导的性能和参数效率,使其成为理想的特征提取骨干(尤其是在参数受限的环境中)。然而,目前尚缺乏对这些模型在较小规模下所利用的效率和归纳偏差是否能够转移到大规模成功,并威胁到纯 ViT 在更高参数量下主导地位的探索。
大型多模态模型(Large Multimodal Models,LMMS)如大语言和视觉助理(LLaVA)及其他需要自然语言理解视觉数据的应用,依赖于从 ViT-L 特征生成的对比语言-图像预训练(Contrastive Language–Image Pretraining,CLIP)嵌入,因此继承了 ViT 的优势和劣势。如果关于扩展分层 Transformer 的研究表明它们的好处,例如增强了细粒度理解的多尺度特征,使它们能够以比 ViT-L 更高效的参数效率实现更好或类似的性能,那么这将对使用 CLIP 的任何内容:LMMS、机器人技术、辅助技术、增强/虚拟现实、内容审核、教育、研究等对社会和产业有广泛和直接的影响,可以改进和提高效率,降低这些技术的开发和部署门槛。
因果关系的科学与艺术(第一部分)
如果我们不能直接测试因果关系,我们应该怎么办?
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 1 月 5 日
–
图片由作者提供
让我带你踏上一段旅程,探索我的专业领域,这也是我的激情、我的执着,我喜欢称之为:因果关系的科学与艺术。
“因果关系”指的是因果之间的关系。它是指一个事件或行动可以引发另一个事件或结果。换句话说,因果关系关注的是理解事情如何发生以及为何发生。
在这篇文章中,我们首先将回答两个问题:为什么理解因果关系如此重要,以及为什么评估因果关系如此困难(因果推断的根本问题)。然后,我们将看到两种主要的因果效应测量方法(随机对照试验和自然实验)。最后,我将展示如何质疑因果关系,并提供实际工具来实现这一点。
为什么因果关系?
首先,我们为什么要关注因果关系?我为什么对这个话题如此执着?因为我们所做的每一个决定,无论是个人还是组织,都是基于某些行动会导致某些结果的假设。
例如,如果我们决定去素食,是因为我们相信这将对健康或环境有益。或者,如果一家公司改变了其广告策略,是因为它希望提高销售或订阅量。同样,政府必须考虑其行动将如何影响我们周围的世界,例如,过渡到可再生能源是否有助于实现气候目标。然而,理解因果关系可能很复杂,需要仔细分析以确定真正的关系。这就是因果关系的科学和艺术所在——它帮助我们审视和解读数据,以更好地理解我们周围的世界,并做出明智的决策。
问题是,如果我们未能正确评估关系和因果链,后果可能非常昂贵。
问题是,如果我们未能正确评估关系和因果链,后果可能非常昂贵。首先,政府可能会投入大量精力和资源来获取数据,然后花费更多资源来分析这些数据。但如果结论错误,他们可能会因跟随错误的路径而损失更多资源。因此,这就是为什么评估因果关系如此重要的原因。
此外,理解因果关系还帮助我们在阅读新闻、听取政治家发言、与他人讨论时,减少被操控或受到虚假信息影响的风险。
此外,好消息是,你可以学习一些工具和实用的方法,以每天应用这些方法来对抗虚假信息,并做出更好的或更有根据的决策。
为什么评估因果关系如此困难?
那么,为什么评估因果关系如此困难呢?问题在于,你没有统计测试可以告诉你你的效应是否是因果关系。你可以在这方面做很多事情。可以进行许多统计测试来挑战我们称之为识别假设(识别因果效应的假设),但通常,我们无法直接测试这些假设。
这正是理解因果关系如此令人兴奋的原因!它需要扎实的统计基础和深厚的领域知识。你必须运用批判性思维来考虑不同变量和事件之间的关系。仅凭数学是不够的。主要挑战通常在于我们如何解释所使用的统计指标——我们可能会把相关性误认为因果关系。
因果推断的根本问题
关键问题来自于因果推断的根本问题。让我用下面的两个图表来说明这个概念。在左侧,你可以看到全球的直接初级能源消费,分为可再生能源和其他能源生产来源。第二个图表表示世界的 CO2 排放量,从 1900 年到 2020 年。两个数据都在增长。然而,我们倾向于认为使用可再生能源有助于减少 CO2 排放。利用这样的汇总数据,有些人可能会被诱导认为可再生能源未必有用。
作者提供的图像。
显然,凭借如此简单的统计数据无法回答这个问题(可再生能源对 CO2 排放的影响是什么?)。问题在于,我们不知道如果没有可再生能源会发生什么。我们没有一个没有可再生能源的世界。
作者提供的图像。
可再生能源与 CO2 排放之间的关系是复杂的。一方面,像太阳能和风能这样的可再生能源可以作为化石燃料的替代品,从而导致 CO2 排放减少。另一方面,提取资源以生产光伏电池的过程是耗能的,并且“反弹效应”(即当能源由可再生资源提供时,人们会消耗更多能源)也可能导致 CO2 排放增加。没有更多的数据,就无法明确回答可再生能源是否导致更高或更低的 CO2 排放。底线是,仅凭这样的数据无法回答这个问题。
要完美回答这个问题,我们需要两个平行的世界。
在两个世界中,一个世界有可再生能源,而另一个世界没有。而由于这是两个世界之间唯一的不同,如果在 CO2 排放方面存在差异,那很可能是由于可再生能源的使用造成的。
可再生能源对 CO2 排放的影响是什么?要完美回答这个问题,我们需要两个平行的世界。作者提供的图像。
不幸的是,我们无法访问平行世界,在那里我们可以观察到相同情况在有和没有特定处理或行动时的表现。这就产生了**“因果推断中的基本问题”**,因为我们无法观察反事实——即没有进行处理或行动的替代现实。例如,我们无法同时观察一个既在服药又不在服药的病人。在因果推断中,我们尝试尽可能接近这种理想情况,在这种情况下,我们可以比较相同情况在有和没有特定处理或行动时的结果。这使我们能够更好地理解因果关系。
黄金标准
通常,解决这个问题的第一种方法,通常被称为黄金标准,并且可以说是最佳解决方案是随机对照试验(A/B 测试)。
随机对照试验的示意图。图片由作者提供。
简而言之,这个概念如下。我们抽取一个样本,这个样本希望能够代表更大的人群,并将受试者随机分配到两个组(处理组和对照组)或更多组中。受试者通常不知道他们是否接受了处理(这个过程称为盲法)。因此,这两个组可以说是可比的。由于唯一的区别是处理,如果我们观察到一个效果,那它可能是因果关系,前提是没有其他偏差存在。
然而,随机对照试验(RCT)有两个主要的缺点。第一个是我们不能总是使用 RCT。有时候这是不可能的,例如为了实验的需要改变一个受试者的性别。在其他情况下,这也是不道德的。例如,我有一篇论文评估武器出口对非洲冲突概率的影响。我们不会随机将武器发送到不同的国家,以观察这是否会影响冲突的概率。
第二个主要缺点是,当我们完全控制实验环境时,可能会以牺牲外部有效性(即我们可以将结果推广到研究范围之外的程度)为代价。例如,医学研究通常使用近交系的老鼠/小鼠。这些动物在基因上几乎是相同的,因此我们接近平行世界的情况。但问题是,我们失去了外部有效性。
因此,通常在完美测量因果效应和结果是否能很好地反映现实生活情况之间存在权衡。
图片由作者提供
让我用一篇精彩的论文来说明这个想法:《使用降落伞预防从飞机上跳下时的死亡和重大创伤:随机对照试验》 (www.bmj.com/content/363/bmj.k5094
)。这篇论文发表在顶级医学期刊:英国医学杂志(BMJ)。
在这个实验(这是一个随机对照试验)中,他们在 2017 年至 2018 年间的一年时间内成功招募了 23 名志愿者,并让他们从飞机上跳下。参与者被随机分为两组,一组使用降落伞,另一组则背着空背包。他们直接在碰撞后测量了死亡或重大身体创伤的概率。作者没有发现两组在这些结果(死亡和重大创伤)之间存在差异。
那么,问题在哪里呢?这确实是一个真实的实验,但为了能够进行这些实验,他们当然是在一个从未离开地面的静止飞机上进行的。人们从离地面约一米的高度跳下。论文的目的是强调有时候你试图通过实验来完美控制环境,但结果却不一定真实反映现实。
自然实验
所以,如果进行随机对照试验不一定可能,那我们该怎么办呢?我们可以求助于所谓的准实验设计或自然实验。
“自然实验是 观察性研究 ,其中 事件或情况允许随机 或看似随机的 研究对象分组 ,以回答特定问题。” 大英百科全书
让我举一个例子来说明这样的实验。假设你想评估污染对健康的影响(例如呼吸系统疾病的风险)。你可以在实验室中使用动物,但你不会让人类暴露在致命或非常危险的污染水平下。即使动物研究可能有用,我们也可能会难以将结果外推到人类身上。我们可能对现实生活中的情况感兴趣,例如长时间暴露在污染中,人类如何度过日常生活。然而,如果我们比较城市和农村的人,他们并不可比,其他因素使他们非常不同,例如:他们的饮食、运动方式、工作类型等。
另一个选择是比较城市居民与农村居民,但这些群体可能在其他方面(如饮食、身体活动和工作类型)有所不同,这可能会混淆结果。因此,准确评估污染与健康之间的因果关系可能很困难。
因此,为了回答这样的问题,我们可以借助自然实验。这正是一些研究人员在 2016 年所做的 (www.sciencedirect.com/science/article/abs/pii/S0095069616300237
)。作者利用北京奥运会来衡量空气污染对死亡率的因果效应。政府在比赛前和比赛期间实施了非常严格的减排法规(例如,关闭电厂,减少汽车使用)。这种情况使得可以精确观察到相同的人在之前、期间和之后。这些个体经历了其污染暴露水平的突然变化,从高水平降到显著较低的水平。作者发现“PM10 [空气中的细颗粒物] 浓度下降 10%会使月度标准化全因死亡率降低 8%。”
如何挑战因果关系?
质疑因果关系的两个主要因素(内生性问题)是:你所关注的关系往往受到其他因素的影响(遗漏变量偏差),或者效应可能是双向的(同时性,或反向因果关系)。
由于我们不能直接测试因果关系,我们可以做什么?
由于我们不能直接测试因果关系,我们可以做什么?你可以总是询问是否有其他因素解释了这个结果,或者是否在同一时间反向关系成立。使用这些问题来挑战一个因果声明。
图片来源于作者
首先让我阐述第一个概念:遗漏变量偏差(是否有其他因素?)。研究发现咖啡消费与心血管疾病之间存在高度负相关 (academic.oup.com/eurjpc/article-abstract/29/17/2240/6704995?redirectedFrom=fulltext
)。论文(Chieng 等,2022)中提到的咖啡消费与健康之间的联系可能并不一定是因果关系。需要考虑的是,其他因素可能会影响咖啡消费和整体健康。例如,身体活动更多的人可能会消费更多的咖啡,并且由于其身体活动而更健康。重要的是始终考虑可能存在其他因素,而不是在没有进一步分析的情况下将观察到的关系解释为因果效应。
图片来源于作者
你应该问的第二个问题是:可能是反向关系吗?例如,当你观察到喝酒更多的人更抑郁时,是因为他们喝酒,还是因为他们抑郁才喝酒?研究发现了这两种效应。因此,凭借这种关联,很难评估彼此的影响,因为它们混合在一起。
结论
由于我们无法直接测试因果关系,运用批判性思维挑战你听到或读到的任何因果声明是很重要的。首先考虑平行世界的情况,尝试发现理想情况和你所面临的情况之间的差异,以评估因果效果。这通常有助于发现问题可能出现的地方。然后,运用你的两个问题:可能是其他原因吗?是否存在反向因果关系?
要更深入了解,请阅读第二部分。
因果性的科学与艺术(第二部分)
让我们站在侦探的角度,探索因果推断。
·
关注 发布于 Towards Data Science · 8 min read · 2023 年 1 月 6 日
–
正如我们在这篇两部分文章的第一部分中看到的,测量因果效应对于得出正确结论至关重要,因为你做出的每一个选择或决定通常都是预期因果关系的结果。
例如:
个人选择:
-
如果我选择素食,我将减少我的生态足迹。
-
如果我喝下这杯龙舌兰酒,我会跳得更好。
公司:
-
在家办公会降低生产力
-
向用户投放 YouTube Premium 广告会增加订阅者数量。
政策制定者:
-
用可再生能源替代核电站将有助于实现《巴黎协定》。
-
封锁措施将减少 COVID-19 的传播
问题在于没有统计测试可以证明你的效果是因果关系。要挑战因果关系,正如文章的第一部分所解释的,你可以提出两个主要问题:是否还有其他因素可以解释因果关系,或者是否可能是反过来的(即效果导致了原因)?
[H]如何在没有直接测试因果关系的统计测试的情况下找到因果效应的证据?
这些问题使我们能够挑战因果声称。但是,如何在没有直接测试因果关系的统计测试的情况下找到因果效应的证据?在这篇文章中,我将向你展示研究人员如何通过一篇引人入胜的科学论文:‘伦敦雾:1866–1965 年污染与死亡率的一个世纪’(Hanlon (2018))。
为了做到这一点,我们将把自己置于警探的角度。警探们不断试图回答因果问题:是谁造成了这个人的死亡?是文官芥末在温室里用烛台吗?你确定不是用扳手或者其他人犯的罪吗?在我们的例子中,我们有一个嫌疑人,或者更准确地说,是一个我们想要验证的假设(例如,污染增加了死亡率)。然后我们问自己,这真的只是污染,还是健康服务的发展?或者实际上是天气的结果?等等。
通常,如果你想知道谁犯了罪,你很少有某人犯案的录像。即使有,也许画面模糊,也许是伪造的。因此,你可能永远无法百分之百确定罪犯的身份。为了克服这一限制,你会积累证据,尝试排除所有可能的罪犯不在场证明,直到你有足够的证据,并设法排除主要的其他故事(是否是其他人?这个人是否在做其他事情?)。在研究中,我们要寻找因果证据时,也非常类似。
案例研究:伦敦雾与死亡率
让我用以下论文*‘伦敦雾:1866–1965 年污染与死亡率的一个世纪’*(Hanlon (2018))来说明这些概念。伦敦在 19 世纪时已经是一个人口密集、污染严重的地区。这篇研究论文的作者回答了一个非常重要的问题:暴露对死亡率的影响是什么?
这篇文章的有趣之处在于,空气污染数据自 1950 年代才开始有。然而,准确的气象数据自 1850 年代就已存在。因此,文章的想法是利用雾作为污染的指标,因为在雾天,污染水平较低,而市民的污染暴露增加(见下图)。
你可以在这里找到一个完整的 Python 笔记本,其中包含我的代码,以复制论文并生成我将在本文中使用的图表:Deepnote notebook。
因果图。图片由作者提供。
初步了解影响
论文研究了严重雾霾对死亡率的影响。因此,让我们首先查看从雾霾周前五周到雾霾周后五周,死亡率(所有原因一起)如何变化。结果显示,死亡率在冲击时(第 0 周)以及后续几周都增加。然而,许多因素可能解释这一效应(例如季节性)。
数据集包含 1850 年至 1940 年伦敦的每周天气和死亡数据,并排除了第一次世界大战的年份。
图片由作者提供
我们可以忽略季节性和时间趋势的影响吗?
首先,让我们查看一下雾霾事件在一年中的分布情况。可以看到非常强的季节性(遇到严重雾霾的概率在冬季较高)。因此,在我们的模型中捕捉季节性效应非常重要,因为寒冷的天气与更多的雾霾相关,同时也可能与更多的死亡相关(寒冷天气容易让人们生病)。
雾霾的季节性。图片由作者提供。
其次,让我们查看一下 1850 年至 1940 年间出现严重雾霾的周频率。我们再次可以看到强相关性。我们观察到 1900 年后雾霾周的数量平均较少。模型必须考虑这一演变,以避免将这一效应与我们关注的效应混淆。这是因为医疗系统的质量随时间变化,降低了死亡率,同时雾霾周的数量也随时间减少。因此,如果我们不捕捉时间趋势,可能会夸大系数(高估雾霾对死亡率的影响)。
每年的雾霾事件数量。红色虚线表示 1900 年前/后的平均值。图片由作者提供
请注意,即使你对下面呈现的模型不熟悉,你仍然应该能够理解这个想法。估计模型是一个简单的线性回归:
用于表示一周的变量是t。Fog^s 是一个虚拟变量,当第s+t周出现严重雾霾时取值为一。X 是一个气象控制向量,包括降雨量、温度、气压和湿度。Year 和 Week 分别是固定效应集,用于捕捉年份效应和日历周效应(季节性)。e 是一个误差项。
因此,这种模型允许在有大雾的周的之前、期间和之后测量对死亡率的影响,同时考虑气象条件、季节性和年固定效应(时间的演变)。
森林图表示线性回归的系数。垂直轴代表死亡率,而水平轴代表距离有大雾的一周的周数。柱状图代表 95%的置信区间。作者提供的图片。
上图比较了一个不考虑季节性的模型(粉色方框)和一个考虑季节性的模型(橙色圆圈)。我们可以看到,季节性的影响确实扭曲了系数(在粉色模型中,死亡率增加大于橙色模型)。此外,考虑季节性的模型中死亡率在两周后恢复到雾前水平。
现在让我们加入年度固定效应。这组控制变量捕捉了污染物随时间的演变,但也捕捉了健康部门质量的演变,例如。因此,系数的解释略有不同。现在我们探讨年度 t 的死亡率与平均死亡率的偏差。
下面的图表显示了在大雾的那一周和接下来的一周,死亡率增加了。此外,我们还可以看到天气控制对估计值影响不大。
森林图表示线性回归的系数。垂直轴代表死亡率,而水平轴代表距离有大雾的一周的周数。这里的两种模型都包括了周和年的固定效应。柱状图代表 95%的置信区间。作者提供的图片。
现在让我们来质疑这种效应的因果关系。理由是雾会使污染物浓度降低,进而增加死亡率。使用我在本文第一部分中介绍的工具:“如果这是其他因素导致了这种效应呢?”。
这可能是一则意外和犯罪的故事吗?
如果有更多的雾,很难看清楚,所以会发生更多的事故或犯罪。为了排除这个替代的故事,作者比较了基于记录的死因(例如事故/犯罪 vs. 肺炎)的死亡率。
下面的图表显示,围绕大雾周的事故/犯罪引起的死亡率没有影响,而我们观察到肺炎引起的死亡率有显著的影响。
我们正逐渐接近抓到我们的嫌疑人:污染物。还有一个替代故事我想和大家探讨一下。
森林图表示线性回归的系数。纵轴表示死亡率(不同原因:事故/犯罪与肺炎),横轴表示与浓雾周的距离(以周为单位)。条形图表示 95%的置信区间。图像来源:作者
这可能是一个关于天气与流行病学的故事吗?
当天气恶劣(有雾)时,人们待在家里。因此,如果人们待在家里,就有更高的风险传染给他人,因此死亡人数的增加(例如,由于肺炎)仅仅是这种情况的后果,而不是污染的结果。这种说法似乎很难被反驳,对吧?
作者采用了非常优雅的方式来驳斥这一替代性故事。作者比较了两种不同的天气冲击:浓雾和大雨。确实,大雨会有类似的效果:人们可能会待在家里更久。然而,关键点在于雾霾使污染保持在低水平,而雨水则清洁空气。因此,如果这是一个污染的故事,我们应该会发现雨水(死亡人数较少)与雾霾相比的相反效果。
下面的图表正好揭示了这一效果:雾霾致死,大雨拯救生命。
森林图表示线性回归的系数。纵轴表示总体死亡率,横轴表示与浓雾周的距离(以周为单位)。条形图表示 95%的置信区间。图像来源:作者
结论
这篇论文提出了强有力的论据,证明了伦敦一个世纪以来雾霾与健康污染之间的因果关系,并使用证据来排除各种替代解释。为了评估因果关系,提出两个问题是有帮助的:‘是否有其他因素可能导致这种效果?’以及‘是否可能正好相反?’此外,下次当你需要质疑一个因果声明时,把自己置于侦探的角色,考虑和收集支持和反对不同解释的证据。通过这些技术,让我们做出更明智的决策,并对抗虚假信息。
提高投资回报率的秘密:实施全面漏斗营销方法
营销绩效与品牌建设相遇
在推动漏斗效率的同时,与客户建立更深层次的联系。
·发表于数据科学走向 ·阅读时间 12 分钟·2023 年 3 月 2 日
–
由Minator Yang提供的照片,来自Unsplash
作为数据科学家,你在通过数据解锁新的商业机会以推动增长和成功方面至关重要。快速变化的营销环境要求一种超越传统绩效营销或品牌建设战术的新方法。现在是时候接受挑战,全面审视将这两个元素结合起来的营销策略,以创建一个驱动实际结果的全面方法。作为数据科学家,你还扮演着变革推动者和业务顾问的角色;通过帮助转向全面漏斗营销方法,你可以产生变革性的影响。让我们更多了解一下:
营销世界不断发展,未能适应新现实的企业面临被淘汰的风险。COVID-19 大流行导致了消费者行为的变化,企业必须采取全面的营销策略才能保持竞争力。通过关注品牌建设和短期转化,企业可以与客户建立更有意义的联系,培养品牌忠诚度,并增加长期收入。
在这两个领域之间找到平衡可能具有挑战性,尤其是当它们在组织中历史上独立运作时。企业必须找到将品牌建设和短期转化策略整合在一起的方法,以创建一个驱动结果的全面营销策略。这需要建立测量系统和相应的 KPI。
在这篇文章中,我将提供见解,帮助营销人员在这个不断变化的环境中导航,建立一个成功的营销策略,这个策略将品牌建设与效果营销连接起来,以便这种方法能够在情感层面上再次与客户产生共鸣。
我们试图实现什么目标,为什么现在需要关注这个问题?
很多首席营销官将营销支出转向漏斗底部,这里更容易证明客户的获取,尤其是在经济困难时期,这是一个众所周知的事实。然而,通过这样做,营销人员常常忽视了品牌建设的重要性以及在漏斗顶部产生客户需求和关注。这种错误可能代价高昂,抹去了品牌与客户的情感联系。我亲眼见过这种情况,首席执行官会每小时监控销售,并要求额外的电子邮件发送,当然是促销的,一天又一天。在我们的年度品牌价值跟踪器中,你可以看到品牌价值和认知的恶化。
忽视品牌建设的重要性是一个代价高昂的错误,抹去了品牌与客户的情感联系。
许多品牌错误地只关注销售漏斗底部,即推动即时销售或转化。相比之下,更成功的组织意识到,关注整个漏斗,包括上部和中部,更重要的是建立品牌意识和推动对产品或服务的考虑。这种方法被称为全漏斗营销。它结合了不同的团队、测量系统和 KPI,创建一个更加连贯和有效的营销策略。通过这种方法,企业可以更好地理解各种营销努力如何在考虑到客户旅程转化的所有阶段相互作用,从而影响销售。
图片由作者创作
虽然全漏斗营销不是一个新概念,但营销环境中最近发生的两项重大变化使得企业必须迅速接受这种方法:
-
由于数字媒体费用的通货膨胀和目标广告市场的饱和,企业看到效果营销的回报趋于平稳或下降。随着自动化工具的商品化,在效果营销中获得竞争优势变得越来越困难,因此必须采用全漏斗方法。
-
由于 COVID-19 大流行,客户行为的急剧变化加速了数字化转型。在这种变化的环境中,客户对品牌的期望发生了变化,许多人将品牌的目标视为购买决策的关键因素。营销人员可以获得大量关于这些新行为的数据,为理解客户在整个漏斗中的决策过程提供了机会。
全漏斗营销的影响不仅仅体现在效率和投资回报率的提升上。更在于理解漏斗的每个阶段如何影响其他阶段以及整体客户体验。
当客户与品牌产生情感联系时,他们更有可能成为忠实客户,并在长期内花费更多。企业如果仅仅关注漏斗的下层,将面临摧毁品牌和侵蚀客户忠诚度的风险。
成功采用全漏斗营销方法的领先组织包括可口可乐和耐克等公司。他们认识到结合品牌建设和绩效营销的重要性,以创建完整的客户体验。
图片来源:Hal Gatewood 在 Unsplash
你如何将这两个要素整合并优化整个漏斗?
开发一个成功的全漏斗营销计划需要优先考虑四个关键要素。这些要素是推动业务增长和成功的基础,以全面和整合的方式进行:
-
衡量品牌建设
-
关注连接的关键绩效指标
-
进化到新型媒体组合建模
-
在漏斗中部署运营模型
无论是推出新产品、重新定位品牌,还是寻求实现季度销售目标,营销人员必须战略性地开发他们的营销计划,关注客户参与、品牌忠诚度和可衡量的结果。通过遵循这些关键构建块,营销人员可以创建强大且有影响力的营销计划,实现他们期望的结果。让我们深入探讨这四个支柱中的每一个。
1. 衡量品牌建设
在过去几年中,广告界发生了许多重大变化。传统的电视广告曾是许多品牌建设活动的支柱,能够在时间上持久。然而,这些努力难以跟踪,使得营销人员难以获取详细的信息来了解他们的广告活动如何产生影响。这是因为传统的电视广告没有数字广告的精准定位和跟踪选项。然而,这对营销人员来说是个好消息,因为数字电视使他们能够比以往更详细地跟踪谁在观看他们的广告。营销人员现在可以根据客户的兴趣、人口统计特征或其他属性展示不同的广告。因此,广告变得更相关和个性化,最终使得广告活动更有效。
此外,还有一些新的品牌活动成功测量方法超越了基本指标,如广告的观看人数和频次,主要关注广告活动的覆盖范围。如今,新的数据驱动方法可以为市场营销人员提供更多关于其活动表现的洞察,这可以帮助他们做出更好的投资决策。通过将这些新的测量技术与个性化广告定位相结合,市场营销人员可以创建更有效和更有影响力的活动,与目标受众产生共鸣。品牌建设领域的三个具体亮点:
可寻址电视和音频意味着电视和广播广告可以发送到特定的家庭或个人。借助这项技术,市场营销人员可以更精确地测量广告的效果,并将特定广告曝光与消费者采取的行动联系起来。相比之下,在传统的线性电视广告中,广告主选择一个网络或节目来播放广告。根据你试图销售的产品或服务,基于面板的研究可以帮助确定广告播放的时间和地点。而在可寻址电视广告中,你可能能够向每个目标家庭展示不同的定制广告。
所以,可寻址电视通过将客户的信息汇总并安全地匹配到他们的私人 IP 地址来工作。市场营销人员可以通过跟踪他们拥有的设备以及客户在不同设备上的使用情况,包括访问的网站、观看电视的时间以及客户喜欢的节目,来了解客户的很多信息。
品牌影响力测量与调查使市场营销人员能够看到他们的活动如何影响消费者对品牌的看法,重点关注上游测量。在当今世界,消费者比以往任何时候都更具信息化和广告意识。这意味着他们更倾向于选择那些与他们价值观相符并且有情感联系的品牌。虽然广告信息至关重要,但情感联系驱动了消费者行为。通过使用调查来衡量广告的影响,市场营销人员可以更好地理解如何创建与目标受众产生共鸣的信息。这可以帮助他们与消费者建立更强的联系,并推动品牌销售的增长。
营销信息很重要,但观众情感驱动着消费者行为。
品牌影响调查可以揭示广告如何影响消费者。品牌影响调查通过从数字、传统和混合接触点收集数据,覆盖了一个活动的效果。营销人员可以测试各种格式和创意方法的社交和展示内容,以评估他们从头到尾的活动,如品牌内容和影响者营销。品牌影响调查可以确定媒体策略是否成功地促进了品牌建设或活动。
这些调查通过严格的方法、大样本量和先进建模揭示人们的想法和行为。广告回忆、意识和考虑可以帮助品牌衡量活动成功。为了确定每个品牌的 KPI,调查通常将控制组和曝光组进行划分。这种方法使营销人员能够快速做出影响品牌认知的明智决策。
C. 营销归因 提供了另一种衡量品牌活动的核心方式。通过这些工具,营销人员可以在客户旅程中确定哪些广告导致了客户的哪些行为。这使他们能够找到广告与个人行为之间的更直接联系,并衡量其活动如何影响销售和收入等关键业务指标—最终帮助确定和调整跨接触点的营销策略,如广告、电子邮件、社交媒体等。这些“相关结果”没有像定性调查那样的消费者差异。
2. 相关 KPI
随着公司努力最大化其营销效果,将渠道和漏斗阶段之间的关键绩效指标(KPI)联系起来,已成为更好地理解其活动对实际商业结果的真正影响的强大方式。
例如,一家公司希望增加其网站的流量。通过跟踪社交媒体参与率(点赞、评论、分享)和网站流量,公司可以确定其社交媒体参与是否推动了更多的网站流量。如果社交媒体参与率很高,但网站流量仍然很低,则公司可能需要调整其社交媒体策略,以更有效地推动流量到网站。
同样,公司可能会使用展示广告来提高品牌知名度,并跟踪展示次数。通过将这一关键绩效指标(KPI)与搜索查询量挂钩,公司可以确定展示广告活动是否驱动了更多品牌搜索查询。如果展示广告活动成功,公司可能会看到品牌搜索查询的增加,而这些查询的每次点击成本低于普通产品类别搜索的成本。
邮件营销是公司接触目标受众的另一种渠道。通过跟踪邮件的打开率和销售转化率,公司可以确定其邮件营销是否有效地推动了销售。如果打开率很高,但销售转化率仍然很低,公司可能需要调整邮件营销策略,以更好地定位和转化潜在客户。
品牌在进行广告宣传时,通常会使用多个渠道(如社交媒体、电子邮件或搜索引擎)来接触不同阶段的潜在客户。理解哪些渠道效果最好以及哪些需要改进可能是具有挑战性的。
在关键绩效指标(KPI)之间建立的连接视图可以帮助营销人员了解不同触点(如电子邮件、广告或网站访问)如何协同作用以影响客户的购买决策。通过跟踪跨渠道和购买过程各阶段的 KPI,营销人员可以识别对业务底线影响最显著的互动。
一旦确定了这些关键互动,营销人员可以更好地决定在哪里投入广告预算,以获得最大的效果。通过更好地理解哪些渠道和策略最有效,营销人员可以调整或重新平衡他们的营销策略,以最大限度地利用预算并推动业务增长。
在建立了正确的关键绩效指标(KPI)并清楚地理解它们与业务结果的关系后,公司可以做出更明智的决策,并优化其营销工作,以实现更大的投资回报率(ROI)。
3. 新一代媒体混合建模
虽然媒体混合模型(MMM)一直是营销人员在不同类型的广告和营销中分配预算的宝贵工具,但它们也有局限性。最大的问题之一是它们无法捕捉短期变化,例如客户行为的变化,如 COVID-19 或广告活动结果的变化。这可能使得营销人员难以及时做出基于数据的决策。
领先的组织正在通过增量测试和多触点归因(MTA)模型等附加输入来现代化他们的媒体混合模型(MMM),以解决这些局限性。增量测试包括进行结构化实验,对照组的消费者不会看到广告,从而提供关于广告活动的实时和详细数据,帮助营销人员评估其努力的真实影响,并相应调整渠道的归因。近年来的技术和分析进步使得进行这些测试变得更加容易和便宜。这些增量测试有助于确定哪些策略或渠道在推动客户购买方面真正有效,而不仅仅是为那些本来就会发生的购买获得功劳。
此外,MTA 模型可以帮助确定客户购买的信用归属。通过将其与观众倾向评分(本质上是衡量某人购买的可能性)结合,营销人员可以更好地了解哪些策略和渠道在推动销售方面是有价值的。如果 TikTok 上的一个活动主要转换了那些可能会购买的人,那么它在推动增量销售(即没有这个活动就不会发生的销售)方面会被分配较低的价值。
通过现代化 MMMs 并添加这些额外的输入,营销人员可以更准确、详细地了解其活动的表现,并更迅速地做出数据驱动的决策。这对于全漏斗营销至关重要,因为它使营销人员能够理解漏斗的每个阶段如何影响其他阶段,从而提供完整的客户体验。因此,尽管 MMMs 有其局限性,但通过增量测试和 MTA 模型的现代化可以帮助营销人员优化支出并提高营销投资回报率。
4. 漏斗中的操作模型
除了提到的三个领域,全漏斗营销还需要投资于合适的技术和数据基础设施。例如,拥有一个集中的数据仓库和跨功能的共同数据语言对于确保团队使用相同的数据和定义至关重要。这使得准确测量和归因整个漏斗中的结果成为可能,并帮助团队基于数据驱动的洞察做出更好的决策。
支持数据和技术的同时,拥有合适的人才和能力也至关重要。这包括技术和分析技能的组合以及创意和战略专长。培训和提升计划可以帮助弥补人才差距,并确保团队具备执行全漏斗营销策略的正确技能。
最后,持续测量和优化绩效以确保全漏斗营销方法产生结果是非常重要的。这需要实验和测试以发现新的洞察和改进机会,同时还需要根据市场条件或消费者行为的变化进行调整和转变。
David Travis 的照片,来自 Unsplash
结论
关于新客户行为的数据丰富,为营销人员提供了深入了解客户及其决策过程的有利机会。为了保持竞争力,企业必须采用结合品牌建设和绩效营销的全漏斗营销策略。这种全面的方法可以增加客户参与度,培养品牌忠诚度,并推动收入增长。将你的努力集中在四个关键构建块上:
-
品牌建设的衡量
-
创建关联的 KPI
-
采用新波媒体混合建模
-
在整个漏斗中建立一个运营模型
通过全面的漏斗营销,企业可以提高效率和投资回报率,同时与客户建立牢固的情感联系。
一如既往,请告诉我你的想法,评论或提出任何问题;我很乐意听到你的声音。同时,祝你在与客户建立更强联系的过程中好运!
想要联系吗?
提升 NLP 性能的秘诀:深入了解 PyTorch 中的 nn.Embedding 层
解剖 PyTorch 中的 nn.Embedding
层及其工作原理的完整指南
·发表于 Towards Data Science ·阅读时间 8 分钟·2023 年 1 月 24 日
–
OpenAI DALL-E 生成的图像
你可能在涉及自然语言处理(NLP)的多个神经网络架构中见过著名的 PyTorch nn.Embedding() 层。这是设计先进 NLP 架构时最简单且最重要的层之一。让我用简单的术语来解释它是什么。
在花了一些时间研究其 C++ 源代码后,我发现了以下内容。nn.Embedding 层是一个简单的查找表,将索引值映射到具有特定维度的权重矩阵。这一简单操作是许多先进 NLP 架构的基础,允许在连续空间中处理离散输入符号。在训练过程中,神经网络中的 nn.Embedding 层的参数会被调整,以优化模型的性能。具体来说,通过反向传播更新嵌入矩阵,以最小化损失函数。这可以被视为学习将离散输入标记(如单词)映射到高维空间中的连续嵌入向量,其中向量被优化以表示输入标记的意义或上下文,相关于模型训练任务(例如文本生成、语言翻译)。
现在让我们通过一些具体的代码示例来了解:
nn.Embedding 层至少需要两个参数:词汇表大小和每个单词编码表示的大小。例如,如果你的词汇表有 10,000 个单词,那么第一个参数的值将是 10,000。词汇表中的每个单词将由一个固定大小的向量表示。第二个参数是每个单词学习到的嵌入的大小。
import torch
import torch.nn as nn
# Define the embedding layer with 10 vocab size and 50 vector embeddings.
embedding = nn.Embedding(10, 50)
这里发生的事情是 PyTorch 创建了一个名为 embedding
的查找表。这个表有 10 行和 50 列。每一行代表一个单词的嵌入,初始化时是从均匀分布中随机抽取的。它们是使用 torch.nn.init
模块中的 nn.init.uniform_()
函数进行初始化的,权重被初始化为在 -1 到 1 之间的随机值。要检查给定单词的嵌入(例如,表中的第一个单词),你可以运行:
embedding(torch.LongTensor([0]))
输出是一个大小为 50 的向量:
这些数字在训练过程中被调整和优化,以传达特定单词的含义。初始化方法对模型的性能有显著影响。不同的初始化方法可能导致优化过程的起始点不同,这会影响网络收敛到良好解的速度或难易程度。例如,如果权重初始化为非常小或非常大的值,反向传播过程中梯度也会很小或很大,这可能会减慢甚至阻止收敛。另一方面,如果权重初始化为接近零的值,梯度将更为合理,网络更可能快速收敛。
此外,不同的初始化方法被设计成与不同类型的激活函数配合良好。例如,Xavier 初始化设计用于与 sigmoid 和 tanh 激活函数配合使用,而其他方法可能与 ReLU 及其变体效果更佳。现在,让我们看看如何使用不同的方法初始化 nn.Embedding 层:
nn.init.normal_()
:它用从均值为 0 和标准差为 1 的正态分布中抽取的随机值初始化权重。这也被称为高斯初始化。
nn.init.normal_(embedding.weight)
nn.init.constant_()
:这个函数用特定的常数值初始化权重。例如,你可以使用 nn.init.constant_(my_layer.weight, 0)
将层的权重初始化为 0。
nn.init.xavier_uniform_()
和 nn.init.xavier_normal_()
:这些函数基于 Xavier Glorot 和 Yoshua Bengio 的研究,设计用于与 sigmoid 和 tanh 激活函数配合使用。它们将权重初始化为接近零但不太小的值。
nn.init.xavier_uniform_(embedding.weight)
nn.init.kaiming_uniform_()
和 nn.init.kaiming_normal_()
:这些函数基于 He 等人的研究,设计用于与 ReLU 及其变体(LeakyReLU、PReLU、RReLU 等)配合使用。它们还将权重初始化为接近零但不太小的值。
nn.init.kaiming_normal_(embedding.weight, nonlinearity='leaky_relu')
这些权重还可以使用预训练的词向量,如 GloVe 或 word2vec 进行初始化,这些向量在大型语料库上进行过训练,并且已被证明对许多自然语言处理任务有用。使用预训练词向量的过程称为——微调。将预训练的词嵌入与nn.Embedding
层结合使用,对各种自然语言处理(NLP)任务非常有用。这是因为:
-
提高模型性能: 预训练的词嵌入已经在大量文本数据上进行训练,并且已被证明对各种 NLP 任务有用。当作为神经网络的输入使用时,它们可以通过提供一组好的初始权重来提高模型的性能,这些权重捕捉了词语的意义。
-
节省计算时间和资源: 从头开始训练神经网络以学习词嵌入可能是一个耗时且计算开销大的任务,尤其是当你处理大语料库时。通过使用预训练的词嵌入,你可以节省大量计算时间和资源,因为这些嵌入已经在大语料库上学习过。
-
允许迁移学习: 预训练的词嵌入可以用于迁移学习,这意味着你可以将学到的嵌入作为起点,用于不同但相关的任务。这在标签数据稀缺或获取成本高的 NLP 任务中特别有用。
让我们看看如何实现它:
import torch
import torch.nn as nn
# Load a pre-trained embedding model
pretrained_embeddings = torch.randn(10, 50) # Example only, not actual pre-trained embeddings
# Initialize the embedding layer with the pre-trained embeddings
embedding.weight.data.copy_(pretrained_embeddings)
你还可以使用from_pretrained()
方法直接加载预训练的嵌入:
embedding_layer = nn.Embedding.from_pretrained(pretrained_embeddings)
你还可以使用来自流行库如 GloVe 或 fastText 的预训练嵌入,例如:
import torchtext
# Load pre-trained GloVe embeddings
glove = torchtext.vocab.GloVe(name='6B', dim=300)
embedding_layer = nn.Embedding.from_pretrained(glove.vectors)
在进行迁移学习时,你可能需要在训练过程中冻结预训练的嵌入,以防在反向传播步骤中更新它们,并且只有最后一个全连接层被更新。要做到这一点,设置embedding_layer.weight.requiresGrad = False
以防止此层被更新。
变换器的兴起
另一个常见的地方是变换器架构中。nn.Embedding
层是变换器架构的关键组件,它是一种广泛用于自然语言处理任务的神经网络架构,如语言翻译、文本摘要、问答以及创建大型语言模型如 GPT3。
在变换器架构中,nn.Embedding
层用于将输入的令牌序列(例如单词或子词)转换为连续表示。这是通过在学习的嵌入矩阵中查找每个令牌的嵌入向量来完成的。
嵌入层的输出随后通过几个多头自注意力和前馈神经网络层,这些层用于在上下文感知的方式中处理和理解输入序列。自注意力机制是变换器的关键组件,它允许模型在进行预测时衡量输入序列中每个标记的重要性。这些都在 PyTorch 中的nn.Transformer
层内实现。
经过变换器层处理后,模型的输出通常会通过一个最终的线性层,用于进行任务预测。例如,在语言翻译模型中,最终的线性层会用于预测目标语言中每个单词的概率,给定源语言中的输入序列。让我们看看Transformer
类在 Python 中的样子:
import torch
import torch.nn as nn
class Transformer(nn.Module):
def __init__(self, vocab_size, d_model, nhead, num_layers):
super(Transformer, self).__init__()
# This is our holy embedding layer - the topic of this post
self.embedding = nn.Embedding(vocab_size, d_model)
# This is a transformer layer. It contains encoder and decoder
self.transformer = nn.Transformer(d_model, nhead, num_layers)
#This is the final fully connected layer that predicts the probability of each word
self.fc = nn.Linear(d_model, vocab_size)
def forward(self, x):
# Pass input through the embedding layer
x = self.embedding(x)
# Pass input through the transformer layers (NOTE: This input is usually concatenated with positional encoding. I left it out for simplicity)
x = self.transformer(x)
# Pass input through the final linear layer
x = self.fc(x)
return x
# Initialize the model
vocab_size = 10
d_model = 50
nhead = 2
num_layers = 3
model = Transformer(vocab_size, d_model, nhead, num_layers)
需要注意的是,这只是一个用于演示目的的简单示例,展示了嵌入层在Transformers
中的使用,而真实的变换器模型通常会有额外的组件,如位置编码,这是一种提供每个标记相对位置的信息的技术。还有层归一化,用于规范化层的激活,以提高模型的稳定性和性能。此外,常见做法是使用预训练的嵌入来初始化嵌入层,以利用从大量文本数据中学到的知识。
有趣的事实:
- 上述具有仅 10 个词汇、50 维向量嵌入、2 个多头注意力机制以及编码器和解码器中的 2 层的变换器层共有 2,018,692 个可训练参数。这些是我们在训练过程中优化的参数数量。要得到这个数字,我运行了下面的代码:
sum(p.numel() for p in model.parameters() if p.requires_grad)
-
模型维度或 d_model 必须能够被多头自注意力机制中的头数整除,因为多头注意力机制将模型维度分割成几个较小的子空间。每个子空间用于计算不同标记集的注意力权重。
-
变换器模型也被用于计算机视觉任务,如物体检测和图像分割,通过使用自注意力机制将图像像素处理为标记。
总结来说,nn.Embedding
层是许多 NLP 模型中的基本资产,并且在 transformer 架构中扮演着关键角色。nn.Embedding
层用于将输入的标记序列转换为模型可以有效处理的连续表示。使用预训练的嵌入允许 transformer 模型利用从大量文本数据中学到的知识,这可以提高它们在各种自然语言处理任务上的表现。nn.Embedding
层还具有一些我们在这篇文章中没有涉及的参数,如 sparse
选项、padding_idx
、max_norm
和 norm_type
,这些参数可以用来根据具体任务的需求自定义嵌入层。理解 nn.Embedding
层及其工作原理是使用 PyTorch 构建有效自然语言处理模型的重要步骤。
有助于数据科学家成长的技能
·
关注 发表在 Towards Data Science ·发送至 Newsletter ·3 分钟阅读·2023 年 6 月 29 日
–
即使你处于数据科学学习的早期阶段,你也可能对入门所需的核心技能有一个明确的认识:一些统计知识、对一种(或两种)编程语言的基本了解,以及对如何处理、分析和可视化数据的合理掌握,仅仅列举了一些明显的技能。
那么,哪些技能能帮助你在职业生涯中长期蓬勃发展呢?这正是事情变得更加模糊的地方——也是本周重点内容发挥作用的地方。我们挑选了一些数据专业人士撰写的文章,他们根据自身经验和非线性职业路径分享了可操作的见解。这些文章可能专注于特定领域和角色,但它们提供的经验教训对许多其他实际情况同样适用。让我们深入了解一下。
-
对于将技能视作硬技能与软技能二分法的观点,有人可以提出有力的反驳,但不可否认的是,我们通常将“软技能”归类的那些技能,确实是数据导向职业成功的关键。Eirik Berge最近重点讨论了包括协作和指导在内的五项关键技能,并提供了令人耳目一新的具体建议。
-
从软件工程师的角度出发,Naomi Kriger整理了一个帮助你在项目展示中取得成功的实用路线图,适用于工作面试。在这里你找到的教训,同样适用于你下一个季度规划会议、ML 流程复盘或任何其他需要讲述清晰且引人入胜故事的场合。
-
首次参与 TDS 贡献的Fiona Victoria最近完成了申请人工智能博士项目的艰巨过程,并撰写了关于(众多)需要考虑的因素和步骤的文章。即使研究生院不在你的计划中,Fiona 的深思熟虑的方法可能会帮助你应对未知的压力,这一特质也贯穿于许多其他职业决策中。
-
“如果你是一个有志成为数据科学家的人,你可能会惊讶地发现,职业倦怠的风险其实从未真正消失过,”Matt Chapman警告道——对于那些从其他角色转行进入这个领域的人尤其如此。Matt 关于以长期可持续性为目标建立职业生涯的建议集中在更好的优先级排序和休息上,这些建议对我们所有人无论处于职业的哪个阶段都大有裨益。
如果你已经读到这里,恭喜你:你出色的时间管理技能显然让你能够继续阅读——我们希望你能继续阅读,因为我们的其他每周亮点也非常精彩:
-
Conor O’Sullivan关于 AI 工具对跨性别群体的风险的入门介绍讨论了一个紧迫且及时的问题,并呼吁所有数据科学和机器学习从业者提高意识和行动意愿。
-
你是如何解释数据中的异常的?Mariya Mansurova的易懂的根本原因分析新指南详细回答了这个问题。
-
通过关注Federico Peccia对这个日益重要领域近期工作的有用概述,了解推进绿色 AI 的最新技术。
-
想要从新的角度了解成功留住用户的推荐系统,请阅读Christabelle Pabalan对新颖性和意外性的深入讲解。
-
Elena Samuylova为初学者提供的漂移检测介绍中提出了五种测量机器学习嵌入漂移的方法。
-
MusicGen 是一个新的音乐生成模型,可以基于参考旋律创建新主题。 Max Hilsdorf测试了该模型的能力,并反思其对音乐创作过程的潜在影响。
感谢你对我们作者的支持!如果你喜欢在 TDS 上阅读的文章,考虑成为 Medium 会员 — 这将解锁我们的整个档案(以及 Medium 上的其他所有帖子)。
直到下一个 Variable,
TDS 编辑团队
在 Kubernetes 上运行代码的智能灵活方式
原文:
towardsdatascience.com/the-smart-flexible-way-to-run-code-on-kubernetes-94d1ae6c46f3
·发表于Towards Data Science ·6 分钟阅读·2023 年 1 月 23 日
–
当我还是一个 Kubernetes 初学者时,我主要关心的是如何让代码在集群上运行。被投身于一个全新的世界,我看到所有这些令人困惑的 YAML 文件,每一行和缩进都带来新的意义。
照片由Thais Morais提供,来源于Unsplash
一旦我学会了将代码最快速度放入文件中,我很快就用绝对路径填满了它。你可以看到下面的截断示例,我称之为初学者的方式。请注意,这是一种完全有效的运行代码的方式;只是缺乏即将到来的部分所关注的特性。
快速但容易出错的方式:代码目录的完整路径
让我们深入探讨一下.yaml 文件,并重点突出最相关的部分,以便将代码(和数据)放入容器中。简单来说,容器只是一个包含相关内容的盒子。这个盒子由 Pod 使用,我们可以将 Pod 视为一个虚拟计算机。这个虚拟计算机帮助我们运行容器中的内容。
(如果你对容器化更熟悉,你可能会注意到这个描述是简化的。a) 这是正确的,b) 你可能已经知道比这篇博客文章教你的更多内容)
在 YAML 文件中与本文相关的部分是我们决定运行哪些代码的“command:”部分。这已经是个棘手的地方。假设你的本地开发机器使用以下虚构路径:/local/user/path/script.py。假设我们将此代码发布到计算集群并存储在*/remote/cluster/path/script.py*下。既然我们知道脚本在 Kubernetes 集群中的位置,我们可以转到 YAML 文件中并粘贴它的路径。如果运气好,它会工作。
但很可能不会,特别是如果我们在script.py中使用了来自其他用户定义的 Python 文件的导入时。我们的代码很可能会因*“ModuleNotFoundError: No module named ‘xyz’”*而失败。这种错误发生是因为当我们运行 script.py 时,Python 在已知的位置搜索要导入的模块。如果我们希望读取/导入的文件不在这些位置,我们的代码将无法运行。
为了说明这一点,设想你站在烈日下,夏天的热浪让你汗流浃背。你想喝点水,哪怕是自来水,现在也非常棒,于是你走进屋里。
从厨房水槽的水龙头中,你倒了一杯水,急切地喝下去。水很不错,所以你也想在外面喝点东西。因为水来自水龙头,所以你简单地撕下水龙头并带着它。再次走到外面,你会发现:“WaterNotFoundError: No water。”
在这个构造的示例中,很明显为什么仅仅撕下水龙头并不能魔法般地获得无限的水供应。问题在 Python 示例中也是一样的:我们操作的点不知道所需文件的位置。在本地开发环境中,所需的代码可能与主脚本在同一文件夹中。但当我们——类似于水龙头的情况——从集群上的另一个文件夹(即从其他位置调用 Python 脚本*)操作时,代码会发生什么?答案是*“ModuleNotFoundError: No module named ‘xyz’”*
这里有一个快速但粗糙的修复方法(我之前也用过):在所有从其他用户创建的 Python 文件中导入的 Python 文件中,将这些辅助文件所在的目录添加到 Python 的搜索路径中:
sys.path.append("directory/with/auxiliary/files")
对所有目录重复这一步,你就完成了。
这个修复能解决问题,但不是最好的方法。代码膨胀;我们不需要本地设置中的额外行,如果我们把辅助文件完全移动到文件系统上呢?我们将不得不更新所有硬编码路径!这太麻烦了。幸运的是,还有更好的方法。
智能且灵活的方法:相对路径和包含代码
运行代码的更好方法是使用相对路径。在运行的示例中,我们将 Python 文件存储在集群的*/remote/cluster/path/script.py*下。
我将假设,根据良好的惯例,所有辅助代码(例如,“utils.py”)都位于此目录或其子目录中。考虑到这一点,采取更智能的方法有两个步骤,一个涉及 Docker 镜像,另一个涉及 .yaml 文件。
改进后的 Docker 镜像 在展示改进版本之前,这里有一个相当标准的 Dockerfile:
尽管这个默认的 Dockerfile 能完成工作并安装所有包,但我们可以通过稍微改变内部文件夹结构来进一步改进它:
在改进后的 Dockerfile 中,我们首先安装所需的 Python 包,然后将代码直接存储在镜像中。这一步非常重要,和之前的方法相比差别巨大:之前我们从底层文件系统调用 Python 脚本;现在我们从Dockerfile 内部调用它们。
为此,我们创建一个名为code的文件夹,将其设置为工作目录,并通过运行“COPY . .”将所有代码/数据等复制到 Docker 镜像中的此目录中。之后,我们授予自己在容器中运行代码的权限。
改进后的 .yaml 文件 使用这个设置,可以构建改进后的 .yaml 文件。我们之前调用脚本的完整路径,例如,
command: [ "python3", "/home/scripts/research/audio/preprocessing.py" ]
现在我们可以将其简化为
command: [ "python3", "preprocessing.py" ]
再次,差异可能看起来很小:毕竟,我们只是移除了脚本的路径(* /remote/cluster/path*)。
但在幕后,我们现在正在读取并运行镜像中的 Python 文件。换句话说,我们有了一个可移植的环境。
不过有一个警告:如果我们将新代码推送到远程服务器,从而更新我们的脚本,这一变化会反映在镜像中吗?不会,镜像保持为我们最后构建的状态。
两个 Dockerfile 以加快构建时间
幸运的是,对于这种常见情况,有一个简单的技巧:维护两个 Dockerfile。在这种情况下,我的常规步骤如下。
- 我有一个主要的 Dockerfile,恰当地叫做Dockerfile_base。
我首先用这个文件构建一个镜像,文件中包含足够的命令来创建一个项目的基础。例如,这可能是这样的一个起始点:
细看,你会发现我在这一步并没有复制任何代码相关的文件。相反,我只安装了 pip 包,并通过 apt-get 获取一些额外的需求。这个结构不太可能发生变化,所以我将生成的镜像存储为,例如,project-audio-analysis-base:0.0.1。注意镜像名称中的 -base 标签。
2. 构建基础镜像后,我构建第二个 Docker 镜像。
对于这个镜像,我拉取了之前创建的project-audio-analysis-base:0.0.1。这个新镜像是从一个单独的 Dockerfile 构建的,我通常称之为Dockerfile_update或类似名称。其内容示例如下:
关键是,我将生成的镜像不是存储在与之前基础脚本相同的位置——这会覆盖我们干净的起点。相反,我将其存储在一个不同的位置;通常,我只是省略 base tag,如:project-audio-analysis:0.0.1。每当代码有已发布的更改时,我使用较薄的 Dockerfile_update。这种方法的好处是始终有一个干净的起点(对于你破坏了代码的情况)并显著减少构建时间。
如果我们每次创建新的 Docker 镜像时都重新安装所有软件包,我们每次都需要等待。这是不必要的。只需存储一个已预装所有这些软件包的固定基础,并在此基础上构建镜像即可。
总结
在开始使用 Kubernetes 时,常常会遇到这些巨大的、复杂的 YAML 文件。可以理解的是,一旦代码按预期运行,人们会感到相当释然。然而,一个错误——或错误的设计决策——是使用绝对路径并从文件系统中运行代码。
为了缓解这个问题,我提出了一种更好、更清洁、更便携的在 Kubernetes 集群中运行代码的方法。它包括两个部分:首先,将代码包含在镜像内,其次,维护一个基础和更新的 Dockerfile。拥有这些工具后,可以快速生成改进的镜像。
- 从另一个位置调用 Python 脚本:通常,我们使用命令行进入 Python 脚本所在的文件夹,然后执行 python script.py。但是,当在另一个文件夹中时,我们也可以运行 python /path/to/script.py。在这种情况下,博客文章中描述的问题会出现。
成为数据科学家成功所需的软技能
原文:
towardsdatascience.com/the-soft-skills-you-need-to-succeed-as-a-data-scientist-ceac760230d3
成为数据科学家职业生涯中可以提升的前五种软技能
·发表于Towards Data Science ·阅读时长 14 分钟·2023 年 6 月 19 日
–
图片由Jason Goodman提供,来源于Unsplash
你的旅程概览
-
介绍
-
技能 1 — 沟通
-
技能 2 — 协作
-
技能 3 — 好奇心
-
技能 4 — 项目管理
-
技能 5 — 导师指导
-
总结
介绍
当你在数据科学家的职业生涯中工作时,很容易专注于硬技能。你可能想学习一种新的 ML 算法,比如带有非线性核的 SVM,新的软件技术,如 MLflow,或新的 AI 趋势,比如 ChatGPT。
这些技能容易学习,因为它们的成功衡量标准很简单。以 MLflow 为例。你可能首先会学习 MLflow 能为你的 ML 生命周期提供什么。你了解模型工件、ML 项目结构和模型注册。你完成了一个课程,花了几个小时阅读用户指南,甚至在实际项目中实现了它。太棒了!完成这些后,你可以自信地说你了解了一些 MLflow,并可以将其作为技能添加到你的简历中。
那么,软技能比如时间管理呢?你会如何进行相同的过程?真的停下来好好想一想这个问题。虽然确实有关于时间管理的书籍可以阅读,但它远不如阅读 MLflow 文档那样具体。你可以在日常生活中实施时间管理,但这不如在 ML 项目中实现 Mlflow 那样直观。你可以在简历中列出时间管理,但那到底意味着什么呢?😧
生活的事实是,软技能更难以衡量,因其本质上更难以捉摸。许多人得出结论,软技能的价值低于硬技能。但这是一个严重的错误!仅仅因为某些东西难以衡量,并不意味着它不值得投入精力去提升!
我相信我们都遇到过那种时间管理得当的同事,他们的产出几乎是其他人的两倍。这是通过硬技能几乎无法获得的提升。我从未见过有人学习了 MLflow,然后产出是其他数据科学家的两倍。所以即使软技能难以衡量,它们也能提供远超许多典型硬技能的价值 🔥
这在数据科学中尤其如此。数据科学家的积极刻板印象是具有出色的问题解决能力。负面刻板印象是他们在商业环境中成功所需的一些常见软技能上稍显欠缺。通过花费一些时间来提升软技能,你可以获得巨大的优势,并用它来开辟你自己的职业道路。
在这篇博客文章中,你将了解数据科学家成功所需的前 5 项软技能。这当然只是基于我个人的观点。然而,这一观点是通过观察许多其他数据科学家以及看到他们中的一些人如何从其他人中脱颖而出而形成的。
技能 1 — 沟通
第一个技能是最基础的。你应该学会如何进行良好的沟通。这包括很多方面:
-
你应该能够清楚地传达你在探索性数据分析(EDA)阶段的发现。 出于上天的爱,务必根据听众调整你的沟通方式。CEO 不想听你关于选择数据拟合分布的决定,或是你使用了哪个 Docker 镜像来运行实验。CEO 可能对数据科学很热情,但他或她还有成百上千的其他事情需要考虑。给出 EDA 的高级概述,并专注于业务结果。
-
在演讲时,确保你说的内容对观众有价值。 这听起来显而易见,但显然并非如此。不要仅仅为了显得聪明而谈论复杂的架构或精密的超参数调整。这只是一种防御机制。相反,确保你说的话能够给听众带来有价值的东西。这样,你会突然发现有人来找你讨论你所讲的内容。
-
与他人交谈时,确保你在听他们说什么。 这并不等于点头并等待轮到自己发言。真正的倾听意味着将自己置于发言者的立场,认真尝试理解他们的观点。假设一个产品负责人向你解释他们希望加快进度而减少探索。与其等待解释他们的错误,不如花点时间真正倾听。产品负责人可能会因为进展被评估,并且可能不了解探索分析的好处。如果你诚实的话,探索分析可能确实进行了比必要的时间更长。试着倾听,然后与产品负责人合作,找到一个对双方都好的解决方案。
此外,你应该改进你的写作。不,真的。虽然写得并不糟糕,但可以更简洁。你有写复杂句子、进行不必要解释和拖延细节的倾向,而这些细节的重要性远没有你想象的那么高。别担心,我也是这样 😳
简洁可以传达自信。 比较以下两种对下周五到期的功能请求邮件的回复:
-
我认为在下周五之前这是可能的。我将从研究问题开始,理解解决方案空间,然后以迭代的方式进行工作。如有需要,我会寻求建议,并朝着你要求的目标,即在下周五之前完成功能,继续推进。我相信一切会顺利进行,我会在下周五之前交付令人满意的结果。
-
我将致力于在下周五之前完成这个任务。如果出现任何障碍,我会寻求建议。
在第一种陈述中没有更多信息,除了对你工作的迭代和对令人满意结果的模糊承诺。想象一下作为一个项目经理或技术负责人,必须天天阅读这样的内容。删掉这些吧!以专业的方式说出你想说的话,然后继续解决当前的问题。
技能 2 — 协作
只有很少的数据科学工作是由单个数据科学家完成的。当然,也有少数例外。但大多数有影响力的数据科学工作是由数据专业团队完成的,这些团队通常得到前端/后端开发人员、平台工程师、测试人员、领域专家、项目经理等其他职业的支持。
这意味着协作不仅有用,而且对于成功的数据科学至关重要。以下是一些可以提升你协作技能的方法:
-
当依赖其他角色时,了解你的工作与他们工作的接口。 比如,你正在与一位在 Databricks 或 Synapse Analytics 中编写 Spark 的数据工程师合作。他们工作的输出是经过清理且格式正确的数据表。但什么是正确的格式呢?这完全取决于你想要使用的特性和你计划使用的算法。你不希望数据工程师费尽心思清理一个你马上就会丢弃的表格中的列,因为你不打算使用它。这是协作不良的表现。
-
当其他角色依赖于你时,提前规划如何确保良好的协作。 比如,你正在计划开发一个处理实时数据并预测值的 ML 模型。预测结果将被发送到前端应用的用户和 Power BI 仪表盘进行内部跟踪。那么,前端开发人员和数据分析师应该了解数据的未来格式。你甚至可以为他们提供具有确切数据结构的模拟数据。通过这种方式,你可以确保依赖于你的人员不必等你完成后才能做他们的工作。当人们协作良好时,就像并行处理;当他们不协作时,就变成了单线程,一切都变得缓慢。
-
与其他数据科学家合作时,明确责任归属。 由于数据科学家来自不同的背景,他们的技能集差异很大。你可能会有一位数据科学家非常擅长为模型提升那些额外的准确率。另一位数据科学家可能非常擅长将模型投入生产并监控数据漂移。不同的人可以根据他们的经验承担不同的方面的责任。尽管如此,每个数据科学家仍然可以对各个方面做出贡献。
最后,还有一些与数据科学无关的更通用的内容。团队中的每个人都值得被尊重。这与他们的技术背景、技能水平、性别或任何其他与基本礼貌无关的因素无关。
人们有时会犯错误。重要的是要承认错误,同时确保错误是学习的自然部分。你应该优化以在团队中形成一种可以在不怕报复或嘲笑的情况下承认错误的文化。 如果你未能创造这样的文化,那么错误不会停止发生。它们只会在雷达下发生,并在修复它们变得更困难时浮出水面。
技能 3 — 好奇心
图片由 nine koepfer 提供,来源于 Unsplash
我一直觉得数据科学家天生是好奇的人。他们喜欢学习新的 ML 算法或跟踪自己领域中的新发展。但是,这种好奇心是否扩展到邻近学科中的技术、方法和方法则差异很大。
一些数据科学家对学习更多关于软件开发、设计原则、项目管理、数据分析、数据工程、商业影响等方面的内容感到兴奋。其他人则希望坚持自己的领域,只专注于数据科学。虽然这完全没问题,但如果你被评估得低于那些有好奇心探索邻近领域的同事,你也不应感到惊讶。
这不公平吗?其实不然 🤷
知道软件开发的数据科学家显然比不知道软件开发的人更有用。软件开发技能确保数据科学家可以从事更多可能的项目。同时,与前端/后端开发人员和数据工程师等角色的接口也突然变得更容易管理。
其他人只能通过他们自己的视角来看待你的输出。 比如,假设一个测试人员负责运行你编写的几个组件的集成测试。如果你的组件文档完备、模块化良好、编码标准高且有单元测试,那么测试人员的工作就会轻松很多。另一方面,如果你只是有大量自由流动的代码在一个庞大的 R 脚本中,那么追踪错误对测试人员来说将是一项繁重的工作。自然,测试人员会认为那个在软件方面投入更多努力的人更有技能。这与脚本中的 ML 模型做什么无关。
商业影响是另一个经典问题。数据科学家最常收到的负面反馈之一就是他们与商业目标过于脱节。一个理解业务并提出能产生 ROI 的数据科学用例的数据科学家自然会对业务更有价值。
那么如何在这广泛的好奇心上努力呢?你们可以花费在其他学科上的时间是有限的,但我有两个一般性建议:
-
花一些时间去了解其他角色真正从事的工作。 与业务分析师交谈时很快就能掌握一些 KPI 和 OKR 的知识,但这些知识可能非常宝贵。就我个人而言,我对计算机网络了解甚少,因为我没有信息学背景。但我知道为什么有人会使用私有网络,如何粗略设置一个,以及何时可能适合投资于此。我主要是通过与网络工程师交谈获得这些知识的。虽然这些知识比较肤浅,但了解何时联系网络工程师是很有价值的。
-
在做项目时,抓住机会做一些稍微超出你舒适区的事情。是否有人需要在持续集成管道中实现自动化的代码检查?我来试试吧!即使你对 CI/CD 或 YAML 文件了解不多,你也可能会弄明白。如果不行,你总是可以寻求帮助。通过抓住学习新事物的机会,你会…学习到新事物。我知道,这挺深刻的 😉
技能 4 — 项目管理
回顾一下之前需要团队协作的项目。想想那些未能按时完成或超出预算的项目。共同的因素是什么?是超参数调优不足?还是模型工件日志记录不佳?
可能不行,对吧?项目失败的最常见原因之一是项目管理不善。 项目管理负责将项目分解为可管理的阶段。每个阶段都应持续估算剩余的工作量。
作为一个决策明确的项目经理,还需要负责很多其他的事务,从冲刺执行到回顾总结。但是我不想把重点放在项目管理的角色上。我想将重点放在项目管理的技能上。就像团队中的任何人都可以展现领导力作为一种技能一样,团队中的任何人也可以展现项目管理作为一种技能。对于数据科学家来说,这确实是一个非常有用的技能。
为了具体说明,让我们专注于估算单个阶段。实际上,大部分数据科学工作很难估算:
-
数据清理阶段需要多长时间? 完全取决于你正在处理的数据。
-
探索性数据分析阶段需要多长时间? 完全取决于你在过程中发现了什么。
你明白我的意思了。这使得许多人认为估算数据科学项目中各阶段的持续时间是没有意义的。
我认为这是一个错误的结论。更准确的说法是,在开始阶段之前准确估算数据科学阶段的持续时间是非常困难的。但是项目管理是与持续估算打交道的。或者说,良好的项目管理应该是这样的 😁
想象一下,不是提前估算数据清理工作的时间,而是你已经开始清理数据一周了。你现在知道有三个数据源存储在不同的数据库中。其中两个数据库缺乏适当的文档,而最后一个缺少数据模型,但文档比较齐全。在所有三个数据源中都有一些数据缺失,但没有你担心的那么多。你能对此说些什么?
当然,你并非没有任何信息。你知道你不会在明天完成数据清理工作。另一方面,你非常确定三个月的时间对于这项工作来说太长了。因此,你有一种分布,给出阶段完成的概率。这种分布有一个“均值”(对阶段持续时间的估计)和一个“标准差”(估计的不确定性)。
重要的是,这种概念分布每天都在变化。 你会得到越来越多关于需要完成工作的的信息。随着你对阶段完成时间的确定性增加,“标准差”自然会逐渐缩小。你的工作是将这些信息量化给利益相关者。而且在向利益相关者解释时,不要使用我用的分布语言,这可以留在我们之间。
能够说出这样的话的数据科学家是非常宝贵的:
“我认为这个阶段需要 3 到 6 周的时间。我可以在一周内给你一个更准确的更新估计。”
技能 5 — 指导
指导初级数据科学家通常被视为一种必要的“恶行”。这无疑是诚实的工作,但并不被强调。如果初级数据科学家能神奇地自学这些概念,那不是更好吗?
正如你可能猜到的,我不同意。指导初级数据科学家对你和他们都非常有帮助。 这里有三个原因:
-
你通过解释概念学到了很多: 这一点非常直接。通过向初级数据科学家解释概念和想法,你自己对这些概念的理解也会更深刻。我常发现,向初级数据科学家解释某些东西有助于我更清晰地表达某些内容。通常,只有当有人问你时,你才会意识到你可能对某些东西的理解没有你想象的那么好。这是一个深入了解话题的绝佳机会。此外,你可以向初级数据科学家强调,不知道所有事情是没关系的。实际上,这也是不可避免的。
-
你获得了初步的管理经验: 不久后,你可能会踏入更高级的角色,例如首席数据科学家。这些角色通常没有正式的员工管理责任。然而,期望是你能够领导和影响他人。像任何其他技能一样,这需要实践。在你日常的数据清理和模型调整中,你很少有这样的实践机会。因此,如果你从未指导过任何人,那么如果你在领导和影响他人时遇到困难,也不要感到惊讶。如果你在考虑转向管理轨道,那么过去没有任何指导责任可能是一个警示信号。你为什么从未指导过任何人?是因为你不想做,还是因为其他人不希望你做?这些可能性都不太理想。
-
你可以与初级数据科学家建立联系: 当然,导师和初级数据科学家之间存在自然的权力不平衡。尽管如此,如果导师做得好,初级数据科学家通常会与导师建立最紧密的联系。通过承担责任和指导初级数据科学家,你很快会发现自己被你指导过的人包围。这些人往往会尊重你,重视你的建议。这并不是一种糟糕的境地。
我的建议是尽快在你的职业生涯中成为一名导师。以上三点好处只有在你认真对待指导工作时才有效。如果你指导工作做得不好,你会获得很少的好处,甚至可能被认为是一个糟糕的导师 😬
一些公司对指导的期望很低。你可能被要求每月与初级数据科学家喝一次咖啡。我建议你超越职责范围。 向初级数据科学家提供,他们可以向你提出问题和困惑。这样主动担当对于初级数据科学家来说,表明你可以在没有明确要求的情况下承担责任。
总结
图片来源:Spencer Bergen 在 Unsplash
在这篇博客文章中,我们看到软技能对于数据科学家职业发展是非常有价值的。在面试高级数据科学家时,我会关注他们积累的软技能和硬技能一样多。如果你认为还有其他对数据科学家至关重要的软技能,请给我留言。
如果你对数据科学、编程或其他相关领域感兴趣,欢迎在 LinkedIn 上关注我,并打个招呼 ✋
喜欢我的写作吗? 查看我其他的文章,获取更多 Python 内容:
-
用优雅的类型提示来现代化你的罪恶 Python 代码
-
用 Python 可视化缺失值非常简单
-
使用 PyOD 在 Python 中引入异常/离群点检测 🔥
-
5 个绝妙的 NumPy 函数,能在关键时刻救你一命
-
5 个专家技巧让你的 Python 字典技能飞跃 🚀
SQL 单元测试现状:2023
原文:
towardsdatascience.com/the-sql-unit-testing-landscape-2023-7a8c5f986dd3
提升 SQL 开发的速度和安全性
·发表于Towards Data Science ·阅读时间 9 分钟·2023 年 5 月 2 日
–
图片由Ilse Orsel拍摄,来源于Unsplash
SQL 开发的演变
尽管 SQL 已经有将近50 年的历史(或者说正因为它已经将近 50 年了),但该语言的发展在采用现代实践和工具方面一直较为缓慢。几十年来,我们主要的“IDE”是像SSMS和DBeaver这样的管理工具,版本控制则通过像stored_proc_2022_03_15这样的命名约定来完成,测试则是通过将查询运行在生产表上并导出到电子表格中进行手动核对。
但在过去十年中,情况已经迅速改善。那些数据库管理工具以及云数据仓库控制台都具有丰富的功能,包括语法高亮、代码自动完成和计划可视化。我们通过像Liquibase、Flyway和dbt这样的工具实现了数据库版本控制。即使是测试/审计方面也不再那么令人头疼,有了像dbt-audit-helper这样的工具。
最后一个前沿领域之一是 SQL 单元测试,它仍然处于起步阶段。让我们深入探讨一下!
我们为什么要进行单元测试?
对于那些从软件工程之外进入 SQL 开发领域的人来说,单元测试的价值可能并不明显。通常,查询开发是一个相对快速的“编写查询、运行查询、查看结果集、修改查询、重复”的过程。但随着查询复杂性和数据量的增长,你会遇到一些障碍。
首先,你可能会进行大量冗余测试。考虑以下转换:
select
id,
case when order_count < 50
then 1
else 0
end as is_small_customer
from customer
查询本身并不令人兴奋。但从测试的角度来看,我们实际上需要测试什么,正在测试什么?我们只需要四条记录来覆盖我们的案例:
-
order_count < 50
-
order_count > 50
-
order_count = 50
-
order_count IS NULL
对于你的生产数据库,你可能需要扫描数百万或数十亿条记录进行测试,这可能会积累成本。你可以(也应该)拥有一个数据子集较小的开发环境,但这仍然可能留下比实际测试查询所需的记录更多的数据。
另一个主要问题是,即使是你的完整生产数据集也可能不包含所有合理的测试用例。特别是当你引入新的数据源,或者开发或运维团队更改生成数据的方式时,你可能会需要覆盖尚不存在于数据中的场景的测试!
最重要的收益通常是在维护阶段获得的。当你需要对现有功能进行更改时,一个稳健的单元测试套件将让你确信不会破坏你的查询。如果你的更改破坏了一个单元测试,你就知道你需要做工作:要么调整你的查询以保持所需的功能,要么修改你的测试以反映新的逻辑。由于你使用的是非常小的模拟数据集,你可以在开发过程中几乎立即获得反馈。
是的,你仍然需要进行集成测试和审计,但通过良好的测试覆盖率,你可以在此阶段之前捕捉到许多错误,从而节省时间和计算成本。
为了说明这些想法,这里有一个场景。
假设我在处理一个特别复杂的查询,这个查询在生产数据集上执行需要 30 分钟和 10 美元。在开发过程中,我必须执行这个查询 5 次,每次进行小调整,每次花费 1 美元和 2 分钟,同时我的审计查询完成(注意,如果我手动审计结果,这一步可能会更昂贵)。作为一名平均年薪为 12 万美元的美国数据工程师,我的时间成本约为 57 美元/小时(不考虑税收、福利等)。我的开发成本为 171 美元的劳动成本和 55 美元的计算成本,总计 226 美元。
实现单元测试以覆盖相同的查询可能需要我一到两个小时,因为这真的很复杂。但现在我的前四次查询基本上是免费的;由于我们处理的记录数量有限,执行只需几秒钟,而我仅需为最终验证负责:$11 和 30 分钟的时间($28.50),总共为 $39.50。加上单元测试的开销,我们总共花费 $153.50,节省了大约 33%。
还有很多反驳的观点。如果查询本身只花费几分钱并且执行时间仅需几秒钟呢?如果我只需进行一个微不足道的更改并在一次操作中完成工作呢?如果我在一个削减了成本一半或更多的缩减开发环境中工作呢?
你的具体使用案例可能决定了你能从单元测试中获得多少价值,但我认为无论你的情况如何,你都需要考虑你的测试策略和你所做的权衡。测试会产生成本,但对开发高质量产品至关重要;在数据世界中,这一点与软件世界没有不同。
那我们的 pytest 等价物在哪里?
回答标题:没有一个。希望我已经让你相信,单元测试可以以非常少的数据给我们所需的覆盖范围。但当然,也有陷阱。在 SQL 世界中,什么是“单元”?对于像 Python 或 Java 这样的语言,单元测试通常在方法或类级别进行;正如名称所示,这些是需要测试的最基本的功能“单元”。
在 SQL 中,一个单元是整个查询吗?是单个 CTE 吗?还是传达某种意义的一组 CTE?后者为单元测试解决方案增加了复杂性,因为 SQL 并不像通用语言那样容易组合;你不能像调用方法那样“调用”一个 CTE。良好的数据建模可以通过使查询更具模块化来帮助减少这种负担,但良好的测试设计仍然具有挑战性,而且可用的最佳实践很少。
对于 SQL 单元测试,有许多现成的选项,但目前还没有标准。有一个很棒的 dbt 讨论帖 详细介绍了这些选项的优点和潜在方法,但不幸的是,这还没有成为路线图的一部分。让我们回顾一些更流行的选项。
tSQLt
tSQLt 是一个开源项目,由 SQL Server 开发领域的知名公司 Redgate 维护。像许多 SQL Server 解决方案一样,tSQLt 实现为一系列存储过程,作为数据库对象添加到你的目标数据库中。
从 教程 中,这里是一个存储过程的测试,模拟作为变量处理:
EXEC tSQLt.NewTestClass 'testFinancialApp';
GO
CREATE PROCEDURE testFinancialApp.[test that ConvertCurrency converts using given conversion rate]
AS
BEGIN
DECLARE @actual MONEY;
DECLARE @rate DECIMAL(10,4); SET @rate = 1.2;
DECLARE @amount MONEY; SET @amount = 2.00;
SELECT @actual = FinancialApp.ConvertCurrency(@rate, @amount);
DECLARE @expected MONEY; SET @expected = 2.4; --(rate * amount)
EXEC tSQLt.AssertEquals @expected, @actual;
END;
GO
tSQLt 还提供了模拟表和视图的功能,以及断言各种数据库对象的相等、不等和存在性。这个工具特别适合应用数据库开发,因为这正是 SQL Server 的主要用例之一。你当然可以将其应用于分析(而且 SQL Server 实现了许多数据仓库),但显然关注的是不同类型的开发。
dbt-unit-testing
dbt-unit-testing 是一个由 Equal Experts 维护的 dbt 软件包,Equal Experts 是一家全球技术咨询公司。其方法是创建利用软件包宏的自定义测试,然后运行它们。在这里,模拟数据可以用查询风格或 CSV 风格定义,一些巧妙的 Jinja 宏处理将模拟数据替换为实际的表引用。根据他们的文档:
{{ config(tags=['unit-test']) }}
{% call dbt_unit_testing.test('customers', 'should sum order values to calculate customer_lifetime_value') %}
{% call dbt_unit_testing.mock_ref ('stg_customers') %}
select 1 as customer_id, '' as first_name, '' as last_name
{% endcall %}
{% call dbt_unit_testing.mock_ref ('stg_orders') %}
select 1001 as order_id, 1 as customer_id, null as order_date
UNION ALL
select 1002 as order_id, 1 as customer_id, null as order_date
{% endcall %}
{% call dbt_unit_testing.mock_ref ('stg_payments') %}
select 1001 as order_id, 10 as amount
UNION ALL
select 1002 as order_id, 10 as amount
{% endcall %}
{% call dbt_unit_testing.expect() %}
select 1 as customer_id, 20 as customer_lifetime_value
{% endcall %}
{% endcall %}
一旦你建立了测试,只需运行 dbt test,你就会得到格式化的输出(也来自文档):
MODEL: customers
TEST: should sum order values to calculate customer_lifetime_value
Rows mismatch:
| diff | count | customer_id | customer_lifetime_value |
| ---- | ----- | ----------- | ----------------------- |
| + | 1 | 1 | 20 |
| - | 1 | 1 | 30 |
除了手动创建模拟数据之外,这个软件包还支持通过直接查询你的数据仓库来推断列。我个人的经验是,在使这一功能正常工作时遇到了困难,但对于宽表来说,这一功能可能非常有用,因为否则你必须列举每一列。
dbt-datamocktool
dbt-datamocktool 是另一个 dbt 软件包,它采用了稍微不同的方法。模拟数据和期望值被创建为种子文件,然后测试被定义为 schema.yml 中软件包现有测试的输入。
models:
- name: stg_customers
tests:
- dbt_datamocktool.unit_test:
input_mapping:
source('jaffle_shop', 'raw_customers'): ref('dmt__raw_customers_1')
expected_output: ref('dmt__expected_stg_customers_1')
depends_on:
- ref('raw_customers')
columns: ...
- name: stg_orders
tests:
- dbt_datamocktool.unit_test:
input_mapping:
ref('raw_orders'): ref('dmt__raw_orders_1')
expected_output: ref('dmt__expected_stg_orders_1')
columns: ...
除了提供模拟数据之外,你还会将模型的来源和引用映射到适当的模拟数据上。这种方法更加动手操作,但在项目需求方面也更加轻量。与 dbt-unit-testing 不同,你不必用软件包特定版本覆盖 ref() 和 source() 宏。大多数功能来自 dbt 的原生组件:测试和种子。
另一个令人兴奋的功能是对支持 MERGE 操作的适配器的增量模型的支持。这在 dbt 领域尤为重要,因为增量物化根据 dbt 调用的“模式”运行不同的查询;你需要能够测试查询的两个“版本”以实现全面覆盖。
SQLMesh
新 comer 进入 SQL 建模/模板空间,SQLMesh 带来了大量的好东西,其中之一是原生单元测试支持。测试在 YAML 文件中定义,然后在按需执行或重建 SQLMesh 计划时执行。
模拟数据非常简单,模拟数据和期望值都定义在同一个文件中。我没有见过的一个杀手级功能是能够测试单个 CTEs:
test_example_full_model:
model: sqlmesh_example.example_full_model
inputs:
sqlmesh_example.example_incremental_model:
rows:
- id: 1
item_id: 1
ds: '2020-01-01'
- id: 2
item_id: 1
ds: '2020-01-02'
- id: 3
item_id: 2
ds: '2020-01-03'
outputs:
ctes:
filtered_orders_cte:
rows:
- id: 1
item_id: 1
- id: 2
item_id: 1
query:
rows:
- item_id: 1
num_orders: 2
正如我们在 dbt-unit-testing 中看到的,测试输出的可视化效果很好:
$ sqlmesh test
F
======================================================================
FAIL: test_example_full_model (/Users/izeigerman/github/tmp/tests/test_suite.yaml:1)
----------------------------------------------------------------------
AssertionError: Data differs
- {'item_id': 1, 'num_orders': 3}
? ^
+ {'item_id': 1, 'num_orders': 2}
? ^
----------------------------------------------------------------------
Ran 1 test in 0.008s
FAILED (failures=1)
对我来说,这是我见过的最有说服力的 SQL 单元测试选项;然而,复杂之处在于它是一个不是 dbt 的 SQL 框架。这是一个处于早期阶段的工具,面临着一个事实上的行业标准,在功能缺口和采纳惯性(即,它必须足够好才能让团队转而使用)之间,它的未来充满不确定性。
尽管如此,还是要阅读文档,了解为什么即使单元测试框架很令人印象深刻,这也不是 SQLMesh 最好的特性。
我们已经准备好了吗?
不。归根结底,现有的解决方案在功能上都非常接近,并且都存在类似的缺陷。所有工具或多或少做着相同的事情:使用模拟输入运行查询,并将查询输出与预期进行比较。在构建测试场景和生成输出的方式上,有些工具比其他工具更复杂,但差异相对较小。
我能看到三个采纳障碍:
-
行业尚未达成一个标准。尽管 dbt 取得了显著成功,但它也存在着重要的缺陷(列级血缘追踪、单元测试、可扩展性);采纳度推动了更多的采纳。没有任何单元测试解决方案达到了临界规模。
-
文化尚未成熟。SQL 开发人员在没有单元测试的情况下已经成功地解决了几十年的问题。尽管分析工程师的兴起将越来越多的软件工程最佳实践带入了 SQL 开发,但并未涵盖所有最佳实践,单元测试就是一个缺口。
-
数据模拟仍然是一个未解决的问题。在所有记录的例子中,我们看到的是少数几列和少数几张表之间的简单关系。实际查询可能复杂得多。开发人员感受到许多、甚至数十个连接条件以及为了使测试通过所需的引用完整性的负担。在某种程度上,这可以被视为一种特性:即开发人员必须考虑每个连接和每条记录。但从实际角度来看,这是一项繁重的任务,特别是当真正能用的数据就在眼前时,尤其令人心痒。
我希望关于行业中测试、质量和标准重要性的对话越来越多,这将推动我们朝着更成熟的实践和更好的成果前进。当我们能够像开发其他软件一样安全迅速地开发查询时,我们将释放出大量的价值。
《将你的 Python 项目迁移到 R 的入门指南》
R 教程
在 R 中探索电动车趋势
·
关注 发表在 数据科学前沿 · 11 分钟阅读 · 2023 年 6 月 2 日
–
照片由 Milad Fakurian 提供,来源于 Unsplash
你是否对深入了解 R 编程感到好奇?虽然 Python 在数据科学社区中仍然是主要选择,2022 年约有 60% 的开发者使用它¹,但有时 R 也会出现。这是因为 R 针对统计和数据进行了优化。如果你像我一样有 Python 基础,但现在遇到了需要 R 技能的职位列表和公司内部任务,这篇文章旨在为你提供帮助。我们将探索 Python 和 R 之间的基本区别,并将项目整合到数据清理和可视化教程中,以确保顺利过渡到 R。
注:如果你对绿色技术和电动汽车特别感兴趣,教程中包括一些有趣的视觉图像,展示了电动和混合动力汽车在加拿大的受欢迎程度,所以可以跳到教程部分,亲自查看这些视觉效果和相关分析!
R 的简要概述
R 是一种开源编程语言,主要用于统计建模和数据可视化领域。R 最初由统计学家 Robert Gentleman 和 Ross Ihaka 于 1993 年开发,旨在处理统计分析和数据转换任务。它仍然保持着以统计为重点的程序声誉。然而,得益于超过 18,000 个软件包的大量库,R 多年来也发展到了支持数据科学以外的广泛项目和应用领域。
在设置和应用方面,R 通常在 RStudio 环境中使用,该环境既免费又易于安装。你可以在这里找到安装指南。现在我们已经介绍了一些初步的说明,让我们进入从 Python 到 R 的速查表过渡指南。
探索 Python 和 R 之间的主要区别
虽然不可能通过一张图来捕捉 Python 和 R 之间所有的细微差别,但下图提供了两种编程语言之间关键差异的良好初步概述:
Python 到 R 过渡指南 — 作者提供的图像
请注意,这张图并不详尽,也没有涵盖 Python 和 R 之间的所有区别。有关更详细和全面的分解,量身定制你的特定项目,MIT 提供了一个很好的转换资源这里。
总结 Python 和 R 之间的关键区别,让我们重点突出几个关键项目:
-
语法:Python 采用更直接和简洁的语法,而 R 的语法往往涉及更多的括号、方括号和符号。这可能使得 R 代码最初看起来更复杂,但我们将在教程中进一步探讨这一概念。
-
数据操作:Python 更多地依赖于像 NumPy 和 pandas 这样的外部库进行复杂的数据操作任务。相比之下,R 通常提供专门为数据操作设计的内置函数和功能。
我们可以通过实际探索来了解 Python 和 R 的对比方面,从而更全面地理解这些差异。让我们转到教程部分,我们将进行一些简单的数据清洗和转换,并使用数据来探索这些可视化效果。
我们的 R 包简介
在开始之前,让我们熟悉一下我们将使用的 R 包:
-
**tidyverse**
: 这个包是按照整洁数据的原则(正如其名字所示)创建的,包含许多基本的包。其中,dplyr
因其在数据操作和转换方面的能力而受到欢迎,而ggplot2
则提供了强大的工具套件用于数据可视化。 -
**sqldf**
:这个 R 包允许您在 R 数据框上执行 SQL 查询,为在 R 环境中应用 SQL 语法进行数据操作和分析提供了更方便的方式。
教程:加拿大的电动车许可证
在本教程中,我们将重点研究加拿大联邦政府推出的零排放车辆激励计划(iZEV)后轻型和零排放车辆的受欢迎程度。这是一个国家性的计划,为购买电动车辆(包括插电混合动力车辆)的加拿大人提供财政回扣。幸运的是,我们可以访问加拿大政府的数据,涵盖了该计划自 2019 年起至 2023 年 3 月的数据。
分析将分为两个部分:数据加载和清洗,它对 Python 和 R 进行了一些轻微的比较,然后是数据分析。首先,让我们概述一些我们希望通过我们的可视化回答的问题,重点是理解随时间推移受欢迎程度的变化:
-
多年来,根据 iZEV 计划注册的车辆数量如何发展?
-
自该计划实施以来,汽车制造商品牌偏好发生了哪些变化?
-
哪些车型的受欢迎程度增长或减少最为显著?
为了保持教程简洁,并专注于 Python 到 R 的过渡,分析将保持相对广泛,但我们将展示最终生成的数据可视化结果,以完成对加拿大迄今为止注册的 iZEV 许可证情况的全面了解。如需更深入的分析,包括完整的 R 代码、额外的 Markdown 和可视化效果,以及数据集的链接,请参阅我的 GitHub 仓库这里。
数据加载和清洗
首先,我们将安装和加载前面提到的 R 包,可以使用以下代码:
接下来,让我们将包加载到我们的 R 环境中:
为了加载我们的数据,我们将使用<-
操作符为变量分配值,这与 Python 中通常使用的=
操作符不同。之后,我们可以快速使用dim
函数获取我们加载的数据集的行数和列数,这相当于 Python 中的numpy.shape()
函数。
在 R 中,你可以使用head()
函数探索数据框的前几行。这类似于 Python pandas 库中的df.head()
函数。以下是如何在 R 中实现这一操作的示例:
%>%
操作符有助于链式操作。在上面的代码中,它让 R 知道要取df
数据框然后显示前 5 行,你将得到如下输出:
在获得数据框和形状的初始概述之后,我们可以立即看到一些无关紧要的列,这些列可以删除。同时,我们可以修改一些冗长的列名,以便以后更容易地进行参考。
所有这些步骤都可以在 Python 中使用drop()
、rename()
和map()
函数复制。
数据清洗过程的下一步通常涉及删除空值和重复行,但对于这个特定的数据集,我们只会删除空值,因为我们有许多重复行,但在没有唯一行标识符(如许可证 ID)的情况下,如果我们删除重复项,则可能会丢失宝贵的数据,因此我们需要相信行输入是正确的。以下是如何在 R 中删除空值的方法:
Python 中删除空值的等效函数可以使用dropna()
调用。
这最后一步是为了准备关于车辆品牌和型号受欢迎程度的最后一个问题,我们可以使Vehicle_Make_and_Model
列中的车型命名约定一致。例如,我们将考虑‘Hyundai Ioniq PHEV’与‘Hyundai Ioniq 插电混合动力’相同。我们可以通过创建列表,并使用str_replace_all
函数来实现这一点。
很好,数据清洗就这样进行了一些简单的步骤,现在进入有趣的部分,分析!
数据分析
下面的可视化内容将探讨我们在开始时列出的三个问题,并解释ggplot
与matplotlib
的区别。让我们先来看看我们的第一个问题:
iZEV 计划下注册的车辆数量随年份的变化如何?
在这里,我们需要可视化注册年份和许可证数量,可以使用 R 的dplyr
包编辑clean_df
数据框,使其处于适当的格式中。数据集的每一行都计为一个车辆条目,因此需要使用summarise(total=n())
来获取总行数:
在绘制数据时,R 的ggplot
和 Python 的绘图库之间存在一些差异。在ggplot
中,使用层叠语法,通过+
运算符添加不同的组件。
让我们来比较一下你可能用matplotlib
和 Python 绘制这个图表的方法:
总体而言,两者之间的代码长度相似,但 R 中的代码看起来更为简洁,使用了+
运算符。现在,让我们展示这个可视化:
数据截至到 2023 年 3 月
我还包含了第二个图表,展示了各省 iZEV 受益者的分布情况(完整的 R 代码可以在我的 GitHub 仓库这里找到):
数据截至到 2023 年 3 月
观察
那么我们从这两个可视化中能看到什么?我们可以看到,从 2019 年的 33,611 个许可证到 2022 年的 57,564 个许可证,在 iZEV 计划下注册的零排放车辆总数有所增加,支持了加拿大向电动车的过渡。注意:电动车市场在整体乘用车注册中占比很小,约为 5%⁴。
按省份划分,我们看到魁北克省占据了许可证的最大份额,超过了 BC 省、安大略省和其他省份的总和,这可能部分归因于魁北克省提供的额外最高$8,000 的折扣,而联邦政府计划则提供相应的补贴(相比之下,BC 省仅提供最高$3,000)。此外,对公用事业公司 Hydro-Québec 的明确要求,帮助省内的电动车充电基础设施的发展,缓解了司机对充电地点的担忧。
按省份划分,我们看到魁北克省占据了许可证的最大份额,超过了 BC 省、安大略省和其他省份的总和,这可能部分归因于魁北克省提供的额外最高$8,000 的折扣,而联邦政府计划则提供相应的补贴(相比之下,BC 省仅提供最高$3,000)。此外,对公用事业公司 Hydro-Québec 的明确要求,帮助省内的电动车充电基础设施的发展,缓解了司机对充电地点的担忧。
2. 自计划实施以来,汽车制造商品牌偏好发生了什么变化?
为了回答我们的第二个问题,我们想要研究汽车制造商的受欢迎程度变化。与第一个问题中关注绝对总数不同,我们将探讨相对变化。通过分析比例,我们可以在类似的尺度上比较不同汽车制造商的表现,从而进行更有意义的比较。
现在请耐心一点,因为在我们到达下一个可视化之前,有相当多的 R 代码,但我们最终要实现的是按品牌显示的子图,展示年复一年比例变化。到目前为止我们拥有的数据仅代表绝对计数,所以我们需要计算‘每 1,000 辆车’来进行比例缩放。
首先,我们将创建一个显示车辆品牌按年份和计数的表格,我们可以使用sqldf
R 包来实现。
接下来,我们希望将每年拆分到自己的行中,在这里我们可以使用pivot_wider
函数(与 Python 中的pivot
函数相似)。
然后,我们需要计算每年的‘per_1K’
许可证,这可以通过将每种品牌的车辆计数除以登记的总车辆数,并将其乘以 1,000 来完成。
现在我们想计算 2022 年和 2019 年的差异,只关注按比例变化的完整年份。
下一步将计算出的年份和per_1K
列重新透视成一个长的透视表,以帮助准备这些数据用于图表。之后,我们将绝对计数和per_1K
计数合并成一个长的透视表。
query_vehicle_counts
已经包含了我们所需的一切,所以我们只需将这两个数据集连接起来:
最后,我们希望按年份对每辆车进行排名,这样我们可以重用sqldf
并利用窗口函数轻松完成。
最后,为了绘制车辆比例如何随时间变化,我们可以使用子图(在ggplot
中称为facet_wrap
,类似于 Python matplotlib
库中的subplots
)。
这是我们的可视化:
观察
上面的子图显示,当比较比例时,特斯拉拥有最大的汽车份额,占 2019–2022 年加拿大道路上每 1,000 辆电动汽车(EV)中的 300 辆。近年来,他们的市场份额有所下降,因为新进入的竞争者如奥迪、吉普、马自达和极星等已经进入加拿大市场。
3. 哪些车型经历了最显著的受欢迎程度增加和减少?
我们最后一组问题关注特定车型的受欢迎程度,我们可以检查 2019–2022 年之间购买车型比例的变化。这个分析的代码与我们之前使用的代码非常相似,只需将Vehicle_Make
替换为Vehicle_Make_and Model
(有关更详细的逐步指南,请参阅我的 GitHub 链接这里)。
让我们编写这些代码并探索为什么我们可能会看到这些模式:
观察
根据上面的图表,我们可以看到现代 IONIQ 5 的受欢迎程度最大增长,在 2022 年每 1,000 个许可证中比 2019 年多了 83 个。从百分比上看,该模型在此期间增长了 8.3%。值得注意的是,大多数受欢迎程度上升的模型都是 SUV,如上面列出的前五名所示。这与北美市场的偏好一致,消费者期待更大的电动车选项,转向较小的轿车格式,如特斯拉 Model 3。
让我们继续进行模型减少的编码和绘图:
观察
尽管仍然在前五名最受欢迎汽车制造商中,丰田普锐斯 Prime 的受欢迎程度却出现了最大下降,从 2019 年到 2022 年每 1,000 个许可证减少了 114 个。这一下降可能是由于供应问题和价格上涨影响了受欢迎程度,但也因为它是插电式混合动力车型,iZEV 财政激励措施减少,你只能获得全额可用折扣的一半。
特斯拉 Model 3 的受欢迎程度有所下降,每 1,000 个许可证减少了 64 个许可证,但值得注意的是,当从绝对总数来看,特斯拉依然是市场上的主导汽车制造商。
结束语
总结来说,我们开展了一个小型数据分析项目,以探索 Python 和 R 之间的编码差异。希望这能让 R 变得更易接近,并为你们创造引人注目的视觉效果提供一些灵感!
作为最后的友好提醒,若要访问完整的 R 代码,请查看我的 GitHub 仓库 这里。祝编程愉快!
除非另有说明,所有图片均由作者提供。
参考文献
-
SlashData, 开发者国家状况, 2023 年第一季度
-
M.Omar, 101 数据科学与 Munira:入门 R 和 RStudio,2020 年 6 月 Medium
-
A.Amidi 和 S.Amidi,数据操作 R-Python 转换指南,MIT.edu
-
统计加拿大,汽车统计,2023 年 3 月
关于为什么你的 Instagram 帖子会如此少点赞的统计理论
一个计算难以计数事物的实用技巧——从 Instagram 影响力,到企鹅数量以及北伦敦的曼城球迷
·发布于 Towards Data Science ·6 min read·2023 年 12 月 28 日
–
我们都经历过……
带着希望的眼神,我们通过 iPhone 15 镜头框住了圣诞节的氛围,捕捉了晚餐桌上的每一刻,像艺术家一样执着,像诗人一样富有灵魂,每一张照片都是充满朋友的节日小插曲,火鸡的闪亮光泽和装饰品的闪烁。我们精心为每张照片配上振奋人心的标题,从我们思绪的丝线中编织而成,希望每一帖都能在 Instagram 繁忙的节日流量中引起共鸣。
尽管我们为向数字世界展示节日精神付出了如此多的努力,但我们的 Instagram 帖子仍仅获得 15 个点赞(如果算上 Facebook 上的交叉帖子,则为 20 个)
图片来源:Gerd Altmann 来自 Pixabay
也许期望在仅有 300 个 Instagram 关注者的情况下出现社交媒体轰动是不现实的,但这些发自内心的圣诞节帖子肯定会让超过 5%的朋友做出反应,对吗?问题是,你的总关注者数与“真实观众规模”——那些活跃的并能看到你帖子的人——并不相同。你可能有很多关注者,但大多数人可能并不活跃,或者他们的关注图谱过于密集,以至于你的帖子实际上从未到达他们。
那么问题是,如何衡量你的真实社交媒体覆盖范围,你的互联网影响力?
林肯-彼得森指数——或者如何计算不可计数的事物
假设你在一个有 300 个关注者的私人账户上发布了一条 Instagram 帖子,并获得了 40 个点赞。你知道你的观众人数在 40 和 300 之间。如果你对自己制作 Instagram 帖子的能力有极大的信心,你可能会假设你的反应率为 100%,你的观众仅为 40 个用户,是 Instagram 限制了你的覆盖范围。
但也许你的照片看起来并不好,或者你可能没有你想象的那么机智。也许你确实达到了 300 个关注者,但你只能让其中 5%的人小心翼翼地给你虚拟的支持。单凭一篇帖子是无法知道的。但如果你有两篇帖子,即使你不知道每篇帖子有多吸引人,你也可以得到一个不错的估计。
假设你的第二篇帖子有 60 个点赞,而有 15 个互相点赞(喜欢这两篇帖子的人)。我们可以天真的利用老式的集合交集理论来计算真实的观众规模,但这是一种非常保守的估计,因为我们可能忽略了那些不喜欢这两篇 Instagram 帖子的用户。
图片由作者提供
幸运的是,有一种巧妙的方法叫做林肯指数,它允许我们估算总观众人数。设n1, n2分别为第一篇和第二篇帖子的点赞数,而m为同时喜欢这两篇帖子的用户数:
根据林肯指数,我们的估计覆盖范围是 40 x 60/15 = 160。请注意,这比 85 的天真估计要大得多,这为 Instagram 免除了压缩我们社交影响力的责任。
它在数学上是如何工作的?——或者你可以随意跳过的部分
让我们重新框定问题:我们要从N人中选择n2人来点赞第二篇帖子,其中正好有m人来自n1个点赞第一篇帖子的用户。显然,这种选择的概率是:
我们可以使用最大似然法来找到估计值N。对于数学爱好者,这里是证明的概要:
- 诀窍是与其尝试找到最大化似然函数的N:
- 我们找到满足不等式的最大𝑁:
- 经过一些算术运算后,这显然等同于:
- 因此,最大化似然函数的估计值N_hat为:
一次模拟
我知道并不是每个人都喜欢数学证明,所以我将通过一个小模拟来说服你。
假设我们知道我们的社交媒体覆盖率为 100,并且我们有 2 个 Instagram 帖子,其点赞概率分别为p1和p2。我们可以模拟不同的p1和p2场景,观察每个帖子的点赞数量,然后重新应用朴素方法和林肯方法,看看它们与 100(真实值)的距离。
我们可以看到,在不同的p1和p2值下,林肯估计保持无偏(10,000 次模拟的平均值接近真实人口 100),而 95%的置信区间随着p1和p2接近 1 而缩小。这是有道理的,即如果我们确信我们帖子上的点赞率接近 100%,我们应该对我们的观众规模估计相当有信心。不幸的是,朴素估计大大低估了我们的社交覆盖率。
图片来源于作者
像往常一样,所有好的统计方法都有一些警告:
-
(1) 我们假设每个帖子之间,真实的受众规模不变。这对于普通的 Instagram 用户来说可能是对的,但如果你是一个拥有不断增长的粉丝基础的影响者,这可能不切实际。
-
(2) 我们还假设这两个帖子之间存在重叠的相互喜欢。我们的一些读者可能发现了我代码中的一个小漏洞:
尽管数学仍然有效,但在较小样本量下,估计变得相当粗略且有偏差。幸运的是,我们有一个很好的修改方法,称为查普曼估计,以解决这个问题:
那么这个计数方法有什么用呢?这种统计技术——估计任何一个人口规模,尤其是在不切实际地计数每个个体时,非常流行于生态学中,通常被称为标记-重捕方法。在科技公司中也常见到这种方法,例如在发布前估计视频游戏中的错误数量,或社交媒体平台中的政策违规内容数量。
我曾在一次面试中被挑战估计伦敦的曼城球迷数量。如果在特拉法加广场做一些抽样,采访人们并根据林肯估计进行数据分析,将会是一个有趣的周末活动。虽然我怀疑曼城球迷如此之少,你可能实际上可以数清楚他们!
如果你喜欢这篇文章,你可能也会喜欢我关于有趣的统计事实和经验法则的另一篇文章
-
优化生活的统计规则:林迪效应
-
三法则:计算尚未发生事件的概率
关于其他深入分析:
-
贝叶斯统计如何说服我去健身?
-
通过数据驱动的体育博彩策略赚取丰厚收益
-
疫情期间的爱情:一种概率论的约会方法
本项目的完整代码(包括模拟和图表)可以在我的Github中找到。
从 TensorFlow 转换到 PyTorch 的细微差别
确保成功的建议和技巧
·
关注 发表在 Towards Data Science · 5 分钟阅读 · 2023 年 2 月 7 日
–
图片由 Jan Böttinger 提供,来自 Unsplash
将模型检查点从一个框架转换到另一个框架可能是一个复杂的过程,如果你想保持相同的性能水平。
过去,我被要求评估 我的工作 在 MLPerf 推理基准套件 上的表现。MLPerf 是一个用于测量机器学习(ML)模型在部署场景中表现的行业基准。该基准提供了一个标准化框架,用于比较不同 ML 系统的性能,使其成为评估 ML 模型硬件性能的宝贵工具。在我的工作中,使用 MLPerf 推理基准套件中的模型和检查点使我能够呈现符合广泛接受的行业标准的结果,从而提高了读者对我工作的信心。然而,我的整个模拟框架建立在 PyTorch 之上,而我所需的模型权重则编码在 TensorFlow 中。
在这篇博文中,我想分享将 TensorFlow 模型迁移到 PyTorch 的过程,突显其中的细微差别。重要的是要注意,一般来说,任何 两个不同框架之间可能存在小差异;因此,在转换任何两个框架时,请考虑这里的提示。最终结果,即 MLPerf ResNet-50 和 MobileNet-v1 PyTorch 检查点,可以从我的 GitHub 仓库 获得。
使用 PB 文件
TensorFlow pb
(protobuf)文件包含模型图的描述以及层权重和参数。因此,第一步是解析模型 pb 文件。MLPerf 检查点文件可以从 这里 下载。受到 这个 博文的启发,我使用了这个类来从 pb 文件中提取所有必要的数据:
一旦创建了 NeuralNetPB
类,它将包含所有的权重在其“weights”属性中。拥有权重张量后,就可以将它们分配到 PyTorch 模型中的适当层。
层映射
首先,重要的是要注意,我们没有模型定义的 TensorFlow Python 文件。尽管如此,由于我们使用的是知名的模型架构,这些层大体上是相似的。
提示 #1: 使用 Netron 来可视化你的模型图。这将有助于理解
pb
文件中的模型结构,以及将 pb 文件中的层映射到 PyTorch。
现在,让我们开始映射。我将给出一些 MobileNet-v1 的例子:
-
MobilenetV1/Conv2d_0/weights
(3-3-3-32) — 这是第一个卷积层。我们将其映射到model.0.1.weight
。 -
MobilenetV1/Conv2d_0/BatchNorm/gamma
(32) — 这是第一个批量归一化层。gamma 对应于 PyTorch 中的层权重,因此我们将其映射为model.0.2.weight
。相同的 BatchNorm 层还包含beta
、moving_mean
和moving_variance
字段,它们分别对应于 PyTorch 中的bias
、running_mean
和running_var
。 -
MobilenetV1/Conv2d_1_depthwise/depthwise_weights
(3, 3, 32, 1) — 这是第二个卷积层(或第一个深度卷积层),它映射到model.1.weight
。
DNN 模型结构通常是重复的,所以一旦掌握了思路,你就可以在for
循环中编写部分代码。
由于NeuralNetPB
属性不是 PyTorch 张量,因此需要使用torch.FloatTensor(...)
进行转换。
细节 #1: 排列。 TensorFlow 卷积层的权重张量排列方式不同。根据 TensorFlow 文档,权重张量的形状是
[H, W, IN_C, OUT_C]
,而 PyTorch 的权重张量形状是[OUT_C, IN_C, H, W]
。因此,重新排列张量:torch.FloatTensor(...).permute(3, 2, 0, 1)
。话虽如此,深度卷积应该用.permute(2, 3, 0, 1)
进行重新排列。
完成后,你应该保存所有内容,使用torch.save(...)
。
模型修改
完成前一部分后,你的 PyTorch 模型应该在层组成方面与 TensorFlow 参考模型匹配。然而,还有一些额外的细节需要注意。
细节 #2: 参数。 权重并不是唯一的参数。例如,批量归一化层包含一个
epsilon
属性(用于数值稳定性)。epsilon
的默认值在 TensorFlow 和 PyTorch 中是不同的。但即使它们相等,TensorFlow ResNet-50 模型也会修改 epsilon,所以要注意。另一个例子是 Dropout 层(如果存在的话)。细节 #3: 填充。 TensorFlow 中的填充参数包含一个 PyTorch 中不存在的选项:
SAME
。SAME
意味着输入特征图将被填充,使输出特征图(即卷积操作结果)的空间维度与输入维度相等。然而,如果填充是不对称的,会发生什么?TensorFlow 会在左侧还是右侧填充更多?TensorFlow 在右侧填充,而 PyTorch 在左侧填充。 同样的逻辑适用于垂直方向,即底部可能会有一行额外的零,而在 PyTorch 中,额外的行将出现在顶部。
你可以查看我稍微修改过的ResNet-50和MobileNet-v1模型,看看我如何相应地进行修改。
预处理
如果你想要复现与参考模型(在这个例子中是 MLPerf 模型)完全相同的结果,那么你必须复现完全相同的预处理阶段。处理 ImageNet 数据集时的常见预处理阶段包括(1)调整大小为 256;(2)中心裁剪为 224;(3)归一化为[0, 1]之间的值(即除以 255);(4)每个 RGB 通道的均值和标准差修正。然而,MLPerf 中的标准差没有归一化,偏差的归一化方式也不同(详见此处)。
此外,即使进行完全相同的预处理,我发现 MLPerf 中的插值算法与使用 PIL 的 PyTorch 实现方式不同。由于 MLPerf 预处理整个 ImageNet 验证集并将其保存为 NumPy 数组,我决定避免使用 PyTorch 预处理,直接加载 NumPy 数组。在这一点上,我也达到了与 MLPerf 报告完全相同的性能——ResNet-50 和 MobileNet-v1 的顶级 1 准确率分别为 76.456% 和 71.676%。
调试
所以你映射了权重,并且(你认为)已经根据参考模型微调了所有参数和预处理阶段。你开始推断却什么也没有发生——准确率为 0%。接下来怎么办?没有简单的答案,你必须进行调试。我所做的是逐层比较张量值。结果应该几乎相同(允许有一些浮点差异)。例如,使用 NeuralNetPB 测试函数 nn_pb.test(input, 'import/MobilenetV1/MobilenetV1/Conv2d_1_depthwise/Relu6:0')
可以得到其中一个 ReLU 结果。你应该已经从映射阶段知道了所有的层名称。
结论
要有耐心,这最为重要。将任何模型在框架之间迁移都需要注意所有细节。可能有一些工具可以帮助自动转换,但即使你成功使用了它们,它们也可能忽略了我在这里提到的微妙差异,从而无法创建一对一的副本,这意味着模型表现会有所不同——这就是我遇到的情况。
你可以在我的 GitHub 仓库中找到我的 MLPerf ResNet-50 和 MobileNet-v1 PyTorch 检查点和模型。
随时可以在LinkedIn上联系我,或者在 Medium 上关注我。
参考文献
[1] Shomron, Gil, and Uri Weiser. “Non-Blocking Simultaneous Multithreading: Embracing the Resiliency of Deep Neural Networks.” 国际微架构研讨会 (MICRO). IEEE, 2020.
[2] Reddi, Vijay Janapa, et al. “MLPerf Inference Benchmark.” 国际计算机架构研讨会 (ISCA). IEEE, 2020.
高维数据的惊人行为
探索高维数据的惊人世界:机遇与挑战
·
关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 12 月 15 日
–
摄影:Guillermo Ferla,来源于 Unsplash
著名物理学家理查德·费曼曾说过:“我可以肯定地说,没有人理解量子力学。”在他的采访 “与理查德·费曼一起想象的乐趣” 中,他谈到了原子和亚原子粒子级别的奇怪行为,并指出这些行为经常违背我们的常识。有趣的是,我们在高维数据级别也可以注意到类似的行为。它并不像量子力学那样,但在从低维到高维的过渡中,我们会发现类似的惊奇和美丽——混合着一些/很多挑战。
在本文及未来的文章中,我希望提供一些关于这个迷人主题的见解。我的目标是激发兴趣,并鼓励那些对高维数据世界不熟悉的人进行学习。
高维数据,或在数据分析和机器学习中提到的高维数据,通常指的是具有大量变量、特征或属性的数据集。每一个变量、特征或属性都代表了我们数据中的一个不同的“维度”。
首先,让我们检查一些基本的例子,这些例子突出了从低维空间到高维空间时出现的区别。
高维空间中的体积集中
首先,让我们探索高维空间中的体积集中概念。考虑在一个边长范围从 0 到 1 的超立方体内生成随机点。当维度增加时,这些点落在这个超立方体中间区域的可能性有多大?
图片由作者提供
在上面的图像中,假设 x 是一个小值,比如 0.1。我们旨在确定点随机落在这个中间区域(而不是边缘)时,随着维度的增加,概率是如何变化的。
- 一维空间(直线)
想象一个从 0 到 1 的线段。中间部分在 0.1 和 0.9 之间,随机点落在这里的机会就是这个中间段的长度除以总长度,即 0.8。
2. 二维空间(方形)
现在,设想一个边长范围从 0 到 1 的正方形。中间区域是一个边长从 0.1 到 0.9 的小正方形。概率计算涉及比较这个小正方形的面积与总面积,从而得到 0.64 的概率。
3. 三维空间(立方体)
对于一个每条边长为 1 的立方体,中间区域是一个边长从 0.1 到 0.9 的较小立方体。这里,概率是这个小立方体的体积除以总的体积,结果为 0.512。
4. 高维(超立方体)
在一个 n 维的超立方体中,随着维度的增加,中间区域的‘体积’会急剧缩小。例如,在 4 维中,概率是 0.4096;在 5 维中,它变为 0.32768;而在 10 维中,它降至大约 0.10737。
这个想法的概括从考虑边长为小距离 x 开始,如上图所示。对于一条线,点落在中间区域的概率是 1–2x。对于一个正方形,它是(1–2x)*(1–2x),因为一个点必须落在两个维度的中间。
这种模式在 n 维中继续,其中落在中间区域的概率为(1–2x)^n,在高维中变得非常小。
注意这里我们通过将每边长度简化为 1 来进行简化。
在超立方体内刻画一个超球体
为了进一步说明体积集中概念,我用 python 进行了一次简单的模拟,在模拟中我们在超立方体内刻画一个超球体,然后比较超球体与超立方体的体积比,随着维度的增加。
什么是超立方体?
想象一个正方形。现在,把它膨胀成一个立方体。这是从 2D 到 3D 的跳跃。现在,发挥想象力跳到第四维及更高维度——这就是超立方体的出现。超立方体本质上是扩展到更高维度的立方体。它是一个各边相等的形状,在我们的模拟中,我们考虑边长为 2 的超立方体。它的体积公式是什么?就是 2^*n(2 的 n 次方)*对于 n 维超立方体。
那么超球体呢?
超球体是球体的高维等效体,当你将 2D 圆扩展到 3D(形成一个球体),然后继续扩展到更高维度时,就会出现超球体。重点?它的体积计算并不那么简单。它涉及到π(是的,就是那个著名的 3.14159…)和 Gamma 函数,这类似于阶乘的“增强版”。简而言之,半径为 1 的超球体在n
维空间中的体积是:
Gamma 函数Γ(n)将阶乘函数扩展到实数和复数。对于正整数n,Γ(n)=(n−1)!,对于非整数值,它是数值计算的。
要使用 python 计算这个比例,我们可以使用以下代码:
import math
import matplotlib.pyplot as plt
def hypersphere_volume(dim):
""" Calculate the volume of a hypersphere with radius 1 in 'dim' dimensions. """
return math.pi ** (dim / 2) / math.gamma(dim / 2 + 1)
def hypercube_volume(dim):
""" Calculate the volume of a hypercube with side length 2 in 'dim' dimensions. """
return 2 ** dim
# Number of dimensions to consider
max_dim = 20
# Lists to hold volumes and dimension values
dimensions = range(1, max_dim + 1)
sphere_volumes = [hypersphere_volume(dim) for dim in dimensions]
cube_volumes = [hypercube_volume(dim) for dim in dimensions]
ratios = [sphere_volumes[i] / cube_volumes[i] for i in range(max_dim)]
# Plotting the results
plt.figure(figsize=(10, 6))
plt.plot(dimensions, ratios, marker='o')
plt.xlabel('Number of Dimensions')
plt.ylabel('Ratio of Volumes (Hypersphere/Hypercube)')
plt.title('Volume Concentration in Higher Dimensions')
plt.grid(True)
plt.show()
上述代码的输出是以下图表:
作者提供的图片
我们可以清楚地看到,随着维度的增加,比例迅速下降,剩余的体积集中在超立方体的角落。
这些例子表明,在更高维度中,中间区域的体积成为总体积的一个越来越小的部分,突显了高维空间的反直觉特性。
问:这种体积集中现象对机器学习算法的性能有哪些影响?
论文和 DVD 实验
考虑这样一个实验:你试图通过一个有正方形孔的纸张塞入 DVD。起初,看似不可能,因为正方形的对角线比 DVD 的直径小。然而,折叠纸张可以让 DVD 通过。
纸张的折叠,一个小但有效的空间维度调整,是谜题的关键。在这个实验中可以找到一个有趣的类比,用于理解高维景观的复杂性。
当纸张首次展开时,它形成了一个二维平面。由于设定的尺寸,方孔似乎过于狭窄,无法让 DVD 通过。
这种假设情境与我们在三维环境中的日常体验一致,其中长度、宽度和高度是尺寸和距离的测量单位。但当我们开始折叠纸张时,我们增加了另一个维度。这个折叠动作完全改变了孔和 DVD 的空间连接。
在这个新的三维环境中,距离的概念在二维中是如此不灵活和明确,但在三维中变得更加灵活和不直观。纸张被折叠,这有效地改变了纸张边缘生成的角度和环绕孔的点之间的距离。
这种新三维形式中的孔可以容纳 DVD,展示了引入第三维度如何使在二维空间中看似绝望的任务变得可行。
Weiwei Lin 等人进行的一项引人入胜的研究中全面解释了这一实验所涉及的数学原理。
摘要。通过在纸上设置简单的折痕并沿这些折痕折叠,折纸可以形成各种形状…
你还可以观看由“The Action Lab”制作的这段美丽视频,它直观地展示了这个想法:
一个圆盘通过一个较小的方孔
这种视角的转变具有重要意义,尤其是在数学、物理学和机器学习领域。这个想法在像支持向量机(SVM)这样的机器学习方法中得到了体现。
支持向量机(SVM)及其核技巧
支持向量机(SVM)中的核技巧展示了类似的想法。在 SVM 中,我们经常遇到不可线性分离的数据。核技巧通过将数据转换到更高维空间来克服这个问题,类似于折叠纸张改变了其空间属性。(实际上,SVM 并不会将数据实际转换到更高维度,因为这在计算上代价昂贵。相反,它们使用核技巧计算数据点之间的关系,就好像它们处于更高维度一样。)
简而言之,SVM 通常在较低维度中找到一个分隔线(或超平面)。但对于非线性数据,这是不可能的。核技巧就像折叠纸张一样,添加了维度,使找到一个适合的超平面变得更加容易。
核心技巧不仅仅是改变维度,它还简化了复杂问题。这确实是一个很好的例子,说明高维思维如何为在低维度中看似不可能的问题提供解决方案。
import numpy as np
import matplotlib.pyplot as plt
from sklearn.svm import SVC
# Here I manullay entered a data that is not linearly seperable in 1D
x = np.array([1,2,3,4,5,6,7,8,9,11,12,13,14,15,16,17,18,19,20,21,23,24,25,26,27,28,29,30]).reshape(-1, 1) # Replace YOUR_X_VALUES with your data
y = np.array([1,1,1,1,1,1,1,1,1,0,0,0,0,0,0,0,0,0,0,0,1,1,1,1,1,1,1,1]) # Replace YOUR_Y_VALUES with your class labels
# Non-linear transformation to 2D, (squaring)
def transform_to_2d(X):
return np.c_[X, X**2]
# Transforming data to 2D
X_transformed = transform_to_2d(x)
# Fitting SVM with a linear kernel in the transformed 2D space
svm = SVC(kernel='linear')
svm.fit(X_transformed, y)
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 1D data plot
axes[0].scatter(x, np.zeros_like(x), c=y, cmap='bwr', edgecolors='k')
axes[0].set_title('Original 1D Data')
axes[0].set_xlabel('Feature')
axes[0].set_yticks([])
# 2D transformed data plot
axes[1].scatter(X_transformed[:, 0], X_transformed[:, 1], c=y, cmap='bwr', edgecolors='k')
axes[1].set_title('Transformed 2D Data')
axes[1].set_xlabel('Original Feature')
axes[1].set_ylabel('Transformed Feature (X²)')
# Plotting the decision boundary in 2D
ax = axes[1]
xlim = ax.get_xlim()
ylim = ax.get_ylim()
xx = np.linspace(xlim[0], xlim[1], 30)
yy = np.linspace(ylim[0], ylim[1], 30)
YY, XX = np.meshgrid(yy, xx)
xy = np.vstack([XX.ravel(), YY.ravel()]).T
# Getting the separating hyperplane
Z = svm.decision_function(xy).reshape(XX.shape)
# Plotting decision boundary and margins
ax.contour(XX, YY, Z, colors='k', levels=[-1, 0, 1], alpha=0.5,
linestyles=['--', '-', '--'])
plt.tight_layout()
plt.show()
上述代码的结果是以下图表:
作者提供的图片
很明显,最初在左侧数据是非线性可分的,但在 2D 中变得可分。正如右侧图表所示,这种转换有效地解决了我们的问题。这不是很神奇吗?
总结
在这篇文章中,我们探讨了有关高维数据世界的一些想法。我们展示了进入高维度如何极大地改变我们的视角和处理问题的方法,从体积集中开始,接着是纸张和 DVD 实验的实际例子,最后是 SVM 中的核心技巧。
在接下来的文章中,我们将讨论“维度诅咒”,即在高维空间中导航所面临的困难和复杂性。我们将探讨这如何影响机器学习和数据分析,以及缓解其影响的策略。
感谢你阅读到这里!非常感谢你抽出时间阅读这篇文章,希望你觉得这个话题很有趣。如果有任何建议或可能的修改,请随时分享!
合成数据领域指南
原文:
towardsdatascience.com/the-synthetic-data-field-guide-f1fc59e2d178
使数据有用
一份关于各种伪数据的指南
·发表于 Towards Data Science ·6 分钟阅读·2023 年 6 月 30 日
–
如果你想获取一些 数据,你有哪些选择?这里有一个尽可能粗略的答案:你可以获取真实数据,也可以获取伪数据。
在 我之前的文章 中,我们认识了合成数据的概念,并讨论了创建它的思维过程。我们比较了真实数据、噪声数据和手工制作的数据。让我们深入了解比让人类选择一个数字更高级的合成数据类型……
一部 经典 的英国滑稽喜剧。
(注意:本文中的链接指向同一作者的解释文章。)
复制数据
也许你测量了 10,000 个真实的人体身高,但你想要 20,000 个数据点。一种方法是假设你现有的数据集已经很好地代表了你的总体。 (假设 总是危险的,需谨慎进行。) 然后,你可以简单地重复数据集或使用古老的复制-粘贴方法重复其中一部分。瞧!更多数据!但这是否是 好 和 有用 的数据呢?这始终取决于你需要它做什么。在大多数情况下,答案是否定的。不过,既然你有头脑,就有理由用它来思考并运用你的最佳判断。
重新采样数据
说到仅重复数据的某一部分,有一种方法可以注入一点随机性来帮助你确定 哪个 部分需要选择。你可以使用 随机数生成器 来帮助你从现有的身高列表中选择哪个身高。你可以进行“无替换”操作,这意味着你最多只复制每个现有的身高一次,但……
自助抽样数据
你会更常见到人们“有放回地”进行操作,这意味着每次你随机选择一个高度进行复制时,你会立即忘记你做过这件事,因此相同的高度可以在你的数据集中作为第二个、第三个、第四个等副本出现。如果评论中有足够的兴趣,我会解释为什么这是一个强大且有效的技术(是的,一开始听起来像是巫术,我也这样认为)用于群体推断。
增强数据
增强数据听起来可能很高级,确实有高级的方法来增强数据,但通常当你看到这个术语时,它意味着你对重新采样的数据添加了一些随机噪声。换句话说,你生成了一个来自统计分布的随机数,并且通常你只是将它加到重新采样的数据点上。就这么简单。这就是增强。
所有图像版权属于作者。
过采样数据
说到只重复数据的一部分,有一种方法可以有意地增强某些特征。也许你在一次典型的 AI 会议上进行了测量,因此女性身高在你的数据中被低估(如今这确实很可悲)。这就是所谓的数据不平衡问题。对于重新平衡这些特征的表现,有一些技术,比如SMOTE(合成少数类过采样技术),这几乎就是它的字面意思。解决这个问题最简单的方法是仅限于少数类数据点进行重新采样,忽略其他数据。因此,在我们的例子中,你只需重新采样女性身高,同时忽略其他数据。你还可以考虑更复杂的增强方法,仍然将努力限定在女性身高上。
如果你想变得更高级,你可以查找像ADASYN(自适应合成采样)这样的技术,并跟踪那些超出本话题快速介绍范围的线索。
边缘情况数据
你也可以制造出完全不同于你(或任何人)见过的数据(手工制作的数据)。如果你打算用这些数据来创建现实世界的模型,那将是非常愚蠢的做法,但如果你用它来测试系统处理奇怪情况的能力,那就很聪明了。为了判断你的模型/理论/系统在遇到异常值时是否会崩溃,你可以故意制造合成的异常值。大胆尝试,比如设置一个 3 米的高度,看看会发生什么。就像工作中的消防演习一样。(测试完成后,请不要在建筑物内留下真正的火灾或数据集中的实际怪物异常值。)
视频中你的作者解释了什么是异常值,以及如何(和如何不)处理它。
模拟数据
一旦你对根据你的规格生成数据的想法感到舒适,你可能会想进一步创建一个配方来描述你希望在数据集中出现的数据的基本性质。如果有一个随机成分,那么你实际上是在模拟一个统计分布,这个分布允许你指定核心原则,如模型所描述(这只是一个花哨的说法,即*“你将用作配方的公式”*),并包含随机部分的规则。与其像普通的数据增强技术那样向现有的数据点添加随机噪声,你可以向你自己提出的一套规则添加噪声,规则可以通过沉思或通过对相关数据集进行统计推断得出。了解更多内容,请点击这里。
所有图片版权归作者所有。
超越单一数字
身高?等等,你是让我提供一个只有一个身高的数据集?真无聊!真是… 磁盘时代的产物。我们称之为单变量数据,而且这种数据在如今很少见。
现在我们拥有了令人难以置信的存储容量,数据可以以更有趣和复杂的形式出现。在我们记录身高时,额外记录一些特征是非常便宜的。例如,我们可以记录每个人的发型,使我们的数据集变得双变量。但为什么止步于此呢?为什么不再记录年龄,让数据变成多变量呢?真有趣!
但现在,我们可以大胆地将其与图像数据(在测量身高时拍照)和文本数据(他们写的关于其统计课有多么无聊的论文)结合起来。我们称之为多模态数据,我们也可以合成这些数据!如果你想了解更多,请在评论中告诉我。
为什么有人会想生成合成数据?有好的理由来喜欢它,也有一些坚实的理由要像瘟疫一样避免它(文章即将发布),但如果你是数据科学专业人士,请前往这篇文章了解我认为应该是你常用的理由。
感谢阅读!怎么样,来上一门课程吧?
如果你在这里玩得开心,并且正在寻找一个以激发 AI 初学者和专家兴趣为目标的有趣领导力课程,这是我为你准备的一点东西:
课程链接: bit.ly/funaicourse
阅读 Cassie Kozyrkov 的每个故事(以及 Medium 上其他成千上万的作者)。你的会员费直接支持…
附言:你试过在 Medium 上点击鼓掌按钮多次看看会发生什么吗? ❤️
喜欢作者?与 Cassie Kozyrkov 联系
成为朋友吧!你可以在 Twitter、YouTube、Substack 和 LinkedIn 找到我。对让我在你的活动中发言感兴趣?使用 这个表单 与我联系。
评估新语言模型的三种基本方法
原文:
towardsdatascience.com/the-three-essential-methods-to-evaluate-a-new-language-model-aa5c526bacfd
如何检查最新、最热门的大型语言模型(LLM)是否符合你的需求
·发布于数据科学之道 ·6 分钟阅读·2023 年 7 月 3 日
–
图片来源(使用 Stable Diffusion)
这是什么?
每周都会发布新的 LLM,如果你像我一样,可能会问自己:这个模型是否终于适合我想要利用 LLM 的所有用例?在这个教程中,我将分享我用来评估新 LLM 的技术。我将介绍我定期使用的三种技术——它们没有新的(实际上,我会提到我之前写的博客文章),但通过将它们结合在一起,每当有新的 LLM 发布时,我可以节省大量时间。我将展示在新的OpenChat模型上进行测试的示例。
为什么这很重要?
对于新的 LLM,了解其能力和限制是很重要的。不幸的是,弄清楚如何部署模型并系统地测试它可能会有些麻烦。这个过程通常是手动的,并且可能消耗大量时间。然而,通过标准化的方法,我们可以更快地迭代,并迅速确定一个模型是否值得投入更多时间,还是应该放弃它。所以,让我们开始吧。
开始使用
利用 LLM 的方式有很多,但当我们提炼出最常见的用途时,它们通常涉及开放性任务(例如,为营销广告生成文本)、聊天机器人应用和检索增强生成(RAG)。相应地,我会使用相关方法来测试这些能力。
0. 部署模型
在开始评估之前,我们首先需要部署模型。我已经准备好了一些模板代码,我们只需更换模型 ID 和要部署的实例(在这个示例中,我使用的是 Amazon SageMaker 进行模型托管),就可以开始了:
import json
import sagemaker
import boto3
from sagemaker.huggingface import HuggingFaceModel, get_huggingface_llm_image_uri
try:
role = sagemaker.get_execution_role()
except ValueError:
iam = boto3.client('iam')
role = iam.get_role(RoleName='sagemaker_execution_role')['Role']['Arn']
model_id = "openchat/openchat_8192"
instance_type = "ml.g5.12xlarge" # 4 x 24GB VRAM
number_of_gpu = 4
health_check_timeout = 600 # how much time do we allow for model download
# Hub Model configuration. https://huggingface.co/models
hub = {
'HF_MODEL_ID': model_id,
'SM_NUM_GPUS': json.dumps(number_of_gpu),
'MAX_INPUT_LENGTH': json.dumps(7000), # Max length of input text
'MAX_TOTAL_TOKENS': json.dumps(8192), # Max length of the generation (including input text)
}
# create Hugging Face Model Class
huggingface_model = HuggingFaceModel(
image_uri=get_huggingface_llm_image_uri("huggingface",version="0.8.2"),
env=hub,
role=role,
)
model_name = hf_model_id.split("/")[-1].replace(".", "-")
endpoint_name = model_name.replace("_", "-")
# deploy model to SageMaker Inference
predictor = huggingface_model.deploy(
initial_instance_count=1,
instance_type=instance_type,
container_startup_health_check_timeout=health_check_timeout,
endpoint_name=endpoint_name,
)
# send request
predictor.predict({
"inputs": "Hi, my name is Heiko.",
})
值得注意的是,我们可以利用新的Hugging Face LLM Inference Container for SageMaker,因为新的 OpenChat 模型基于 LLAMA 架构,该架构在这个容器中受支持。
1. 游乐场
使用笔记本测试一些提示可能很繁琐,也可能会让非技术用户不愿尝试模型。更有效的方式是构建一个游乐场。我之前在这篇博客文章中详细说明了如何轻松创建这样一个游乐场。利用那篇博客文章中的代码,我们可以快速搭建一个游乐场。
一旦建立了游乐场,我们可以引入一些提示来评估模型的回应。我更喜欢使用开放式提示,即提出一个需要一定常识来回答的问题:
我如何提高我的时间管理技能?
作者提供的图片
如果苏伊士运河从未被建造会怎样?
作者提供的图片
两种回应都很有前景,这表明投资额外的时间和资源来测试 OpenChat 模型可能是值得的。
2. 聊天机器人
我们想要探索的第二件事是模型的聊天机器人能力。与游乐场不同,在游乐场中模型始终是无状态的,我们希望了解其在对话中“记住”上下文的能力。在这篇博客文章中,我描述了如何使用 Falcon 模型设置聊天机器人。这是一个简单的即插即用操作,通过更改 SageMaker 端点,我们可以将其指向新的 OpenChat 模型。
让我们看看它的表现:
作者提供的图片
作为聊天机器人的表现相当令人印象深刻。然而,有一次 Openchat 试图突然结束对话,半句话被切断了。实际上,这种情况并不罕见。我们通常不会在其他聊天机器人中观察到这种情况,因为它们使用特定的停止词来迫使 AI 停止文本生成。这种问题在我的应用程序中发生的原因可能是由于我的应用程序中实施了停止词。
除此之外,OpenChat 具有在对话中保持上下文的能力,并且能够从文档中提取关键信息。令人印象深刻。😊
3. 检索增强生成(RAG)
我们想测试的最后一个任务涉及使用 LangChain 进行一些 RAG 任务。我发现 RAG 任务对于开源模型来说可能相当具有挑战性,通常需要我编写自己的提示和自定义响应解析器来实现功能。然而,我希望看到的是一个在标准 RAG 任务中“开箱即用”的模型。这个博客文章提供了一些这样的任务示例。让我们看看它的表现如何。我们要提出的问题是:
谁是英国的首相?他或她出生在哪里?他们的出生地距离伦敦有多远?
作者提供的图片
毋庸置疑,这是我见过的使用 LangChain 标准提示的开源模型中表现最好的。这可能并不令人惊讶,因为 OpenChat 已经在 ChatGPT 对话上进行了微调,而 LangChain 则针对 OpenAI 模型,特别是 ChatGPT。然而,该模型能够准确地使用其可用的工具检索所有三个事实。唯一的缺点是,最后模型未能认识到它已经掌握了所有必要的信息并能够回答用户的问题。理想情况下,它应该说:“我现在有了最终答案,”并向用户提供其收集到的事实。
作者提供的图片
结论
在这篇博客文章中,我向你介绍了我经常使用的三种标准评估技术来评估 LLMs。我们观察到,新版的 OpenChat 模型在所有这些任务中表现都非常出色。令人惊讶的是,它作为 RAG 应用的底层 LLM 非常有前景,可能只需定制化提示来确定何时达到了最终答案。
值得注意的是,这不是一个全面的评估,也不打算成为全面评估。相反,它提供了一个关于特定模型是否值得投入更多时间进行进一步、更深入测试的指示。看来 OpenChat 绝对值得花时间研究 🤗
随意使用所有工具,扩展和定制它们,并在几分钟内开始评估你感兴趣的 LLMs。
海科·霍茨
👋 在Medium和LinkedIn上关注我,以了解更多关于生成式人工智能、机器学习和自然语言处理的内容。
👥 如果你在伦敦,可以加入我们的NLP London Meetups。
作者提供的图片
三大数据架构趋势(以及 LLMs 将如何影响它们)
拥抱数据架构的下一个时代:揭示三大趋势及 LLM 的影响力
·发布于Towards Data Science ·5 分钟阅读·2023 年 6 月 28 日
–
图片来源:Google DeepMind 在Unsplash
我去年发布了一篇关于数据架构趋势的文章。
这是在大型语言模型(LLMs)成为热门并影响大多数行业之前。Gartner 报告称,“风险投资公司在过去三年中已向生成式 AI 解决方案投资超过 17 亿美元。” 毫无疑问,LLMs 将影响数据架构的大多数领域。
记住这一点,让我们探讨三种架构趋势以及 LLMs 将如何影响它们。
1. 使用副驾驶进行成本优化
我非常喜欢能够帮助最终用户高效完成任务的副驾驶。
作为Grammarly的常规用户,我非常欣赏它在加快任何书面内容编辑过程中的帮助。同样,副驾驶将成为我们大多数工作的主要角色,包括数据架构。
数据架构师的日常工作包括数据模型设计、制定标准和实施治理结构。像微软这样的协助工具可以帮助完成邮件中的句子,并根据规范文档创建公告。类似地,数据架构师的协助工具可以仅基于用户需求,通过理解你的设计限制来完成实体关系图(ERD)。协助工具可以与架构师一起工作,帮助加速他们的日常工作流程。
如果生产力开始飞速增长,企业开始寻找优化成本的方式也就不足为奇了。一些估计表明,成千上万,甚至数百万的工作将受到影响。
例如,管理顾问一直在通过寻找效率来帮助组织进行重组和减少开销。同样,协助工具的实施将会减少对人力资源的依赖,因为更多的任务会依赖于人工智能的完成。任务包括撰写设计文档、按照批准的模式创建数据架构图、创建数据模型及相关的 SQL 查询、审计 SQL 以符合批准的标准等。
协助工具将带来效率和成本节约!
2. 背景驱动的分析
我们可能已经解决了云存储问题,但我们仍需要解决背景问题。
数据本身只是一系列文本/数字;当你为其添加背景时,价值才会显现。而“数据背景”是一个数十亿美元的行业。
数据背景包括业务或技术元数据、治理或隐私需求,以及可访问性或安全性要求。尽管预计到 2028 年这一行业将翻一番,但我想知道这一增长有多少会被大型语言模型(LLMs)所占据。例如,使用语义嵌入和向量数据库,组织将能够快速对数据进行背景化,而无需实施大量的数据背景工具。如果我能通过嵌入检测异常,是否还需要一个全面的治理框架?这进一步强调了由于 LLMs 而进行成本优化的第 1 点。
将 AI(故意的双关语!)嵌入数据管道、转换和血统中,可以帮助建立上下文。这个上下文可以用于回答最终用户的分析或监管需求问题。例如,这些数据是否包含个人信息?如果包含,则从特定的分析用例中过滤掉。
图片由作者提供
这张图片说明了上下文层如何捕获信息,就像传统的数据目录一样,只不过它利用了 LLMs 的强大功能,大幅减少了人工干预。
上下文使数据有价值;使用 LLMs 可以更快地实现这一点。
3. 启动数据架构生态系统
我们厌倦了孤立且分散的架构。
在这种架构中,治理工具未能与数据湖集成,源系统的设计未考虑分析需求,或者存在多个信息源。
生态系统需要反映像苹果这样的消费公司所提供的产品。一个关键产品与各种支持的可组合产品,这些产品单独有用,但共同创建了一个令人惊叹的生态系统。例如,一个数据产品市场(iPhone)展示了来自数据可观察性框架(Watch)的信息,并由单一访问方法(Face ID)进行治理。数据架构将位于一个集成不再是弱点的生态系统中。这将是一个颠覆性的改变。
生态系统还将减少不同来源之间的信息冗余风险(就像你的 iMessages 在所有设备间同步)。已有一些初创公司正利用 OBT(One Big Table)等概念来进行革命性的改进。生态系统还意味着数据定义;标准被设定一次并传播到每个区域,降低了复制成本。
例如,一个客户交易表格捕获了来自 CRM 系统的信息;默认情况下,CRM 系统设计用于捕获分析所需的强制字段[1]。一旦数据传输,它会经过一系列的数据质量检查,以确保其适用性[2]。数据转换后,回溯信息会被捕获,以确保数据没有丢失[3]。在使用之前,它会被分类到个人数据桶中,并设置适当的治理措施[4]。所有这些过程本身都很重要;然而,当数据最终被产品化时,它们会显得更为强大,你可以在数据集中可视化[1] — [4],进而信任这些数据。
结论
好像现代数据堆栈已经有了自己的炒作,现在我们还得应对 GenAI 的炒作。看看这些趋势在接下来的 12 到 18 个月如何发展将会很有趣。我预计已经在基础设施上进行投资的公司将会利用这些趋势,而那些没有投资数据质量或治理的公司将会持续滞后。
所有这些趋势的根本要求是良好的数据。没有良好的数据,你无法进行副驾驶、添加背景或拥有有效的数据架构。这是最难以实现的事情之一,但因此也带来了最大的投资回报。
你想发现自己在组织中的数据和人工智能能力如何,以及哪些方面可以改进吗?通过参加这个免费的数据精通指数(DMI)评估来了解。 点击这里访问评分卡评估,提升你的数据策略。
或者,扫描二维码以访问评估
晋级下一轮所需的前三种 SQL 技能
原文:
towardsdatascience.com/the-top-3-sql-skills-needed-to-get-to-the-next-round-51ad1699a213
数据专业人士的技术面试帮助
·发表于 Towards Data Science ·阅读时间 6 分钟·2023 年 8 月 28 日
–
图片来源:Arnold Francisa at Unsplash
如果你有志于从事数据科学家、数据分析师和数据工程师等角色,并且正在面试,那么你可能会遇到一个或多个需要现场编码的技术面试,通常涉及 SQL。虽然后续面试可能需要其他编程语言,如 Python,这在数据领域很常见,但让我们专注于我在这些面试中遇到的典型 SQL 问题。为了讨论的目的,我会假设你已经熟悉基础的 SQL 概念,如 SELECT
、FROM
、WHERE
以及聚合函数如 SUM
和 COUNT
。让我们进入具体细节吧!
1. 掌握连接和表类型
毫无疑问,最常见的 SQL 问题是关于表连接的。这个问题可能看起来太显而易见,但我参与过的每一次面试都围绕这个话题展开。你应该对内连接和左连接感到游刃有余。此外,处理自连接和并集的熟练程度也是非常重要的。执行这些连接操作时,特别是在事实表和维度表等不同表类型之间的能力同样重要。以下是我对这两个术语的宽松定义:
事实表: 一种包含大量行但属性或列相对较少的表。比如,在线零售商维护一个“订单”表,包含如下列:date, customer_id, order_id, product_id, units, amount
。这个表属性较少,但记录量巨大。
维度表: 具有较少行但许多属性的维度表。例如,某在线零售商的“customer”表可能包含每个客户的一行,具有customer_id, first_name, last_name, ship_street_addr, ship_zip_code
等属性。
理解这两种主要表类型是很重要的。掌握如何合并事实表和维度表以确保准确结果是至关重要的。让我们考虑一个实际的例子:面试问题提出了两个表(“orders”和“customer”)并询问:
有多少客户在其一生中至少购买了 3 个单位并且邮政编码为 90210?
仅仅运行内连接后再进行计数、求和和/或过滤可能会由于重复计算产生巨大的差异。即使在这个看似简单的问题中,许多部分也需要仔细思考。为了有效地将你的思路传达给面试官,请将这些事项考虑清楚并说出来:
-
哪个表包含所有客户?*“orders”事实表仅包括已确认购买的客户,而“customer”*维度表包括所有客户,包括那些已注册但未购买的客户。通过快速的
count(distinct )
对比,可以看到哪个表有更多独特的客户。 -
计算每个客户购买的单位总数或计数需要使用带有
GROUP BY
子句的聚合函数。 -
需要与*“customer”*表进行连接,以缩小邮政编码为 90210 的客户范围。在使用连接时,我建议为表创建别名,这可能需要你在 select 语句中添加该别名以及相关属性。
概述了思路后,让我们将其转化为代码!
-- cte of sum of units per customer from orders fact table
WITH customer_units_agg as (
SELECT
customer_id
, SUM(units) as UNIT_SUM
FROM
orders
GROUP BY
customer_id
)
-- join CTE table with customers dim table to filter by units and zip
SELECT
COUNT(DISTINCT ca.customer_id) as CUSTOMER_CNT
FROM customer_units_agg ca
INNER JOIN customer ctmr on ca.customer_id = ctmr.customer_id
WHERE 1=1
AND ca.UNIT_SUM >= 3
AND ctmr.ship_zip_code = 90210;
这个解决方案使用了公共表表达式(CTE),但请记住,有很多不同的方法可以在 SQL 中实现相同的结果。只要你能得到正确的结果并且能够解释你的方法,你就走在了正确的道路上。
2. 使用子查询、临时表和 CTE 处理复杂性
与之前的例子一样,几乎每次 SQL 编码面试都需要多步骤程序。这时候子查询、临时表和 CTE 就非常有用。熟练掌握这些技术或至少对它们有所了解是必须的。让我们深入探讨每种方法:
-
**子查询:**这些嵌套查询涉及像
select * from (select * from table)
或过滤select * from table1 where table1_value < (select max(table2_value) from table2)
这样的构造,包含内查询和外查询。子查询是有用的,但它们的语法可能对编写者和阅读者都变得困难或混乱。对于复杂场景,避免过多的子查询。 -
临时表: 正如其名称所示,临时表仅在会话期间存在。你可以逐步创建它们,一个接一个地构建,以帮助故障排除或逻辑排序。它们有助于将复杂问题分解成较小、更易管理的步骤。
-- mysql and others
CREATE TEMPORARY TABLE my_temp_table as
SELECT
column1
, column2
, column3
FROM original_table
WHERE some_conditions;
-- sql server
SELECT
column1
, column2
, column3
INTO #my_temp_table -- this is the new temp table created
FROM original_table
WHERE some_conditions;
- 公共表表达式(CTE): 如连接示例所示,CTE 提供了一种灵活的查询结构方式。它们类似于临时表,但不会在下一个
SELECT
语句之后持续存在。这要求将 CTE“链式”连接(见下方代码示例),如果有很多步骤的话。在链式连接 CTE 时,请记住只使用一个WITH
语句,后接 CTE,最后是主查询。此外,分号位于 CTE 链中的最终SELECT
语句之后。由于其有限的作用域,CTE 不能在这一点之外被引用。
-- chaining multiple cte together
WITH cte_tabl_1 as (
SELECT * FROM table1
),
cte_tabl_2 as (
SELECT * FROM table2
),
cte_tabl_3 as (
SELECT * FROM table3
)
select * from cte_tabl_1, cte_tabl_2, cte_tabl_3;
实际上,一旦你被聘用,你可以决定最佳的方法或其他人正在使用的方案。目前,CTE 似乎是更受欢迎的选择。鉴于面试情境通常不需要链式 CTE,融入 CTE 应该相对简单且有效。
3. 高级分析中的窗口函数导航
几乎每次面试中都会出现的一个反复话题是窗口函数。 PostgreSQL 文档 有效地解释了窗口函数,并突出了与分组操作的区别。
窗口函数在一组与当前行相关的表行上执行计算。这类似于可以通过聚合函数完成的计算。但与常规的聚合函数不同,使用窗口函数不会将行分组为单个输出行——行仍然保持各自的独立性。在幕后,窗口函数能够访问的不仅仅是查询结果的当前行。
关键在于最后两句话。行的非分组和窗口函数在幕后访问当前行以上的更多内容。面试问题通常涵盖从理论性问题,例如“你使用过哪些类型的窗口函数?”到需要使用窗口函数的编码问题。考虑诸如“显示每个部门中收入最高的三名员工”或“显示每位客户购买的最新三项商品”等场景。根据我的经验,最常见的窗口函数包括rank()
、row_number()
和dense_rank()
,它们都使用OVER
函数。值得注意的是,大多数聚合函数可以与OVER
函数一起使用。有关排名函数差异的良好说明可以参见这个 Stack Overflow 示例。此外,LearnSQL 提供的这个 窗口函数备忘单 是一个很好的视觉参考,展示了不同类型的函数以及一些输出视觉效果。
结论
这些是过去一个月中面试官在编码面试中反复询问的前三大 SQL 技能。掌握这些领域应能轻松帮助你应对几乎所有 SQL 面试场景。
最后,这里是我的最后一个建议:如果你在编码面试中对语法或术语不确定,请写下逐步的大纲来展示你的问题解决过程。强调你的思维过程和创造力,因为它们往往比严格的语法更重要。祝你在即将到来的数据相关面试中好运!
GPT 模型的 Transformer 架构
原文:
towardsdatascience.com/the-transformer-architecture-of-gpt-models-b8695b48728b
了解 Transformer 架构的详细信息
·发表于 Towards Data Science ·阅读时间 22 分钟·2023 年 7 月 25 日
–
2017 年,谷歌的作者们发布了一篇名为《Attention is All You Need》的论文,在其中引入了 Transformer 架构。这一新架构在语言翻译任务中取得了前所未有的成功,该论文很快成为任何从事这一领域的必读文献。像许多人一样,当我第一次阅读这篇论文时,我能看出其创新思想的价值,但没有意识到这篇论文对更广泛的 AI 领域会产生如此巨大的影响。在短短几年内,研究人员将 Transformer 架构应用于语言翻译之外的许多任务,包括图像分类、图像生成和蛋白质折叠问题。特别是,Transformer 架构革新了文本生成,为 GPT 模型和我们当前在 AI 领域经历的指数级增长铺平了道路。
鉴于 Transformer 模型在当前行业和学术界的广泛应用,理解它们的工作细节是每位 AI 从业者的重要技能。本文将主要关注 GPT 模型的架构,这些模型是使用原始 Transformer 架构的一个子集构建的,但最后也会涉及原始 Transformer。关于模型代码,我将从我找到的最清晰的原始 Transformer 实现开始:哈佛大学的注释 Transformer。我将保留与 GPT transformer 相关的部分,移除不相关的部分。在此过程中,我会避免对代码做任何不必要的修改,以便你可以轻松地将 GPT 类似版本的代码与原始代码进行比较,理解它们的差异。
本文面向经验丰富的数据科学家和机器学习工程师。特别是,我假设你对张量代数非常熟悉,已经从头实现了神经网络,并且对 Python 使用自如。此外,尽管我尽力使这篇文章独立完整,如果你读过我之前关于 GPT 模型如何工作的文章,理解起来会更容易。
本文中的代码可以在相关的 GitHub 项目 中找到。
如何调用我们的 GPT Transformer
在我们深入了解如何构建 GPT 模型之前,先来理解如何调用它。我们暂时假设已经有一个可工作的 GPT 模型,重点讨论如何准备输入、调用模型以及解读输出。总体思路是提供几个单词作为输入来启动生成,并返回可能跟随该输入的文本。例如,如果我们给 GPT 模型输入“很久以前”,模型可能会返回“在一个遥远的星系”。
让我们看看用于调用模型的代码,传入输入“很久以前”并生成 10 个新词。我使用了注释来展示每个张量的形状。代码之后我会解释更多细节。
import tiktoken
def tokenize(text, batch_size):
"""Convert text to numerical tokens and repeat batch_size times."""
encoding = tiktoken.encoding_for_model("davinci")
token_list = encoding.encode(text)
token_tensor = torch.tensor(token_list, dtype=torch.long) # (input_seq_len)
token_tensor = token_tensor.unsqueeze(0) # (1, input_seq_len)
token_tensor = token_tensor.repeat(batch_size, 1) # (batch_size, input_seq_len)
return encoding, token_tensor
def limit_sequence_length(input_tokens, block_size):
"""Limit the input to at most block_size tokens."""
input_seq_len = input_tokens.size(1)
seq_len = min(input_seq_len, block_size)
block_tokens = input_tokens[:, -seq_len:] # (batch_size, seq_len)
return block_tokens
def generate_next_token(model, tokens):
"""Use the highest probability from the Transformer model to choose the next token."""
mask = subsequent_mask(tokens.size(1)) # (1, seq_len, seq_len)
decoder_output = model.decode(tokens, mask) # (batch_size, seq_len, vocab_size)
distribution = model.generator(decoder_output[:, -1, :]) # (batch_size, vocab_size)
next_token = torch.argmax(distribution, dim=1, keepdim=True) # (batch_size, 1)
return next_token
# Define constants.
input_text = "A long time ago"
new_token_count = 10
batch_size = 1
block_size = 1024
# Tokenize the input text.
encoding, tokens = tokenize(input_text, batch_size)
# Create the model.
model = make_model(encoding.n_vocab)
# Iterate until we've generated enough new tokens.
for _ in range(new_token_count):
block_tokens = limit_sequence_length(tokens, block_size) # (batch_size, seq_len)
next_token = generate_next_token(model, block_tokens) # (batch_size, 1)
tokens = torch.cat([tokens, next_token], dim=1) # (batch_size, input_seq_len + 1)
# Print each of the generated token sequences.
print(tokens)
for row in tokens:
print(encoding.decode(row.tolist()))
由于我们从字符串“A long time ago”开始,你可能会倾向于认为 Transformer 接收字符串作为输入。然而,和其他神经网络一样,Transformer 需要数值输入,因此输入字符串必须首先转换为一系列数字。我们在tokenize
函数中使用一个分词器(在我们的示例中是来自 OpenAI 的tiktoken
)进行转换,它将文本拆分为几字母的块,并为每个唯一的块分配一个称为令牌的数字。为了获得 Transformer 的正确输入,我们将令牌序列放入一个张量中,并扩展它以包含一个批量维度。这是因为,和其他类型的神经网络一样,Transformer 最有效的训练方式是使用批量处理,以利用 GPU 上的并行计算。我们的示例代码在一个序列上运行推断,因此我们的batch_size
为一,但如果你想一次生成多个序列,可以尝试更大的数字。
在对输入进行分词后,我们使用make_model
函数创建 Transformer 模型,稍后我们将详细讨论这个函数。你可能会认为调用模型会返回几个令牌作为输出,因为这通常是文本生成的场景。然而,Transformer 每次只能生成一个令牌。由于我们希望生成多个令牌,我们使用for
循环多次调用模型,在每次迭代中,我们使用torch.cat
将新生成的令牌附加到原始令牌序列中。
GPT 风格的 Transformer 模型通常有一个明确定义的令牌限制:例如,gpt-35-turbo
(Chat GPT)的限制为 4096 个令牌,而gpt-4-32k
的限制为 32768 个令牌。由于我们将输入令牌和到目前为止生成的所有输出令牌连接传递给 Transformer 模型,模型的令牌限制指的是输入加输出令牌的总数。在我们的代码中,我们使用block_size
常量定义这个令牌限制,并通过在limit_sequence_length
函数中简单地将更长的令牌序列截断到最大支持长度来处理更长的序列。
我们在generate_next_token
函数中通过调用model.decode
和model.generator
来调用 Transformer 模型,这对应于 Transformer 架构的两个主要部分。解码部分需要一个掩码,我们使用subsequent_mask
函数创建这个掩码。我们将在本文后面详细分析这些函数。生成阶段返回一个概率分布序列,我们选择最后一个(稍后我们将看到原因),用它来预测下一个令牌。这个分布包含每个可能的令牌的概率值,表示该令牌在句子中接下来出现的可能性。
为了简化和提高示例代码的可读性,我们选择输出分布中概率最高的标记(使用 torch.argmax
)。在实际的 GPT 模型中,下一个标记是通过从概率分布中进行采样来选择的,这会在输出中引入一些变异,使得文本感觉更加自然。如果你可以访问 Azure AI Studio 的“Completions playground”,你可能会注意到“Temperature”和“Top probabilities”滑块,这些滑块可以让你控制采样的方式。
tensor([[ 32, 890, 640, 2084, 3556, 48241, 26430, 34350, 28146, 43264,
3556, 6787, 45859, 13884]])
A long time ago</ spaghetti Rapiddx Rav unresolved</ rail MUCHkeeper
在这篇文章中,我们将重点关注 Transformer 模型架构的代码,而不是模型训练的代码,因为这正是 Transformer 创新大部分所在的地方。最后我会给出一些训练模型的指引,以防你有兴趣扩展这段代码以生成更好的结果。
我们现在对 Transformer 模型的输入和输出有了很好的理解,以及如何实例化和调用它。接下来,我们将深入探讨模型本身的实现细节。
Transformer 架构概述
让我们熟悉一下 GPT transformer 的高层架构:
在这个图示中,数据从下到上流动,这在 Transformer 插图中是传统的。最初,我们的输入标记经过几个编码步骤:首先使用嵌入层进行编码,然后是位置编码层,最后将这两种编码加在一起。接下来,我们的编码输入经过一系列 N 解码步骤,然后是一个归一化层。最后,我们将解码后的数据通过一个线性层和一个 softmax,得到一个概率分布,用于选择下一个标记。
在接下来的部分中,我们将仔细研究这个架构中的每一个组件。
嵌入层
嵌入层将输入序列中的每个标记转换为长度为 d_model
的向量。Transformer 的输入由标记序列的批次组成,形状为 (batch_size, seq_len)
。嵌入层接受每个标记(一个单一的数字),计算其嵌入(一个长度为 d_model
的数字序列),并返回一个张量,用每个嵌入替代对应的原始标记。因此,这一层的输出形状为 (batch_size, seq_len, d_model)
。
import torch.nn as nn
class Embeddings(nn.Module):
def __init__(self, d_model, vocab_size):
super(Embeddings, self).__init__()
self.lut = nn.Embedding(vocab_size, d_model)
self.d_model = d_model
# input x: (batch_size, seq_len)
# output: (batch_size, seq_len, d_model)
def forward(self, x):
out = self.lut(x) * math.sqrt(self.d_model)
return out
使用嵌入而不是原始标记的目的是为了确保对于语义相似的标记,我们有类似的数学向量表示。例如,考虑“she”和“her”这两个词。这两个词在语义上是相似的,因为它们都指代女性或女孩,但相应的标记可能完全不同(例如,当使用 OpenAI 的tiktoken
分词器时,“she”对应的标记是 7091,而“her”对应的标记是 372)。这两个标记的嵌入在开始时也会非常不同,因为嵌入层的权重是随机初始化的,并在训练过程中学习。但如果这两个词在训练数据中经常出现在一起,最终嵌入表示将趋于相似。
位置编码
位置编码层添加了每个标记在序列中的绝对位置和相对距离的信息。与递归神经网络(RNN)或卷积神经网络(CNN)不同,变换器本身并没有固有地认识到每个标记在序列中的位置。因此,为了捕捉序列中标记的顺序,变换器依赖于位置编码。
编码标记位置的方法有很多。例如,我们可以通过使用另一个嵌入模块(类似于前面的层)来实现位置编码层,如果我们将每个标记的位置而不是每个标记的值作为输入。同样,我们会从随机选择的权重开始。然后在训练阶段,权重会学习捕捉每个标记的位置。
The Annotated Transformer的作者决定实现一个更复杂的算法,该算法预计算了序列中标记位置的表示。由于我们希望尽可能紧密地跟随他们的代码,我们将使用相同的方法:
class PositionalEncoding(nn.Module):
def __init__(self, d_model, dropout, max_len=5000):
super(PositionalEncoding, self).__init__()
self.dropout = nn.Dropout(p=dropout)
pe = torch.zeros(max_len, d_model)
position = torch.arange(0, max_len).unsqueeze(1) # (max_len, 1)
div_term = torch.exp(
torch.arange(0, d_model, 2) *
-(math.log(10000.0) / d_model)) # (d_model/2)
pe[:, 0::2] = torch.sin(position * div_term) # (max_len, d_model)
pe[:, 1::2] = torch.cos(position * div_term) # (max_len, d_model)
pe = pe.unsqueeze(0) # (1, max_len, d_model)
self.register_buffer('pe', pe)
# input x: (batch_size, seq_len, d_model)
# output: (batch_size, seq_len, d_model)
def forward(self, x):
x = x + Variable(self.pe[:, :x.size(1)], requires_grad=False)
return self.dropout(x)
这种位置编码使用不同频率的正弦和余弦函数来填充pe
张量。例如,在下面的插图中,蓝色和红色的值是使用两种不同频率的正弦波计算得出的,而橙色和绿色的值是使用相同频率的余弦波计算得出的。
正弦和余弦图的值最终填充了pe
张量的列,如下所示:
然后在“前向”阶段,我们将前一个嵌入层的结果x
作为输入,并返回x
和pe
的和。
预计算位置编码的值(而不是使用可训练的嵌入)的主要优点是我们的模型训练参数减少。这种参数减少提高了训练性能,这在处理大型语言模型时至关重要。
解码器
正如我们在 Transformer 架构的图示概述中看到的那样,嵌入和位置编码层之后的下一个阶段是解码器模块。解码器由N个解码器层副本组成,后面跟着一个层归一化。以下是Decoder
类,它将单个DecoderLayer
实例作为类初始化器的输入:
class Decoder(nn.Module):
def __init__(self, layer, N):
super(Decoder, self).__init__()
self.layers = clones(layer, N)
self.norm = LayerNorm(layer.size)
def forward(self, x, mask):
for layer in self.layers:
x = layer(x, mask)
return self.norm(x)
“克隆”函数简单地创建一个包含N个模块副本的 PyTorch 列表:
def clones(module, N):
return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])
层归一化接收形状为(batch_size, seq_len, d_model)
的输入,并在其最后一个维度上进行归一化。经过这一步,每个嵌入分布将开始时为单位正态分布(以零为中心,标准差为一)。然后,在训练过程中,分布会随着参数a_2
和b_2
在我们场景中的优化而改变形状。你可以在层归一化论文中了解更多关于层归一化的内容,这篇论文来自 2016 年。
class LayerNorm(nn.Module):
def __init__(self, features, eps=1e-6):
super(LayerNorm, self).__init__()
self.a_2 = nn.Parameter(torch.ones(features))
self.b_2 = nn.Parameter(torch.zeros(features))
self.eps = eps
def forward(self, x):
mean = x.mean(-1, keepdim=True)
std = x.std(-1, keepdim=True)
return self.a_2 * (x - mean) / (std + self.eps) + self.b_2
我们克隆的DecoderLayer
类具有以下架构:
以下是对应的代码:
class DecoderLayer(nn.Module):
def __init__(self, size, self_attn, feed_forward, dropout):
super(DecoderLayer, self).__init__()
self.size = size
self.self_attn = self_attn
self.feed_forward = feed_forward
self.sublayer = clones(SublayerConnection(size, dropout), 2)
def forward(self, x, mask):
x = self.sublayer0)
return self.sublayer1
从高层次看,DecoderLayer
包含两个主要步骤:注意力步骤,负责令牌之间的通信,以及前馈步骤,负责预测令牌的计算。在这些步骤周围,我们有残差(或跳跃)连接,用图中的加号表示。残差连接为数据在神经网络中的流动提供了替代路径,从而允许跳过某些层。数据可以通过残差连接中的层流动,也可以直接通过残差连接跳过其中的层。实际上,残差连接通常用于深度神经网络,因为它们有助于训练更好地收敛。你可以在论文深度残差学习用于图像识别中了解更多关于残差连接的内容,这篇论文来自 2015 年。我们使用SublayerConnection
模块来实现这些残差连接:
class SublayerConnection(nn.Module):
def __init__(self, size, dropout):
super(SublayerConnection, self).__init__()
self.norm = LayerNorm(size)
self.dropout = nn.Dropout(dropout)
def forward(self, x, sublayer):
return x + self.dropout(sublayer(self.norm(x)))
前馈步骤通过两个线性层实现,中间夹一个 Rectified Linear Unit (ReLU) 激活函数:
class PositionwiseFeedForward(nn.Module):
def __init__(self, d_model, d_ff, dropout=0.1):
super(PositionwiseFeedForward, self).__init__()
self.w_1 = nn.Linear(d_model, d_ff)
self.w_2 = nn.Linear(d_ff, d_model)
self.dropout = nn.Dropout(dropout)
def forward(self, x):
return self.w_2(self.dropout(F.relu(self.w_1(x))))
注意力步骤是 Transformer 中最重要的部分,因此我们将在下一节中专门讨论它。
掩蔽多头自注意力
上图中的多头注意力部分可以扩展成以下架构:
顾名思义,多头注意力模块并行处理多个注意力计算实例,并对数据进行一些额外的前处理和后处理。
class MultiHeadedAttention(nn.Module):
def __init__(self, h, d_model, dropout=0.1):
super(MultiHeadedAttention, self).__init__()
assert d_model % h == 0
self.d_k = d_model // h
self.h = h
self.linears = clones(nn.Linear(d_model, d_model), 4)
self.attn = None
self.dropout = nn.Dropout(p=dropout)
def forward(self, query, key, value, mask=None):
if mask is not None:
mask = mask.unsqueeze(1) # (1, 1, seq_len, seq_len)
nbatches = query.size(0) # batch_size
# (batch_size, seq_len, d_model) => (batch_size, h, seq_len, d_k)
query, key, value = \
[l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
for l, x in zip(self.linears, (query, key, value))]
# (batch_size, h, seq_len, d_k)
x, self.attn = attention(query,
key,
value,
mask=mask,
dropout=self.dropout)
# (batch_size, h, seq_len, d_k) => (batch_size, seq_len, d_model)
x = x.transpose(1, 2).contiguous().view(nbatches, -1, self.h * self.d_k)
return self.linears-1
多头注意力层的输入包括三个张量,分别称为query
(Q)、key
(K) 和 value
(V)。在我们的特定模型中,我们将前一层的输出 x
传递给这三个参数,它的形状为 (batch_size, seq_len, d_model)
(这就是为什么我们称之为 自注意力)。我们通过先将每个张量传递通过一个线性层,然后将它们分割成 h
个注意力头,每个注意力头的大小为 d_k
,其中 h*d_k = d_model
,得到形状为 (batch_size, seq_len, h, d_k)
的张量。然后我们交换维度 1 和 2,得到形状为 (batch_size, h, seq_len, d_k)
的张量。接下来,我们为每个头计算注意力,得到相同形状的张量。最后,我们的后处理将所有头连接回形状为 (batch_size, seq_len, d_model)
的张量,并通过另一个线性层。通过使用张量操作在每个头中并行进行所有注意力计算,我们可以充分利用 GPU。
注意力的计算使用以下公式:
这里是实现公式的代码:
# Dimensions of query, key, and value: (batch_size, h, seq_len, d_k)
# Dimensions of mask: (1, 1, seq_len, seq_len)
def attention(query, key, value, mask=None, dropout=None):
d_k = query.size(-1)
# (batch_size, h, seq_len, d_k) x (batch_size, h, d_k, seq_len) -> (batch_size, h, seq_len, seq_len)
scores = torch.matmul(query, key.transpose(-2, -1)) / math.sqrt(d_k)
if mask is not None:
scores = scores.masked_fill(mask == 0, -1e9)
p_attn = F.softmax(scores, dim=-1) # (batch_size, h, seq_len, seq_len)
if dropout is not None:
p_attn = dropout(p_attn)
# (batch_size, h, seq_len, seq_len) x (batch_size, h, seq_len, d_k) -> (batch_size, h, seq_len, d_k)
return torch.matmul(p_attn, value), p_attn
从高层次来看,注意力算法决定输入序列中哪些标记应该受到更多关注,然后使用这些信息来预测下一个标记。在下图中,深色的橙色表示在预测中更相关的标记。
更具体地说,注意力实际上预测了我们输入序列的多个部分的下一个标记。它查看第一个标记,并预测第二个标记是什么,然后它查看第一个和第二个标记,并预测第三个标记是什么,以此类推。
在推理过程中,这似乎有些浪费,因为我们只对最后的预测感兴趣。然而,这在训练期间极为有用。如果你给 Transformer n 个标记作为输入,它将被训练接收从 1 到 n-1 长度的输入,从而使模型在未来更好地处理不同长度的输入。
上图中的思想由代码中的 p_attn
张量表示。这个张量的形状为 (batch_size, h, seq_len, seq_len)
,但我们暂时忽略批量大小和头的数量(每个批量和每个头的工作方式相同),只考虑一个形状为 (seq_len, seq_len)
的张量切片。p_attn
张量中的每一行包含一个概率分布,表示其他所有键标记对该行对应的查询标记的相关性。结果张量封装了前一图像中显示的所有值:
你可以在代码中看到这个张量是如何计算的。我们首先对查询和转置的键进行矩阵乘法。如果忽略批量大小和头数,查询和键由形状为(seq_len, d_k)
的嵌入序列组成,这些嵌入是将输入x
通过不同线性层得到的。当我们将形状为(seq_len, d_k)
的查询张量与形状为(d_k, seq_len)
的转置键张量相乘时,我们实际上是在对查询中的每个嵌入和键中的所有其他嵌入进行点积,最终得到一个形状为(seq_len, seq_len)
的scores
张量。点积的较大值表示查询中的某个嵌入对键中的某个嵌入“感兴趣”,换句话说,模型发现了输入序列中两个位置之间的关联。大致而言,我们现在有了一个表示每个标记在序列中发现其他所有标记的“有趣”或“重要”的张量。
下一步是对scores
张量应用掩码,使其上三角的值被忽略(这就是我们称之为masked注意力的原因)。我们这样做是因为在 GPT 风格的文本生成场景中,模型在预测下一个标记时仅查看过去的标记。我们使用以下代码定义一个掩码,它在对角线和下三角中包含True
值,在上三角中包含False
值:
def subsequent_mask(size):
"""
Mask out subsequent positions.
"""
attn_shape = (1, size, size)
subsequent_mask = torch.triu(torch.ones(attn_shape),
diagonal=1).type(torch.uint8)
return subsequent_mask == 0
我们使用masked_fill
函数将这个掩码应用到scores
张量中,用一个非常大的负数替换掉上三角的所有值。
最后,我们应用一个 softmax,将张量中的每一行转换为概率分布。记得 softmax 的公式吗?
由于e的负幂非常大的情况下接近零,p_attn
张量的上三角中的所有值本质上变为零。剩余的值(在下三角和对角线中)变成每行总和为一的概率。
你可能注意到,在代码中,当我们将查询和键张量相乘时,我们将结果矩阵中的所有值除以d_k
的平方根。这样做是为了保持方差接近于一,这样可以确保 softmax 给出的概率值在整个范围内(从零到一)分布良好。如果我们不这样做,softmax 计算的分布可能接近于 one-hot 向量,其中一个值为一,其他值都为零——这会使模型的输出显得可预测且机械。
此时,我们有一个包含概率分布的p_attn
张量,表示各标记之间的相互兴趣程度。下一步是利用这种兴趣程度来确定在生成输出标记时我们应该对每个输入标记给予多少关注。自然地,我们会对最感兴趣的标记给予更多关注。我们通过将概率张量与value
张量相乘来生成下一个标记,value
张量包含经过线性层处理后的输入标记嵌入x
。结果张量将包含每个标记子序列的预测:
这个图表的直观解释是:对于输入子序列“A”,我们对唯一的输入标记给予完全的关注,并可能产生下一个标记预测,比如“person”。对于输入子序列“A long”,我们的模型已经训练得更加关注标记“long”而非标记“A”,并可能产生下一个标记预测“dress”,依此类推。在推理时,我们希望考虑到完整的输入序列“A long time ago”,所以我们只关注这个图表中的最后一行。我们最关注“ago”,次之是“long”,对其他两个标记的关注最少,我们产生下一个标记预测“in”。
在我们计算了所有头的注意力,并将结果重新拼接在一起后,我们得到一个维度为(batch_size, seq_len, d_model)
的输出张量。这个张量包含每个子序列的标记预测,几乎准备好返回给用户。但是在此之前,我们需要最后一步来最终确定其形状和内容。
生成器
我们的 Transformer 中的最后一步是生成器,它包括一个线性层和一个顺序执行的 softmax:
class Generator(nn.Module):
def __init__(self, d_model, vocab):
super(Generator, self).__init__()
self.proj = nn.Linear(d_model, vocab)
def forward(self, x):
return F.log_softmax(self.proj(x), dim=-1)
线性层的目的是将张量的第三维度从内部的d_model
嵌入维度转换为vocab_size
维度,这是调用我们 Transformer 的代码所理解的。结果是一个维度为(batch_size, seq_len, vocab_size)
的张量。softmax 的目的是将第三维度中的值转换为概率分布。这一概率分布张量就是我们返回给用户的内容。
你可能还记得,在本文开头,我们解释了 Transformer 的输入由形状为(batch_size, seq_len)
的标记序列批次组成。而现在我们知道 Transformer 的输出由形状为(batch_size, seq_len, vocab_size)
的概率分布序列批次组成。每个批次包含一个预测第一个输入标记之后的标记的分布,另一个预测第一个和第二个输入标记之后的标记的分布,依此类推。每个批次的最后一个概率分布使我们能够预测整个输入序列之后的标记,这也是我们在做推理时所关心的。
生成器是我们 Transformer 架构的最后一个部分,因此我们准备好将所有组件结合起来了。
将所有组件结合起来
我们使用DecoderModel
模块来封装 Transformer 架构的三个主要部分:嵌入层、解码器和生成器。
class DecoderModel(nn.Module):
def __init__(self, decoder, embed, generator):
super(DecoderModel, self).__init__()
self.embed = embed
self.decoder = decoder
self.generator = generator
def forward(self, x, mask):
return self.decode(x, mask)
def decode(self, x, mask):
return self.decoder(self.embed(x), mask)
调用decode
仅执行嵌入层和解码器,所以如果我们想执行 Transformer 的所有步骤,我们需要调用decode
,然后调用generator
。这正是我们在本文开头展示的推理代码中的generate_next_token
函数所做的。
推理代码还调用了一个make_model
函数,该函数返回DecoderModel
的一个实例。这个函数初始化了我们迄今为止讨论的所有组件,并按照本文开头的架构图将它们结合在一起:
def make_model(vocab_size, N=6, d_model=512, d_ff=2048, h=8, dropout=0.1):
c = copy.deepcopy
attn = MultiHeadedAttention(h, d_model)
ff = PositionwiseFeedForward(d_model, d_ff, dropout)
position = PositionalEncoding(d_model, dropout)
model = DecoderModel(
Decoder(DecoderLayer(d_model, c(attn), c(ff), dropout), N),
nn.Sequential(Embeddings(d_model, vocab_size), c(position)),
Generator(d_model, vocab_size))
for p in model.parameters():
if p.dim() > 1:
nn.init.xavier_uniform_(p)
return model
我们现在已经拥有了实现 GPT 风格 Transformer 架构所需的所有组件!
我们将以一些关于训练的思考结束,并且对完整的 Transformer 与 GPT 风格子集进行简要比较。
训练
训练 GPT 风格的 Transformer 的代码与训练任何其他神经网络的代码相同——只是,在我们的场景中,对于每个输入的标记序列,我们期望输出是从右边一个位置开始的序列。例如,如果我们给它输入“A long time ago”,我们期望模型返回的概率采样会产生“long time ago in”。
如果你有训练神经网络的经验,并且希望自己训练 Transformer,你可以重用你过去编写的任何代码,并将其调整到我们的场景中。如果你需要指导,我建议你参考Annotated Transformer 的第二部分中的代码和解释。无论如何,你都需要访问快速 GPU,无论是本地还是云端。自然,我更倾向于在 Azure 云端进行训练!
与完整 Transformer 的比较
一旦你理解了 GPT 风格 Transformer 的架构,你就离理解Attention is all you need论文中呈现的完整 Transformer 只差一步。下面你可以看到论文中呈现的 Transformer 架构图,我们在本文中覆盖的部分被橙色框圈出。
插图改编自 Attention is all you need 论文
完整的 Transformer 有一个编码器部分(在左侧)和一个解码器部分(在右侧)。论文的原始意图是提出一种用于机器翻译的架构。在这种背景下,编码器用于处理输入语言,而解码器用于生成输出语言。
你可以看到,除了 GPT 风格 Transformer 中使用的掩码多头自注意力外,完整的 Transformer 还有另外两个多头注意力块。编码器中的那个未被掩码,这意味着我们在注意力部分看到的p_attn
张量在其上三角区域的值没有被置零。这是因为在机器翻译中,生成一个输出语言的单一令牌可能需要模型关注输入语言中所有序列位置的令牌,包括早期和晚期位置。解码器部分的附加多头注意力块是一个“交叉注意力”(与“自注意力”相对)层,这意味着它的键和值来自不同于查询的源,如图所示。这是因为模型需要理解在预测输出语言中的令牌时,应该关注输入语言中的每个令牌的程度。图中其余部分类似于 GPT 风格 Transformer 的部分,并已在本文中解释过。
结论
在这篇文章中,我们详细讨论了 GPT 风格的 Transformer 模型的架构,并在较高层次上覆盖了原始 Transformer 的架构。鉴于 GPT 模型在行业中的日益流行,以及最近论文中经常出现的原始 Transformer 模型的变体,我希望你能在工作或学习中找到这些知识的用处。如果你想深入了解,我鼓励你克隆相关的 GitHub 项目 并探索代码。设置断点于你不清楚的代码部分,运行它,并检查其变量。如果你有 GPU 访问权限,可以编写代码训练模型,并查看其预测性能的提升。
希望你运用新获得的知识,帮助人工智能社区揭开一些与这些模型相关的误解。许多人误以为这些模型对世界有更高层次的理解,而你现在知道事实并非如此。它们只是由数学和统计运算组成,旨在基于之前的训练数据预测下一个令牌。也许未来的生成模型会对世界有更好的理解,并生成更准确的预测?如果是这样,我会在这里告诉你所有的细节。
注意
所有图片均由作者提供,除非另有说明。你可以在这篇博客文章中出于任何目的使用原始图片,并需注明出处(即链接到本文)。