TowardsDataScience-2023-博客中文翻译-四-
TowardsDataScience 2023 博客中文翻译(四)
制胜自评指南
原文:
towardsdatascience.com/a-guide-to-writing-a-winning-self-appraisal-a9d4f5ee3d35
在你的软件工程师或数据科学家的绩效评估中应突出什么
·发布于 Towards Data Science ·7 分钟阅读·2023 年 2 月 21 日
--

作者提供的图像
自我评估是个人和职业发展的重要组成部分,提供了一个反思自己成就和改进领域的机会,承担自身成长的责任,并对自己的职业有更多掌控感。
自评在任何薪酬讨论中都是关键因素,帮助管理者确保员工因其对组织的贡献而获得适当的报酬。此外,通过评估过程展示个人的价值,能够展示其成就并增强获得认可的机会(加薪、奖金、晋升)。
随着评估时间的临近,一些我的受指导者询问如何写一份有说服力的自我评价。意识到这是一个常见问题,尤其是在初级 IT 专业人员中,我整理了这份简要指南,分享了我的观点。因此,软件工程师、数据科学家和应用程序开发人员需要了解的关于撰写全面且有影响力的自我评估的内容包括:展示他们的优异表现,同时也涉及成长领域,以确保评估全面且平衡。
超越基础
在这一部分中,我将讨论五个关键提示,帮助你撰写一份超越基础的稳固自评,平衡地突出你的优势。
➊ — 全年跟踪成就
在职业生涯初期,我曾陷入一个常见的陷阱,即没有彻底记录我一年来的成就,导致在二月份年终评估前急忙寻找记录。意识到我记不清楚所有的事情,不得不翻阅多个记录,包括 Jira 票据、拉取请求、电子邮件和设计文档,以编制我的评估报告,这让我感到沮丧。最终,我开始保持工作日志(即自夸文档由 Julia Evans 提供)。这个简单的习惯让我能够刷新记忆,减少对我贡献价值的猜测。
⭐️ Pro Tip: 将工作日志转化为自我评价时,保持内容简短而精炼。如果你在审查自我评价时感觉顶部需要一个“TL;DR”总结,这表明内容太长了——你可能需要考虑使用项目符号而不是写长篇大论。此外,识别出真正突出的成就——这些可能是你在日常工作中超越自我的实例(例如,周末工作以完成一个功能;在实施前评估多个选项等)。
➋ — 用可测量的指标备份贡献
Google X-Y-Z 公式常用于简历写作,也可以应用于撰写自我评估。它建议关注具体成就 X、可测量的指标 Y 和实现结果的方法 Z,通过使用它,开发者可以突出他们的成就,量化他们的影响,并用具体数据支持他们的贡献。
“按[X]衡量,通过做[Z]实现了[ Y]”
例如,一个全栈开发者可以这样表述:通过实施缓存模式和简化 HTML DOM(Z),将整体页面加载时间减少了 80%(X),这一点通过基准测试证明了(Y)。
这里值得注意的是,X-Y-Z 公式是一种数据驱动的方法,要求被评估者量化他们的成就(使用数字或统计数据)以提供具体的贡献证据。像代码行数、错误修复或服务水平协议等指标,都是展示工作成效的典型方式。
⭐️ Pro Tip: X-Y-Z 公式非常“干燥”,未能为成就的背景增添“色彩”。为了让你的成就更为突出,尝试使用描述性形容词和相关关键词来强调其重要性(例如,不仅仅说:“交付了 ABC 功能”,而是可以加上:“尽管 ABC 功能的范围扩大了,但部署仍按承诺的冲刺目标和时间表进行”)。
➌ — 定期与管理层沟通
在我职业生涯的初期,我会定期与我的经理进行 1 对 1 会议,但我们的重点通常会被紧迫的工作事务所主导。因此,绩效反馈往往被忽视。为了解决这个问题,我学会了更有意识,并开始专门安排会议来讨论绩效相关的话题。因此,我能够收到关于目标进展的反馈,并识别改进的领域,避免任何意外在年终评审时出现。
此外,我有机会提出这三个重要问题:
-
到目前为止,我在实现我的目标和期望方面表现如何?
-
我可以访问任何额外的支持或资源来帮助我在角色中更有效吗?
-
我是否有机会在团队或组织内承担新的挑战或责任?
让我们暂停一下,思考最后这个问题:项目并不总是与评估周期的开始对齐;它们可以随时出现。因此,通过频繁提及这一点,我可以确保自己了解任何当前或即将到来的机会,并采取必要的步骤为这些机会做好准备。
⭐️ 专业提示: 将你的行动与你的经理成功所需的内容联系起来。他们是否向高级领导层撰写进展报告?— 向他们提供指标和额外的信息。他们是否在范围扩展的情况下谈判时间表/预算?— 提前通知他们,以便他们能保持项目在正轨上。
➍ — 考虑非技术能力
成功的 IT 专业人士明白,单靠技术技能不足以实现职业成长和晋升。我花了一段时间才认识到,通过展示一系列全面的能力,我可以为自己争取一个领导角色。根据你的组织,可能会有一些对你工作至关重要的人力资源能力,但通常这些包括沟通、团队合作、批判性思维、主动性、适应能力或领导力。务必将这些属性的应用直接与您所取得的结果联系起来。
⭐️ 专业提示: 包括你从直接经理之外的人那里收到的反馈。例如,从内部/外部客户那里收到的邮件摘录或来自同事的赞誉。
➎ — 认可不足之处
被评估者常常认为领导只对卓越的成功感兴趣,而忽视了影响其表现的挑战。相反,管理层希望看到的是如何克服这些挑战,或者如何应对这些挑战,或者从中学到了什么教训。认识到这些障碍对个人成长和发展至关重要,这种透明度和自我提升的承诺不会被忽视。我曾经说过这样的话:“我只实现了部分目标是因为{非常明确的原因},我现在正在使用{这种新方法}来克服这一点。”
⭐️ 专业提示: 不要害怕寻求建议或在需要时请求额外的培训。认识到自己并不懂得所有事情,总是有改进的空间。这也是一个找到导师的绝佳机会。
指标-指标-指标
到现在你可能会想知道技术人员包括哪些指标来支持他们的成就,如上述第 2 种策略所述,以及他们如何衡量这些指标。使用数字来量化贡献提供了一种基于不可争议事实总结表现的客观方式。以下是你可以在自我评估中包含的可衡量技能列表:
编码
‣ PR 数量
‣ 代码评审数量
‣ 技术文档数量
‣ 解决的技术债务票务数量
‣ 重构对性能/代码质量等的影响
‣ 测试覆盖率增加了 X%
‣ 创建的可重用框架/库数量
‣ 成功发布的数量(即发布后没有紧急发布)
可靠性
‣ SLA 提高了 X%
‣ TTD/TTM/TTR 平均时间
生产力
‣ 创建的工具用于自动化重复任务,减少 X% 的时间
‣ 提前完成的票务数量
‣ 加班小时数
成本节省
‣ 通过使用开源而非付费库节省的 $$$
‣ 通过使用云服务节省的 $$$
‣ 通过实施 DevOps 流水线节省的 $$$
人员
‣ 新入职人数
‣ 面试人数
‣ 指导的人员数量
‣ 在团队环境中对上述事项的影响(例如,更快的交付能力)
学习
‣ 完成的课程数量
‣ 因跟进技术而引入的新技术栈数量
其他 粘合工作
‣ 组织的 KT 会议数量
‣ 组织的利益相关者会议数量
‣ 实施的过程改进数量
‣ 作为跨职能团队之间的联络人,促进沟通和理解
‣ 主动解决问题的影响
‣ 参与的设计讨论数量
进行精心编写的绩效自我评估可以是最有效的内部营销活动之一。希望这些笔记对你有所帮助。
感谢阅读!
P.S. 你有没有试过多次点击点赞按钮看看会发生什么?❤
我在 Medium 上定期撰写关于领导力、技术和数据的文章 — 如果你想阅读我未来的帖子,请 ‘关注’我!
我的新书《技术智慧》现在已经上市了 — 点击查看!*
在数据科学领域建立职业资本的一个高度被低估的方法
原文:
towardsdatascience.com/a-highly-underrated-way-to-build-career-capital-in-data-science-231c4f2c654e
在线写作:开始很简单,非常有利于提高沟通技能,并且比传统的数据科学个人简历更具可持续性
·发表于Towards Data Science ·9 分钟阅读·2023 年 6 月 28 日
--

个人简历和 LinkedIn 个人资料在数据科学家的职业生涯中只能帮到你有限的程度。
当然 — LinkedIn 是建立职业网络的绝佳方式,而个人简历则提供了展示你所做的酷炫项目的好方法。但是,如果没有人点击你的简历链接,或者你不想成为LinkedIn 上的炒作达人,你的个人资料将会被搁置在那儿,积满数字灰尘,你也永远无法从中获得任何价值。
在这篇文章中,我想提倡另一种补充策略:在线写作。
直到 3 个月前,我从未为了乐趣而写过任何东西,我对写作的记忆大多来自学校和考试,都是负面的。因此,如果你觉得在线写作的想法很疯狂(或者超出了你的舒适区/兴趣区),我完全理解你的感受。但在实践了 3 个月后,我已经成为了它的坚定信徒,如果你给我 2 分钟时间,我将解释在线写作如何使数据科学家能够:
-
发展强大的个人品牌
-
与数据科学社区中的其他人建立有意义的联系
-
发展经常被忽视的讲故事和沟通技能
-
以招聘者友好的、易于分享的格式展示你的技能
然后,我将分享一些帮助我开始写作的经验,并给你一些关于如何入门的建议。
如果你喜欢这个故事,点击我的“关注”按钮对我来说意义重大 — 只有 1%的读者会这么做!感谢阅读。
在线写作的 4 个好处
1. 写作有助于你建立强大的专业品牌
在我大部分职业生涯中从事数据工作时,我采用了相当标准的专业品牌建立方式。
我创建了一个个人作品集,维护了一个 LinkedIn 个人资料,并偶尔发布一些重要的职业成就。
这种方法的问题在于,作品集和 LinkedIn 个人资料被藏在互联网的一个不起眼的角落。除非有人主动去找,否则没人能找到你的东西,而你也没有真正的机制来获得对你工作的反馈。
相比之下,当你在线写作时,你能够将你的工作分享给更广泛的观众,并获得关于阅读时间和阅读比率等方面的非常好的反馈。如果你坚持下去,写作是一种建立个人品牌并成为某个特定领域“首选”人物的绝佳方式。
例如,我的一个目标是尽力成为数据科学职业领域的“首选”人物。如果我完全依赖个人作品集或偶尔的谦虚自夸的 LinkedIn 帖子,很难建立这种小众品牌,但通过在 Medium 上写作,很容易塑造这种在线身份。
而且不仅仅要听我的话——比如看看卡西·科齐克罗夫。卡西是谷歌的首席决策科学家,她在数据领域是一个非常激励人心的榜样。通过在线写作,她建立了一个出色的个人品牌,甚至像我这样的无知者也知道她是谁。再看看克里斯·阿尔邦,他是维基媒体基金会的机器学习总监,他在数据领域的 Twitter 账号可能是世界上最有趣的。克里斯通过多年的积累建立了一个了不起的个人品牌,他利用这个平台与来自世界各地的人分享优质的学习资源。
2. 在线写作帮助你与业内其他人建立有意义的联系
在线写作的下一个好处是,它是建立你网络的一个绝佳方式,比 LinkedIn 等其他途径要愉快得多。
在多年在 LinkedIn 上发帖之后,我发现观众看到的内容与你希望看到你内容的人之间经常存在不匹配。
你的大多数观众将是你的关注者,但除非你已经拥有一个非常活跃且参与度高的受众,否则你不太可能有很多感兴趣的关注者。你的关注者往往是已经认识你的人,而且——更糟的是——他们中的一半可能只是无兴趣的半接触者。即便如此,我的 LinkedIn 网络中还包括我在当地报刊亭第一份周六工作时合作过的人。保持联系固然愉快,但我分享有关人工智能革命的想法对我们双方都没有太大价值。
相比之下,当你在线写作时,你能够以一种长期可持续和有价值的方式,将你的想法分享给非常有针对性的社区。
例如,我将大多数数据科学文章分享在像 Towards Data Science 和 Towards AI 这样的出版物中。我能够针对那些在我的领域的人,而不是让普通人被我关于卷积神经网络细节的沉思打扰。当我在像 Medium 这样的平台上分享这些沉思时,我能够将我的内容呈现给真正感兴趣的人。
3. 写作有助于掌握沟通和讲故事的能力
任何数据科学招聘人员都会告诉你,找到一个知道什么是无监督 ML 模型的人很容易。困难的是找到一个能够简洁地向业务中其他非技术人员解释它的人。
随着你在数据科学领域的发展,建立这些沟通技能非常重要,你不应该只专注于技术技能。当然——当你刚开始时,你可能会花大部分时间进行编码,而在团队内部会议之外的展示不多。但是,如果你想推进你的职业生涯,发展强大的讲故事和沟通能力是绝对关键的。记住:高级和首席数据科学家花在展示上的时间远多于编码。写作有助于做到这一点,因为你会不断思考如何有效地表达观点,以及在哪里削减不必要的废话。
4. 作品集在你刚开始职业生涯时很有用;而写作在长期推进职业生涯方面更有效
许多有志于成为数据科学家的人员可能没有意识到,作品集很快会变得过时。
我记得在不到一年前,我正在完成数据科学硕士学位时,创建了我的第一个个人作品集网站。那时正是我开始申请数据科学职位的时候,我想找一个展示我所做的一些项目的方法。我听说过建立在线作品集的重要性,于是我用 GitHub Pages 和 Jekyll 制作了一个简单的静态网站。
尽管这个作品集对我获得第一个工作非常有帮助,但一旦我开始全职做数据科学家,我发现自己没有时间(或兴趣)在工作之外做数据科学项目。雇主们也对我在工作中的成就更感兴趣,而不是我个人的项目,不久之后我就放弃了我的作品集。
相比之下,我的经验表明,在线写作是一种长期建立职业资本的更可持续的方式。
为什么?首先,因为它很快。做一个完整的数据科学项目需要很长时间,但写一篇快速的博客文章(像这样)可以在一两个小时内完成。
其次,因为你可以写任何你想写的东西。这使得写作成为一种非常可持续的长期策略来建立职业资本,因为你可以随着时间改变你写的内容。当你“在一线”作为程序员工作时,你可以分享编程技巧或项目总结。当你在更高的职位上,大部分时间都在与利益相关者会面或制定团队策略时,你可以写关于大局的内容。无论你处于职业的哪个阶段,将你的在线写作分享给技术和非技术招聘人员都非常简单,你可以通过展示你的写作来展示你的沟通技巧。
如果你仍然没有被说服,可以考虑这一点:如果过去一年的 AI 发展教会了我们什么,那就是算法和编程语言可能会改变,但讲故事的技巧将永远需要。在 5 年内,我预计 AI 工具将使我编写代码的时间大大减少(或者,至少,我的编程会更快)。但我肯定还会继续用数据讲故事,所以这项技能的投资看起来是值得的。
如何开始写作
如果写在线文章的想法让你感到畏惧,不用担心:我曾经有过完全一样的感觉。
然后,在 2023 年 3 月,我决定一试,写了我的第一篇 Medium 文章。以下是帮助我开始这段旅程的一些做法和禁忌。
做:
-
从小做起 — 你不需要写关于突破性的或非常冷门的东西。我的第一篇文章是关于我职业故事的个人反思,第二篇则是关于如何用SQL 处理数据的指南。这两篇都是非常简单的话题,但这实际上是一个优势,因为它们写起来很快,使得“起步”非常容易。
-
分享你的个人经历 — 市面上有很多编程指南,如果你的写作像技术文档一样,很难脱颖而出。无论你写什么,都试着展示一个个人角度:如果你在分享编程技巧或理论解释,这种{技巧/概念}如何在你的个人工作/项目中帮助了你?你花了多长时间来学习?有什么建议可以帮助你将这些技巧付诸实践?
-
找到适合你的节奏 — 我在 Medium 上发布文章的频率相当规律,因为我喜欢写作,但这并不是唯一的方法。许多优秀的数据科学作家每月或每几个月才发布一次。关键是找到一个适合你的节奏并坚持下去。
-
写关于常青话题 —— 如果你写编码指南或分享个人故事,这些内容在你写完后会长期保持有用。即使它们一开始没有得到关注,你也在投资于一个长期的优质内容库,这些内容如果有人在两年后偶然发现,依然会有价值。
不要:
-
不要觉得你需要是专家 —— 即使你是初学者数据科学家或有抱负的数据科学家,我可以保证你有值得分享的东西。读者并不关心你是否拥有量子编织学的博士学位;他们只关心你能否帮助他们解决他们面临的具体问题。我的个人经历证明了你不需要成为专业人士或写复杂的话题。
-
不要担心寻找利基市场 —— 花时间尝试不同的话题,把寻找利基市场的事留到以后。正如 YouTuber Ali Abdaal 经常建议的那样,先“开始做”,然后专注于“做得好”,只有在后期你才需要担心“变得聪明”。不要通过对自己施加严格限制来束缚你的想象力;允许自己尝试新事物,看看你喜欢什么。你甚至可能会惊讶于自己的发现!
就这些啦!
感谢阅读。我希望你觉得这些内容有用,如果你想聊天,请随时联系我 😃
哦,还有一件事——
我开始了一份名为 AI in Five 的免费通讯,每周分享关于最新 AI 新闻、编码技巧和数据科学家/分析师的职业故事的 5 个要点。没有炒作,没有“数据是新石油”的废话,也没有来自 Elon 的推文——只有实用的技巧和见解,帮助你在职业生涯中发展。如果这些内容对你有帮助,请 在这里订阅!
[## AI in Five | Matt Chapman | Substack
最新的数据科学和 AI 世界的新闻、职业故事和编码技巧,总结成 5 个要点……
aiinfive.substack.com](https://aiinfive.substack.com/?source=post_page-----231c4f2c654e--------------------------------)
MLOps 的关键起点:探索其核心组件
原文:
towardsdatascience.com/a-key-start-to-mlops-exploring-its-essential-components-27646238372d
面向初学者的 MLOps 介绍
·发布于 Towards Data Science ·9 分钟阅读·2023 年 11 月 9 日
--

图片由 Aaron Burden 提供,来源于 Unsplash
对于像 MLOps 这样具有多个不同方面和组件的概念,尤其是对初学者来说,总是很不方便开始学习。我自己曾是 MLOps 的初学者,想要对它有一个清晰的整体了解相当困难。本教程旨在帮助你获得对 MLOps 的清晰而广泛的视野。它将帮助你更好地理解 MLOps,并指导你在学习过程中取得更好的进展。由于我写的关于 MLOps 的内容,本教程是一个很好的起点,所以让我们开始吧!
目录:
· 1. 介绍
· 2. MLOps 工作流程
· 3. MLOps 原则
· 4. MLOps 工具
· 5. MLOps 最佳实践
· 6. 结论
我的 MLOps 教程:
-
教程 1:MLOps 的关键起点:探索其核心组件
-
教程 2:面向初学者的 MLOps 工作流程介绍
-
教程 6:实践中的测试:代码、数据和 ML 模型
-
教程 7:实践中的追踪:代码、数据和 ML 模型
[我将在发布相关文章时更新此列表]
1. 介绍

随着在日常任务和应用程序中使用机器学习模型的需求增长,集成这些模型到软件中出现了若干挑战。因此,专业人士和研究人员提出了 MLOps,作为 DevOps 在机器学习模型上的扩展,以克服这些挑战。MLOps 定义为:
一套用于以高效、优化和有组织的方式设计、构建和部署机器学习模型的技术和实践。(你可以在我的文章中阅读更多关于 MLOps 的动机和定义:MLOps 初学者友好介绍)
从前面的定义中,我们可以看出有很多“东西”需要了解和学习。开始你的学习之旅可能会很困难,尤其是对初学者来说:你可能会想,这些“东西”是什么?每个“东西”有什么用途?我从哪里开始学习?
为了回答所有这些问题,我决定编写这个教程。要涵盖的主题很多,已经写过或计划将来写的文章也很多,因此我决定将它们归纳在一起,组织成四个不同的主要组别。我将这些称为 MLOps 的主要成分!
嗯,我知道,我知道!我的回答引出了一个新问题:这些成分是什么?简单来说,就是MLOps 工作流、MLOps 原则、MLOps 工具和 MLOps 最佳实践。在本教程中,我们为每个组成部分专门设立了一个章节。
2. MLOps 工作流

MLOps 工作流和生命周期(箭头的方向)。
MLOps 工作流定义了从业务问题到模型部署和性能监控的项目流程。它包括以下步骤:
-
业务问题: 代表了 MLOps 工作流中的第一步。它包括定义模型工作流、人工智能画布和/或机器学习画布,在这些画布中,我们指定模型的输入和输出,以及过程及其各种步骤。
-
数据工程: 包括一系列从收集原始数据到准备好的数据集的不同步骤,这些数据集可以被 ML 模型使用。
-
ML 模型工程: 包括一系列关键步骤,用于定义和选择符合业务问题要求的 ML 模型。它涉及:模型训练、模型评估、模型测试和模型打包。
-
代码工程: 是 MLOps 工作流中的第四步。这个阶段专注于将模型部署到生产环境,并通过观察模型的整体行为以及记录和保存预测结果来确保其预测质量。
示例: 自动驾驶车辆通常使用计算机视觉技术的组合,具体取决于任务和要求。对于本教程,假设你正在开发一个图像分类系统,用于识别各种道路标志、车辆、行人和道路状况。因此:
-
在业务问题中: 我们定义第一个问题是使自动驾驶车辆能够准确感知和解读其周围环境。更具体地说,使其能够识别和分类道路标志、车道边界、行人和其他车辆,以做出明智的驾驶决策。其次,系统还必须优先考虑安全,不仅要考虑车上乘客的安全,还要考虑所有道路使用者的安全。因此,根据系统的实时输出和情境感知,预定义和建立了一系列动作,如减速和其他与安全相关的响应。
-
在数据工程中, 第一步是收集一个多样化且具有代表性的图像数据集,这些图像应在不同的天气和光照条件、道路类型以及交通场景下拍摄。由于这是一个分类任务,高质量的标记数据对于训练和评估至关重要。然后,通过处理数据质量问题(如数据不平衡)以及准备调整、规范化和增强图像的脚本来准备数据。最后,选择一个数据存储和管理系统,以便于访问训练和验证数据。
-
在机器学习模型工程中, 我们首先选择适当的深度学习架构,如卷积神经网络(CNN),用于图像分类,并尝试各种模型架构和超参数(你可以查看我的教程,了解图像分类的实际介绍)。其次,我们在数据集上训练选择的模型,同时优化准确性,并考虑效率和推理速度,因为自动驾驶车辆需要实时处理。然后,我们应用严格的评估指标,如准确性、精确度、召回率和 F1 分数,以评估模型性能;并实施交叉验证技术和微调。在打包模型之前,我们将其调整为适应车辆硬件的实时推理,考虑延迟和资源限制。
-
在代码工程中, 那些经过严格训练和评估的模型被集成到自动驾驶车辆的感知系统中。在实时推理过程中,我们确保模型预测的质量,进行广泛的测试和监控:观察和记录模型在各种实际驾驶场景中的行为。这些行为用于检测异常和任何意外或错误的模型行为。
在开发过程中以及部署后,可能需要返回到之前的步骤,以保持模型的长期性能。因此,这些阶段通常以循环的方式进行。这后一种定义了 MLOps 生命周期!为了维护 MLOps 生命周期,理解并尊重 MLOps 原则是必须的。欲了解更深入的信息或详细见解,请参阅我的教程:适合初学者的 MLOps 入门。
3. MLOps 原则

MLOps 原则是一组旨在维护 MLOps 生命周期以避免/减少技术债务的概念。它们需要在之前描述的不同工作流步骤中应用。这些原则包括:
-
版本控制:确保对机器学习模型、代码和数据所做的更改得到有效跟踪和管理,使工程师和数据科学家能够在需要时回滚到之前的版本,比较不同版本之间的性能,并重现结果。
-
测试:确保 MLOps 管道的正确性和效率,从而减少错误和 bug 的风险,并能够快速检测和修复问题。
-
自动化:使重复任务的高效和可靠实施成为可能,并标准化包括构建、测试、部署和管理在内的过程。它定义了项目的成熟度,简单来说,就是定义系统从人工干预中解放的程度。
-
监控和跟踪:确保生产中 ML 模型的稳定性和可靠性。它们帮助工程师和数据科学家跟踪对模型和数据所做的更改,促进协作并在问题出现时进行故障排除。
-
可重复性:允许在执行地点不同的情况下,给定相同的输入生成相同的结果。通过之前的原则来保持结果的一致性和可信度,并满足监管或合规要求。
成功实施这些原则需要利用合适的工具和遵循最佳实践,我们将在接下来的两个部分中介绍。欲了解更深入的信息或详细见解,请参阅我的教程:MLOps 原则简介。
4. MLOps 工具

MLOps 工具包括所有用于建立和维护 MLOps 工作流和原则的软件、硬件和系统。主要包括:
-
编程语言:如 Python 和 R。
-
库:如机器学习和深度学习库(Scikit-Learn、OpenCV 和 Keras)。
-
平台:如机器学习和深度学习平台(MLflow 和 Jina)。
-
框架: 如机器学习和深度学习框架(LightGBM 和 CNTK)。
-
Notebook 环境: 如 Jupyter 和 Google Colab。
-
基础设施: 如基于云计算和本地部署的基础设施。
选择合适的工具取决于几个标准,如项目需求、数据类型和量以及部署目标。然而,必须了解 MLOps 实践应该独立于这些工具,换句话说,它应以独立于编程语言、机器学习框架或基础设施的方式设计和实施,以确保 MLOps 实践具有通用性、适应性,并且不锁定于任何特定的技术栈。
5. MLOps 最佳实践

MLOps 最佳实践是一组经过验证的行为、规范、技术或/和方法,这些实践在 MLOps 中已证明能持续产生良好的结果和效果。在机器学习项目中工作时,您会发现自己在编写代码和协作,因此有三个关键要素:代码、团队合作,当然还有您自己!因此,我将它们分为三个不同的组:代码最佳实践、团队合作最佳实践和个人发展最佳实践。
代码最佳实践:
-
最重要的实践是尊重 MLOps 工作流并适应其原则。没有描述的工作流和原则,就没有 MLOps。
-
许多工具是专门为 MLOps 应用设计的,因此,技术栈的慎重选择至关重要。这个选择不仅有利于工作流中的和谐,还能确保兼容性,在大多数情况下,无需编写可能上千行的代码。
-
代码和资源优化提升用户体验,降低运营成本,确保应用程序高效运行,特别是在资源受限的环境中,如移动设备或云计算平台。
团队合作最佳实践:
-
数据科学家、工程师和领域专家之间的合作。有效的跨职能团队对于 MLOps 的成功至关重要。
-
建立编码规范有助于确保代码库的一致性、可读性和可维护性,同时促进团队成员之间的有效协作。
-
项目结构的优化提高了团队合作效率,帮助创建一个组织良好、协作高效、富有生产力的环境,使团队成员能够高效地协同工作并促进项目的成功。有关更深入的信息或详细见解,我邀请您查阅我的教程:从 MLOps 角度结构化您的机器学习项目。
-
记录代码和项目流程可以确保所有团队成员都能获取到关键信息,理解彼此的贡献,并有效地协作。此外,这还促进了知识转移,减少了误解,并支持了项目的维护和可持续性。
-
在代码中加入注释,以解释代码的目的、假设和限制,是代码文档的另一个重要方面。
个人发展最佳实践:
-
保持自己对最佳实践和最新技术的更新。
-
了解文化,探索新方法和技术。
-
对于最佳实践保持开放态度,因为它们可能会随着技术进步、行业标准变化或新研究的发展而不断演变。
-
在专业社区中分享最佳实践,以帮助他人受益于经过验证的方法,并为集体知识做出贡献。
最后但同样重要的是,这些实践只是星空中的一部分,但它们是最亮的星星!
6. 结论
本文到此结束。希望你觉得它有价值并具有启发性!我撰写本文的目的是为你提供一个扎实的 MLOps 基础。在即将到来的文章中,我将深入探讨 MLOps 工具和最佳实践。此外,我还计划分享各种 MLOps 技术的教程和实际示例,请继续关注更多有价值的内容。
感谢阅读本文。你可以在我的GitHub 主页找到我提供的各种教程示例。如果你喜欢我的教程,请通过关注我和订阅来支持我。这样,你将收到关于我新文章的通知。如果你有任何问题或建议,欢迎留言。
图片来源
本文中所有未在说明中提及来源的图片和图表均由作者提供。
初学者的最大似然估计(附 R 代码)
原文:
towardsdatascience.com/a-laymans-guide-to-maximum-likelihood-estimation-with-r-code-9e992a10ecd9
最大似然方法的直观解释
·发布于 Towards Data Science ·4 min read·Mar 14, 2023
--
最大似然原理是数据科学、机器学习和人工智能中大量模型的基本估计方法。它适用于从分类的 logit 模型到深度学习中的信息理论等各种方法。本文旨在提供对该原理的直观介绍。
假设你有三个数据点 x = (1, 2, 3),你认为它们是从均值未知(μ)且标准差为 1 的正态分布中生成的,即 N(μ,1)。
考虑这些数据点,最可能的μ值是什么?
这是最大似然估计方法旨在回答的问题。

图片由作者创建
假设研究人员考虑了μ = (0, 2, 6)这三个可能的值作为候选值。哪一个值最有可能是观察到的数据的值?
上图分别用蓝色、黑色和绿色绘制了 N(0,1)、N(2,1)和 N(6,1)的三个正态分布概率(密度)函数。即,它们分别是 f(X|μ=0)、f(X|μ=2)、f(X|μ=6)。
作者创建
底部的红色方块点表示 x=(1, 2, 3),这些是观察到的数据点。
从上面的图表可以明显看出,数据点 x 最有可能来自 N(2,1)。它们来自 N(0,1)的可能性相当低,来自 N(6,1)的可能性更低。因此,我们可以说μ = 2 的值与 x = (1, 2, 3)的兼容性最高。
如果我们考虑所有其他可能的μ值,并且确信 2 最有可能生成 x,那么它就是μ的最大似然估计。
让我们定义一些数学细节:
f(X1,X2,X3|μ):联合 概率 密度函数,给定 μ。它展示了在给定 μ 的情况下 X 的概率密度。
L(μ|x1,x2,x3):似然函数,给定 x。它展示了在给定观察数据 x=(x1, x2, x3) 的情况下 μ 的似然。
区别在于密度函数是通过随机变量 X 索引,给定如 μ 这样的参数值;而似然函数是通过参数索引,给定观察到的数据 x。
这两个函数的关系为
L(μ|x1,x2,x3) = k f(x1,x2,x3|μ)。
其中 k > 0 是任何常数。为了简便起见,我们假设 k = 1。那么这两个函数几乎相同,区别在于它们的参数和条件值。如果我们为简单起见假设 X 们是独立的,那么我们可以写出(因为在独立情况下联合概率是个体概率的乘积)
L(μ|x1,x2,x3) = f(x1|μ) × f(x2|μ) × f(x3|μ)。

图片由作者创建
上表显示了当 x=(1, 2, 3) 时似然函数 L(μ|x1,x2,x3) 的值:最后一列列出了这些值的乘积。最大似然值出现在 μ = 2。
现在我们考虑 μ 的所有可能值,并将似然和对数似然函数作为 μ 的函数进行绘制。对数似然函数定义为
l(μ|x1,x2,x3) = log[L(μ|x1,x2,x3)],
其中 log() 是自然对数函数。对数似然是似然函数的单调变换。它被广泛使用,因为它在分析上是可处理的,具有可加性和线性。

图片由作者创建
上述函数如图所示。从上面的图中可以清楚地看到,似然或似然对数在 μ = 2 时最大,这也是 x = (1, 2, 3) 的最大似然估计。
在分析上,可以证明样本均值是从 N(μ,1) 独立生成的样本的最大似然估计量,而 x = (1, 2, 3) 的样本均值确实是 2。
计算和绘图的 R 代码如下:
x = c(1,2,3) # Data observed
X=seq(-5,9,0.01) # X range
par(mfrow=c(1,1))
# plot density functions
plot(X,dnorm(X, mean=2, sd=1), type ="l",
col="black", lwd=2, add=TRUE, yaxt="n",ylab="density")
curve(dnorm(x, mean=0, sd=1), type ="l", col="blue",
lwd=2, add=TRUE, yaxt="n")
curve(dnorm(x, mean=6, sd=1), type ="l", col="green"
, lwd=2, add=TRUE, yaxt="n")
# points for data X
points(x,c(0,0,0),col="red",pch=15)
legend("topleft", legend=c("N(0,1)", "N(2,1)","N(6,1)"),
col=c("blue", "black","green"), lty=1,cex=1,lwd=2)
abline(v=c(0,2,6),col=c("blue","black","green"))
# Calculation of Likelihood vales at different mean values
prod(dnorm(x,mean=0,sd=1))
prod(dnorm(x,mean=2,sd=1))
prod(dnorm(x,mean=6,sd=1))
# Plotting likelihood and log-of-likelihood
m=seq(-4,8,0.1)
m1=rep(0,length(m))
m2=rep(0,length(m))
for(i in 1:length(m)) {
# Likelihood
m1[i]=prod(dnorm(x,mean=m[i],sd=1))
# log-of-Likelihood
m2[i]=sum(log(dnorm(x,mean=m[i],sd=1)))
}
# plotting
par(mfrow=c(1,2))
# Likelihood
plot(m,m1,type="l",ylab="L(mu|X)",xlab = "mu",lwd=2)
abline(v=2,col="red")
# Log-of-Likelihood
plot(m,m2,type="l",ylab="log of L(mu|X)",xlab = "mu",lwd=2)
abline(v=2,col="red")
总之,最大似然估计方法被广泛应用于数据科学中的许多模型和方法。其概念和原理通常未被研究人员和实践者完全理解。本文旨在提供一种直观的解释,而不介绍分析细节。
感谢阅读!
请关注我,获取更多有趣的故事!
通过示例了解精益数据管道
原文:
towardsdatascience.com/a-lean-data-pipeline-by-example-e08bfce58133
强大的云数据平台,如 Snowflake 和 Databricks,改变了我们对标准形式和数据仓库的思考方式
·发表于Towards Data Science ·7 分钟阅读·2023 年 4 月 18 日
--

图片由David Di Veroli拍摄,发布在Unsplash上
介绍
在最小化云数据管道成本时,首先想到的是寻找最便宜的 ETL 解决方案。例如,Databricks 是否比 Snowflake 便宜?我应该使用云服务还是在内部集群上本地运行 Spark?
更好的成本最小化方法是利用精益原则。精益创业原则之一是通过小步骤进行验证[1]。对于云数据管道来说,关注价值尤为重要,因为每一次规范化和维度都需要花费金钱。此外,50 年的数据管道经验提供了大量我们可以做的转换,但只有对客户的分析价值才能定义我们应该做的事情。
在这篇文章中,我描述了如何通过专注于发展业务价值来进行成本最小化,即仅在需要回答特定业务问题时才转换数据。这样,你可以根据独特的业务案例而不是成本最小化的练习来选择大数据平台。只要你了解在最小化数据转换中的权衡,如果你的业务用例不需要,就最好避免进行转换。
一个(过于)简单的问题
因为我想说明一个想法,而不是建立实际的数据管道,所以我将以一个玩具问题来演示我们如何实现一个多阶段管道,然后假设一个转换管道是不必要的。
玩具业务用例是,我想使用在另类电台中播放的顶级艺术家来构建一个个人音乐播放列表。因此,我现在有一个商业问题需要提问,这将指导任何后端开发活动并限制其范围。
自 2020 年 3 月以来,另类电台的前 10 名艺术家有哪些?
ChatGPT 对这个问题的回应是:“对不起,作为一个语言模型,……” 然后将我指向 Billboard Top 100 的另类电台榜单,这同样没有帮助。我想知道电台上播放的内容,并使用准确的数据为我提供个人播放列表的新想法。
准确的数据,真实的模型,真实的答案
公共 API 端点用于下载播放列表数据是丰富的。开始于
作为数据工程师,我们确定数据源,并专注于将播放列表数据干净地下载到我们喜欢的云提供商的数据湖中。这个过程中的每一步都必须保持数据的清洁。也就是说,每一步都应确保数据完整且无重复。像 dbt 这样的现代工具具有内置功能,可以使这一过程变得简单[8]。
播放列表通常具有基于播出日期的模式以及“艺术家、专辑、歌曲和节目”等属性。节目是另一个基于时间的实体,跟踪每位 DJ 在电台的表现。API 生产者可能会反映他们的数据库,最佳实践是将数据按原样同步到你的数据湖中。关键是我们从 API 接收到的数据足够用于分析。
一个简单的 Python 代码可以将原始数据从网络同步到数据湖。因此,回答问题的第一步是展示单次播放的数据是怎样的。

JSON 输出示例快照(来源 KEXP API)
关键策略是同步来自 API 的所有数据,并确保我们能够定期同步数据。
让你的数据转换选择具有目的性
一旦数据同步到数据湖中,它将会有类似于源头的数据输出。数据湖会以原始形式捕获所有数据,以便在需要时随时使用。数据湖需要一个严格的结构和治理,以保持数据的清晰界限和安全性[9]。
当数据在数据湖中时,你准备好建立一个数据仓库(或者如果你愿意,可以是数据湖仓)。数据仓库有丰富的理论,数据质量至关重要。如果你回答了问题,但问题本身是错的,那比不回答问题更糟。因此,完全理解你能做什么与知道你应该做什么一样重要。
我喜欢将对历史数据仓库的全面理解视为我数据管道中的“未来防护知识”。我可能不需要每一个转换,但如果当前数据无法解决新的客户问题,我需要有一个预想的转换。
规范化和清理
标准范式 (1NF、2NF 和 3NF) 是最常见的数据转换的基础 [2]。
第一范式 (1NF) 规定每一列应只有单一值,因此列中不应有数组或用逗号分隔的列表。保持所有列值为原子的原因很充分,因为在同一查询中进行聚合和展开会导致笛卡尔连接。然而,现代的数据仓库/湖泊架构支持复杂类型,如列表和映射。复杂结构对于维护数据血缘关系和在管道下游提供灵活性非常有益,并且受到像 Snowflake 和 Databricks 这样的商业应用的良好支持。
第二范式 (2NF) 规定将 1NF 的数据根据一组属性去重。在大数据世界中,我们不能依赖系统强制唯一性,因为分布式数据处理的原因。数据在多台机器上的列中处理,我们希望避免全局操作,因为这些操作在时间和金钱上都是昂贵的。在现代大数据世界中,我们按处理时间收费,因此管理效率至关重要。
第三范式 (3NF) 规定将 2NF 的数据移入一个单独的表中,每个表都有一组特定于其领域的数据,并且一个外键将特定表中的数据与原始数据相关联。这有助于性能,因为 3NF 中的数据只需要在一个地方查询,外键可以用于生成报告。
Kimball 数据仓库模型
Kimball 数据仓库(维度模型)是《Kimball 数据仓库工具包》中描述的星型模式 [3]。它扩展了 3NF 的思想,并以度量或衡量指标作为数据的核心分析组件,以维度作为这些度量相关实体的属性集合。它通常被认为是数据分析的黄金标准。
Kimball 数据仓库是为关系型数据库设计的,因此实施 Kimball 模型的所有方面可能是可选的。例如,在分布式 Spark 环境中,顺序替代键可能会成为需要避免的障碍 [4]。如果你可以在没有替代键的情况下回答所有客户的问题,那么你就不需要它们。
使用分析 GUI 验证小增量
“建造它,他们就会来”并不是数据产品的一个好模型。大多数数据项目的潜在来源和分析用例的数量庞大,很容易陷入专注于后端而排除客户的陷阱。
回到成本控制:每次转换和每次管道执行都会花费金钱。高效的数据管道尽可能少做。如果我们能够回答业务问题:停止。展示给客户。获取他们的反馈。回答下一组问题。
一个好的分析图形用户界面(GUI)非常有价值,因为它让公众可以访问数据。此外,它让客户可以询问有关数据的问题。客户想要的解决方案将会是关于引入新数据的。
我使用了 Tableau,并为简单应用发布了一个具体答案。
自 2020 年 3 月以来,替代电台上的十位艺术家是谁?(来源 作者的公开表格)
在这个例子中,我不需要进行任何转换来实现预期的用例。当你能够回答业务问题时,停止数据转换,然后专注于下一个业务问题是合适的。
此外,通过像 Tableau 这样的图形用户界面(GUI)为客户提供对数据的完全访问权限,然后他们可以自己回答以下一些问题。关键思想是,你发布数据,让客户可以自行探索并提供反馈。
当你不能用现有数据回答问题时,获取更多数据——迭代——重复。
基于分析的数据产品的增量好处
分析驱动的数据产品的最强有力的论据是,用户在每次迭代中都能感受到价值。较不明显的好处是将关注点与数据湖分开。数据湖可以作为无损数据存储与生成用例并行增长。交付用例的工程师可以随着他们满足更复杂的问题,拉取越来越多的数据。
例如,在玩具问题的用例中,播放列表数据可以持续更新,同时我们从如 SeatGeek [10] 等来源加载票务数据。即使我们不立即使用它,更多的数据也有助于建立价值。利用数据湖与大数据平台结合的力量,即使是在玩具问题中也会显现出来。
结论
数据云改变了我们收集和使用数据的方式[5]。我们应该专注于将数据导入云端,并利用 Snowflake、Redshift 或 Databricks 等工具查询数据。在这种情况下,我们可以根据我们能想象或通过机器学习等技术建立的关系,发掘出更多的价值。
在构建数据产品时,至关重要的是通过分析用例拉动后端功能,仅构建所需的数据结构,最小化成本,并确保您的数据产品提供客户价值。
参考文献
-
E.F. Codd, (1970) 大规模共享数据银行的关系模型
-
Kimball, Ross (2013) 数据仓库工具包
-
M. Karasou (2019) 向 Spark 数据框添加顺序 ID
-
Seatgeek (2023) 客户 API
处理大数据集的小技巧,适用于有限内存
Pandas 默认设置并不理想。一个小小的配置就可以将你的数据框压缩到适合你的内存。
·发布于 Towards Data Science ·阅读时间 8 分钟·2023 年 1 月 19 日
--

你可以在不丢失属性的情况下压缩一个巨大的 Pandas 数据框,就像挤压一个汉堡一样。通过这个小技巧节省内存,并提高工作效率。— 图片由 Leonardo Luz 提供
我从未认为我的代码需要改进。我总是抱怨内存不足或数据集过大无法处理。
我常用的解决方案是将数据放在 Postgres 数据库中,并编写 SQL 查询。毕竟,这是一种处理大规模数据集的可接受方式。每当我获得一个大数据集时,我都会这样做。
但我无法获得 Python 中的完全灵活性。因此,我不得不将两者结合使用并交替进行。例如,我将数据集加载到 SQL 数据库中,并编写一个 Python 脚本来运行 SQL 查询,通常使用 Sqlalchemy。
虽然这让我在两者之间拥有灵活性,但我在与其他团队成员共享代码时遇到了问题。其他成员应该具备关系数据库的知识和设置。
这个问题的经典解决方案是增加内存并进行并行任务。在基础设施方面,迁移到云端是我最喜欢的解决方案。我可以只为高性能资源的使用付费。而在并行执行方面,像 Dask 这样的技术可以为我提供支持。
然而,我最近发现代码优化是首选且通常是最有成效的解决方案。如果你的代码在单线程下表现不佳,我们如何保证并行化能够提升性能?如果你的代码没有充分利用本地计算机的资源,那迁移到云端又有什么意义呢?
在我学到的许多 Pandas 优化技术中,有一种特别深刻。
诀窍是…
使用正确的数据类型来节省内存。
Pandas 根据每列中存在的值来推断数据类型。但它没有上下文。而你有!
如果列中的所有值都是整数,Pandas 通常会将 int64 分配为该列的数据类型。然而,还有更多的 int 变体可供使用,例如 int8、int16 等。
对于大多数情况,默认设置是可以的。但有时,对于较小的值,这个占位符过大。打开一个笔记本,输入以下代码并查看其输出。
import numpy as np
np.iinfo('int64')
# >> iinfo(min=-9223372036854775808, max=9223372036854775807, dtype=int64)
Int64 可以容纳从 -9223372036854775808 到 9223372036854775807 的值。我甚至不知道这些数字怎么称呼。
但在大多数实际情况下,你可以推测可能的列值。例如,在酒店预订系统中,房间中的客人数几乎总是个位数。如果是宿舍,可能是两位数。
标准的 Python 库可以告诉你每一行的内存使用情况和执行时间。
为什么不使用 int8 呢?它可以容纳从 -128 到 +128 的值。这个更小的占位符相比浪费的 int64 占用更少的空间。
我们在一个示例数据集上测试一下这个方法。
以下脚本生成了一个随机数据集。在实际生活中,你不会生成数据集。注意它的数据类型和内存使用情况。
import pandas as pd
import numpy as np
size = 1000000
df = (
pd.DataFrame(
{
"room_rate": np.round(np.random.randint(35, 100, size) / 23 * 100, 2),
"number_of_guests": np.random.randint(0, 6, size),
"channel": np.random.choice(
["Online", "Corporate", "Walk In", "Complementary"], size
),
"booking_status": np.where(
np.random.randint(0, 100, size) > 90, "Canceled", "Not-Canceled"
),
}
)
# .pipe(lambda df: display(df) or df)
.pipe(lambda df: display(df.dtypes) or df)
)
"""
room_rate float64
number_of_guests int64
channel object
booking_status object
dtype: object
"""
df.memory_usage(deep=True)
"""
Index 128
room_rate 8000000
number_of_guests 8000000
channel 65748930
booking_status 68640436
dtype: int64
"""
df.memory_usage(deep=True).sum()
"""
150389494
"""
df.describe(include='all').fillna('-')
"""
| | room_rate | number_of_guests | channel | booking_status |
|:-------|:------------------|:-------------------|:----------|:-----------------|
| count | 1000000.0 | 1000000.0 | 1000000 | 1000000 |
| unique | - | - | 4 | 2 |
| top | - | - | Corporate | Not-Canceled |
| freq | - | - | 250099 | 910109 |
| mean | 291.2777862699999 | 2.50315 | - | - |
| std | 81.59777471018273 | 1.708107430793215 | - | - |
| min | 152.17 | 0.0 | - | - |
| 25% | 221.74 | 1.0 | - | - |
| 50% | 291.3 | 3.0 | - | - |
| 75% | 360.87 | 4.0 | - | - |
| max | 430.43 | 5.0 | - | - |
"""
生成的数据集有一百万条记录,占用约 180 MB 的内存。room_rate 变量的范围是 152.17–430.43\。但它被分配了 float64 数据类型。实际上,float16 已经足够了。同样,将 number_of_guests 转换为整数,将 channel 转换为类别。
有趣的是,预订状态是以分类方式表示的布尔值。为了分析目的,我们可以将其更改为布尔值。但也有人可能会争辩说还是将其保留为类别更好。
下面是我们如何将数据类型转换为更理想的类型,以及现在需要多少内存。
(
df.assign(
room_rate=df.room_rate.astype("float16"),
number_of_guests=df.number_of_guests.astype("int8"),
channel=df.channel.astype("category"),
booking_status=df.booking_status == "Canceled",
)
.pipe(lambda df: display(df.dtypes) or df)
.pipe(lambda df: display(df.memory_usage(deep=True)) or df)
.memory_usage(deep=True)
.sum()
)
"""
room_rate float16
number_of_guests int8
channel category
booking_status bool
dtype: object
Index 128
room_rate 2000000
number_of_guests 1000000
channel 1000435
booking_status 1000000
dtype: int64
5000563
"""
我们通过将数据类型转换将 180MB 的数据集减少到 5MB。这大约节省了 96%。
压缩数据集并将其存储为最佳格式以备后用是个好主意。
## CSV 文件被高估了!我放弃了一些它的优点以获得更多的好处。
我使用了什么来获得更小的文件大小和更好的性能。
如何为每一列选择数据类型?
Pandas 的默认数据类型几乎总是不最优的。正如我们在上一节中看到的,整数无论范围如何,都被分配为 int64。对于较小的数据集,我们不需要担心这个问题。即使在数十万行数据中,性能影响通常也不显著。
但是当我们的计算变得复杂且数据集规模更大时,我们应该更认真对待这个问题。挑战在于在不丢失信息的情况下,分配尽可能小的占位符。
第一步是获取数据集的描述。它可以在一个地方提供大量的信息。查看我们上面示例中的 describe 输出。
对于非数值变量,你将获得唯一值的数量、最频繁的值及其计数。看着这些数据,我们可以猜测 channel 可能是一个类别,而 booking_status 可能是布尔值。因为在我们 1M 的记录中,只有 4 个独特的渠道在重复出现。
对于数值列,我们得到最小值和最大值。通过查看四分位数值,你可以得出这个列是否需要是整数或浮点数。在每种情况下,你也可以决定其大小。
这是一个数值数据类型及其值范围的表格。
__Integer Types__
| dtype | min | max |
|:--------|---------------------:|---------------------:|
| int8 | -128 | 127 |
| int16 | -32768 | 32767 |
| int32 | -2147483648 | 2147483647 |
| int64 | -9223372036854775808 | 9223372036854775807 |
| uint8 | 0 | 255 |
| uint16 | 0 | 65535 |
| uint32 | 0 | 4294967295 |
| uint64 | 0 | 18446744073709551615 |
__Float Types__
| dtype | min | max | resolution |
|:--------|------------------:|-----------------:|-------------:|
| float16 | -65504 | 65504 | 0.0010004 |
| float32 | -3.40282e+38 | 3.40282e+38 | 1e-06 |
| float64 | -1.79769e+308 | 1.79769e+308 | 1e-15 |
仔细观察你的数据集,并选择能够工作的最小值。
对于非数值数据集,Pandas 通常将其类型指定为 ‘object’。对象数据类型在大多数情况下是复杂且低效的。因此,如果列值不是自由格式的,你可以将其数据类型指定为 ‘category’。以下是它节省了多少内存:
df.channel.astype('object').memory_usage()
# >> 8000128
df.channel.astype('category').memory_usage()
# >> 1000332
通过简单地转换为类别数据类型,你可以节省大约 8 倍的内存。
## 如何将 Python 数据管道的速度提高到 91 倍?
一个 5 分钟的教程,可能会为你的大数据项目节省数月的时间。
towardsdatascience.com
转换数据类型需要更多的领域专业知识。
我们的演示过于简单,因为我们生成了数据集。但是现实世界中的数据集通常是复杂的,类型转换也是如此。
我们可以在任何数据框上使用 .astype 方法直接转换类型。但在我的例子中,我使用了 assign 方法。因为我们通常需要在进行类型转换之前修改列。
想象一个情况,当 number_of_guests 列有空值时。我们不能在没有填补合理默认值的情况下将该列转换为 int8。零可能没有意义,因为不可能有一个没有人的预订。我们可能认为 1 是一个好的默认值。但是,如果大多数酒店的预订有两个人,那么 2 更好。我们还可以引入 channel 变量来仔细选择每个渠道的默认值。
以下示例是条件分配默认值的方式。注意,我们仅在填补适当的缺失值后才将其转换为正确的数据类型。
df.assign(
number_of_guests=np.where(
df.channel == "Online",
df.number_of_guests.fillna(2),
np.where(
(df.channel == "Corporate") | (df.channel == "Walk In"),
df.number_of_guests.fillna(1),
df.number_of_guests.fillna(3),
),
).astype("int8")
)
同样,选择默认值需要领域知识,这在数据集中并不存在。
另一个挑战是未来的值。如果你进行离线分析,你可以仅通过查看数据集来选择数据类型。但如果你的代码在数据管道中运行,你还应考虑未来的值。
我正在将我所有的 ETL 工作从 Airflow 迁移到这个超级酷的框架。
当你获得不兼容的列值时,你可能会得到错误或错误的值。请参见以下示例。
s = np.random.randint(0, 255, 10)
pd.DataFrame({"original": s, "int8": s.astype("int8")})
| original | int8 |
|-----------:|-------:|
| 221 | -35 |
| 184 | -72 |
| 216 | -40 |
| 177 | -79 |
| 9 | 9 |
| 19 | 19 |
| 16 | 16 |
| 191 | -65 |
| 6 | 6 |
| 71 | 71 |
请注意,任何小于 128 的值已正确转换。但 128 及以上的值转换不正确。
或许你应该在将数据集传递给转换之前进行验证。
这里是你如何确保 Pandas 数据框在数据管道每个阶段的质量
结语
作为数据专业人士,我们处理的是大规模的数据集。我们对性能问题的终极解决方案是增加更多资源和并行执行。
我发现一些优化可以让我们在较小的计算机上处理更大的数据框。诀窍是分配正确的数据类型。
选择正确的数据类型需要领域知识。此外,如果你正在构建数据管道,你还应考虑未来的值。在离线分析中你不需要担心这一点。
希望这对你有所帮助。
谢谢你的阅读,朋友!在 LinkedIn、Twitter 和 Medium 上跟我打招呼吧。
还不是 Medium 会员?请使用这个链接 成为会员,因为你没有额外费用,我会因为推荐你而获得小额佣金。
一种机器学习方法预测胶质母细胞瘤患者的 MGMT 甲基化状态
肿瘤学中的放射组学
·
关注 发表在 Towards Data Science · 5 分钟阅读 · 2023 年 7 月 26 日
--
介绍
今天,我们将探讨一项针对胶质母细胞瘤患者的研究,该研究发表在《自然》杂志的 Scientific Reports 上:通过优化放射组学特征使用基于遗传算法的机器学习方法改善胶质母细胞瘤 MGMT 甲基化状态预测。该研究的目标是尝试预测 O6-甲基鸟嘌呤-DNA-甲基转移酶(MGMT)甲基化状态。能够预测这一状态的重要原因在于,它可以很好地指示化疗药物替莫唑胺(TMZ)的效果。
替莫唑胺概述
TMZ 是一种烷化剂,通过损伤癌细胞中的 DNA 来发挥作用,最终导致细胞死亡。TMZ 还使细胞对放射线更敏感。这在癌症治疗中是一个重要因素,因为放射线用于帮助杀死癌细胞。
这项研究旨在通过机器学习寻找预测 MGMT 甲基化状态的新方法。如果成功,这可以帮助缓解目前为获取肿瘤标本而需进行的技术限制和侵入性程序。
由于胶质母细胞瘤(GBM)对患者的致命威胁,处理这种癌症的效率和有效性非常重要。其中位生存期为 14 至 16 个月,占所有恶性中枢神经系统肿瘤的约 45%。
方法
该团队寻求利用两阶段方法来预测适当的 MGMT 甲基化状态。首先是通过消除噪声放射组学特征,然后将分类算法实施到遗传算法中,以帮助识别最佳预测特征。
在这项研究中测试了各种机器学习技术。目的是寻找对预测最有意义的放射组学特征。他们通过从磁共振成像(MRI)的多模态图像中提取放射组学特征来实现这一目标。两阶段特征选择方法从 eXtreme Gradient Boosting(XGBoost)模型开始,随后使用基于遗传算法(GA)的包装模型。GA 模型的工作方式类似于自然选择,通过识别“最适合”的特征集进行预测。
使用的数据是来自癌症基因组图谱的预处理和分割的多模态 MRI 特征。总共包括了 53 名 GBM 患者,并获得了 704 个放射组学特征。
遗传算法的工作流程阶段包括六个不同的步骤:初始种群的生成、适应度评估、父母选择、交叉、突变和下一代的种群替换。用于选择概率的公式(其中特征是根据其在适应度评估阶段的表现进行选择)如下所示:

选择概率公式
一旦从 XGBoost 算法中提取了初始特征,就该使用这些特征进行分类,以预测哪些患者属于 MGMT 甲基化和非甲基化类别,并将其作为适应度评估。他们尝试将三种不同的机器学习算法应用于遗传算法工作流中的适应度评估部分。他们使用了随机森林(RF)、XGBoost 和支持向量机(SVM)。为了实现这些算法,他们利用了 Python 机器学习库 SKlearn。
结果
一旦训练了三种不同版本的算法,他们使用三种不同的测量标准来评估模型性能:准确率、特异性和召回率。为了比较性能,他们使用了运行 20 次交叉验证的平均值,并使用了 Kolmogorov-Smirnov 检验进行评估。
最佳结果是通过将随机森林算法融入遗传算法(GA-RF)实现的。该技术在所有评估指标中都优于其他方法,准确率为 0.925,灵敏度为 0.894,特异性为 0.966。
在 GA-RF 模型完成后,得到了一个最佳的 22 个放射组学特征的子集。这些特征中包括 17 个纹理特征、3 个基于直方图的特征、1 个体积特征和 1 个强度特征。纹理特征可以通过反映空间强度相关性和体素的分布,帮助临床医生定量评估血流、浮肿和坏死的“多区域变化”。直方图特征可以用来说明图像中强度值的频率分布。
扩展的使用案例
为了测试模型的更广泛适用性,团队使用了在新数据集上学到的特征。他们这次使用的数据集是低级别胶质瘤(LGG)患者的数据集。他们直接应用了学到的特征,没有进行任何额外的特征选择。
GA-RF 模型在 LGG 数据集上的结果为准确率 0.75,灵敏度 0.78,特异性 0.62。没有进行任何迁移学习或微调,这些结果还是相当有前景的。
通过在 LGG 数据集上应用这些特征取得强劲表现,研究人员能够展示这些特征有可能被重用于其他类似疾病。
限制
该技术的一个潜在限制是,当患者数量较少时,研究人员可能会遇到高维度相关的问题。这种情况发生在特征数量相对于可用训练数据量较高时。如果发生这种情况,学习数据和目标变量之间的准确关系可能会很具挑战性。
高维度问题在放射组学领域普遍存在,而不仅仅是本研究中的问题。为了克服这一限制,团队使用了交叉验证,以更确定他们得到的结果。
交叉验证概述
交叉验证通过将用于训练和测试的数据分成不同的组来进行每次验证的迭代。通过多次这样做并取结果的平均值,你可以更有信心地认为你的模型能够重复地获得它给出的结果。
在癌症治疗中,临床医生依赖于肿瘤的特征和等级,以便优化化疗、放疗和手术治疗。
结论
如果这一技术继续得到发展并最终被利用,它可能成为医生非侵入性了解患者 MGMT 甲基化状态的一种方式。这些信息可以帮助他们做出更明智的治疗决策,从而有助于改善患者的预后。这也为放射组学在肿瘤学中可能的其他应用打开了大门。
Do, D.T., Yang, MR., Lam, L.H.T. 等。通过优化放射组学特征来改善胶质母细胞瘤 MGMT 甲基化状态预测,使用基于遗传算法的机器学习方法。 Sci Rep 12, 13412 (2022)。
doi.org/10.1038/s41598-022-17707-wEberhart, Karin, Ozlem Oral, 和 Devrim Gozuacik。“第十三章 — 通过抗癌药物诱导自噬性细胞死亡。” ScienceDirect,2014 年,
www.sciencedirect.com/topics/neuroscience/temozolomide#:~:text=Temozolomide%20(TMZ)%20is%20a%20small,damage%20and%20tumor%20cell%20death。访问日期:2023 年 7 月 12 日。国家癌症研究所。‘试验产生了对脑癌治疗有影响的结果。’ 癌症动态博客,2016 年 6 月 9 日,
www.cancer.gov/news-events/cancer-currents-blog/2016/asco-temozolomide-brain。访问日期:2023 年 7 月 18 日。美国癌症学会。“化疗药物的作用机制。” 美国癌症学会,2019 年 11 月 22 日,
www.cancer.org/treatment/treatments-and-side-effects/treatment-types/chemotherapy/how-chemotherapy-drugs-work.html。访问日期:2023 年 7 月 25 日。欧洲医学肿瘤学会。“胶质瘤中的 MGMT 启动子甲基化:ESMO 生物标志物简报。” Oncology Pro,[更新于 2019 年 1 月 18 日],
oncologypro.esmo.org/education-library/factsheets-on-biomarkers/mgmt-promoter-methylation-in-glioma。访问日期:2023 年 7 月 9 日。创作共用许可证链接:
creativecommons.org/licenses/by/4.0/
机器学习工程师的必备工具
从技术和生产力两个方面
·
关注 发表在 Towards Data Science ·4 min read·2023 年 10 月 19 日
--
图片由 JESHOOTS.COM 提供,来源于 Unsplash
大约四年前,我将职业从 SAP 顾问转为数据科学家。在遵循我为自己设计的课程之后,我在一年内获得了一家初创公司的机器学习工程师职位。
将我过去四年所学到的知识、使用过的工具和经历凝结成一篇文章并不容易。不过,我会重点介绍那些特别对我有帮助的内容。
随着我角色的进展,我通过使用各种工具和在工作中遵循一些最佳实践来培养软件开发技能:
-
Git 与版本控制
-
编写可读且清晰的代码
-
探索不同的开发工具
我不仅会在本文中解释它们,还会提到它们如何帮助我提高软技能和生产力。让我们开始吧🚀
Git 与版本控制
Git是一个开源版本控制系统,广泛用于软件开发。它组织项目并管理开发者之间的协作。我在独自工作时并没有使用 Git,而是手动管理我的代码和笔记本🙃
当涉及到协作时,Git 变得必不可少。它有助于跟踪项目进展,并促进协作。
这是一个广泛的学习主题,很多优秀的资源可供参考(比如这个)。今天,我将重点讲解“commit”这个术语,以及它如何帮助我整理思路。
Git 提交就像是对你代码的一个时间快照。
我在早期学习的第一件事之一就是要有组织的 Git 提交和简洁的提交消息。
后来我意识到,提前考虑你的提交及其结构也有助于你组织工作并用更好的逻辑模式设计代码。
这是一个例子,展示了如何在数据科学的背景下组织 Git 提交,来自我最近的一个项目:

作者提供的图片
从协作的角度来看,分解提交——每次代码更改应有一个目的——也将帮助你的同事更快地审查你的代码。
编写可读且清晰的代码
我之前的导师曾经告诉我:“即使是你奶奶也应该能读懂你的代码”——免责声明:这不是从年龄歧视的角度出发,而是意味着每个人都应该能够轻松阅读和理解你的代码。
开玩笑说,反映你的思维过程在代码中并编写自解释的代码将帮助任何人更快地审查和理解你的工作。
我通过工作和阅读几本行业内的书籍学会了如何编写可读且清晰的代码:
-
《代码整洁之道》 由罗伯特·C·马丁著作
-
《软件设计哲学》 由约翰·奥斯特豪特著作。
“所以如果你想快速完成任务,如果你希望你的代码容易编写,就让它易于阅读。”
罗伯特·C·马丁。
以及我最近工作中这一做法的实现:
相信我,你的未来自己会感谢你编写可读且清晰的代码,不仅仅是你的团队!
探索不同的开发工具
当涉及到机器学习模型的实验或原型测试其可行性时,使用笔记本通常是首选。
而 Jupyter 笔记本是一个很好的媒介。
在担任机器学习工程师角色之前,我主要使用 Jupyter 笔记本进行项目开发。当时,我的前团队强制使用 PyCharm,这使我首次接触到集成开发环境(IDE)。
作为一个主要使用一次性笔记本解决方案的数据科学家,我对 PyCharm 的众多功能和用户界面感到有些不知所措。
随着时间的推移,PyCharm 已经成为我的第二天性工具。
其代码补全和错误高亮功能已经成为我不可或缺的助手,为我的生产力做出了很大的贡献。
在 IDE 中掌握 Git 大大提高了我组织工作的能力,加快了我的编码速度,并使我能更高效地专注于手头的任务。此外,每当我几个月后重温代码时,我总是对自己编写的干净代码心怀感激😉
感谢您的阅读!这篇博客文章对我而言具有特别的意义。它标志着我重返博客和技术写作的旅程🤗
对于评论或建设性的反馈,您可以通过回复、Twitter 或 Linkedin 联系我!
机器学习与优化算法的结合
原文:
towardsdatascience.com/a-marriage-of-machine-learning-and-optimization-algorithms-e6c680454f06
模式检测和模式利用如何将彼此提升到一个新的层次
·发布于 Towards Data Science ·阅读时间 12 分钟·2023 年 12 月 2 日
--

我们不应将基准优化和机器学习算法相互比较,而应考虑它们如何相互加强 [图片由 Wedding Dreamz 提供,在 Unsplash 上]
尽管我们大多数人看不到,但优化算法(OAs)无处不在。它们为我们的超市规划货架,为机场制定航班时间表,并为我们提供前往度假目的地的最短路线。特别是精确算法在利用已知结构方面表现优异 — 例如,凸结构 — 即使在具有众多约束的大型决策空间中也能找到解决方案。在过去几十年中,硬件和算法的改进相结合,带来了数百万倍的速度提升。一个可能在 90 年代需要几个月才能完成的规划任务,如今可能只需一秒钟。
同样,机器学习(ML)在过去十年左右取得了惊人的进展。MuZero 展现了在不知道游戏规则的情况下学习超人类游戏策略的能力,图神经网络学习了人眼无法感知的复杂关系,而变换器(Transformers)催生了 ChatGPT 及其竞争对手。这些算法的共同点在于它们都能够从环境中检测模式,无论是文本数据库还是视频游戏。新颖且极其复杂的架构不断被引入,通常解决新的问题并提供无与伦比的性能。尽管取得了诸多成功和突破,对于许多现实世界的问题,端到端的机器学习仍难以取得竞争性结果。定制的优化算法通常仍然优于机器学习,但可能需要大量的计算时间。
这两种方法并不需要竞争。有趣的是,优化算法擅长于利用模式,而机器学习则在检测模式方面表现出色。 不如将它们作为补充的两个部分结合起来,而不是对比它们的优劣,是否更有意义?
在合并优化和机器学习时,通常归结为统计学习被用来以某种形式改进优化例程。这样,我们可以通过利用学到的模式来加速搜索。这种集成解决方案的开发近年来已成为一个新兴的研究领域,未来有许多令人兴奋的可能性。
我们如何将机器学习和优化算法结合起来?
我们已经确定了优化算法擅长利用结构,而机器学习擅长检测结构,因此它们之间存在自然的协同效应。然而,具体来说,什么构成了优化算法与机器学习之间的结合?广义上,我们可以将其分类为以下四个类别:
I. 优化算法(OA)为机器学习(ML)提供输入。 优化算法可能提供一个启发式解决方案,该方案可以通过机器学习进一步改进,或者它可能在算法管道中执行计算密集型的预处理步骤。
II. 机器学习(ML)为优化算法(OA)提供输入。 例如,机器学习可以建议一个用于热启动的起始解决方案,或者学习问题结构供优化算法利用。
III. 机器学习(ML)用于加速优化算法(OA)。 机器学习可以用于迭代检测结构,帮助优化算法更快地找到解决方案。
IV. 优化算法(OA)解决机器学习中的子例程。 像树搜索或动作空间评估这样的例程可以通过优化算法高效执行。
让我们对此进行一些实际的解释。由于数据记录和利用的增加,算法管道变得越来越复杂。它们通常由多个任务组成,有些更适合优化算法,有些则更适合机器学习。因此,一个范式向另一个范式提供输入已经变得非常自然。
此外,公司通常会重复解决特定问题的变体。例如,运输公司可能每天解决一次车辆路径问题,处理在客户、时间窗口和负载大小方面变量但可比的实例。如果通用求解器能够利用这些相似性,优化算法的运行效率将会提高。
最后,由于机器学习作为端到端的范式通常尚未与优化算法相竞争,因此优化某些子程序往往会有所帮助。这通常会加速机器学习算法并增强其竞争力。
一般来说,当以下情况成立时,结合优化算法和机器学习是有意义的:
-
优化算法(或人类专家)在合理的时间内提供解决方案的速度太慢。
-
启发式解决方案仍有改进的空间,或者尚不清楚它们实际效果如何(好或差)。
-
仍需识别良好的解决方案。
-
需要一种快速的近似解决方案。
-
算法流程涉及模式检测和模式探索的元素。
我们将优化算法和机器学习进行背景化处理,然后提供一些说明性的例子。
优化算法
有必要简要介绍优化算法。为了与机器学习集成一致,我们粗略地将其分类为精确算法(最优解但速度较慢)和启发式方法(次优解但更快)。
精确算法
精确算法是用于优化问题的强大技术,能够在可行的解决方案空间内找到可证明的最优解。通常,问题被表述为数学规划(例如,线性或二次规划),可以使用如 CPLEX、SCIP 或 Gurobi 等优化软件进行求解。这些求解器系统地探索解决方案空间,并保证找到最佳解。
尽管现成的方案功能强大,但可以处理的解决方案空间是有限的。对于非常大的问题,我们通常需要投入大量设计工作来设计分支、定价和/或裁剪方案,以减少搜索复杂性并有效地导航解决方案空间,同时保留最优性保证。
启发式方法
启发式方法提供了一种替代问题解决方案,以牺牲最优性保证换取计算效率。启发式方法特别适用于大规模或复杂问题,这些问题在合理的时间框架内寻找最优解是不切实际的。
基本启发式方法提供快速的经验规则解决方案。它们通常会产生次优结果,但通常会在有限的计算时间内返回不错的解决方案。例子包括最近邻插入或 2-opt 交换。
元启发式方法采用更高层次的指导搜索解决方案的策略,通常包含随机性和适应性元素。例子包括遗传算法、模拟退火和粒子群优化。元启发式方法在调整工作和运行时间方面更为密集,但通常能提供比基本启发式方法更优的结果。
机器学习
我们讨论了两种机器学习范式;演示学习和经验学习。尽管与现有分类(无论是监督学习还是强化学习)密切相关,我们在这里旨在更多地连接到优化背景。
演示学习
演示学习通过最小化提供的专家策略与预测策略之间的距离(监督学习)来进行。它可以被视为一个师生模型,其中学生(机器学习模型)旨在模仿老师(例如,精确算法、量身定制的启发式方法或人工制定的解决方案)。需要注意的是,这种学习形式因此需要某种形式的理论或经验知识。
机器学习模型预测的解决方案与已知专家解决方案之间的差异允许计算损失函数,算法随后旨在最小化这个损失函数。如果机器学习算法展示了许多高质量解决方案的例子,我们希望它最终能够复制这些解决方案。
演示学习的缺点在于它无法超越专家策略。如果专家策略是次优的,那么预测的策略也是如此。因此,演示学习只有在获取原始解决方案花费时间过长且希望在较短时间内获得相同解决方案时才有意义。然而,如果生成示例解决方案需要过长时间,这个想法也会失败。这使得演示学习处于一种相对尴尬的境地。
演示学习是一种模仿学习。我们尝试更快地再现专家提供的相同决策。

在演示学习中,机器学习算法旨在尽可能接近专家解决方案[照片来源于Andre Mouton于Unsplash]
经验学习
与演示学习相对,经验学习利用通过部署策略获得的观察数据,旨在改进这些策略。这种学习形式通常与强化学习(RL)相关联,通过观察从环境中获得的奖励迭代执行和改进策略。
与示范学习相比,经验学习不需要关于解决方案质量或结构的先验知识。缺点是——在其最纯粹的形式下——它不利用有关环境的信息,仅通过状态、动作和奖励与环境进行交互。与经验学习相关的常见问题包括陷入局部最优、需要适当地定义奖励以及广泛探索解决方案空间。
经验学习利用来自环境的反馈信号,以便随着时间的推移改进其决策政策。

在经验学习中,ML 算法旨在通过与环境的交互来改进其决策政策 [照片由 Alex Kondratiev 提供,在 Unsplash 上]
请注意,这两种学习形式都有显著的缺点,这在一定程度上解释了为什么端到端机器学习在现实世界问题上仍然面临困难。因此,OA 和 ML 的集成确实是互利的。
当然,可以在一个单一的流程中应用这两种学习形式,或者说这可能更为优越。一个例子是(i)通过示范学习生成初始解决方案,(ii)通过优化求解器进行改进,以及(iii)利用经验学习来指导对最优解决方案的搜索。
MO-OA 示例
接下来是一些具体的例子。以下是基于学术研究的三个代表性例子。
I. 学习分支
也许最为研究透彻的例子是集成了 OA 和 ML 的分支学习,或者更具体地说,是利用 ML 来指导分支限界算法。对于那些不熟悉这个概念的人:分支限界通过系统地探索一个非常大的搜索树来解决整数问题,根据既定的界限修剪那些已被证明是次优或不太有希望的分支。如果子问题的松弛解不包含最优解,那么它的相邻整数也不会包含。
尽管分支限界是一个著名的算法,用于在大动作空间中寻找最优解,但在幕后仍然会做出一些相当基本的启发式选择:
-
变量选择:下一步要分支的变量。一个常见的决策规则是简单地选择最“接近 0.5”的变量进行分支。
-
节点选择:首先处理当前开放的节点中的哪个。极端的做法是总是探索最有希望的节点(最优先)或总是完全探索一个节点的子树(深度优先)。

分支限界过程的例子。选择下一个要探索的节点和要分支的变量通常基于启发式规则 [图片来自 WikiMedia]
说实话,这种简化并未能公正地反映多年来开发出的更复杂的启发式算法。然而,尽管有最终找到最优解决方案的保证,实际搜索过程并没有像预期的那样智能,这是通用求解器的代价。
鉴于树搜索是顺序的且高度动态,用动态学习基础的规则替代静态启发式规则似乎是合理的。例如,节点、分支和树的动态特征可以设计用来预测合适的变量和节点,其值由机器学习进行学习。
过去几年提出了许多解决方案,从离线示范学习到在线经验学习。一个直观的例子是上置信界,基于节点的感知价值和访问次数来平衡探索和利用。
学习分支的例子说明了机器学习可以在精确算法的背景下应用,可能会加快搜索过程,而不牺牲 最优性保证。
基于 Lodi & Zarpellon (2017) 和 Balcan 等 (2018) 的示例
II. 使用 GNN 进行路由学习
路由问题的实例通常通过图表示。客户位置和旅行距离是这些图中明显的属性,但节点和弧线可以有各种标签,例如时间窗口和交通密度。图神经网络(GNNs)是一种强大的技术,用于嵌入并有意义地表示这些图在问题背景中。

车辆路由问题是一个经典的组合优化问题,随着节点数量的增加,规模呈指数爆炸增长 [图片来自 WikiMedia]
传统上,路线是使用精确求解器(例如 Concorde TSP Solver)或元启发式算法(例如混合遗传搜索)生成的。然而,这些求解器需要相当长的运行时间,精确算法在面对大型实例时完全无法提供解决方案。
与其每次遇到新的问题实例时运行优化算法数小时,不如通过检测其解决方案中的模式来学习复制它们。这个例子是一种示范学习,试图最小化计算密集型最优解决方案与机器学习生成的解决方案之间的性能差距。在这个过程中会有一定的质量损失,但解决方案可以在更友好的时间框架内生成。
通过对解决方案运行 GNNs,可以估计边被纳入解决方案的概率。随后,构建一个稀疏的热图,保留最有前景的边。根据这个热图,运行一个动态规划子程序进行逐步构建路线。如果某条路线被其他路线主导,可以进行剪枝。
基于 Kool 等人(2019)和 Kool 等人(2022)的示例
III. 大动作空间的数学编程
在这个最终案例中,数学编程作为强化学习中的一个子程序被部署,目的是处理大动作空间。
组合优化问题有迅速规模爆炸的趋势,例如,访问一组节点的序列数是节点的阶乘。它们通常需要处理许多约束,如卡车的容量和最大驾驶时间或客户节点的时间窗口。枚举所有动作并进行可行性检查变得非常繁琐。
如果问题结构允许,一阶决策问题可以被形式化为线性规划。这种程序可以非常高效地解决,大大扩展了传统 RL 算法——依赖于明确枚举——所能处理的动作空间大小。

线性规划可以高效地探索带约束的大动作空间 [图片来源 WikiMedia]
OA 与 ML 的整合并未止步于此。接下来,考虑 Q 值的近似。在许多现代 RL 应用中,这通常通过深度 Q 网络来完成,该网络通过定义在其输入上执行非线性变换。然而,ReLU 激活函数可以通过分段线性函数嵌入到目标函数中,支持一组附加约束。
另一个 OA 层?我们可以施加领域限制和局部分支来控制动作空间,仅探索我们怀疑(基于 Q 值)最佳动作所在的邻域。
基于 Van Heeswijk, W.J.A. & La Poutré(2020)和 Akkerman 等人(2023)的示例
正如你所见,机器学习与优化算法的整合机会广阔而多样。这已经是一个活跃的研究领域,潜力尚未完全释放。
总结
-
机器学习和优化算法在计算时间、性能保证、知识利用等方面都有缺陷。鉴于这两种范式的性质,它们可能自然互补。
-
机器学习专注于模式检测,优化算法专注于模式利用。它们可以结合起来构建强大的算法管道。
-
在实践中,公司通常会重复解决单一问题,并且这些问题的变异性相对有限。从解决方案数据中提取模式(即统计学习)可以提升现有的精确方法或启发式方法。
-
机器学习可以以示范学习的形式使用(在极短时间内近似优化算法的解决方案)或经验学习的形式使用(与环境互动以迭代地增强策略)。
你可能还会喜欢以下文章:
在强化学习中,大规模和高维的动作空间通常是计算瓶颈。制定……
towardsdatascience.com ## 五种处理强化学习中大规模动作空间的方法
动作空间,特别是在组合优化问题中,可能会变得庞大不便。本文讨论了……
towardsdatascience.com
对于那些对组合优化中的机器学习感兴趣的人,我热情推荐以下由 Andrea Lodi 教授做的主题演讲:
参考文献
Akkerman, F., Luy, J., Van Heeswijk, W.J.A., & Schiffer, M. (2023). 通过动态邻域构造处理大规模离散动作空间。arXiv 预印本 arXiv:2305.19891。
Balcan, M. F., Dick, T., Sandholm, T., & Vitercik, E. (2018 年 7 月). 学习分支。见 国际机器学习会议 (第 344–353 页)。PMLR.
Khalil, E., Le Bodic, P., Song, L., Nemhauser, G., & Dilkina, B. (2016 年 2 月). 在混合整数规划中学习分支。见 人工智能协会会议论文集(第 30 卷,第 1 号)。
Kool, W., Van Hoof, H., & Welling, M. (2019). 注意,学习解决路线问题!国际学习表示大会 2019。
Kool, W., Van Hoof, H., Gromicho, J., & Welling, M. (2022 年 6 月). 针对车辆路线问题的深度策略动态规划。在 国际约束编程、人工智能和运筹学整合会议 (第 190–213 页)。Cham: Springer International Publishing.
Lodi, A., & Zarpellon, G. (2017). 关于学习和分支的综述。Top, 25, 207–236.
Parmentier, A. 和 T’kindt, V.(2023)。基于结构学习的启发式方法解决具有释放时间和完成时间总和的单台机器调度问题。欧洲运筹学期刊,305(3),1032–1041。
Santana, Í., Lodi, A., 和 Vidal, T.(2023 年 5 月)。车辆路径中的局部搜索和交叉的神经网络:可能的过度杀伤?在约束编程、人工智能与运筹学集成国际会议(第 184–199 页)。Cham:Springer Nature Switzerland。
Van Heeswijk, W.J.A. van 和 La Poutré, H.L.(2020 年 12 月)。线性离散动作空间中的深度强化学习。在2020 年冬季模拟会议(WSC)(第 1063–1074 页)。IEEE。
Vazacopoulos, A.(2020)。使用 Python 结合机器学习和数学优化集成。通过 LinkedIn。
数据建模与设计的成熟度模型
数据定义标准化掌握的最佳实践
·
关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 4 月 25 日
--
照片由 Clint Adair 拍摄,来自 Unsplash
作为曾在美洲各主要金融机构与所谓的主模型师合作的人,我有幸目睹数据建模如何帮助标准化组织定义和描述其数据的方式。我亲眼见证了一个健全的数据模型如何加速了数据目录的实施和数据质量项目的启动,以及缺乏数据模型如何导致类似项目的自我毁灭。
从这一经验中,这种观点探讨了组织如何通过关注三个关键子能力来提高数据建模能力:元建模、概念和逻辑数据建模,以及物理数据建模。对于每个子能力,它将描述高成熟度的表现,并总结整体最佳实践和成功因素。
数据建模的子能力
让我们从数据建模的基础组件开始:

-
元建模—— 元建模是数据建模的一个子能力,它涉及创建一个定义其他模型的结构、概念和关系的模型。元模型提供了一种标准化的方式来定义和描述模型及其组件,这有助于确保在这些模型的开发和使用中保持一致性和清晰性。元模型跨越概念模型、逻辑模型和物理模型,并定义了这些组件如何一致地连接在一起。
-
概念和逻辑数据建模—— 概念和逻辑数据建模是数据建模的子能力,涉及创建数据的业务导向视图,以捕捉特定领域中的主要实体、关系和属性,例如顾客、员工和产品。逻辑数据建模通过添加更多细节来完善概念模型,如指定数据类型、键和实体之间的关系,并将概念领域细分为逻辑属性,如顾客姓名、员工姓名和产品 SKU。

概念模型在最高层次上解释了各个领域或概念是什么,以及它们之间的关系。在这个示例中,你可以看到事务形成了一个关键概念,其中每个事务可以与被售出的产品、购买它们的顾客、支付方式和购买地点的商店相关联——这些都构成了各自的概念。此外,连接器显示,每个单独的事务至多可以有一个顾客、商店或员工相关联,但这些反过来可以与多个事务相关联——这当然是有意义的。

在逻辑模型中,概念(或领域)被细分为逻辑属性。这种逻辑模型与底层元模型一致,现在可以被转换为物理模型,在其中详细说明这些数据属性的确切位置(例如,在哪个表中)和格式。
- 物理数据建模 — 物理数据建模涉及将逻辑数据模型转换为可以在特定技术平台上实现的数据库模式。这包括定义表、列、索引和其他数据库对象,以及指定数据类型、约束和实现模型所需的其他细节。

在物理模型中,进一步的技术细节会被添加到逻辑模型中,例如澄清表名、列名和数据类型。
好的表现是什么样的
数据建模的高成熟度要求你从多个维度进行思考:

-
战略 — 组织的整体数据建模战略,包括数据建模工作与业务目标和目标的一致性。
-
人员/人才 — 具体角色及其责任的明确,以及所需的专业知识、技能和培训。
-
流程 — 包括数据建模方法论的文档、数据建模模板和标准的制定以及质量控制和审查流程的建立。
-
技术 — 支持数据建模工作的工具,如数据建模软件和工具,以及数据建模工具与其他系统和应用程序的集成。
-
采纳 — 数据建模实践在组织内外的采纳和使用。这可能包括社交化程序、解决采纳障碍以及跟踪指标以衡量数据建模工作的有效性和影响。

让我们将这些应用到我们定义的三个子能力上。
元模型
-
高成熟度的元建模涉及拥有一个定义明确且被广泛采纳的元模型,该模型在整个组织中一致使用。确认一下——你需要一个,仅一个。如果没有元模型(可以把它看作是通用语言的基本语法和词汇),各种数据模型(方言甚至完全不同的语言)可能会出现,导致跨领域、跨过程和跨系统的解释性和互操作性问题。
-
你需要懂得元模型的人,并且能够维护和解释它。建议有一个人对元模型拥有最终的权威。他或她可以接受反馈并收集变更请求,以确保它符合并保持适用。
-
应该有一个定义明确的过程来描述如何在建模活动中使用元模型。它的采纳应该在数据建模人员、数据架构师和数据工程师中广泛进行——实际上,它应该让他们的工作变得更容易,因为他们有了明确的起点。
-
如数据目录或元数据管理系统等工具可以提供一个集中式存储和管理元模型的库,并支持协作和版本控制,但这并非绝对必要。一些专家可能不同意我的观点,但许多完全适用的元模型存在于如 Microsoft Excel、PowerPoint 或 Visio 这样的工具中,根据你组织的规模和复杂性,这也可以是适当的只要你的概念和逻辑数据建模人员严格遵守。
概念和逻辑数据建模
-
高成熟度涉及对概念和逻辑数据建模有一个定义明确且一致的方法,严格遵守元模型。概念领域应有所有者,并且应有一个结构化的流程来创建新的逻辑属性,然后进行审查、批准和发布。
-
建模领域和逻辑属性是一项技能——且是一项罕见的技能。这需要核心的数据建模专长以及将其投射到实际业务领域的能力,以用逻辑属性描述业务和技术组织都能理解的内容。不要低估这有多难——成熟的组织通常有一个“首席建模师”来维护和传播最佳实践(Paul Carey,在想念你!)。
-
概念数据模型应(明确地)与企业或业务架构对齐,并与数据所有权分配的数据领域对齐。
-
数据建模软件可用于创建和维护大规模的概念和逻辑数据模型。这些工具可以提供模型的可视化表示,并支持协作、版本控制以及与其他系统(如数据目录或元数据管理系统)的集成。业务术语表可用于定义和标准化模型中使用的业务概念和术语。

物理数据建模
-
物理数据建模的高成熟度水平通常涉及具有良好设计和高效的数据库模式,这些模式满足适用的性能和可扩展性要求。这需要能够设计和实施模式的人员、定义良好的模式开发和维护流程以及支持模式设计和实施的适当技术工具。
-
数据库设计软件可用于创建和维护物理数据模型。这些工具可以从逻辑数据模型生成数据库模式,并支持协作、版本控制以及与其他系统(如数据目录或元数据管理系统)的集成。数据字典也可用于定义和标准化技术细节,如数据类型、约束及其他数据库对象。
高级最佳实践和成功因素
为了提升他们的数据建模能力,组织可以遵循一些最佳实践和成功因素:
-
首先确保元模型正确。元模型推动整个企业的可重用性和一致性。它确保所有后续的建模工作逐步构建整体模型。如果没有一个合适的元模型,未来就要面对调整和桥接现有不兼容模型的艰巨任务。
-
考虑预先制作的行业或通用模型。根据您在旅程中的位置,您可以考虑采用现有的数据模型。这可以推动与国际最佳实践和标准的对齐,节省您构建完全新模型的时间和精力,并实现与外部方的高效可靠数据交换。例如,BIAN提供了一个标准化的银行服务参考模型,定义了银行业的通用语言、分类法和业务流程框架。
-
在概念、逻辑和物理层面之间进行迭代。数据建模需要时间 —— 这项工作永远不会完成。建议优先考虑领域 —— 像客户和产品这样的参考领域是很好的候选对象 —— 并从中选择 1 或 2 个,首先完成逻辑模型,然后是物理模型的指导,然后再转向下一个领域。
-
不要过度强调物理层面。数据建模可能复杂、耗时,因此昂贵。完成基本的概念和逻辑模型几乎肯定是值得的,但一旦涉足物理领域,您可能不需要集中管理和捕获所有物理模型。在这里也可能需要优先考虑 —— 例如,确定“关键任务”系统并为其文档化物理模型,但对于其他系统,只需确保本地应用所有者遵守特定的建模规范和标准可能就足够了。
-
战略性地实施技术。它们可能很昂贵,您可能在第一个领域不需要它们,但最终您的数据模型将在尺寸和复杂性方面成倍增长。考虑数据目录、业务词汇表和数据字典,或者能同时兼顾所有这些功能的工具。如果没有这些工具,消费(因此价值创造)将很差。

最后一个建议是找到并赋予主模型师权力,这值得单独一节来详述。
主模型师的角色

照片由Usman Yousaf拍摄于Unsplash。
“高级建模师”是数据建模领域的专家,拥有对建模技术和工具的深入理解,能够定义最佳实践,并具备商业敏锐性,不让“完美”分散对“良好”的关注。这样的人可以负责并引导上述提到的五项其他建议。
此外,高级建模师可以为组织中的所有逻辑和物理数据建模师提供指导和支持。这可以通过创建可重用的文档,如上述的元模型,也可以包括基本的数据建模蓝图和说明手册来实现。高级建模师还可以通过定义标准和指南、促进培训来保护和传播最佳实践。通过保持对最新趋势和进展的了解,他们可以向组织引入新的技术和工具,推动持续改进。他们还可以建议是否利用现有的、预制的数据模型,而不是从头开始构建。

数据建模是设计或优化解决方案、产品和服务过程中至关重要的输入。业务团队可以从高级建模师提供的支持中受益,因为这将加速他们的工作,并最终提高组织数据模型的整体质量和一致性。
下一步
数据建模是一项关键能力,有助于组织确保其数据设计良好、一致,并有效支持业务需求。通过关注诸如元建模、概念和逻辑数据建模以及物理数据建模等子能力,组织可以提升其成熟度,并使业务用户和数据科学家能够持续找到适合其各自用例的正确数据。如果这一切听起来一下子难以承担,你的第一步可能是找到一位高级建模师。
参考文献和进一步阅读推荐
-
通用数据模型,Microsoft。
-
BIAN 文档,银行业架构网络。
-
内容元模型,TOGAF。
-
什么是数据建模?, TechTarget。
-
Oracle 零售数据模型的逻辑数据模型,Oracle。
-
FIBO 本体, EDM Council。
-
数据仓储 — 数据建模,TeradataPoint。
-
如何进行基于战略的数据成熟度评估,ZS。
所有图片(除非另有说明)均由作者提供。
对分析流处理的简要介绍
构建可靠分布式系统的架构基础。
·
关注 发表在 Towards Data Science ·23 分钟阅读·2023 年 8 月 15 日
--
分布式流数据网络是无限的,并且以惊人的速度增长。图片由 作者的 MidJourney 创建
流处理的基础
基础是结构建立的坚固、牢不可破的基础。在构建成功的数据架构时,数据是整个系统的核心要素和基础的主要组成部分。
鉴于数据流入我们的数据平台的最常见方式是通过像Apache Kafka和Apache Pulsar这样的流处理平台,本文涵盖了这一主题领域。
因此,确保我们(作为软件工程师)提供清洁的能力和无摩擦的保护措施,以减少数据进入这些快速流动的数据网络后的数据质量问题,是至关重要的。
这意味着建立围绕我们数据的架构(类型和结构)、字段级可用性(可为空等)和字段类型有效性(预期范围等)的 API 级合同,成为我们数据基础的重要支撑,尤其是在当今现代数据系统的去中心化、分布式流性质下。
然而,为了达到建立盲目信任——或高信任数据网络的程度,我们必须首先建立智能系统级设计模式。
构建可靠的流数据系统
作为软件和数据工程师,构建可靠的数据系统实际上是我们的工作,这意味着数据停机时间应像业务的其他组件一样进行测量。你可能听说过SLAs、SLOs和SLIs这些术语。这些缩写词与合同、承诺和实际度量相关,帮助我们评估端到端系统的表现。
作为服务负责人,我们将对自己的成功与失败负责,但通过一些前期的努力,标准化的元数据、共同的标准和最佳实践可以确保各方面的运行顺利。
此外,相同的元数据还可以提供关于我们数据在传输过程中质量和可信度的宝贵见解,直到数据找到其终端区域休息。数据的血统本身就讲述了一个故事。
采用拥有者心态
例如,你的团队或组织与客户(包括内部和外部客户)之间的服务水平协议(SLAs)用于创建一个关于你所提供服务的有约束力的合同。对于数据团队来说,这意味着根据你的服务水平目标(SLOs)识别和捕捉指标(KPMs — 关键绩效指标)。SLOs 是你基于 SLAs 打算遵守的承诺,这可以是从接近完美(99.999%)的服务正常运行时间(API 或 JDBC)的承诺,到简单的承诺,例如某特定数据集的 90 天数据保留。最后,你的服务水平指标(SLIs)是你按照服务水平合同运营的证明,通常以操作分析(仪表板)或报告的形式呈现。
知道我们想要去哪里可以帮助制定到达那里的计划。这段旅程从数据的插入(或摄取点)开始,特别是每个数据点的正式结构和身份。考虑到“越来越多的数据通过像 Apache Kafka 这样的流处理平台进入数据平台”的观察,编译时保证、向后兼容性和快速的二进制序列化都对这些数据流中的数据至关重要。数据责任本身就是一个挑战。让我们看看原因。
管理流数据责任
流处理系统全天候运行,每周 7 天,每年 365 天。如果没有在前期投入适当的工作,这可能会使问题复杂化,其中一个时常出现的问题是数据损坏,即飞行中的数据问题。
处理飞行中的数据问题
有两种常见的方法可以减少飞行中的数据问题。首先,你可以在数据网络的边缘引入门卫,使用传统的应用程序编程接口(APIs)来协商和验证数据;或者作为第二种选择,你可以创建和编译辅助库或软件开发工具包(SDKs),以强制执行数据协议并使分布式写入者(数据生产者)进入你的流处理数据基础设施,你甚至可以同时使用这两种策略。
数据门卫
在数据网络的边缘(前端)添加网关 API 的好处在于,你可以在数据生产点执行认证(这个系统能访问这个 API 吗?),授权(这个系统能否将数据发布到特定的数据流中?),以及验证(这些数据是否可接受或有效?)。下图 1–1 展示了数据网关的流动情况。

图 1–1:一个分布式系统架构图,显示了数据摄取网关的认证和授权层。从左到右,批准的数据被发布到 Apache Kafka 中进行下游处理。图像来源 Scott Haines
数据网关服务充当你受保护(内部)数据网络的数字门卫(保镖)。其主要角色是控制、限制甚至阻止未经认证的访问(见上图 1–1 中的 API/服务),通过授权哪些上游服务(或用户)可以发布数据(通常通过使用服务ACLs来处理),以及提供的身份(比如服务身份和访问IAM,网络身份和访问JWT,以及我们老朋友 OAUTH)。
网关服务的核心责任是验证传入的数据,在发布潜在的损坏或一般性差的数据之前。如果网关正确地执行其职责,只有“良好”的数据才会通过并进入数据网络,这个网络是事件和操作数据的传输通道,通过流处理进行消化,换句话说:
“这意味着产生数据的上游系统可以在生成数据时快速失败。这可以阻止损坏的数据进入流数据或静态数据管道,并通过错误代码和有用的消息以更自动的方式与生产者建立对话,了解具体原因和问题。”
使用错误消息提供自助解决方案
好与坏体验的差异在于从坏到好的转变所需的努力程度。我们都可能曾经与或工作过,或听说过,服务出现无缘无故的故障(空指针异常抛出随机 500)。
为了建立基本的信任,一点点就足够了。例如,从 API 端点获取 HTTP 400,并附带以下消息体(见下文)
{
"error": {
"code": 400,
"message": "The event data is missing the userId, and the timestamp is invalid (expected a string with ISO8601 formatting). Please view the docs at http://coffeeco.com/docs/apis/customer/order#required-fields to adjust the payload."
}
}
提供了 400 的原因,并赋予向我们(作为服务拥有者)发送数据的工程师修复问题的能力,无需安排会议、响起警报或在 Slack 上联系每个人。当你可以时,请记住,每个人都是人,我们喜欢闭环系统!
数据 API 的利弊
这种 API 方法有其优缺点。
优点是大多数编程语言与 HTTP(或 HTTP/2)传输协议即开即用——或者通过添加一个小库——并且 JSON 数据现如今几乎是最通用的数据交换格式。
从另一方面(缺点)看,可以说对于任何新的数据领域,还有另一个服务需要编写和管理,如果没有某种形式的 API 自动化,或遵循像OpenAPI这样的开放规范,每个新的 API 路由(端点)最终都会花费更多的时间。
在许多情况下,未能以“及时”的方式更新数据摄取 API,或者扩展和/或 API 停机、随机故障,或只是人员沟通不畅,为人们绕过“愚蠢”API 提供了必要的理由,进而尝试直接将事件数据发布到 Kafka。虽然 API 可能会觉得阻碍,但在数据质量问题如事件损坏或意外混合事件开始破坏流处理梦想后,保持一个公共的门卫有强烈的论据。
要彻底解决这个问题(并几乎完全消除它),良好的文档、变更管理(CI/CD)以及包括实际单元测试和集成测试在内的一般软件开发卫生——能够实现快速的功能和迭代周期,而不会降低信任度。
理想情况下,数据本身(模式/格式)可以通过启用字段级验证(谓词)、生成有用的错误信息并维护自身利益,来制定其自身数据层合同的规则。嘿,凭借一些路由或数据级元数据和一些创造性思维,API 可以自动生成自定义的路由和行为。
最后,网关 API 可以被视为集中式的麻烦制造者,因为每次上游系统未能发出有效数据(例如,被门卫阻挡)都会导致有价值的信息(事件数据、指标)被丢失。这里的责任问题也往往是双向的,因为门卫部署不当可能使一个未设置为处理网关停机(即使是几秒钟)的重试的上游系统失明。
抛开所有优缺点不谈,使用网关 API 来阻止腐败数据在进入数据平台之前的传播意味着,当出现问题时(因为问题总是会出现),问题的表面面积将减少到某个特定服务。这肯定比调试分布式的数据管道、服务,以及无数的最终数据目标和上游系统要好,因为那样你要弄清楚坏数据是由公司中的“某人”直接发布的。
如果我们去掉中间人(网关服务),那么治理“预期”数据传输的能力将落在“库”这一形式的专用 SDKs 上。
软件开发工具包(SDKs)
SDKs 是导入到代码库中的库(或微框架),用于简化某个操作、活动或其他复杂操作。它们也被称为 客户端。以之前提到的使用良好错误信息和错误代码为例。这个过程是必要的,用于 通知客户端 他们之前的操作是无效的,但直接将适当的保护措施添加到 SDK 中可以减少潜在问题的表面面积。例如,假设我们有一个 API 设置来通过事件跟踪跟踪客户的咖啡相关行为。
通过 SDK 保护措施减少用户错误
理论上,客户端 SDK 可以包含 管理与 API 服务器交互所需的所有工具,包括身份验证、授权,而对于验证来说,如果 SDK 发挥作用,验证问题将迎刃而解。以下代码片段展示了一个可以可靠地跟踪客户事件的 SDK 示例。
import com.coffeeco.data.sdks.client._
import com.coffeeco.data.sdks.client.protocol._
Customer.fromToken(token)
.track(
eventType=Events.Customer.Order,
status=Status.Order.Initalized,
data=Order.toByteArray
)
通过一些额外的工作(即客户端 SDK),数据验证或事件损坏的问题几乎可以完全消除。额外的问题可以在 SDK 内部处理,例如当服务器离线时如何重试发送请求。与其让所有请求立即重试,或在某个循环中无限制地淹没网关负载均衡器,SDK 可以采取更智能的措施,比如采用指数回退。有关当事情出错时会发生什么的深入探讨,请参见“雷鸣之 Herd 问题”!
雷鸣之 Herd 问题
比如说,我们有一个单一的网关 API 服务器。你编写了一个很棒的 API,公司的许多团队都在向这个 API 发送事件数据。事情进展顺利,直到有一天,一个新的内部团队开始向服务器发送无效数据(他们没有尊重你的 http 状态码,而是将所有非 200 的 http 代码视为重试的理由。不过,他们忘记添加任何重试启发式算法,如指数回退,因此所有请求都会无限重试——在一个不断增加的重试队列中)。请注意,在这个新团队加入之前,根本没有理由运行多个 API 服务器实例,也没有必要使用任何服务级别的速率限制器,因为一切都在商定的 SLA 内顺利运行。

非失败鲸。当你解决问题并重新摆脱困境时可能发生的情况。图片来源 Midjourney 通过作者
好吧,那是今天之前的事了。现在你的服务离线了。数据在备份,上游服务正在填充他们的队列,人们很不满,因为他们的服务现在因为你的单点故障而开始出现问题……
这些问题都源于一种资源枯竭形式,称为“雷鸣之 Herd 问题”。当许多进程在等待某个事件时,比如系统资源变得可用,或者在这个例子中,API 服务器重新上线时,就会发生这个问题。现在,所有进程争先恐后地竞争资源,在许多情况下,单个进程(API 服务器)上的负载足以使服务再次离线。不幸的是,这会导致资源枯竭的循环重新开始。除非你能平息“ Herd”现象或将负载分布到更多的工作进程上,从而减少网络负载,使资源再次有空间呼吸。
虽然上面的初始示例更多的是一种无意的 分布式拒绝服务攻击(DDoS),这些问题可以在客户端(通过指数回退或自我限流)和 API 边缘通过负载均衡和速率限制来解决。
最终,如果没有正确的眼睛和耳朵,通过操作指标、监控和系统级别的(SLAs/SLIs/SLOs)警报来实现,数据可能会消失,这可能是一个解决的挑战。
无论你决定在数据网络边缘添加一个数据网关 API,使用自定义 SDK 来保证上游一致性和可靠性,还是决定采取其他方法将数据引入数据平台,了解你的选项总是好的。无论数据以何种方式流入数据流,这篇关于流数据的介绍都不完整,必须适当地讨论数据格式、协议和二进制可序列化数据的话题。谁知道呢,也许我们会发现更好的处理数据可靠性问题的方法!
选择合适的数据协议来完成任务
当你想到结构化数据时,首先想到的可能是 JSON 数据。JSON 数据具有结构,是标准的基于网络的数据协议,而且无论如何,它都非常容易使用。这些都是快速入门的好处,但随着时间的推移,如果没有适当的保护措施,你可能会在标准化 JSON 用于流系统时遇到问题。
与 JSON 的爱恨关系
第一个问题是 JSON 数据是可变的。这意味着作为数据结构它是灵活的,因此也容易脆弱。数据必须保持一致性才能可靠,而在跨网络传输数据的情况下(在传输过程中),序列化格式(二进制表示)应该高度紧凑。使用 JSON 数据,你必须为每个对象的所有字段发送键。这不可避免地意味着你会为每个额外的记录(在一系列对象中)发送大量额外的负荷。
幸运的是,这并不是一个新问题,恰好有针对这些问题的最佳实践,以及关于最佳序列化数据策略的多种思想流派。这并不是说 JSON 没有它的优点。只是在建立一个坚实的数据基础时,结构越多越好,压缩水平越高越好,只要它不消耗大量 CPU 资源。
可序列化的结构化数据
在高效编码和传输二进制数据时,两种序列化框架常常被提及:Apache Avro和 Google Protocol Buffers(protobuf)。这两个库提供了 CPU 高效的行数据结构序列化技术,并且这两种技术还提供了各自的远程过程调用(RPC)框架和功能。让我们先来看avro,然后是protobuf,最后再看看远程过程调用。
Avro 消息格式
使用avro时,你可以通过记录的概念定义结构化数据的声明式模式。这些记录只是 JSON 格式的数据定义文件(模式),存储为文件类型avsc。以下示例展示了 avro 描述符格式中的 Coffee 模式。
{
"namespace": "com.coffeeco.data",
"type": "record",
"name": "Coffee",
"fields": [
("name": "id", "type: "string"},
{"name": "name", "type": "string"},
{"name": "boldness", "type": "int", "doc": "from light to bold. 1 to 10"},
{"name": "available", "type": "boolean"}
]
}
处理 avro 数据可以有两种路径,取决于你在运行时如何工作。你可以采取编译时的方法,或者在运行时按需解决问题。这种灵活性可以增强交互式数据发现会话。例如,avro 最初被创建为一种高效的数据序列化协议,用于在 Hadoop 文件系统中长期存储大型数据集合,以分区文件的形式存储。由于数据通常是从一个位置读取,写入到 HDFS 中的另一个位置,avro 可以每个文件存储一次模式(用于写入时)。
Avro 二进制格式
当你将一组 avro 记录写入磁盘时,该过程会将 avro 数据的模式直接编码到文件中(仅一次)。在 Parquet 文件编码中也有类似的过程,模式会被压缩并作为二进制文件尾部写入。我们在第四章结束时,经历了将 StructField 级文档添加到我们的StructType的过程。这个模式用于编码我们的 DataFrame,并且在写入磁盘时,它在下一次读取时保留了我们的内联文档。
启用向后兼容性并防止数据损坏
在读取多个文件作为单一集合的情况下,当记录之间的模式发生变化时,可能会出现问题。Avro 将二进制记录编码为字节数组,并在反序列化时(将字节数组转换回对象)应用模式。
这意味着你需要额外注意以保持向后兼容,否则你可能会遇到ArrayIndexOutOfBounds异常。
破坏模式承诺也可能以其他微妙的方式发生。例如,假设你需要将某个字段的整数值更改为长整数值。不要这样做。这将破坏向后兼容性,因为从 int 到 long 的字节大小增加。这是由于使用模式定义来定义记录中每个字段的字节数组的起始和结束位置。为了保持向后兼容性,你需要在 avro 定义中保留整数字段,并向模式中添加(附加)一个新字段以供今后使用。
流式 avro 数据的最佳实践
从静态 avro 文件(具有有用的嵌入模式)转移到无限的数据流时,主要的区别在于你需要 带上你自己的模式。这意味着你需要支持向后兼容性(以防需要在模式更改之前和之后回滚和重新处理数据),以及前向兼容性,以防你已经有现有的读取器正在从流中消费数据。
这里的挑战是支持两种兼容性形式,因为 avro 无法忽略未知字段,而这是支持前向兼容性的要求。为了应对 avro 的这些挑战,Confluence 的团队开源了他们的 模式注册表(用于 Kafka),它在 Kafka 主题(数据流)级别启用了模式版本控制。
在不使用模式注册表的情况下支持 avro,你必须确保在更新写入者的模式库版本之前,更新任何活动的读取器(spark 应用程序或其他)。否则,切换开关的那一刻,你可能会发现自己处于事故的开端。
Protobuf 消息格式
使用 protobuf,你使用消息的概念定义结构化数据定义。消息以类似于定义 C 中的结构的格式编写。这些消息文件以 proto 文件名扩展名写入。协议缓冲区具有使用 imports 的优势。这意味着你可以定义常见的消息类型和枚举,这些类型可以在大型项目中使用,甚至可以导入到外部项目中,实现大规模重用。一个使用 protobuf 创建 Coffee 记录(消息类型)的简单示例。
syntax = "proto3";
option java_package="com.coffeeco.protocol";
option java_outer_classname="Common";
message Coffee {
string id = 1;
string name = 2;
uint32 boldness = 3;
bool available = 4;
}
使用 protobuf,你只需定义一次消息,然后为所选择的编程语言编译。例如,我们可以使用来自 ScalaPB 项目的独立编译器生成 Scala 的代码(由 Nadav Samet 创建和维护),或利用 Buf 的卓越,这些工具和实用程序围绕 protobuf 和 grpc 创建了宝贵的工具集。
代码生成
编译 protobuf 可以实现简单的代码生成。以下示例取自/ch-09/data/protobuf 目录。章节 READMEj 中的说明涵盖了如何安装 ScalaPB,并包括了设置正确环境变量以执行命令的步骤。
mkdir /Users/`whoami`/Desktop/coffee_protos
$SCALAPBC/bin/scalapbc -v3.11.1 \
--scala_out=/Users/`whoami`/Desktop/coffee_protos \
--proto_path=$SPARK_MDE_HOME/ch-09/data/protobuf/ \
coffee.proto
这个过程从长远来看节省了时间,因为它免去了你需要编写额外的代码来序列化和反序列化你的数据对象(无论是跨语言边界还是在不同代码库内)。
Protobuf 二进制格式
序列化(即二进制传输格式)使用了编码的概念,即二进制字段级分隔符。这些分隔符用作标记,以识别序列化 protobuf 消息中封装的数据类型。在示例文件 coffee.proto 中,你可能注意到每个字段类型旁边都有一个索引标记(例如 string id = 1;),这用于协助消息的编码/解码。与 avro 二进制格式相比,这意味着有一点额外的开销,但如果你阅读编码规范,你会看到其他效率上的优势足以弥补这些额外的字节(例如位打包、数值数据类型的高效处理,以及对每条消息前 15 个索引的特殊编码)。在选择 protobuf 作为流数据的二进制协议时,总体来看,优点远大于缺点。其中一个显著的优点是支持向后和向前的兼容性。
启用向后兼容性和防止数据损坏
在修改 protobuf 模式时需要记住类似于 avro 的规则。作为一个经验法则,你可以更改字段的名称,但你绝不能更改字段的类型或位置(索引),除非你愿意破坏向后兼容性。随着团队对 protobuf 使用的熟练程度提高,这些规则在长期支持任何数据时可能会被忽视,特别是当需要重新排列和优化时。如果不小心,这可能会带来麻烦。(有关更多背景,请参见下面名为保持数据质量随时间的提示。)
流式 protobuf 数据的最佳实践
鉴于 protobuf 支持 向后 和 向前 兼容,这意味着你可以在不担心首先更新读者的情况下部署新的写入器,读者也是如此,你可以用更新版本的 protobuf 定义来更新它们,而不必担心所有写入器的复杂部署。Protobuf 通过未知字段的概念支持向前兼容。这是 avro 规范中不存在的附加概念,用于跟踪由于 protobuf 本地版本与当前读取版本之间的差异而无法解析的索引和相关字节。这里的好处是你可以在任何时候 选择 更新 protobuf 定义中的新更改。
比如,假设你有两个流应用程序 (a) 和 (b)。应用程序 (a) 正在处理来自上游 Kafka 主题 (x) 的流数据,为每条记录增加额外的信息,然后将其写入一个新的 Kafka 主题 (y)。现在,应用程序 (b) 从 (y) 中读取数据并执行其操作。假设有一个更新的 protobuf 定义版本,而应用程序 (a) 尚未更新到最新版本,而上游 Kafka 主题 (x) 和应用程序 (b) 已经更新,并期望使用从升级中获得的新字段。令人惊讶的是,依然可以将未知字段通过应用程序 (a) 传递到应用程序 (b),甚至不需要知道它们的存在。
参见 “维护数据质量的技巧” 以获取更多深入的探讨。
提示:维护数据质量的技巧
当使用 avro 或 protobuf 时,你应该像对待需要推送到生产环境的代码一样对待这些模式。这意味着要创建一个可以提交到你公司 github(或你使用的任何版本控制系统)的项目,这也意味着你 应该 为你的模式编写单元测试。这不仅提供了如何使用每种消息类型的实际示例,而且测试数据格式的一个重要原因是确保模式的更改不会破坏向后兼容性。更进一步的是,为了单元测试模式,你需要首先编译 (.avsc 或 .proto) 文件,并使用相应的库代码生成。这使得创建可发布的库代码变得更加容易,你还可以使用发布版本(版本 1.0.0)来记录每次对模式的更改。
一种简单的方法是通过在项目生命周期内序列化并存储每个消息的二进制副本,来使这一过程得以实现。我发现将这一步骤直接添加到单元测试中很有效,利用测试套件创建、读取和写入这些记录到项目测试资源目录中。这样,每个二进制版本在所有模式更改中都可以在代码库中找到。
通过一点额外的前期努力,你可以在整体方案中节省大量麻烦,并且可以安心入睡,知道你的数据是安全的(至少在生产和消费方面)。
使用 Buf 工具和 Protobuf 在 Spark 中
自从 2021 年撰写本章以来,Buf Build (buf.build/) 已经发展成了一切 protobuf 公司。他们的工具简单易用,免费且开源,并在合适的时机出现,为 Spark 社区的一些倡议提供了支持。Apache Spark 项目引入了对 Protocol Buffers in Spark 3.4 的全面原生支持,以支持 spark-connect,并使用 Buf 编译 GRPC 服务和消息。毕竟,Spark Connect 是一个 GRPC 原生连接器,用于嵌入 JVM 外的 Spark 应用程序。
传统的 Apache Spark 应用必须在某处作为驱动程序应用运行,过去这意味着使用pyspark 或原生 spark,这两者仍然运行在 JVM 进程之上。

目录结构 通过 Spark Connect 显示了 protobuf 定义,以及 buf.gen.yaml 和 buf.work.yaml,这些文件有助于代码生成。
到头来,Buf Build 为构建过程带来了安心。为了生成代码,只需运行一个简单的命令:buf generate。对于简单的 lint 检查和一致的格式化,可以使用 buf lint && buf format -w。不过,最棒的是变更检测。buf breaking --against .git#branch=origin/main 就可以确保对消息定义的新更改不会对当前在生产环境中运行的内容产生负面影响。未来,我会写一篇关于使用buf进行企业分析的文章,但现在,是时候结束这一章了。
那么我们刚才讲到哪里了?你现在知道,在长期数据管理策略中使用 avro 或 protobuf 有其好处。通过使用这些与语言无关的、基于行的结构化数据格式,你减少了长期语言锁定的问题,为将来的任何编程语言打开了大门。说实话,支持遗留库和代码库是一项难以回报的任务。此外,序列化格式有助于减少与发送和接收大量数据相关的网络带宽成本和拥堵。这也有助于降低长期存储数据的开销。
最后,让我们看看这些结构化数据协议如何在通过远程过程调用发送和接收数据时实现额外的效率。
远程过程调用
RPC 框架,总的来说,使得 客户端 应用程序可以通过本地函数调用透明地调用 远程(服务器端)方法(过程),通过来回传递序列化的消息。客户端 和 服务器端 实现使用相同的 公共接口 定义来定义可用的功能性 RPC 方法和服务。接口定义语言(IDL)定义协议和消息定义,并作为客户端和服务器端之间的契约。让我们通过流行的开源 RPC 框架 gRPC 来看看实际情况。
gRPC
首先由 Google 设想并创建的,gRPC 即“通用”远程过程调用,是一个强大的开源框架,用于高性能服务,从分布式数据库协调(如 CockroachDB)到实时分析(如 微软 Azure 视频分析)。

图 1–2。RPC(在此示例中是 gRPC)通过在客户端和服务器之间传递序列化消息来工作。客户端实现相同的接口定义语言(IDL)接口,这作为客户端和服务器之间的 API 合同。(图片来源: grpc.io/docs/what-is-grpc/introduction/))
图 9–3 显示了 gRPC 工作的示例。服务器端代码使用 C++ 编写以提高速度,而用 ruby 和 java 编写的客户端可以使用 protobuf 消息作为通信手段与服务进行互操作。
使用协议缓冲区进行消息定义、序列化以及服务的声明和定义,gRPC 可以简化你捕获数据和构建服务的方式。例如,假设我们想继续创建一个用于跟踪客户咖啡订单的 API。API 合同可以在一个简单的服务文件中定义,从而可以使用相同的服务定义和消息类型构建服务器端实现和任意数量的客户端实现。
定义 gRPC 服务
你可以像 1–2–3 一样轻松定义服务接口、请求和响应对象以及需要在客户端和服务器之间传递的消息类型。
syntax = "proto3";
service CustomerService {
rpc TrackOrder (Order) returns (Response) {}
rpc TrackOrderStatus (OrderStatusTracker) returns (Response) {}
}
message Order {
uint64 timestamp = 1;
string orderId = 2;
string userId = 3;
Status status = 4;
}
enum Status {
unknown_status = 0;
initalized = 1;
started = 2;
progress = 3;
completed = 4;
failed = 5;
canceled = 6;
}
message OrderStatusTracker {
uint64 timestamp = 1;
Status status = 2;
string orderId = 3;
}
message Response {
uint32 statusCode = 1;
string message = 2;
}
通过添加 gRPC,实现和维护数据基础设施中使用的服务器端和客户端代码将变得更容易。由于 protobuf 支持向后和向前兼容,这意味着旧版 gRPC 客户端仍可以向新版 gRPC 服务发送有效消息,而不会遇到常见问题和痛点(如之前在“飞行中的数据问题”中讨论的)。
gRPC 使用 HTTP/2
作为额外的好处,对于现代服务栈,gRPC 能够使用 HTTP/2 作为其传输层。这也意味着你可以利用现代数据网格(如 Envoy)进行代理支持、路由和服务级别的 认证,同时减少标准 HTTP over TCP 中的 TCP 数据包拥塞问题。
减轻飞行中的数据问题并在数据责任方面取得成功,从数据开始,并从那个中心点向外扩展。对于如何将数据引入你的数据网络,建立相应的流程应该被视为在投入流数据之前需要完成的前提条件。
摘要
本文的目标是呈现所需的移动部件、概念和背景信息,以便在盲目从传统的(静态)批处理思维方式跳跃到理解实时流数据风险和回报的方式之前,武装好自己。
实时利用数据可以带来快速、可操作的洞察,并打开通往最先进的机器学习和人工智能的大门。
然而,分布式数据管理如果未考虑正确的步骤,也可能成为数据危机。请记住,如果没有建立在有效(值得信赖)数据之上的强大、坚实的数据基础,实时数据的道路将不是一条简单的道路,而是充满了颠簸和绕行。
我希望你喜欢第九章的下半部分。要阅读本系列的第一部分,请前往 对分析流处理的温和介绍。
为工程师及其他人构建心理模型
towardsdatascience.com
— — — — — — — — — — — — — — — — — — — — — — — —
如果你想深入了解,请查看我的书,或给我一个击掌支持。
[## 使用 Apache Spark 进行现代数据工程:构建关键流处理的实用指南…
Amazon.com: 使用 Apache Spark 进行现代数据工程:构建关键流处理的实用指南…
www.amazon.com](https://www.amazon.com/Modern-Engineering-Apache-Spark-Hands/dp/1484274512?source=post_page-----db58b3694263--------------------------------)
如果你有访问权限 O’Reilly Media,你也可以完全免费阅读这本书(对你有好处,对我则不然),但如果有机会,请在其他地方找到这本书的免费版本,或者获取电子书以节省运费(或避免寻找一本超过 600 页的实体书的地方)。
[## 使用 Apache Spark 进行现代数据工程:构建关键流处理的实用指南…
利用 Apache Spark 在现代数据工程生态系统中。本实用指南将教你如何编写完全…
learning.oreilly.com](https://learning.oreilly.com/library/view/modern-data-engineering/9781484274521/?source=post_page-----db58b3694263--------------------------------)
一种新的工程类型
如何基于 LLM 的微型 AGI 将需要向建模思维过程的范式转变
·
关注 发表在Towards Data Science ·4 分钟阅读·2023 年 4 月 15 日
--
作者根据ALAN DE LA CRUZ在Unsplash上的照片制作的图片
在撰写本文时(2023 年 4 月),如 langchain [1]等框架正在开创越来越复杂的 LLM 应用场景。最近,增强了 LLM 推理能力的软件代理已开始向人类水平的机器智能迈进。
我们在谈论什么?
智能体是软件系统中的一种模式;它们是能够做出决策并与环境相对自主互动的算法。在 langchain 智能体的情况下,环境通常是基于文本输入/输出的互联网、用户或其他智能体和工具的接口。
根据这个概念,其他项目 [2,3] 已经开始研究更通用的问题解决者(某种‘微型’通用人工智能,或 AGI——一种接近人类水平推理能力的 AI 系统)。尽管这些系统的当前版本仍然相当单片,因为它们作为一个软件整体接受目标/任务/想法的输入,但从它们的执行中可以很容易地看出,它们依赖于多个不同的子系统。

图片来源:Significant Gravitas (github.com/Significant-Gravitas/Auto-GPT, 30/03/2023)
我们在这些系统中看到的新范式是,它们建模思维过程:“批判性思考并检查你的结果”、“查阅多个来源”、“反思你的解决方案的质量”、“使用外部工具进行调试”等,这些都接近于人类的思维方式。
现在,在日常(人类)生活中,我们雇佣专家来完成需要特定专业知识的工作。我的预测是,在不久的将来,我们将雇佣某种认知工程师来建模 AGI 的思维过程,可能通过构建特定的 多智能体系统,以更高质量地解决特定任务。
我为什么会这么认为?为什么整体 AGI 不一定足够好?
从我们今天如何使用 LLMs 来看,我们已经在做这件事——建模认知过程。我们通过特定的方式,利用提示工程和大量相邻研究领域的成果,来实现所需的输出质量。即使我上面描述的可能看起来很未来,这已经是现状。
那我们接下来该怎么做?我们可能会看到越来越智能的 AI 系统,甚至可能在某个时点超越人类水平。随着它们变得越来越智能,与我们的目标——我们希望它们做的事情——保持一致将变得越来越困难。AGI 对齐以及对过于强大的未对齐 AI 的安全关注已经是一个非常活跃的研究领域,风险很高——例如,Eliezer Yudkowski [4] 就详细解释了这一点。
我的直觉是较小的,即‘更简单的’系统更容易对齐,因此它们会以更高的概率以某种质量产生某种结果。这些系统正是我们可以使用认知工程方法构建的。
我们应该做什么
-
我们应该对如何构建专门的 AGI 系统有一个良好的实验性理解
-
从这些经验中,我们应该创建和迭代正确的抽象,以更好地进行这些系统的建模。
-
有了这些抽象概念,我们可以开始创建可重用的思维模块,就像我们使用可重用的构件来创建用户界面一样。
-
在不久的将来,我们将理解建模这些智能系统的模式和最佳实践,随着经验的积累,我们将了解到哪些架构可以导致哪些结果。
作为一种积极的副作用,通过这项工作和经验的积累,也许可以学习如何更好地调整更智能的人工通用智能系统。
这将会引向何方
我预计很快会看到来自不同学科的知识融入这一新兴领域。
多智能体系统的研究以及如何将其用于问题解决,心理学、业务管理和流程建模的洞察都可以有益地融入这一新范式及新兴的抽象中。
我们还需要考虑这些系统如何能够最好地进行交互。例如,人类反馈循环,或者至少在过程中定期评估,可以帮助取得更好的结果——你可能在使用 ChatGPT 时对此有所了解。
这是一个前所未见的用户体验模式,其中计算机变得更像一个合作伙伴或副驾驶,负责进行低级别的研究、制定方案、头脑风暴、自动化或推理任务。
关于作者
Johanna Appel 是总部位于瑞士苏黎世的机器智能咨询公司Altura.ai GmbH 的联合创始人。
她通过将这些“微型”人工通用智能系统集成到现有的业务流程中,帮助公司从中获利。
参考资料
[1] Langchain GitHub 代码库,github.com/hwchase17/langchain
[2] AutoGPT GitHub 代码库,github.com/Significant-Gravitas/Auto-GPT
[3] BabyAGI GitHub 代码库,github.com/yoheinakajima/babyagi
[4] “Eliezer Yudkowsky: AI 的危险与人类文明的终结”,Lex Fridman 播客 #368,www.youtube.com/watch?v=AaTRHFaaPG8
预测概率分布的新方法
原文:
towardsdatascience.com/a-new-way-to-predict-probability-distributions-e7258349f464
探索 Catboost 的多分位数回归
·发表于 Towards Data Science ·阅读时间 10 分钟 ·2023 年 2 月 14 日
--

骰子。图片由作者提供。
我们对机器学习模型的预测可以有多自信?这个问题在过去十年中一直是一个重要的研究领域,并且在高风险的机器学习应用如金融和医疗保健中具有重大意义。尽管许多分类模型,特别是校准模型,通过预测目标类别的概率分布提供了不确定性量化,但在回归任务中量化不确定性要复杂得多。
在许多提出的方法中,分位数回归是最受欢迎的之一,因为它对目标分布没有假设。直到最近,分位数回归的主要缺点是每个预测的分位数都需要训练一个模型。例如,为了预测目标分布的第 10、第 50 和第 90 百分位数,需要训练三个独立的模型。Catboost 通过多分位数损失函数解决了这个问题——这是一个使单个模型能够预测任意数量分位数的损失函数。
本文将探讨两个使用多分位数损失函数的合成数据示例。虽然这些示例不一定能反映真实世界的数据集,但它们将帮助我们理解该损失函数如何通过预测目标分布的分位数来量化不确定性。有关机器学习中噪声和不确定性的快速回顾,请参见这篇文章:
你的机器学习模型不起作用的真正原因
towardsdatascience.com
这篇文章的后续内容现已发布:
使用 Catboost 的符合性多分位数回归
towardsdatascience.com
分位数回归简要概述
在有监督的机器学习中,通常的任务是训练一个模型,以预测给定一组输入特征时目标的期望值:

有监督机器学习。图片由作者提供。
值得注意的是,这种方式训练的模型产生一个单一预测,指示模型认为在给定特征的情况下目标的最可能值。在回归任务中,这通常是条件特征下目标分布的均值。
然而,如 上一篇文章 中所示,大多数机器学习模型在噪声数据集上训练,而仅仅预测目标的条件期望值不足以充分描述目标分布。要看到这一点,请考虑以下噪声回归数据集:

噪声线性回归数据。图片由作者提供。
即使模型预测与数据最优匹配,它也没有量化不确定性。例如,当 x 接近 0.4 时,最佳拟合线预测 y 为 3.8。虽然在 x 接近 0.4 时 3.8 是 y 最可能的值,但数据中有许多例子 y 的值远高于或低于 3.8。换句话说,目标的条件分布在其例外之外具有高变异性,而分位数回归帮助我们描述这一点。
在分位数回归中,损失函数被修改以鼓励模型学习条件目标分布的期望分位数。

分位数回归损失函数。图片由作者提供。
为了更好地理解这个损失函数,假设一个模型正在训练以学习目标分布的第 95 百分位数。在第一个例子中,考虑一个训练样本,其中目标值为 45,模型预测为 60(即模型高估了目标 15)。假设每个训练样本的权重为 1。损失函数在这些值下的评估如下:

第 95 百分位数损失函数高估。作者提供的图像。
现在,假设目标值为 45,模型预测为 30(即模型低估目标 15)。损失函数的值看起来差异很大:

第 95 百分位数损失函数低估。作者提供的图像。
尽管在两个例子中模型预测都偏差了 15,但损失函数对低估的惩罚远高于高估。因为学习的是第 95 百分位数,损失函数对低于目标的任何预测惩罚系数为 0.95,对高于目标的预测惩罚系数为 0.05。因此,当学习第 95 百分位数时,模型“被迫”倾向于高估而非低估。这种情况发生在学习任何中位数以上的百分位数时 — 学习中位数以下的百分位数时情况正好相反。为了更好地理解这一点,我们可以可视化每个预测百分位数的损失函数:

第 95 百分位数损失函数。低估(即目标 — 预测 > 0)受到更重的惩罚。作者提供的图像。

第 50 百分位数损失函数。高估和低估受到相同的惩罚。作者提供的图像。

第 10 百分位数损失函数。高估(即目标 — 预测 < 0)受到更重的惩罚。作者提供的图像。
Catboost 现在通过允许基础决策树在每个节点输出多个百分位数来扩展这个概念。这使得单个模型可以通过最小化新的损失函数来预测多个百分位数:

Catboost 多百分位数损失函数。作者提供的图像。
示例 1 — 简单线性回归
为了理解多百分位数损失函数如何工作,我们从一个简单的数据集开始。以下 Python 代码生成一个带有高斯加性噪声的合成线性数据集:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from catboost import CatBoostRegressor
sns.set()
# Number of training and testing examples
n = 1000
# Generate random x values between 0 and 1
x_train = np.random.rand(n)
x_test = np.random.rand(n)
# Generate random noise for the target
noise_train = np.random.normal(0, 0.3, n)
noise_test = np.random.normal(0, 0.3, n)
# Set the slope and y-intercept of the line
a, b = 2, 3
# Generate y values according to the equation y = ax + b + noise
y_train = a * x_train + b + noise_train
y_test = a * x_test + b + noise_test
结果训练数据应该类似于此:

嘈杂的线性回归数据。作者提供的图像。
接下来,需要为预测指定目标分布的百分位数。为了展示多百分位数损失的强大功能,该模型将寻求为每个观察预测 99 个不同的百分位数值。这几乎可以认为是从每个预测分布中采样 99 个样本。
# Store quantiles 0.01 through 0.99 in a list
quantiles = [q/100 for q in range(1, 100)]
# Format the quantiles as a string for Catboost
quantile_str = str(quantiles).replace('[','').replace(']','')
# Fit the multi quantile model
model = CatBoostRegressor(iterations=100,
loss_function=f'MultiQuantile:alpha={quantile_str}')
model.fit(x_train.reshape(-1,1), y_train)
# Make predictions on the test set
preds = model.predict(x_test.reshape(-1, 1))
preds = pd.DataFrame(preds, columns=[f'pred_{q}' for q in quantiles])
结果预测的数据框如下所示:

百分位数预测。图片由作者提供。
每一行对应一个测试示例,每一列给出一个预测的百分位数。例如,第一个测试示例的第 10 百分位数预测值为 3.318624. 由于只有一个输入特征,我们可以将几个预测的百分位数叠加在测试数据上进行可视化:
fig, ax = plt.subplots(figsize=(10, 6))
ax.scatter(x_test, y_test)
for col in ['pred_0.05', 'pred_0.5', 'pred_0.95']:
ax.scatter(x_test.reshape(-1,1), preds[col], alpha=0.50, label=col)
ax.legend()

测试数据与预测的百分位数叠加。图片由作者提供。
从视觉上看,第 95 百分位数和第 5 百分位数似乎很好地考虑了数据的不确定性。此外,第 50 百分位数(即中位数)大致近似于最佳拟合线。在处理预测的百分位数时,我们通常感兴趣的一个指标是覆盖率。覆盖率是落在两个期望百分位数之间的目标百分比。例如,覆盖率可以使用第 5 百分位数和第 95 百分位数计算如下:
coverage_90 = np.mean((y_test <= preds['pred_0.95']) & (y_test >= preds['pred_0.05']))*100
print(coverage_90)
# Output: 91.4
使用第 5 百分位数和第 95 百分位数,假设完美的校准,我们预计覆盖率为 95–5 = 90%。在这个示例中,预测的百分位数略有偏差,但仍然接近,覆盖率值为 91.4%。
现在让我们分析模型输出的整个预测分布。回顾一下,最佳拟合线是 y = 2x + 3. 因此,对于任何输入 x,预测分布的均值应接近 2x + 3. 同样,由于数据中加入了标准差为 0.3 的随机高斯噪声,预测分布的标准差应接近 0.3. 让我们来测试一下:
# Give the model a new input of x = 0.4
x = np.array([0.4])
# We expect the mean of this array to be about 2*0.4 + 3 = 3.8
# We expect the standard deviation of this array to be about 0.30
y_pred = model.predict(x.reshape(-1, 1))
mu = np.mean(y_pred)
sigma = np.std(y_pred)
print(mu) # Output: 3.836147287742427
print(sigma) # Output: 0.3283984093786787
# Plot the predicted distribution
fig, ax = plt.subplots(figsize=(10, 6))
_ = ax.hist(y_pred.reshape(-1,1), density=True)
ax.set_xlabel('$y$')
ax.set_title(f'Predicted Distribution $P(y|x=4)$, $\mu$ = {round(mu, 3)}, $\sigma$ = {round(sigma, 3)}')

当 x = 4 时 y 的预测分布。图片由作者提供。
令人惊讶的是,预测分布似乎与我们的预期一致。接下来,让我们尝试一个更困难的例子。
示例 2—具有可变噪声的非线性回归
为了看到多百分位数回归的真正威力,让我们使学习任务更加困难。以下代码创建了一个具有异质噪声的非线性数据集,这些噪声依赖于域的特定区域:
# Create regions of the domain that have variable noise
bounds = [(-10, -8), (-5, -4), (-4, -3), (-3, -1), (-1, 1), (1, 3), (3, 4), (4, 5), (8, 10)]
scales = [18, 15, 8, 11, 1, 2, 9, 16, 19]
x_train = np.array([])
x_test = np.array([])
y_train = np.array([])
y_test = np.array([])
for b, scale in zip(bounds, scales):
# Randomly select the number of samples in each region
n = np.random.randint(low=100, high = 200)
# Generate values of the domain between b[0] and b[1]
x_curr = np.linspace(b[0], b[1], n)
# For even scales, noise comes from an exponential distribution
if scale % 2 == 0:
noise_train = np.random.exponential(scale=scale, size=n)
noise_test = np.random.exponential(scale=scale, size=n)
# For odd scales, noise comes from a normal distribution
else:
noise_train = np.random.normal(scale=scale, size=n)
noise_test = np.random.normal(scale=scale, size=n)
# Create training and testing sets
y_curr_train = x_curr**2 + noise_train
y_curr_test = x_curr**2 + noise_test
x_train = np.concatenate([x_train, x_curr])
x_test = np.concatenate([x_test, x_curr])
y_train = np.concatenate([y_train, y_curr_train])
y_test = np.concatenate([y_test, y_curr_test])
结果训练数据如下所示:

示例 2 的训练数据。图片由作者提供。
我们将以与示例 1 相同的方式拟合 Catboost 回归器,并在测试集上可视化预测结果:
model = CatBoostRegressor(iterations=300,
loss_function=f'MultiQuantile:alpha={quantile_str}')
model.fit(x_train.reshape(-1,1), y_train)
preds = model.predict(x_test.reshape(-1, 1))
preds = pd.DataFrame(preds, columns=[f'pred_{q}' for q in quantiles])
fig, ax = plt.subplots(figsize=(10, 6))
ax.scatter(x_test, y_test)
for col in ['pred_0.05', 'pred_0.5', 'pred_0.95']:
quantile = int(float(col.split('_')[-1])*100)
label_name = f'Predicted Quantile {quantile}'
ax.scatter(x_test.reshape(-1,1), preds[col], alpha=0.50, label=label_name)
ax.set_xlabel('x')
ax.set_ylabel('y')
ax.set_title('Testing Data for Example 2 with Predicted Quantiles')
ax.legend()

测试数据与预测的百分位数叠加。图片由作者提供。
通过视觉检查,模型很好地刻画了这种非线性、异方差的关系。注意在 x = 0 附近,三个预测分位数趋向于一个单一值。这是因为 x = 0 附近的区域几乎没有噪声——任何正确预测该区域条件概率分布的模型都应预测出小方差。相反,当 x 在 7.5 到 10.0 之间时,由于该区域的固有噪声,预测的分位数则相距更远。90% 覆盖度可以像以前一样计算:
coverage_90 = np.mean((y_test <= preds['pred_0.95']) & (y_test >= preds['pred_0.05']))*100
print(coverage_90)
# Output: 90.506
使用第 5 和第 95 分位数,假设完美校准,期望覆盖度为 95–5 = 90%。在这个例子中,预测的分位数甚至比示例 1 更好,覆盖度达到了 90.506%。
最后,让我们查看一些输入及其相应的预测分布。

x = -0.06 的预测目标分布。图像作者提供。
上述分布在相对较小的方差下很好地捕捉了目标值。这是可以预期的,因为当 x = 0 时,训练数据中的目标值几乎没有噪声。与此对比的是当 x = -8.56 时的预测目标分布:

x = -8.56 的预测目标分布。图像作者提供。
该分布向右偏斜,方差更高。这在该数据区域是预期的,因为噪声是从高方差的指数分布中采样的。
结论
本文展示了多分位回归在学习任意条件目标分布方面的强大能力。我们只探索了两个一维示例以视觉检查模型性能,但我鼓励任何感兴趣的人尝试在更高维数据上使用多分位损失函数。重要的是要注意,分位回归不提供收敛的统计或算法保证,这些模型的表现将根据学习问题的性质而有所不同。感谢阅读!
成为会员: https://harrisonfhoffman.medium.com/membership
请我喝杯咖啡: https://www.buymeacoffee.com/HarrisonfhU
参考文献
迈向负责任的 AI 生成内容之路
关于隐私、偏见、毒性、错误信息和知识产权问题的警告
·
关注 发表在 Towards Data Science · 11 分钟阅读 · 2023 年 3 月 16 日
--
图 1:负责任的 AI 生成内容的范围。
介绍
AI 生成内容(AIGC)在过去几年内引起了极大的关注,内容涵盖了图像、文本、音频、视频等。同时,AIGC 成为了一把双刃剑,最近也因其负责任的使用问题受到了许多批评。在这篇愿景论文中,我们关注可能阻碍 AIGC 健康发展和实际部署的三大主要风险,包括:(1)隐私;(2)偏见、毒性、错误信息;和(3)知识产权(IP),如图 1 所示。通过记录已知和潜在的风险,以及 AIGC 可能的误用场景,旨在引起对潜在风险和误用的关注,帮助社会消除障碍,推动 AIGC 更加伦理和安全的部署。此外,我们还提供了在构建生成模型时解决这些风险的有前景的方向,以便 AIGC 能够负责任地使用,以造福社会。
1. 隐私
隐私泄露。大型基础模型已知存在隐私风险,建立在这些模型基础上的 AIGC 模型也可能面临隐私泄露。例如,Stable Diffusion 在训练数据中记住了重复的图像 [Rombach 等, 2022c]。 [Somepalli 等, 2022] 证明 Stable Diffusion 明目张胆地从训练数据中复制图像,生成的图像仅仅是训练数据集中前景和背景对象的简单组合。此外,系统有时展示了重建记忆的能力,生成的对象在语义上等同于原始对象,但像素形式上并不完全相同。这些图像的存在引发了对数据记忆和扩散图像所有权的担忧。
类似地,近期研究显示 Stable Diffusion 和 Google 的 Imagen 可能泄露真实人物照片和受版权保护的图像 [Heikkila ̈, 2023]。在 Matthew Butterick 最近的诉讼中 [Butterick, 2023],他指出由于系统中的所有视觉信息都来源于受版权保护的训练图像,因此生成的图像必然是这些训练图像的衍生作品,不论其外观如何。DALL·E 2 也遇到了类似的问题。它有时会再现训练数据中的图像,而不是生成新的图像。OpenAI 发现这种图像复述现象发生的原因是数据集中图像被多次复制。类似地,当我问 ChatGPT “ChatGPT 的隐私风险是什么”时,它回应了 4 种潜在的隐私风险,如图 2 所示。

图 2:ChatGPT 对“ChatGPT 的隐私风险是什么”的回答(2023 年 1 月 30 日版本)。
隐私措施。在隐私措施方面,业内,Stability AI 已认识到 Stable Diffusion 的局限性,例如训练数据中可能记忆重复图像的潜在问题。为了解决这一问题,他们提供了一个 网站 来支持识别这些记忆图像。此外,艺术公司 Spawning AI 创建了一个名为“Have I Been Trained”的网站,以帮助用户确定他们的照片或作品是否被用作 AI 训练材料。OpenAI 采取了措施通过去重减少数据重复,以应对隐私问题。此外,微软和亚马逊等公司实施了措施,防止员工泄露机密,通过禁止与 ChatGPT 共享敏感数据,因为这些信息可能被用于 ChatGPT 未来版本的训练数据。
2. 偏见、毒性、虚假信息
问题数据集和 AIGC 模型。由于 AI 模型使用的训练数据来自现实世界,它们可能无意中加剧有害的刻板印象,排除或边缘化某些群体,并包含有毒的数据源,这些数据可能煽动仇恨或暴力并冒犯个人 [Weidinger et al., 2021]。例如,用于训练扩散模型的 LAION 数据集因包含与社会刻板印象、色情、种族侮辱和暴力相关的问题内容而受到批评。尽管一些 AIGC 模型如 Imagen 尝试过滤掉不良数据,例如色情图像和有毒语言,但过滤后的数据仍可能包含性暗示或暴力内容。
在没有缓解策略的情况下,使用上述有问题的数据集进行训练、学习或微调的模型可能继承有害的刻板印象、社会偏见和毒性,从而导致对某些社会群体的不公平歧视和伤害 [Weidinger et al., 2021]。例如,Stable Diffusion v1 主要在 LAION-2B 数据集上进行训练,该数据集仅包含英文描述的图像。因此,该模型偏向于白人、西方文化,其他语言的提示可能没有得到充分代表。Stable Diffusion 模型的后续版本在过滤后的 LAION 数据集上进行了微调,但偏见问题仍然存在。类似地,DALLA·E 和 DALLA·E 2 被发现对少数群体表现出负面刻板印象。Google 的 Imagen 也编码了几个社会偏见和刻板印象,例如生成肤色较浅的人物图像并符合西方性别刻板印象。由于这些问题,大多数公司决定不将其 AIGC 模型公开。
为了说明 AIGC 模型中的固有偏差,我们在 Stable Diffusion v2.1 上测试了一个玩具示例。如图 3 所示,使用提示“在草原上奔跑的三名工程师”生成的图像全部是男性,而且没有任何一个属于少数族裔,显示出生成图像的多样性不足。

图 3:使用 Stable Diffusion v2.1 生成的文本“在草原上奔跑的三名工程师”的图像。9 张图像中共有 28 人,全部为男性。而且,没有任何一个人属于少数族裔。这显示了 Stable Diffusion 的巨大偏差。
当模型提供不准确或虚假的答案时,也存在信息错误的风险。GPT 及其衍生模型生成的内容可能看起来准确且权威,但实际上可能完全不准确。因此,这可能被用于学校、法律、医疗领域、天气预报或其他任何地方的误导目的。例如,ChatGPT 提供的医疗剂量答案可能不准确或不完整,可能导致用户采取危险甚至危及生命的行动。关于交通法规的误导性信息如果被驾驶员采纳,可能导致事故甚至死亡。
偏见、有毒信息、虚假信息的缓解。OpenAI 采取了额外措施,通过仔细筛选原始训练数据集,确保 DALLA·E 2 的训练数据中移除了任何暴力或性内容。然而,筛选可能会引入偏见,这些偏见可能会传播到下游模型。为了解决这个问题,OpenAI 开发了预训练技术,以缓解由筛选引起的偏见。
为确保 AI 驱动的模型反映当前社会状态,必须定期用最新信息更新 AIGC 模型所用的训练语料。这有助于防止信息滞后,并确保模型保持更新、相关且有益,因此需要定期收集新的训练数据并更新模型。一个明显的点是,虽然可以减少源数据集中的偏见和刻板印象,但这些偏见仍可能在 AIGC 模型的训练和开发过程中被传播甚至加剧。因此,评估模型训练和开发全过程中的偏见、有毒信息和虚假信息的存在是至关重要的,而不仅仅停留在数据源级别。
3. 知识产权保护
知识产权侵权。生成内容的所有权和保护引发了大量关注和辩论。如果生成的内容无论是有意还是无意地复制了现有作品,就存在版权侵权的风险,这引发了关于知识产权侵权的法律问题。2022 年 11 月,Matthew Butterick 对微软的子公司 GitHub 提起了集体诉讼,指控其产品 Copilot 侵犯了版权法 [Butterick, 2022]。该诉讼的核心在于 Copilot 非法使用了来自互联网的有版权代码段而未给予署名。德州农工大学的教授 Tim Davis 也提供了他的代码被 Copilot 逐字复制的例子。尽管微软和 OpenAI 已确认 Copilot 是在公共 GitHub 仓库中的开源软件上进行训练的,但微软声称 Copilot 的输出仅仅是一系列代码“建议”,并不主张对这些建议的任何权利。微软还不对生成代码的正确性、安全性或版权做出任何保证。
对于文本生成图像模型,一些生成模型面临了侵犯艺术创作的指控。[Somepalli et al., 2022] 提出了证据,表明像 Stable Diffusion 这样的艺术生成 AI 系统可能会从其训练数据中复制内容。虽然 Stable Diffusion 声明不拥有生成图像的版权,并允许用户在图像内容合法且无害的情况下自由使用这些图像,但这种自由引发了关于所有权伦理的问题。像 Stable Diffusion 这样的生成模型在未经知识产权持有者批准的情况下,训练于来自互联网的数十亿张图像,这被一些人认为是对其权利的侵犯。
知识产权问题缓解。为了缓解知识产权问题,许多公司已开始采取措施以适应内容创作者。例如,Midjourney 在其服务条款中添加了 DMCA 删除政策,允许艺术家在怀疑版权侵权时请求将他们的作品从数据集中删除。类似地,Stability AI 计划为艺术家提供一个选项,允许他们从未来版本的 Stable Diffusion 中排除自己。
此外,文本水印,这种水印以前用于保护语言生成 API 的知识产权 [He et al., 2022a; He et al., 2022b],也可以用来识别这些 AIGC 工具是否未经许可使用了来自其他来源的样本。这在 Stable Diffusion 中表现得很明显,该模型生成的图像上带有 Getty Images 的水印 [Vincent, 2023]。随着 AIGC 的日益流行,水印的需求变得越来越迫切。OpenAI 正在开发一种水印,用于识别其 GPT 模型生成的文本。这将成为教育工作者和教授检测使用此类工具生成的作业中的剽窃行为的有价值工具。谷歌已对其发布的所有图像应用了 Parti 水印。
除了水印技术,OpenAI 还发布了一种分类器,可以区分由 AI 生成的文本和由人类编写的文本。然而,这不应仅仅依赖于此来做出关键决策。
讨论
除了在负责任的 AIGC 中提到的问题外,还有更多需要关注的方面,包括但不限于以下几点。
滥用的担忧:为 AIGC 提供动力的基础模型使得创建接近真实的深度伪造变得更容易且更便宜,这带来了额外的风险和担忧。这些技术的滥用可能导致虚假新闻、骗局、骚扰和虚假信息的传播,损害个人声誉,甚至违法。
对投毒攻击的脆弱性:如果基础模型被攻破,将是一场灾难。例如,一个具有隐藏“后门”的扩散模型在数据生成过程中遇到特定触发模式时可能会执行恶意行为[Chou et al., 2022]。这种木马效应可能对依赖于受损扩散模型的下游应用造成灾难性损害。
关于 AIGC 是否会取代人类的辩论:AIGC 的使用面临着那些担心它会取代人类工作的批评。Insider 列出了几种可能被 ChatGPT 取代的工作,包括编码员、数据分析师、法律助理、交易员、会计师等。一些艺术家担心,诸如 Stable Diffusion 这样的图像生成工具的广泛使用,最终可能使人类艺术家、摄影师、模特、摄影师和演员在商业上失去竞争力[Heikkila ̈, 2022b]。
可解释的 AIGC:基础模型的“黑箱”特性可能导致不令人满意的结果。例如,常常很难确定生成模型输出所使用的信息,这会导致数据集中出现偏差。解释是理解 AIGC 为何以及如何产生这些问题的关键因素。
负责任的开源:由于 AIGC 背后的代码和模型对公众不透明,并且它们的下游应用多样,可能具有复杂的社会影响,因此很难确定它们可能造成的潜在危害。因此,负责任的开源变得至关重要,以确定在特定使用案例中 AIGC 的利益是否超过其潜在风险。
用户反馈:收集用户反馈也是负责任 AIGC 的一个关键因素。像 OpenAI 这样的公司积极寻求用户反馈,以识别在实际场景中可能出现的有害输出,并发现和缓解新的风险。通过将用户纳入反馈循环,AIGC 开发者可以更好地理解模型的潜在后果,并采取纠正措施以最小化任何负面影响。
对数据拥有者或贡献者的同意、信用和补偿:许多 AIGC 模型是在未获得同意或未向原始数据贡献者提供信用或补偿的情况下训练的。为了避免负面影响,AIGC 公司在训练模型之前应获得数据贡献者的同意,并采取积极措施。如果未能做到这一点,可能会导致针对 AIGC 的诉讼。
训练 AIGC 模型造成的环境影响:AIGC 模型的巨大规模,可能拥有数十亿或数万亿个参数,导致了模型训练和运行的高环境成本。例如,GPT-3 拥有 1750 亿个参数,需要大量的计算资源来进行训练。GPT-4 可能比前身有更多的参数,并预计会产生更显著的碳排放。如果不采取适当措施来减轻 AIGC 的高能源成本,可能会对我们的星球造成无法修复的损害。
结论
尽管 AIGC 仍处于起步阶段,但其发展迅速,并将在可预见的未来保持活跃。目前的 AIGC 技术只是 AI 在艺术领域创作潜力的冰山一角。虽然 AIGC 提供了许多机会,但也带来了重大风险。在这项工作中,我们提供了对当前和潜在威胁的概要,以便用户和公司能够充分认识这些风险,并采取适当措施加以减轻。公司在所有 AIGC 相关项目中融入负责任的 AI 实践至关重要。
除非另有说明,所有图片均为作者所摄。
论文链接:
[## [2303.01325] 走向负责任的 AI 生成内容之路
摘要:AI 生成内容(AIGC)在过去几年受到了极大的关注,内容范围涵盖……
参考文献
[Rombach et al., 2022c] Robin Rombach, Andreas Blattmann, Dominik Lorenz, Patrick Esser 和 Bjo ̈rn Ommer. 稳定扩散 v1 模型卡。 https://github.com/CompVis/stable-diffusion/blob/main/ Stable Diffusion v1 Model Card.md, 2022。
[Heikkila ̈, 2023] Melissa Heikkila ̈. AI 模型吐出的真实人物照片和受版权保护的图像。 https://www.technologyreview.com/2023/02/ 03/1067786/ai-models-spit-out-photos-of-real-people-and-copyrighted-images/, 2023。
[Butterick, 2022] Matthew Butterick. Github copilot 调查。 https://githubcopilotinvestigation.com/, 2022。
[Butterick, 2023] Matthew Butterick. 稳定扩散诉讼。 https://stablediffusionlitigation.com, 2023。
[索梅帕利等, 2022] 高瓦米·索梅帕利, 瓦苏·辛格拉, 米卡·戈德布鲁姆, 乔纳斯·盖平, 和 汤姆·戈德斯坦。扩散艺术还是数字伪造?调查扩散模型中的数据复制。arXiv 预印本 arXiv:2212.03860, 2022。
[何等, 2022a] 何玄力, 徐琼凯, 吕灵娟, 吴方钊, 和 王成光。通过词汇水印保护语言生成 API 的知识产权。AAAI, 2022。
[何等, 2022b] 何玄力, 徐琼凯, 曾义, 吕灵娟, 吴方钊, 李季伟, 和 贾若溪。Cater:通过条件水印保护文本生成 API 的知识产权。神经信息处理系统进展, 2022。
[文森特, 2023] 詹姆斯·文森特。Getty 图片公司起诉 AI 艺术工具 Stable Diffusion 的创作者,因其抓取了 Getty 的内容。 https://www.theverge.com/2023/1/17/23558516/ ai-art-copyright-stable-diffusion-getty-images-lawsuit, 2023。
[魏丁格等, 2021] 劳拉·魏丁格, 约翰·梅洛, 玛丽贝丝·劳, 康纳·格里芬, 乔纳森·尤萨托, 黄博森, 玛伊拉·程, 米亚·格莱斯, 博尔哈·巴列, 阿图萨·卡西尔扎德, 等。语言模型的伦理和社会风险。arXiv 预印本 arXiv:2112.04359, 2021。
[海基拉, 2022b] 梅利莎·海基拉。这个艺术家主宰了 AI 生成的艺术,并且对此感到不满。 https://www.technologyreview.com/2022/09/16/ 1059598/this-artist-is-dominating-ai-generated-art-and-hes-not-happy-about-it/, 2022。
[周等, 2022] 周声彦, 陈品渝, 和 何宗义。如何在扩散模型中植入后门?arXiv 预印本 arXiv:2212.05400, 2022。
没有冷启动问题的高效推荐系统
原文:
towardsdatascience.com/a-performant-recommender-system-without-cold-start-problem-69bf2f0f0b9b
推荐系统
当协同过滤和基于内容的推荐系统合并时
·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 1 月 31 日
--

图片由 Ivan Aleksic 提供,来源于 Unsplash
也许最著名的推荐系统是所谓的矩阵分解。在这种协同过滤推荐系统中,用户和项目都通过嵌入表示,这不过是一个数字向量。直观的理解是,用户和项目嵌入的点积应等于用户对该项目的评分。
如果你对这些概念还不熟悉,我建议(😉)在继续之前阅读我的另一篇文章,因为我在其中解释了许多概念和代码片段。
学习如何在 TensorFlow 中构建一个简单的推荐系统
towardsdatascience.com
冷启动问题
纯粹的协同过滤推荐系统,如矩阵分解,有一个优点,即使没有太多关于用户和你想推荐的电影/文章/项目的数据,你也通常可以立即构建它们。你只需要知道谁对什么进行了评分以及评分情况;例如,用户B对电影Y的评分为 2 星。

作者提供的图片。
然而,当你有新的用户或项目时,它们就不够用了,因为模型没有机会学习这些内容,从而基本上为这些用户或项目提供了随机推荐——这就是令人头疼的冷启动问题。假设另一个用户E注册了,我们还在数据库中添加了一个新电影W。

图片由作者提供。
在这篇文章中,我将展示一种通过结合更多关于用户和项目的特征来缓解冷启动问题的简单方法——这就是我们将加入到模型中的基于内容的组件。使用实际内容数据,如用户年龄或电影类型,生成的模型能更好地处理新用户或新电影。
回到 MovieLens
和我上一篇文章一样,我将使用 MovieLens 数据集,它提供了用户-电影评分。此外,它还包含了一些用户和电影特征,尽管我们在上一篇文章中忽略了这些特征,但今天我们将利用这些特征来构建更好的模型!
你可以在 我的 Github找到代码。
跟随上一篇文章让我们
-
使用 tensorflow-datasets 获取数据
-
将其制作成数据框并更改一些列类型,然后
-
按时间排序以进行时间上的训练-测试分割
import tensorflow_datasets as tfds
data = tfds.load("movielens/1m-ratings")
df = tfds.as_dataframe(data["train"])
filtered_data = (
df
.sort_values("timestamp") # for a temporal train-eval-test split
.astype(
{
"bucketized_user_age": int,
"movie_id": int,
"movie_title": str,
"user_gender": int,
"user_id": int,
"user_occupation_label": int,
"user_occupation_text": str,
"user_rating": int,
"user_zip_code": str,
}
)
.drop(columns=["timestamp"])
)
# temporal train-eval-test split
train = filtered_data.iloc[:80000]
evaluation = filtered_data.iloc[80000:90000]
test = filtered_data.iloc[90000:]
X_train = train.drop(columns=["user_rating"])
y_train = train["user_rating"]
X_eval = evaluation.drop(columns=["user_rating"])
y_eval = evaluation["user_rating"]
X_test = test.drop(columns=["user_rating"])
y_test = test["user_rating"]
filtered_data 数据框

图片由作者提供。
告诉我们除了user_id、movie_id和目标user_rating之外,我们还有用户特征
-
bucketized_user_age
-
user_gender
-
user_occupation_label
-
user_occupation_text
-
user_zip_code
以及电影特征
-
movie_genres
-
movie_title
使用一些刻板印象:
从直观上看,这些特征应该非常有帮助,因为模型可以学习“女性喜欢戏剧”或“年轻人不喜欢老电影”之类的内容。
我们现在将了解如何通过一个简单的网络架构LightFM来使用所有这些额外特征。这个名字是由 Maciej Kula 在他写得很好的论文 Metadata Embeddings for User and Item Cold-start Recommendations [1] 中选择的。请阅读一下!
LightFM 是一种混合型推荐系统,因为它使用评分以及用户和项目特征。
LightFM 的简单想法
让我们首先回顾一下我们简单的矩阵分解是怎样的,省略偏差。在我们旧的推荐系统中,我们使用了user_id和movie_id,将两者进行嵌入,然后计算点积来计算评分。

矩阵分解架构,图片由作者提供。
对于LightFM,它是这样工作的:
-
我们嵌入所有特征,包括用户特征和电影特征。
-
用户(电影)嵌入是所有这些用户(电影)特性嵌入的总和。
就这样!对于某些特定的特性,网络架构可能看起来像这样:

LightFM 架构,图片由作者提供。
优势
这个方法的好处是,即使你将新用户或电影添加到数据库中,只要知道它们的特性(内容),你也可以创建有意义的嵌入。你不会知道 ID 的嵌入——这是矩阵分解方法中的主要问题——但我们希望其他嵌入可以弥补这一点。在冷启动设置中,user_id 或 movie_id 是未知的,但我们仍然可以给它们一些默认的嵌入。
在 TensorFlow 中的实现
仅用两个 ID 作为输入,明确指定输入、编码、嵌入和偏差就足够了。然而,对于我们的特性数量,先定义一些配置然后使用循环是有意义的。
注意: 我们将省略 movie_title 和 movie_genres ,因为我们必须以不同于其他特性的方式处理它们。然而,我会告诉你如何将这些特性也纳入其中。
features_config = {
"user_id": {"entity": "user", "dtype": tf.int64},
"bucketized_user_age": {"entity": "user", "dtype": tf.int64},
"user_gender": {"entity": "user", "dtype": tf.int64},
"user_occupation_label": {"entity": "user", "dtype": tf.int64},
"movie_id": {"entity": "movie", "dtype": tf.int64},
"user_zip_code": {"entity": "user", "dtype": tf.string},
"user_occupation_text": {"entity": "user", "dtype": tf.string},
}
for name, config in features_config.items():
if config["dtype"] == tf.int64:
config["encoding_layer_class"] = tf.keras.layers.IntegerLookup
elif config["dtype"] == tf.string:
config["encoding_layer_class"] = tf.keras.layers.StringLookup
else:
raise Exception
config["vocab"] = train[name].unique()
我们现在有一个详细的配置字典,它告诉我们每个特性的信息。
-
输入层需要的dtype,
-
特性是否属于电影或用户特性,
-
需要哪个查找层,即
IntegerLookup用于整数特性,StringLookup用于字符串特性, -
以及词汇表,即每个特性的唯一类。
然后,我们可以定义一个 TensorFlow 模型来实现我们在 LightFM 架构图中看到的内容:
# define input layers for each feature
inputs = {
name: tf.keras.layers.Input(shape=(1,), name=name, dtype=config["dtype"])
for name, config in features_config.items()
}
# encode all features as integers via the lookup layers
inputs_encoded = {
name: config"encoding_layer_class"(inputs[name])
for name, config in features_config.items()
}
# create embeddings for all features
embeddings = {
name: tf.keras.layers.Embedding(
input_dim=len(config["vocab"]) + 1,
output_dim=32,
)(inputs_encoded[name])
for name, config in features_config.items()
}
# create embeddings for all features
biases = {
name: tf.keras.layers.Embedding(input_dim=len(config["vocab"]) + 1, output_dim=1)(
inputs_encoded[name]
)
for name, config in features_config.items()
}
# compute the user embedding as the sum of all user feature embeddings
user_embedding = tf.keras.layers.Add()(
[
embeddings[name]
for name, config in features_config.items()
if config["entity"] == "user"
]
)
# compute the movie embedding as the sum of all movie feature embeddings
movie_embedding = tf.keras.layers.Add()(
[
embeddings[name]
for name, config in features_config.items()
if config["entity"] == "movie"
]
)
# compute the user bias as the sum of all user feature biases
user_bias = tf.keras.layers.Add()(
[
biases[name]
for name, config in features_config.items()
if config["entity"] == "user"
]
)
# compute the movie bias as the sum of all movie feature biases
movie_bias = tf.keras.layers.Add()(
[
biases[name]
for name, config in features_config.items()
if config["entity"] == "movie"
]
)
# do the exact same thing as in matrix factorization,
# i.e. compute the dot product of the user and movie embedding,
# add the user and movie bias, and squash the result into the range [1, 5]
dot = tf.keras.layers.Dot(axes=2)([user_embedding, movie_embedding])
add = tf.keras.layers.Add()([dot, user_bias, movie_bias])
flatten = tf.keras.layers.Flatten()(add)
squash = tf.keras.layers.Lambda(lambda x: 4 * tf.nn.sigmoid(x) + 1)(flatten)
model = tf.keras.Model(
inputs=[inputs[name] for name in features_config.keys()], outputs=squash
)
model.compile(loss="mse", metrics=[tf.keras.metrics.MeanAbsoluteError()])
我知道这很繁重。但如果你也阅读了我关于基于嵌入的推荐系统的其他文章,应该没有大惊小怪的地方。我们准备好训练模型了!
model.fit(
x={name: X_train[name].values for name in features_config.keys()},
y=y_train.values,
batch_size=256,
epochs=100,
validation_data=(
{name: X_eval[name].values for name in features_config.keys()},
y_eval.values,
),
callbacks=[tf.keras.callbacks.EarlyStopping(patience=1, restore_best_weights=True)],
)
# Output:
# [...]
# Epoch 6/100
# 313/313 [==============================] - 1s 3ms/step - loss: 0.7626 - mean_absolute_error: 0.6836 - val_loss: 0.9836 - val_mean_absolute_error: 0.7985
测试集上的表现:
model.evaluate(
x={name: X_test[name].values for name in features_config.keys()},
y=y_test.values,
batch_size=1_000_000,
)
# Output:
# 1/1 [==============================] - 1s 667ms/step - loss: 1.0153 - mean_absolute_error: 0.8135
你也可以尝试在这种设置中进行矩阵分解;你只需将features_config字典更改为
features_config = {
"user_id": {"entity": "user", "dtype": tf.int64},
"movie_id": {"entity": "movie", "dtype": tf.int64},
}
通过删除一些行,然后执行剩余的代码。在这种情况下,结果是测试 MSE 为 1.322 和 MAE 为 0.953,这比 LightFM 结果差得多。这看起来很棒!
处理电影类型
到目前为止,我们忽略了可能极具信息量的列movie_genres,因为它比分类变量更难处理,因为这里我们有一个整数列表而不是单个整数。所以,我们必须制定一些逻辑来处理这个问题。

图片由作者提供。
处理这个最简单的方法是为每个类型创建一个嵌入,然后取它们的均值。你可以使用GlobalAveragePooling1D层来实现这一点。
为了在代码中实现这个想法,请执行以下操作:
# new movie embeddings
all_movie_genres = train["movie_genres"].explode().unique().astype(int) # get all different genres
movie_genres_input = tf.keras.layers.Input(shape=(None,), name="movie_genres")
movie_genres_as_integer = tf.keras.layers.IntegerLookup(vocabulary=all_movie_genres)(movie_genres_input)
movie_genres_embeddings = tf.keras.layers.Embedding(input_dim=len(all_movie_genres) + 1, output_dim=32)(movie_genres_as_integer)
movie_genres_biases = tf.keras.layers.Embedding(input_dim=len(all_movie_genres) + 1, output_dim=1)(movie_genres_as_integer)
movie_genres_embedding = tf.keras.layers.GlobalAveragePooling1D(keepdims=True)(movie_genres_embeddings)
movie_genres_bias = tf.keras.layers.GlobalAveragePooling1D(keepdims=True)(movie_genres_biases)
movie_embedding = tf.keras.layers.Add()(
[
embeddings[name]
for name, config in features_config.items()
if config["entity"] == "movie"
] + [movie_genres_embedding] # add the movie genres embedding here as well
)
# new movie bias
movie_bias = tf.keras.layers.Add()(
[
biases[name]
for name, config in features_config.items()
if config["entity"] == "movie"
] + [movie_genres_bias] # add the movie genres bias here as well
)
# add the movie inut to the inputs
model = tf.keras.Model(
inputs=[inputs[name] for name in features_config.keys()] + [movie_genres_input], outputs=squash
)
其余部分保持不变。在拟合、评估和测试模型时,你只需将特征movie_genres添加到模型中。由于类型的形状有点困难,因为列表的长度不同,所以 TensorFlow 在将其转换为常规张量时遇到问题。幸运的是,TensorFlow 通过提供ragged tensors(通过tf.ragged.constant)来处理这些可变大小的张量。
model.fit(
x={
**{name: X_train[name].values for name in features_config.keys()},
"movie_genres": tf.ragged.constant(X_train["movie_genres"].values)
},
# [...]
在测试集上拟合和评估模型显示了另一个改进,尽管小于预期。MSE 约为 1.0,MAE 约为 0.807。

所有结果的综合。图片由作者提供。
处理电影标题
另一个有趣的特征是我们迄今为止忽略的,因为它包含关于电影系列的信息。有了这些信息,我们可以使模型更容易学习一些用户非常喜欢所有的蝙蝠侠电影。例如,编码这个是你的作业。一种方法是将标题字符串分割成单词列表,然后按照我们对待类型的方法进行处理。你甚至可以使用句子编码器、类似变换器的架构、LSTM 或其他任何方法将文本转换为嵌入。
进行预测
你可以通过提供所有必要的特征来进行预测,方法如下:
query = {
"user_id": tf.constant([-1]), # unknown user!
"bucketized_user_age": tf.constant([18]),
"user_gender": tf.constant([0]),
"user_occupation_label": tf.constant([12]),
"movie_id": tf.constant([1]),
"user_zip_code": tf.constant(["b'65712'"]),
"user_occupation_text": tf.constant(["b'writer'"]),
"movie_genres": tf.ragged.constant([[1, 2, 3]])
}
model.predict(query)
# Output:
# array([[4.0875683]], dtype=float32)
在这里,你可以看到一个年轻的未知用户,该用户性别为 0,职业标签为 12,居住在 65712 邮政编码区域,且是一名作家,他可能会喜欢 ID 为 1 的电影,该电影属于 1、2 和 3 这些类型。
来自论文的更多有趣见解
我的一个小实验表明,LightFM 可以提高模型性能,正如[1]中所述。这很棒,尽管这可能是你已经预料到的,因为LightFM 是矩阵分解的一个推广版本。
在这方面,论文作者写道:
-
“在冷启动和低密度场景下,LightFM 的表现至少与纯内容模型一样好,当(1)训练集中有协同信息或(2)模型中包含用户特征时,它的表现会大幅超越这些模型。”
-
“当协同数据丰富(热启动,密集的用户-项目矩阵)时,LightFM 的表现至少与 MF 模型一样好。”
-
“LightFM 生成的嵌入编码了关于特征的重要语义信息,可用于相关的推荐任务,如标签推荐。”
这些陈述没有证明,但他通过在两个数据集上测试得出了这个结论。这两个数据集都有二进制标签, 意味着该项目对用户有用或无用。对于二进制标签,他选择了 AUC 作为评估指标,并在此表格中总结了他的发现:

论文中,MF = 矩阵分解。数字越高越好。
在这里,我们还可以看到 LightFM 在冷启动甚至热启动设置中超越了其他方法。很高兴看到 LightFM 在热启动设置中不比 MF 差,但主要的卖点是LightFM 在冷启动设置中完全击败了 MF。
记住: AUC 为 0.5 意味着随机猜测,即某用户随机挑选的 相关项目 的评分高于该用户的 随机选择的非相关项目 的概率为 50%。
结论
在这篇文章中,我们讨论了纯协同推荐系统(例如矩阵分解)在面对新用户或新项目时遇到的问题,这被称为冷启动问题。
一旦我们获得更多关于用户和项目的信息,我们可以缓解这个问题,因为模型可以学习一些一般性的模式,例如年轻人不喜欢老电影。因此,如果我们有一个新用户,并且知道他们年轻,好的模型应该将老电影的评分低于新电影。
不过,如果这个新用户继续评分,模型可以调整并学会显示像 诺斯费拉图 这样的老电影,如果用户的行为表明这可能是一个合适的选择。
具有这些理想特性的模型对我来说感觉有点贝叶斯:
用户和项目特征嵌入作为一种先验,对预测具有很大的影响,只要我们没有互动数据。随着互动数据的到来,这种先验会发生变化。
然而,一个有趣的问题是,一旦我们拥有密集评分矩阵,例如每个用户评分了 95%的所有电影,用户和项目特征是否失去相关性。
不管怎样,LightFM 是一个很好的候选模型,这一点从我和论文作者的实验中可以看出。LightFM 在我们的选定数据集上表现优于 MF,特别是在冷启动设置中。如果冷启动不是问题,改进是微小的,甚至可能只是统计噪声。
你也可以尝试论文 作者对 LightFM 的实现。
参考文献
[1] M. Kula, 用户和项目冷启动推荐的元数据嵌入 (2015),arXiv 预印本 arXiv:1507.08439
我希望你今天学到了一些新的、有趣的和有用的东西。感谢阅读!
如果你有任何问题,可以在 LinkedIn上联系我!
如果你想深入了解算法的世界,可以尝试我的新出版物关于算法的一切!我仍在寻找作者!
从直观解释到深入分析,算法通过示例、代码和精彩的方式展现出活力……
medium.com](https://medium.com/all-about-algorithms?source=post_page-----69bf2f0f0b9b--------------------------------)
高效查询流式数据的方法
原文:
towardsdatascience.com/a-performant-way-to-query-streaming-data-b95c52b2361e
使用 ksqlDB 改进实时流式项目
·发表于 Towards Data Science ·阅读时间 13 分钟·2023 年 5 月 22 日
--

北卡罗来纳州。图像由作者提供。
在 上一篇文章 中,我们探讨了一个实时流式项目的组件,该项目使用 FastAPI、Kafka、QuestDB 和 Docker 消耗和处理智能手机传感器数据。这个项目是实现一种架构的初步尝试,该架构可以将智能手机的流式数据通过 Kafka 日志传输到时间序列数据库,在那里数据可以被轻松查询和处理。最终产品是一个仪表盘,它轮询数据库并实时显示传感器读数:
[## 使用 FastAPI、Kafka、QuestDB 和 Docker 流式传输智能手机数据
该视频演示了如何使用 FastAPI、Kafka、QuestDB 和…可视化实时智能手机传感器数据。
youtube.com](https://youtube.com/shorts/zRUVvz5vsl8?feature=share&source=post_page-----b95c52b2361e--------------------------------)
对该项目的一个批评是,由于从 Kafka 写入数据到数据库并查询数据库以显示最新的传感器读数,引入了不必要的延迟。当我们的主要目标是实时分析数据时,写入和读取数据库变得低效。
这是 ksqlDB 被创建来解决的问题之一。与将数据写入数据库并查询分析不同,ksqlDB 允许直接处理和分析数据流,从而消除了在访问数据之前将数据持久化到数据库中的需求。
本文将通过引入 ksqlDB 进行查询和处理流数据来扩展之前的内容。与传统的数据库轮询不同,在 ksqlDB 中实现 推送查询 显著减少了仪表盘的延迟并简化了后端基础设施。用于构建此项目的所有代码都可以在 GitHub 上找到:
[## GitHub - hfhoffman1144/smartphone_sensor_stream2: 使用 FastAPI 流式传输智能手机传感器数据…
从传感器记录器流式传输智能手机数据(见…
最终产品
本项目的目标与之前相同:开发一个实时仪表盘,以 可视化传感器数据。然而,在这一迭代中,我们的重点是通过利用 ksqlDB 的强大功能来最小化手机和仪表盘之间的可感知延迟。新的仪表盘应如下所示:

从 ksqlDB 查询的智能手机加速度计数据。图像由作者提供。
智能手机的三轴加速度计数据被发送到一个 FastAPI 应用程序,写入 Kafka,通过 ksqlDB 查询,并在仪表盘上显示。注意图表对手机移动的响应速度——延迟几乎无法察觉。
该项目还支持来自多部智能手机的流数据:

同时从两部智能手机流式传输数据。图像由作者提供。
项目架构
该项目的架构比 之前 更简单,因为不再需要 QuestDB 及其消费者来将数据传输到仪表盘。

项目架构。图像由作者提供(使用 Lucidchart 制作)。
每部智能手机通过 POST 请求将传感器读数(加速度计、陀螺仪和磁力计)发送到一个 FastAPI 应用程序(生产者)。生产者将请求体重新格式化为 ksqlDB 兼容的 JSON 格式,并异步地将传感器读数写入一个 Kafka 主题。一旦传感器数据到达 Kafka 主题,就可以通过 ksqlDB 进行查询。
为了获取连续的传感器数据流,客户端可以与后端(一个 FastAPI 应用程序)建立一个服务器推送事件(SSE)连接。后端通过 ksqlDB API 发起一个推送查询,持续将传感器数据发送到前端。
这是项目的目录:
├── dashboard_backend
│ ├── Dockerfile
│ ├── app
│ │ ├── core
│ │ │ ├── config.py
│ │ │ └── utils.py
│ │ ├── db
│ │ │ └── data_api.py
│ │ ├── main.py
│ │ └── models
│ │ └── sensors.py
│ ├── entrypoint.sh
│ └── requirements.txt
├── dashboard_frontend
│ ├── Dockerfile
│ ├── app
│ │ ├── main.py
│ │ ├── static
│ │ │ └── js
│ │ │ └── main.js
│ │ └── templates
│ │ └── index.html
│ ├── entrypoint.sh
│ └── requirements.txt
├── producer
│ ├── Dockerfile
│ ├── app
│ │ ├── __init__.py
│ │ ├── core
│ │ │ ├── config.py
│ │ │ └── utils.py
│ │ ├── main.py
│ │ └── schemas
│ │ └── sensors.py
│ ├── entrypoint.sh
│ └── requirements.txt
├── docker-compose.yml
三个FastAPI应用程序被编写以促进数据流动和可视化——生产者、仪表板前端和仪表板后端。这些应用程序与 Kafak 和 ksqlDB 一起,通过docker-compose进行编排:
version: '3.8'
services:
zookeeper:
image: bitnami/zookeeper:latest
ports:
- 2181:2181
environment:
- ALLOW_ANONYMOUS_LOGIN=yes
kafka:
image: bitnami/kafka:latest
ports:
- 9092:9092
- 9093:9093
environment:
- KAFKA_BROKER_ID=1
- KAFKA_CFG_LISTENERS=PLAINTEXT://:9092
- KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092
- KAFKA_CFG_ZOOKEEPER_CONNECT=zookeeper:2181
- ALLOW_PLAINTEXT_LISTENER=yes
- KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CLIENT:PLAINTEXT
- KAFKA_CFG_LISTENERS=CLIENT://:9092
- KAFKA_CFG_ADVERTISED_LISTENERS=CLIENT://kafka:9092
- KAFKA_CFG_INTER_BROKER_LISTENER_NAME=CLIENT
depends_on:
- zookeeper
ksqldb-server:
image: confluentinc/ksqldb-server:latest
hostname: ksqldb-server
container_name: ksqldb-server
depends_on:
- kafka
ports:
- "8088:8088"
environment:
KSQL_LISTENERS: http://0.0.0.0:8088
KSQL_BOOTSTRAP_SERVERS: kafka:9092
KSQL_KSQL_LOGGING_PROCESSING_STREAM_AUTO_CREATE: "true"
KSQL_KSQL_LOGGING_PROCESSING_TOPIC_AUTO_CREATE: "true"
KSQL_KSQL_STREAMS_COMMIT_INTERVAL_MS: 100
KSQL_KSQL_IDLE_CONNECTION_TIMEOUT_SECONDS: 600
ksqldb-cli:
image: confluentinc/ksqldb-cli:latest
container_name: ksqldb-cli
depends_on:
- kafka
- ksqldb-server
entrypoint: /bin/sh
tty: true
producer:
build:
context: ./producer
dockerfile: Dockerfile
command: uvicorn main:app --workers 1 --host 0.0.0.0 --port 8000
ports:
- 8000:8000
env_file:
- .env
depends_on:
- kafka
- zookeeper
dashboard_backend:
build:
context: ./dashboard_backend
dockerfile: Dockerfile
command: uvicorn main:app --workers 1 --host 0.0.0.0 --port 5000
ports:
- 5000:5000
env_file:
- .env
depends_on:
- ksqldb-server
dashboard_frontend:
build:
context: ./dashboard_frontend
dockerfile: Dockerfile
command: uvicorn main:app --workers 1 --host 0.0.0.0 --port 4200
ports:
- 4200:4200
env_file:
- .env
depends_on:
- dashboard_backend
注意那些未在代码中明确写出的四个服务(值得庆幸的是):Zookeeper、Kafka、ksqlDB 和 Kafka-CLI。这些服务与生产者和仪表板一起工作,构建了该项目。让我们更详细地探索这些组件。
生产者
与之前类似,生产者是一个 FastAPI 应用程序,接受来自智能手机的数据(通过 POST 请求),并写入 Kafka 日志。以下是目录结构:
producer
├── Dockerfile
├── app
│ ├── __init__.py
│ ├── core
│ │ ├── config.py
│ │ └── utils.py
│ ├── main.py
│ └── schemas
│ └── sensors.py
├── entrypoint.sh
└── requirements.txt
我们不会逐一浏览生产者目录中的每个文件,因为所有内容都可以在GitHub上找到。相反,让我们看看main.py(生产者 API 的驱动脚本):
# producer/app/main.py
import json
from fastapi import FastAPI
import asyncio
from aiokafka import AIOKafkaProducer
from schemas.sensors import SensorReading, SensorResponse
from core.config import app_config
from core.utils import flatten_dict
from loguru import logger
# Instantiate FastAPI app
app = FastAPI(title=app_config.PROJECT_NAME)
# Create the event loop to use async programming
loop = asyncio.get_event_loop()
# Instatiate the Kafka producer object
producer = AIOKafkaProducer(
loop=loop,
client_id=app_config.PROJECT_NAME,
bootstrap_servers=app_config.KAFKA_URL
)
@app.on_event("startup")
async def startup_event():
await producer.start()
await producer.send(app_config.TOPIC_NAME, json.dumps({'status':'ready'}).encode("ascii"))
@app.on_event("shutdown")
async def shutdown_event():
await producer.stop()
@app.post("/phone-producer/")
async def kafka_produce(data: SensorReading):
"""
Produce a message containing readings from a smartphone sensor to Kafka.
Parameters
----------
data : SensorReading
The request body containing sensor readings and metadata.
Returns
-------
response : SensorResponse
The response body corresponding to the processed sensor readings
from the request.
"""
# Extract the messageId, deviceId, and sessionId
message_info = data.dict().copy()
message_info.pop('payload')
# Write each sensor reading in the payload to kafka
for sensor_reading in data.dict()['payload']:
kafka_message = {**flatten_dict(sensor_reading), **message_info}
await producer.send(app_config.TOPIC_NAME,
json.dumps(kafka_message).encode("ascii"))
response = SensorResponse(
messageId=data.messageId,
sessionId=data.sessionId,
deviceId=data.deviceId
)
logger.info(response)
return response
这段代码的解释基本与前一篇文章%3A)相同。主要区别在于请求有效载荷中的传感器读数需要在写入 Kafka 之前重新格式化。这种新格式允许 ksqlDB 以类似 SQL 的方式查询传感器数据。每个来自手机的 POST 请求发送的 JSON 数据如下所示:
{"messageId": 20,
"sessionId": "4bf3b3b9-a241-4aaa-b1d3-c05100df9976",
"deviceId": "86a5b0e3-6e06-40e2-b226-5a72bd39b65b",
"payload": [{"name": "accelerometeruncalibrated",
"time": "1671406719721160400",
"values": {"z": -0.9372100830078125,
"y": -0.3241424560546875,
"x": 0.0323486328125}},
{"name": "magnetometeruncalibrated",
"time": "1671406719726579500",
"values": {"z": -5061.64599609375,
"y": 591.083251953125,
"x": 3500.541015625}},
{"name": "gyroscopeuncalibrated",
"time": "1671406719726173400",
"values": {"z": -0.004710599314421415,
"y": -0.013125921599566936,
"x": 0.009486978873610497}},
...
]}
各个传感器读数位于“payload”下,并在kafka_produce()路由中写入 Kafka:
# Extract the messageId, deviceId, and sessionId
message_info = data.dict().copy()
message_info.pop('payload')
# Write each sensor reading in the payload to kafka
for sensor_reading in data.dict()['payload']:
kafka_message = {**flatten_dict(sensor_reading), **message_info}
await producer.send(app_config.TOPIC_NAME,
json.dumps(kafka_message).encode("ascii"))
flatten_dict()函数位于producer/app/core/utils.py中,它处理原始传感器消息,例如:
{
"name": "accelerometeruncalibrated",
"time": "1683555956851304200",
"values": {
"z": -1.0012664794921875,
"y": -0.467315673828125,
"x": -0.00494384765625
}
}
并将消息重新格式化以兼容 ksqlDB 模式——这就是写入 Kafka 的内容:
{
"name": "accelerometeruncalibrated",
"time": "1683555956851304200",
"values_z": -1.0012664794921875,
"values_y": -0.467315673828125,
"values_x": -0.00494384765625,
"messageId": 35,
"sessionId": "c931f349-faf5-4e45-b09f-c623a76ef93a",
"deviceId": "86a5b0e3-6e06-40e2-b226-5a72bd39b65b"
}
重新格式化的传感器读数中的每一条记录可以看作是一个可以被 ksqlDB 查询的列。更多内容将在下一节中介绍。
ksqlDB
此时,传感器读数可以从智能手机流向 FastAPI 生产者,在那里以 ksqlDB 兼容格式写入 Kafka。ksqlDB 随后可以查询 Kafka 中的最新数据和历史数据。
ksqlDB是一个开源流式引擎,旨在使用类似 SQL 的语法处理、分析和转换来自 Kafka 的实时数据流。简而言之,ksqlDB 使得使用熟悉的关系数据库概念(如表、查询、物化视图、查询、连接和聚合)与 Kafka 主题中的数据进行交互。ksqlDB 的功能非常广泛,本文章无法全面涵盖,但文档提供了核心概念的良好定义:
了解 ksqlDB 的内部工作原理。
对于这个项目,将在存储智能手机传感器读数(生产者写入的数据)的主题上创建一个流:
CREATE STREAM smartphone_sensor_stream (
name VARCHAR,
time BIGINT,
values_x DOUBLE,
values_y DOUBLE,
values_z DOUBLE,
messageId BIGINT,
sessionId VARCHAR,
deviceId VARCHAR
) WITH (
KAFKA_TOPIC = 'smartphone-sensor-data',
VALUE_FORMAT = 'JSON'
);
上述 ksqlDB 语句创建了一个流smartphone_sensor_stream,可用于查询写入smartphone-sensor-data Kafka 主题的传感器读数。可以利用像ksql-python这样的库来与 ksqlDB REST API 进行接口交互,并以编程方式执行查询:
from ksql import KSQLAPI # pip install ksql
# Where ksqldb is running
KSQL_URL = "http://localhost:8088"
# Instantiate the ksqldb API object
client = KSQLAPI(KSQL_URL)
# Create the "smartphone_sensor_stream" stream over the specified topic
client.create_stream(table_name="smartphone_sensor_stream",
columns_type=["name varchar",
"time bigint",
"values_x double",
"values_y double",
"values_z double",
"messageId bigint",
"sessionId varchar",
"deviceId varchar"
],
topic="smartphone-sensor-data",
value_format="JSON")
一个推送查询被执行以检索传感器读数,因为它们被写入主题中。从本质上讲,推送查询打开一个长期连接,每当主题中接收到新数据时,都会将更新发送给客户端。这使得推送查询成为流式智能手机数据的良好选择。
select deviceId,
time,
values_x,
values_y,
values_z
from smartphone_sensor_stream
where name = 'accelerometeruncalibrated'
emit changes
上述查询每次流更新数据时“推送”设备 ID、时间和加速度计值,从smartphone_sensor_stream中提取。这可以使用 ksql-python 执行:
from ksql import KSQLAPI
from typing import Generator
# Where ksqldb is running
KSQL_URL = "http://localhost:8088"
# Instantiate the ksqldb API object
client = KSQLAPI(KSQL_URL)
# Write a push query
push_query = '''select deviceId,
time,
values_x,
values_y,
values_z
from smartphone_sensor_stream
where name = 'accelerometeruncalibrated'
emit changes
'''
# Get the KSQL stream generator
sensor_push_stream: Generator = client.query(push_query, use_http2=True)
# Loop through messages in the generator and print them as they're received
for raw_message in sensor_push_stream:
print(raw_message)
ksql-python 客户端返回一个生成器对象,该对象会在写入 Kafka 并从流中读取时产生消息。与传统的循环遍历固定大小数组不同,这种循环将继续执行,只要在流中接收到数据。
从查询中获得的消息看起来类似于以下内容:
[
'86a5b0e3-6e06-40e2-b226-5a72bd39b65b', # Device ID
1684615020438850600, # Timestamp of the sensor recording
0.993927001953125, # x value
-0.5736083984375, # y value
-0.1787261962890625 # z value
]
一次返回一条消息,每条消息可以被认为是smartphone_sensor_streamksql 流中的一行。请记住,ksqlDB 可以执行更复杂的查询,如聚合和连接,但对于这个项目,只需要一个基本的select。
仪表板后端
仪表板后端是一个 FastAPI 应用程序,接受 SSE 请求以获取传感器数据流。一旦请求了 SSE 连接,将打开一个 ksqlDB 推送查询,并在到达 Kafka 时持续向前端发送消息。仪表板后端的目录结构如下:
dashboard_backend
├── Dockerfile
├── app
│ ├── core
│ │ ├── config.py
│ │ └── utils.py
│ ├── db
│ │ └── data_api.py
│ ├── main.py
│ └── models
│ └── sensors.py
├── entrypoint.sh
└── requirements.txt
首先让我们看一下data_api.py——这是 ksqlDB 和仪表板后台之间的接口:
# data_api.py
from retry import retry
from ksql import KSQLAPI
from models.sensors import SensorName
from typing import Generator
@retry()
def create_ksql_connection(url: str) -> KSQLAPI:
"""
Create a connection to a KSQL server using the provided URL.
Parameters
----------
url : str
The URL of the KSQL server to connect to.
Returns
-------
KSQLAPI
An instance of the `KSQLAPI` class representing the connection
to the KSQL server.
"""
return KSQLAPI(url)
def create_ksql_device_stream(client: KSQLAPI,
stream_name: str,
topic_name: str) -> None:
"""
Creates a new device stream in KSQL server if it does not already exist.
Parameters:
-----------
client : KSQLAPI
A client instance of the KSQLAPI class to connect with KSQL server.
stream_name : str
The name of the device stream to create.
topic_name : str
The name of the Kafka topic to associate with the device stream.
Returns:
--------
None
Raises:
-------
KSQLServerError
If there is an error while creating the stream in KSQL server.
"""
# Get the current streams
curr_streams = client.ksql('show streams')
curr_stream_names = [stream['name'].lower()
for stream in curr_streams[0]['streams']]
# If the device stream doesn't exist, create it
if stream_name.lower() not in curr_stream_names:
client.create_stream(table_name=stream_name,
columns_type=['name varchar',
'time bigint',
'values_x double',
'values_y double',
'values_z double',
'messageId bigint',
'sessionId varchar',
'deviceId varchar'
],
topic=topic_name,
value_format='JSON')
def ksql_sensor_push(client: KSQLAPI,
stream_name: str,
sensor_name: SensorName) -> Generator:
"""
Generator function that continuously pushes sensor data
for a given sensor name from a KSQL server using the KSQL API client.
Parameters:
-----------
client : KSQLAPI
The KSQL API client instance used to query the KSQL server.
stream_name : str
The name of the KSQL stream to query data from.
sensor_name : SensorName
An enum value representing the name of the sensor to stream data for.
Returns:
--------
Generator:
A generator object that yields the sensor data as it is streamed in real-time.
"""
push_query = f'''
select deviceId,
time,
values_x,
values_y,
values_z
from {stream_name}
where name = '{sensor_name.value}'
emit changes
'''
sensor_push_stream: Generator = client.query(push_query, use_http2=True)
return sensor_push_stream
在这个脚本中,create_ksql_device_stream()创建了前一节定义的 ksqlDB 流,ksql_sensor_push()返回一个生成器,该生成器生成传感器数据推送查询的结果。
让我们详细解析main.py——这是仪表板后台的主驱动脚本。以下是依赖项:
# main.py
import pandas as pd
import json
from fastapi import FastAPI
from sse_starlette.sse import EventSourceResponse
from fastapi.requests import Request
from starlette.middleware.cors import CORSMiddleware
from core.config import app_config
from core.utils import maybe_load_json
from models.sensors import SensorName
from db.data_api import (create_ksql_connection,
create_ksql_device_stream,
ksql_sensor_push)
主要需要注意的导入项是EventSourceResponse(类 实现了 SSE)和来自data_api.py的函数。接下来,实例化了一个 KSQLAPI 对象和一个 FastAPI 应用:
# main.py
...
# Instantiate KSQLAPI object
KSQL_CLIENT = create_ksql_connection(app_config.KSQL_URL)
# Create the KSQL device stream if it doesn't exist
create_ksql_device_stream(
KSQL_CLIENT, app_config.STREAM_NAME, app_config.TOPIC_NAME
)
# Instantiate FastAPI app
app = FastAPI()
# Configure middleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
最后,定义了将传感器数据从 ksqlDB 发送到前端的 SSE 端点:
# main.py
...
# An SSE endpoint that pushes sensor data from KSQLDB to the client
@app.get("/chart-data/{client_id}")
async def message_stream(request: Request, client_id: str):
async def event_generator():
while True:
# If the client closes the connection, stop sending events
if await request.is_disconnected():
break
try:
# Get the KSQL stream generator
sensor_push_stream = ksql_sensor_push(
KSQL_CLIENT, app_config.STREAM_NAME, SensorName.ACC)
for raw_message in sensor_push_stream:
# If client closes connection, stop sending events
if await request.is_disconnected():
break
# Check if the raw message is the correct format
message = maybe_load_json(raw_message)
# If the message is in the correct format (i.e. a list),
# send to client
if isinstance(message, list):
# Format the sensor timestamp
message[1] = str(pd.to_datetime(message[1]))
# Yield the message as JSON
yield {
"event": "new_message",
"id": "message_id",
"retry": 1500000,
"data": json.dumps(message)
}
except Exception as e:
if await request.is_disconnected():
break
continue
return EventSourceResponse(event_generator())
message_stream()端点接受GET请求,这些请求打开长连接,后台可以通过这些连接持续发送传感器数据。在message_stream()中,定义并返回了一个协程 event_generator()。event_generator()的目的是创建一个 while 循环,在 ksqlDB 处理传感器数据消息时生成这些消息。只有当客户端关闭连接时,这个循环才会终止。
仪表板前端
仪表板前端是由 FastAPI 应用托管的 HTML 页面。它的目的是展示数据如何在项目的各个组件之间流动,并不是一个全面的前端。界面如下:

在仪表板上显示智能手机传感器数据流。图像由作者提供。
当用户点击“开始流媒体传输”时,会与后台建立一个 SSE 连接,并持续发送来自推送查询的数据。所有用于创建前端的代码以及运行所有内容的说明都可以在GitHub上找到。
最后的思考
本文介绍了 ksqlDB 作为一种解决方案,用以改进之前使用 FastAPI、Kafka、QuestDB 和 Docker 构建的实时流媒体项目。该项目旨在创建一个可视化传感器数据的仪表板,接近实时展示数据。面临的挑战之一是由于将数据从 Kafka 写入数据库并查询数据库进行分析所造成的不必要延迟。
ksqlDB 是一个为流处理而构建的数据库,用于解决此问题。与其在访问数据之前将其持久化到数据库中,ksqlDB 允许直接处理和分析 Kafka 中的数据流。通过在 ksqlDB 中实现推送查询,仪表盘的延迟显著减少,传感器读数的显示几乎没有可检测的延迟。
ksqlDB 的使用简化了后端基础设施,并允许对传感器读数进行近实时高效分析。未来的文章将继续对该项目进行改进,并通过新工具和功能增强其功能。我们始终欢迎反馈。感谢阅读!
成为会员: https://harrisonfhoffman.medium.com/membership
参考资料
Apache Kafka:https://kafka.apache.org/
Event-Driven Architectures — The Queue vs The Log: https://jack-vanlightly.com/blog/2018/5/20/event-driven-architectures-the-queue-vs-the-log
Lucidchart:https://www.lucidchart.com/
Kafka Poc using FastApi: https://github.com/GavriloviciEduard/fastapi-kafka
geo-stream-kafka:https://github.com/iwpnd/geo-stream-kafka
18 Most Popular IoT Devices in 2022: https://www.softwaretestinghelp.com/iot-devices/#:~:text=Smart%20Mobiles%2C%20smart%20refrigerators%2C%20smartwatches,there%20by%20the%20year%202022%3F
FastAPI:https://fastapi.tiangolo.com/
QuestDB:https://questdb.io/docs/
Row vs Column Oriented Databases:https://dataschool.com/data-modeling-101/row-vs-column-oriented-databases/
ksqlDB:
docs.ksqldb.io/en/latest/
关于 DALL·E2 的哲学与艺术视角
TLDR: 探索 AI 与艺术的交汇点:关于在创作过程中整合 DALL·E2 的知识性对话
·
关注 发表在 Towards Data Science ·11 分钟阅读·2023 年 7 月 25 日
--
图片来自 Pexels 的 Dids
感谢 Yaron Senderowicz 教授和 Liav Isaac Shopen 愿意接受本博客文章的采访。
介绍和动机
DALL·E2 已经推出一段时间了,我认为从哲学和艺术的角度写一篇关于它的博客文章会很有趣。作为一名数据科学家和业余版画爱好者,我对这种方法在艺术上的看法和使用方式产生了浓厚的兴趣。
尽管工程师们常常专注于让事物正常运作,但值得更多关注其他方面,比如艺术性、美学和社会考量,这些方面往往受到较少关注。这次探索的主要动机是更好地理解和探讨 DALL·E2 在什么意义上具有艺术性和人类特质。换句话说,我们应如何解读 DALL·E2 的输出?
此外,人们对 DALL·E2 的影响及其对就业的后果越来越担忧。它生成的图像非常吸引人,以至于很难分辨它们是由人类还是算法生成的。例如,我们刚刚见证了一张 AI 生成的照片 赢得了摄影奖,这突显了这个问题的重要性。
在这篇博客文章中,我有幸采访了两位有趣的人。第一位采访者是耶路撒冷大学哲学教授耶龙·森德罗维茨,因其对人类意识的研究而闻名。第二位采访者是利亚夫·艾萨克·肖本,他是一位大师级版画家,他的艺术造诣和实践经验使他对 DALL·E2 有独特的理解,并如何将其融入他的艺术作品中。
与耶龙·森德罗维茨教授的访谈
伊多:耶龙,你能分享一下你对 DALL·E2 的看法吗?你认为这个算法在某些方面不仅能模仿,还能展示人类的能力吗?
我不完全确定算法是否真实地复制了人类的能力。例如,人类可以自动创作艺术,但关键问题是这些艺术是否具有审美价值。可以制定标准来判断一件艺术作品是否具备这种价值。我认为其中一个标准是作品对背景的敏感程度。
我们存在于某种文化、时期、空间和文明中。我们可以轻易地辨别不同文化之间的差异。例如,如果莫扎特的作品在 12 世纪或 21 世纪呈现,它的审美价值可能会有很大的不同,因为它本质上与创作时的背景紧密相连。
创造力、意义与背景
伊多:创造力关乎意义以及人类如何感知它。那么艺术家的创作应如何解读和评估呢?
Yaron:技术可以复制梵高的风格,但我们应该质疑的是,我们使用 DALL·E2 创作的艺术作品是否提供了新颖的东西,还是仅仅是现有作品的衍生物。如果你有技术资源可以完美复制梵高的艺术风格,这并不意味着创作出的作品就是艺术。它只是展现了令人印象深刻的模仿能力。
理解 DALL·E2 的一种可能方法是使用“字面意义”的概念。人类有能力判断语言使用是否正确,即使它没有被明确界定或其真实性条件没有被明确列出。美国哲学家约翰·赛尔(John Searle)广为人知的贡献之一是对“字面意义”的质疑(即对句子的直接和直白的解释,不包含任何比喻或隐喻元素)。
意义的概念与……嗯,背景密切相关。当前的问题是,是否可以提供一个描述,包括一个术语适用于世界上一个物体的必要和充分条件。此外,我们对定义的理解(或者说计算机对定义的理解)是否足够准确,以便在一个我们既没有考虑过也没有遇到过的对话背景中正确使用这个术语。
我们理解一个句子,尽管我们的词汇量和使用范围是有限的。基于记忆的用法是确定且有限的。现在,一个涉及到“字面意义”文章中‘on’概念的例子。让我们考虑一个我们想理解的短语——“猫在地毯上”或“猫在地板上”。
有没有一种方法可以准确推导出将这些术语与猫和地毯正确关联的语义必要和充分条件?为了确认一只猫在地毯上,它必须有多少部分在地毯上?我们如何定义那部分?一条腿?三条腿?如果有两条腿在地毯上,两条腿在地毯外呢?
我们能否准确确定什么时候可以说猫在地毯上?让我们进一步说明。想象一下在太空中飞行,你看到一只猫和一块地毯颠倒放置。猫看起来是倒立的,但谁在什么上面?意识到我们使用“在”这个词是基于一个背景——地球的引力。这种无意识的用法只是说明了我们的陈述及其理解中有多少背景。我们的声明总是预设了一个背景,而这个背景从未被明确陈述或充分阐述。

由 Dalle2 生成的图像
为了更精确地描述背景,考虑另一个场景,你到美国旅行。你在沙漠中感到饥饿,于是你在一个餐馆停下来,点了一杯可乐和一个汉堡包。突然,你得到一个 5 米长的汉堡和一个 2 米高的可乐。这是你点的东西吗?可能不是,但你是怎么在点餐时说明的?任何了解背景——“买一个汉堡意味着什么”——的人都知道这不是你点的东西。而且这个背景在不同文化中显然有所不同。
你可能会声称这不是你所要求的,但无数可能的情况无法充分描述。我们总是在上下文中理解。

Dalle2 生成的图像
人类与算法的创造力
Ido:你怎么看待人类与算法的创造力
Yaron:将这一讨论与创造力联系起来——人类创造力表现为理解一个没有明确特定定义的术语的创造性使用的能力。然而,任何对该语言有一定造诣的人会同意这种新应用是正确的,或者他们会否定这种应用。尽管如此,仍会有一些人持不同意见。这里没有什么是精确定义的,而这种精确性需要更丰富的逻辑系统来明确。
我的主要观点是,我们应当始终质疑那些相信自己发现了算法,特别是与艺术相关的算法的人:他们是否仅仅生成了一个成功模仿在算法出现之前开发的风格或作品的算法?我们只能在展示能够产生艺术中新运动的计算机或写一本新书的情况下回答这个问题。
毕加索,作为一位杰出的具象艺术家,通常在探索其他风格之前教授这种风格。艺术在历史上是层叠的,要求对其根源和基础有深入了解。当计算机成功启动一种新的艺术运动——也就是说,当评论家或专家惊叹道,“哇!我们从未见过这样的东西”——如果这种运动确实源于对艺术历史状态的理解,恰当地融入,并引发了变化,那么这就是重要的。当这种情况发生,并且它成功地创作了重新定义诗歌理解的诗歌,正如博尔赫斯所说——在卡夫卡的作品之后,人们突然发现了许多先前存在的卡夫卡。
模仿者与创新者
Ido:那如何看待模仿者与创新者
Yaron:毫无疑问,模仿者与开发创意者之间存在对比。人们可以欣赏艺术作品中的创造力和原创性。
这些算法的许多开发者可能会争辩说,普遍接受的创新并不存在。他们可能认为这过于模糊或难以定义。确实,艺术在不断辩论中。然而,在今天的世界里,学者们引用现代文本时可以将其与非现代文本区分开来。解释学(解释理论)关注这一点。这是任何试图解析文本之间意义的人所关心的关键问题。他们专门处理这种问题,努力应对源自文化背景的历史特征。
要真正成为我们生活经验的一部分并对此作出显著贡献,计算机必须从单纯的模仿者演变为创新者和创造者。它应该致力于创造一些新的东西,而不仅仅是复制他人已经发明的东西。我并不是说这样的算法不存在,但我们应该对过早断言其存在持谨慎态度。我想说的是,考验不在于是否能够复制或伪造他人的画风,而在于是否能够带来新的文化变革。
一个真正具有创造力的计算机应该能够产生一种新的艺术类型。立体主义绘画的出现最初被视为对传统艺术的贬低偏离。只有在我们对莫奈、马奈和雷诺阿等印象派画作进行美学调整后,我们才在回顾中欣赏到印象派的痕迹,这些痕迹延续到 18 世纪。突然间,它弥漫到了公众领域。创新的演变可以比作一个隐喻;它开始时是令人耳目一新的意外之物,随着时间的推移,获得了更明确的字面意义。
这种演变总是与特定的时间、政治、社会和文化背景及我们对过去的解读相关。这是一个复杂的过程,不能仅通过提取梵高画作的规则来描绘。绘画行为远不止于此。梵高的作品是具体个体所做的具体行为,超越了现有的规范。
必须在技术成就和创造力之间建立区别。我们需要调查创造力在我们所知的人类生活中如何表现,然后评估算法生成的结果是否符合这一点。
采访 2 — 利亚夫·伊萨赫·肖本,资深版画家
伊多:利亚夫,你对 DALL·E2 有何看法,我们应该如何采纳这种技术?
利亚夫:我认为 DALL·E2 应被视为一个工具,而不是艺术过程的替代品。从一个初步的想法开始,制定一个提示,并查看结果是否能激发你的灵感。DALL·E2 对我来说像是你心中概念的过滤器。你可以通过将提示的结果作为视觉参考点来丰富你的艺术。关键是将你的独特艺术风格注入其中,同时实验提供的图像。我会对其进行调整,不断寻求原始图像的变体。随着这一过程的重复,艺术变得更加准确,并生成与我的风格相符的新图像。
与其在 Pinterest 或其他平台上筛选,不如使用 DALL·E2 根据你的艺术偏好定制结果。在这方面,我认为它可能取代传统的艺术平台,为艺术家提供个性化的参考。
伊多:有人可能会争辩说,这项技术的成果如此美丽,以至于只有人类才能创作出来。你对此有何看法?
Liav: 一些结果确实很出色,但它们仍然感觉有些冷漠和疏离。在某些方面,这项技术将创作的控制权还给了艺术家,因为他们必须提取他们想要创作的参考,这可能不是他们总能做到的。然而,重要的是要注意,这个参考并不是最终产品,而是艺术过程中的一个中间阶段。
Ido: 你是如何将 DALL·E2 用于你自己的目的的?
基于我生成的一幅图像,我制作了一张 mezzotint 印刷品。我准备了一个详细的文字描述,说明我在寻找什么,这个图案帮助我构思出一幅与我通常创作的作品不同的图像。最终,每位艺术家都有一套图案,这个工具可以帮助扩展这个范围。我不确定没有 DALL·E2 的话,最终的作品是否会是我的首选,但它确实使开发过程更快,更具多样性。
Ido: 从一开始是什么激励你使用 DALL·E2 的?
Liav: 部分原因是工具周围的热议,以及我对其对我的创作过程潜在贡献的好奇。这对我来说更多的是关于如何使用它并将其融入到我的工作中,而不是对其能力的震撼。DALL·E2 通过融入金融、时间和努力等方面,使艺术变得更易于获取,这与当前的 DIY 流行趋势相吻合。
仅仅使用它并不一定让你成为艺术家。我认为仅生成一幅图像并不足以被贴上艺术标签,还需要额外的、个性化的层次或阶段。
Ido: 这次对话回到了我与 Yaron 的采访——这代表了创造力吗?它模仿了人类的某些方面吗?
Liav: 我相信艺术作品需要原创性,需要在过去的创作和特定的背景、语言中进行创作。目前,我们使用的是别人创造的语言,并对其进行变体处理。作为艺术家的角色是贡献我们的视角,从而将创造力还给艺术家,产生新颖性。在美学吸引力方面,我认为这归结于个人喜好。我们最终必须质疑自己是否对最终产品有感情。虽然我相信创造力本质上是人类的,但我们衡量创造力的方式不能应用于计算机。人类常常将人类特征投射到非人类事物上,但这些不应被误认为是现实。
Ido: 所以你是说将 DALL·E2 视为一种类人能力源于我们的世界观?
绝对是。这个现象很常见。以抽象画为例,每个观众看到的都有些许不同。我们投射出我们熟悉的东西,我们渴望的东西。这是人类的一部分。要有效地使用 DALL·E2,你必须投入到创作过程中,并引导算法,特别是在尝试捕捉更抽象的东西,如感觉或氛围时。

Liav 使用 Dalle2 生成的原始图像

由 Liav 创作的 mezzotint 印刷品

由 Liav 创作的 mezzotint 印刷品
结论与主要收获
艺术本质上涉及原创性、发明、好奇心和文化背景的概念。我们应当问自己的根本问题是我们与所使用的 AI 工具的关系。我们只是使用它们创造输出,还是将我们的独特个性注入其中,以真实地表达自己?
在我看来,主要的收获是:
(1) 尽管像 DALL·E2 这样的算法具有模仿人类艺术风格的惊人能力,但关键在于是否真正展现了人类创造力的新颖和原创特质。
(2) 艺术中的真正创新需要生成新类型并引发文化变革,而不仅仅是现有风格的简单复制。
(3) DALL·E2 应被视为一种增强艺术创作过程的工具,而非完全取代它。它可以作为视觉参考点和想法过滤器,允许艺术家将其独特风格注入 AI 生成的图像中。
(4) 人类倾向于将人类特征投射到非人类事物上,例如 DALL·E2,这不应被误认为现实。
参考文献
Searle, J. R. (1978). 字面意义。 Erkenntnis, 207–224.
人工智能技能缺口的(哲学)视角
什么将初级机器学习从业者与高级解决方案架构师区分开来,特别是在快速发展的行业中?
·
关注 发表在 Towards Data Science ·8 分钟阅读·2023 年 9 月 29 日
--
在机器学习中,挫折是自然的,但也是可以避免的。图片来自 Tim Gouw Pexels.com。
尽管有很多人工智能课程可供选择,我们常常发现许多申请者缺乏看似至关重要的能力。本文是一种通过轶事探索为什么会出现这种情况。
-
背景:思维模型与 T 型技能
-
机器学习中的不同之处
-
知识缺失的感知成本
最近,我们的一些项目中有客户询问关于将项目交接给尚不存在的内部团队的问题。“我们如何训练我们的团队以拥有你们所构建的解决方案?”“我们如何确保我们的团队在 AI 变化中保持未来适应性?”这些问题的大部分变体得到了推荐或变更管理计划的回答,但一个关键的主题依然存在:“我们如何在 AI 领域招聘合适的团队成员?”
对于像我们这样的企业 AI 咨询公司来说,这个问题尤其关键。在一个高度动态的行业中,识别、招聘、培训(和货币化)员工的能力需要付出大量的努力。更危险的是缺乏成功项目交付所需的附加技能,如需求管理、客户沟通和项目跟踪。
我们常见的员工和客户的最大障碍是缺乏非常具体且明确的技能,通常只涉及两到三个缺失的专业领域——却足以使他们整个项目陷入停滞。本文探讨了这种现象的表现形式。
背景:心智模型与 T 型技能
为了说明技能缺失如何导致项目突然中断以及如何应对这些问题,有两个简单的框架可以帮助解决这些问题。它们是:
-
心智模型,它们是对系统、概念或模式的抽象;以及
-
T 型技能,是指个体在多个领域拥有一般技能的同时,在其中一个或少数几个领域中高度专业化的能力。
心智模型
心智模型是独立的表征,旨在帮助与构造、概念和系统进行交互——机器学习领域中这些表征并不短缺。简单来说,它们有助于将思想进行分隔。
从研究论文“心智模型:理论与方法的跨学科综合”中,Natalie Jones 和她的团队进一步阐述了这一点:
心智模型是个人对外部现实的内部表征,帮助人们与周围的世界互动。它们由个体基于其独特的生活经历、感知和对世界的理解来构建。心智模型用于推理和决策,可以成为个体行为的基础。它们提供了过滤和存储新信息的机制。
这是你知道自己是否在使用合适的心智模型的方式:
-
它是有限的。
-
虽然是有限的,但在其表征目标上也是完整的。
-
它允许黑箱构造以在正确的抽象层次上简化思想。
然而,心智模型(以及重要的心智模型叠加)也有其局限性。再次引用 N. Jones:
然而,人们准确代表世界的能力始终有限且对每个人独特。心理模型因此被视为现实的不完整表现。它们也被视为不一致的表现,因为它们依赖于上下文,并可能根据使用情况而变化。本质上,心理模型必须是高度动态的,以适应不断变化的环境,并通过学习随时间演变。将认知表现视为复杂系统的动态、不准确模型,承认了人们在构思这些复杂系统方面的局限性。
因此,我们能够概念化、理解、分析、调查、原型设计、构建和部署任何级别的基于机器学习的自动化的能力,需要在多个知识领域中保持概念上的流动性,包括项目的技术和管理方面。
T 型技能
在现代社会,“T 型技能”这个术语有些不准确;真正有用的是在多个领域具有深度专长的多面手。
具有 T 型技能的个人通常会在多个相关知识领域拥有广泛的知识,同时在某一特定主题或职能上具有深入的专长。
随着机器学习工程(即科学机器学习原则的现实世界、风险意识应用)的兴起,对同时具备多学科能力的需求显而易见。
描述 T 型个人的另一种方式是,某人在项目或职责范围内,能够成功处理多个必要的职能,并在其中一些方面是专家。他们在所有工作中都有危险,但在某些方面极具威胁。
这些个人通常表现为对他们整个工作范围有整体把握。尽管他们可能在某些任务方面不是专家,但他们至少知道如何将这些不熟悉的任务分解成具有明确输入和输出边界的工作项,因此他们对整个项目有可见性和能力。
尽管他们以前从未接触过鸭子,但他们知道鸭子应该是什么样子,鸭子应该怎么叫,这足以让他们不被阻碍。
机器学习中的不同之处
与云迁移项目或 SaaS 相比,机器学习通常具有许多按序堆叠的概念(与并发或树状结构相对),以及一系列针对生产级机器学习部署的额外考虑因素。部署依赖于模型类型,模型类型依赖于数据科学,而探索性数据分析依赖于项目需求。
在论文 “机器学习软件应用程序在软件开发生命周期阶段的质量保证挑战” (Alamin2021) 中,作者解释了传统软件开发与机器学习软件应用程序之间的明显差异。来自论文的内容:
在传统软件开发中,我们首先收集需求。然后设计、开发、测试、部署和维护应用程序。对于机器学习系统,我们仍需确定应用程序的目标,但不是设计算法,而是让机器学习模型从数据中学习所需的逻辑 [1]。这些观察引发了一个问题,即机器学习模型是否以及如何在不破坏软件开发生命周期(SDLC)的情况下被采纳。理想情况下,机器学习工作流/管道和 SDLC 阶段应该协同进行,以确保适当的质量保证。然而,如我们上面所提到的,由于机器学习模型设计和传统软件应用开发之间的固有差异,这些期望可能是不切实际的。
(Lwakatare2019) 在她的论文 “机器学习系统的软件工程挑战分类:一项实证研究” 中进一步阐述了:
[…] 尽管在学术界,许多关注点集中在学习算法的理论突破上,但实证研究表明,它们仅占操作性机器学习系统的一小部分 [20]。
因此,在机器学习系统的开发和维护过程中遇到了一些挑战 [6]。为了解决这个问题,新兴证据强调了在机器学习系统开发中需要考虑和扩展已建立的软件工程(SE)原则、方法和工具 [11,19]。
因此,我们可以将 AI 项目描述为需要稍多的思维模型来完成类似规模的项目。但 AI 软件通常需要更多地顺序堆叠这些原则,而云和 SaaS 似乎在理念上更具并发性,从而导致它们之间的关键性相互依赖减少。
知识缺失的感知成本
让我们以一个简单的项目模型为例,其中需要在 9 个假设领域中进行一系列活动。这些领域可以代表项目管理、需求工程、数据科学、机器学习、云计算和 MLOps 技能的混合。虽然非常简化,但我们遇到的大多数项目都有类似的顺序变化的专业技能。

作者插图。
然而,随着新技术的出现,有时你会被要求使用新技术。有时这些是小的变化(例如,用 DVC 替换 Git LFS),有时则是更大、更复杂的变化(如从单体虚拟机方法转向 Kubernetes)。理想情况下,你和你的团队应该完全胜任所有这些任务;实际上,随着行业变化的速度,你可能对大多数这些任务都比较熟悉,有些可能是新的或尚未完全掌握的。
在这种情况下,存在一个知识缺失的单一点。

作者插图。
我认为这是一种非常正常的操作状态。客户想尝试一个新的库;有人建议使用不同的数据库。这种情况时有发生。需要更改的模块可以在心理上进行热插拔,除了阅读一些 API 或学习不同的项目管理方法外,没有其他重大后果。
在 MLOps 中,问题在于通常两个或更多的问题领域会导致在项目中感知到的知识缺失更大。

作者插图。
虽然只有两个问题领域,但感知到的效能缺失影响了你大多数的指定活动。
一 handful 的缺失概念将表现为在项目中无法交付或无法有效工作的令人沮丧的能力缺失。
推荐
为了明确,我们整个团队(包括我自己)不断主动学习新的概念和技术,以确保没有意外的主题或知识领域让我们完全盲目。我们通常遵循以下步骤来发现这些领域:
-
识别。一旦你听到将要使用的新技术或方法,记下它的名称、描述和背景。
-
隔离。虽然很难识别缺失知识中的“未知未知”,我们会通过简单的问题来解决它们:这个概念的背景是什么?它与我已知的有什么相似之处?它与我已知的有什么不同之处?
-
从小开始。“Hello World” 示例仍然是确保你有效使用特定工具的有效方法。
不要将你的技能总量作为进展的衡量标准,你应该关注你技能的组合以及你如何将它们结合起来以交付项目价值和成功。
其他你可能喜欢的文章
-
PyTorch 与 TensorFlow 在基于 Transformer 的 NLP 应用中的比较
-
MLOps 批处理:在 GPU 上运行 Airflow
-
数据集偏见:制度化歧视还是充分透明?
如果你对这篇文章或我们的 AI 咨询有其他问题,请随时通过 LinkedIn或通过 电子邮件联系我。
-Matt.
在 Kubernetes 上进行快速实验的流程
原文:
towardsdatascience.com/a-pipeline-for-fast-experimentation-on-kubernetes-3258389120a1
仅使用原生 Python 包
·发布于数据科学前沿 ·6 分钟阅读·2023 年 1 月 31 日
--
手动为每个新实验创建一个新配置文件是一项繁琐的过程。特别是当你想要在 Kubernetes 集群上快速部署大量作业时,自动化设置是必须的。使用 python,构建一个简单的调度脚本,读取实验的配置,如批量大小,将其写入 YAML 文件,并创建新作业,非常简单。在这篇文章中,我们将讨论具体的方法。最棒的是,我们无需额外的包!

这个流程包含四个文件:两个 bash 脚本(一个用于创建 Kubernetes 作业,一个用于删除 Kubernetes 作业),一个 python 脚本,以及一个 .yaml 文件模板。我们将详细介绍它们,从 python 脚本开始。你可以在这个 GitHub 仓库中找到完整的代码。
python 脚本
这个 python 代码被结构化为两个方法。第一个方法生成实验配置,这些配置填充了示例值。第二个方法进行实际的调度,包括解析 .yaml 文件和与 Kubernetes 通信。让我们从第一个、更简单的函数开始:
get_experiments 方法持有一个内部字典对象,在我的示例中,它包含了 4 个样本实验。每个实验都有一个唯一的编号,每个实验本身也是一个字典。这个字典包含实验的配置,包括标准的机器学习参数,如批量大小、训练轮数和模型。此外,我们指定了我们希望实验运行的数据集,例如 CIFAR10 数据集。这里列出的参数是为了说明目的,你不必局限于这些参数。例如,你可以包含更多的超参数(如学习率)、存储文件路径(如数据集目录)或包括环境变量。简而言之:根据你的需要进行调整。
如果我们调用该方法而不带参数,则等同于使用默认设置“experiment_number=-1”来调用它。使用该设置,会返回所有实验的配置。然而,如果你与 Kubernetes 一起工作了较长时间,你会发现一些作业会因为各种原因失败。如果发生这种情况,最好是修复代码并重新启动那个特定的实验。因此,除了获取所有实验的设置外,我还添加了选择特定实验进行重新运行的功能。当我们用特定实验编号调用该方法时,支持这种用例。一个例子是 4,它将返回第四个实验的配置。
.yaml 模板
get_experiments 方法在脚本的主要函数 schedule 中被调用。在详细介绍之前,我们需要查看我们的模板文件。我包含的 .yaml 文件是以我在超级计算集群上运行机器学习实验时常写的文件为基础。然而,它并不是一个有效的文件,意思是你不能直接使用它。
相反,它旨在展示你在填写模板时的灵活性。说到填写模板,这就是它:
请注意那些有两个大括号:{} 的独特位置。每个这些位置都有一个编号,从零开始。我们在 python 脚本中填补这些空白,我们很快就会回到这一点。最适合我们的机器学习用例,“{}”可以放置在模板中的任何位置;它们并不局限于特定位置。
为了展示多样性,我在 .yaml 文件中放置了标记:我们可以使用它们传递命令行参数、挂载目录、填写环境变量或选择我们的 pod 镜像。此外,我们还可以重用占位符:在模板文件中,我使用了“{1}”占位符两次;一次是将作业分配到作业组中(group-model-{1},第 11 行),另一次是将模型名称传递到命令行中(第 25 和 26 行)。
模板文件的填写通过 python 脚本的 schedule 方法完成。
在函数内部,我们首先按原样解析模板。然后,我们在第 10 行收集所有我们想运行的实验。接下来的三个变量,第 11 到 13 行,是为了激发你的灵感:你可以自动化的不仅仅是我提出的内容。有趣的部分从第 14 行开始,我们遍历所有我们想要创建的实验。正如我所写,实验配置存储在字典对象中。这意味着我们可以在第 17 到 26 行填写模板时查询字典。
为了更容易理解哪个槽位被填充,我在每一行后面留下了注释。例如,第 18 行填充了标记为“{0}”的位置,第 19 行填充了标记为“{1}”的位置,依此类推。为了查看我们在填写模板后创建了什么,我们在第 27 行打印出完成的版本。
到目前为止,我们已经在内存中创建了一个可以使用的 YAML 文件。接下来的步骤是创建相应的 Kubernetes 作业,从第 30 行开始。首先,我们检查是否只想删除旧的实验(例如,因为它由于某种原因失败了,我们需要先修复错误)。如果不是这种情况,我们在重新创建作业之前需要删除旧作业——不能有两个具有相同名称的作业。
如果不存在之前的作业,脚本在尝试终止它时不会失败,而是会打印一行空白并继续创建作业。
Shell 脚本
作业的创建和删除都转发给两个小的 bash 脚本。第一个脚本如下所示,它使用 kubectl 命令根据传递的内容创建一个作业(echo "$1" 部分)。注意,我已经将 kubectl 设置为默认使用我的命名空间。如果你没有这样做,那么可以写 kubectl -n your-namespace 或将你的命名空间注册为默认命名空间:
删除作业的脚本几乎相同;我们只是将“create”标志换成“delete”:
回到 Python 脚本
将各个部分组合在一起后,我们需要一个驱动程序来启动代码。这个任务是通过“main”语句完成的,如下面的代码片段所示:
启动实验时,如前所述,我们可以选择仅(重新)创建一个特定的实验。默认情况下,我们运行 所有 实验;要运行个别实验,我们可以将它们的 ID 传递给命令行。
接下来,我们创建一个标志,告诉调度程序仅删除一个运行或重新启动运行。默认情况下,这个标志被设置为 false,意味着我们首先终止一个实验的现有运行,然后重新启动它。只有当我们在命令行上明确设置标志时,它才会被设置为 true,意味着我们仅终止现有作业而不重新创建它。此外,我们也可以完全不设置它;标志的缺失等同于标志为 false。最后,我们解析参数并运行调度。
总结
在这篇文章中,我们讨论了如何快速部署实验到 Kubernetes 集群。为实现这一目标,我们使用了四个文件:一个.yaml 模板文件、两个 bash 脚本和一个 python 文件。在 python 文件中,我们使用了两种方法来收集实验的配置,然后填充模板。
完成模板后,作业通过两个单行 bash 脚本创建和删除。在这些脚本中,我们使用了原生 Kubernetes 命令。最终,我们可以调用 python 脚本(传递可选的命令行参数),我们的实验将被自动调度。
这个自动化过程在尝试参数组合时非常有用:手动为每个实验创建一个.yaml 文件既繁琐又容易出错。因此,省去这些麻烦,构建一个自动化流水线,如本文所述。用于开始的完整代码可以在我的代码库中找到。
评估业务分析中的正负未标记(PU)分类器的实用方法
一种通过调整常见分类指标以评估 PU 模型的正类先验概率的方法
·
关注 发表在 Towards Data Science ·4 分钟阅读·2023 年 3 月 31 日
--
根据作者的描述,由 DALL-E-2 制作
随着企业越来越多地使用机器学习模型分析收集的数据,一个出现的挑战是正负标签混合(PU)数据集。这些数据集只包含少量已标记的数据,其余样本为未标记数据。虽然未标记样本通常被认为是负类,但其中一些可能是正类。PU 数据集在各种商业场景中得到应用,例如预测客户流失或追加销售机会、销售预测和欺诈检测。
在 PU 数据集上评估机器学习算法可能很困难,因为传统指标可能无法准确反映模型的性能。例如,简单地将正样本留作测试并将未标记条目添加为负类可能会导致高度偏斜的混淆矩阵,且假阳性数目被夸大。这种情况可能发生在模型在测试集中检测到正样本,但它们的标签却是负类时。
PU 分类器评估的实用方法
为解决这一问题,我们团队采用了一种实用的方法,通过使用有关正样本期望频率的信息来估算 PU 数据集上的标准二分类指标。我们的方法涉及使用正类的先验概率(在自学习分类器的拟合过程中估算)来调整测试中观察到的假阳性和真阳性。这种方法使得即使在正类显著不足的情况下,也能更准确地评估模型在 PU 数据集上的表现。
为了展示我们方法的有效性并在受控环境中进行实验,我们首先使用 sci-kit-learn 的make_classification函数创建了一个合成的二分类数据集。正样本代表数据中的少数类,通过随机选择部分正样本并移除其标签来模拟 PU 学习场景。
在实际商业场景中,数据集通常包含这种预设的标记/未标记条目的比例。例如,用于预测下一年客户流失的数据集可能包含上一年未签订新年度合同的标记客户,以及具有类似特征但尚未流失的现有客户。在这种情况下,数据集可能包含多达 40%的流失客户,但只有一半会被标记为流失(显示年流失率为 20%)。

作者提供的图片
接下来,我们使用train_test_split函数将数据分割为训练集和测试集。将特征X和目标变量的伪标签版本y_pu传递给分类器进行训练。为了评估分类器的性能,我们计算测试集中未标记版本的标准机器学习指标,如准确率、精确率和召回率,并进一步将其与原始标记版本计算出的相应指标进行比较。
实现该方法的代码片段
以下是一个代码片段,展示了我们提出的方法在 PU 数据集上评估分类器性能的实现。
我们的compute_confusion_matrix函数确定了测试数据的大小,并识别了训练集中正样本的索引。然后获取模型对训练集中正样本的概率估计,并计算其均值,表示正样本被标记的概率。
然后,该函数应用已拟合的ImPULSE 模型来预测测试数据中正类的概率,并使用 sci-kit-learn 的confusion_matrix函数创建混淆矩阵。假设模型对正类的先验概率(在未标记样本中)大于零,则该函数会调整混淆矩阵以考虑测试数据中可能存在的未标记正样本。该函数估计由于未标记条目导致的假阳性和真阳性的预期数量,并相应调整混淆矩阵。
为了确保生成的混淆矩阵与测试数据的大小匹配,该函数会对其进行舍入和重新缩放,如果需要的话,还会调整真负样本的数量。
在获得调整后的混淆矩阵后,我们可以利用它计算标准机器学习指标,以尽可能准确地评估模型的性能。
你可以在[Jovian 上的对应demo notebook]和GitHub 仓库中找到完整的代码。
结论
我们提出了一种实用的方法,用于评估在商业场景中常见的正未标记(PU)数据集上的机器学习模型。传统评估指标可能无法准确反映模型在此类数据集上的性能。该方法通过使用正类的先验概率,估计 PU 数据集上的标准二分类指标,从而实现对模型性能的更准确评估。
参考阅读
-
Agmon, Alon. “半监督分类未标记数据(PU 学习)”, 2022
-
Saunders, Jack, 和 Freitas, A. “评估正负未标记分类器的预测性能:简要的批判性回顾及改进的实际建议。”,2022
-
Holomb, Volodymyr. “激励行动:正负未标记分类的实用解决方案。”,2023
ChatGPT 和 Bard 的免费版本实用比较
原文:
towardsdatascience.com/a-practical-comparison-of-chatgpts-and-bard-s-free-versions-5fd9ca8b9024
从测试结果来看,比较这些语言模型的能力和局限性,只需一个 URL 即可。
LucianoSphere (Luciano Abriata, PhD)
·发表于 Towards Data Science ·阅读时长 22 分钟·2023 年 9 月 26 日
--

由 Google DeepMind 在 Unsplash 上拍摄的照片
难怪我将 ChatGPT 集成到我的日常工作和活动中。真的非常多。它在头脑风暴、纠正或改进文本、翻译、编写代码和查找错误等方面极为有用。由于其网络性质,ChatGPT 总是只需一个 URL 即可使用,免费,无需安装或下载。OpenAI 模型的这一特性,对于大众化至关重要,只有 Google 的 Bard 可以媲美;然而,由于它推出得较晚,它在我周围的同事和朋友中可能知名度较低。为什么不给它一个机会呢?
在这里,我展示了通过我设计的测试对这两个模型进行的比较,这些测试基于微软今年早些时候发布的一篇非常具有挑衅性的预印本。
[## 挑衅地,微软研究人员称他们在 GPT-4 中发现了“人工智能的火花”
只是一个具有挑衅性的预印本标题,还是通过语言通向人工通用智能的真正路径?
这里我通过我根据微软在今年早些时候发布的一篇非常具有挑衅性的预印本设计的测试,对这两个模型进行了比较
这项研究实际上是如何展开的。
几天前,我需要与 ChatGPT 讨论我为我们即将上线的(敬请期待!)化学教育网站准备的内容,通过基于 WebXR 的虚拟现实工具来进行,结果令我震惊的是,它无法操作。好吧,我对自己说,这也是试试 Bard 的机会,它也只是一个 URL 的距离。
几个小时后,ChatGPT 重新上线。我利用这个机会用尽可能相似的问题进行探测。我只比较了免费版本,并设计了接近我日常工作使用工具的测试。
继续阅读以查看我的结果。一些发现是预期的,其他则是微不足道的,还有一些相当有趣。请阅读结论部分以了解我的“裁决”。
测试 1:为教育工具头脑风暴分子列表
精确来说,我进行的这种“竞争”是在两个语言模型之间进行的,旨在开发一份简洁而全面的有机分子列表,涵盖有机化学的各种主题。我的意图是从该列表开始与语言模型进行头脑风暴,最终确定一套要包含在我们虚拟现实工具中的分子。
这是我第一次提交给 Bard 的提示,当时 ChatGPT 不可用:
我:我正在准备一款虚拟现实工具的材料,应该通过示例介绍有机分子的基本概念。列出我应该包括的分子及其原因。这里有一些示例:
顺-2-丁烯、反-2-丁烯和丁烷,用于比较单键与双键及其效果。
也许还可以添加丁烯以完成单键、双键和三键的对比
- 和 — 酒石酸盐,用于说明手性中心,也许还可以添加一个氨基酸
苯、吲哚、咪唑和其他芳香环
一些简单的胺,用于说明这一功能团
一些简单的醚和酯,以说明这些基团
根据新生有机化学学生需要学习的内容来调整你的列表,也许要考虑哪些概念最困难。理想情况下,我想要大约 20 个,最多 30 个例子,因此必须仔细选择。
起初,我对 Bard 的输出感到惊讶,图形输出非常丰富(截至 2023 年 9 月,免费版 ChatGPT 不具备此功能),似乎理解了我的问题,并返回了初看起来可能完整且非常有用的答案:


作者从 Bard 截图的图像。
答案的第一部分有点道理,但完整的答案没有涵盖我想要涵盖的所有主题,更糟糕的是,最后出现了一些非常严重的错误。凭借一些基本的化学知识,你知道这些分子不是醚和酯!我给了 Bard 机会来修正错误并提供醚和酯的替代例子,但它没有成功。
然后几个小时后,ChatGPT 回来了,我可以将其与 Bard 进行比较。我已经知道它不会给我任何图片,所以这在某种程度上是一个负面因素,但我想知道生成的文本是否更具信息量,特别是列表是否更清晰,没有错误,并且是否提供了更好的分子用于我的教育目的。
这是 ChatGPT 的回答,来自完全相同的提示:
ChatGPT:绝对的,仔细选择有效介绍有机化学关键概念的分子是至关重要的。以下是经过精心挑选的分子列表,并附有它们重要性的解释:
1. 甲烷:有机分子的基本结构和碳的四面体几何的介绍。
2. 乙烯:双键及其对分子几何的影响的介绍。
3. 乙炔(乙炔):三键和 sp 杂化概念的介绍。
4. 丙烷:单键和不同碳骨架的强化。
5. 环丙烷:紧张环结构及其后果的介绍。
6. 苯:芳香性和共振系统的介绍。
7. 乙醇:氢氧基功能团及其性质的说明。
8. 醋酸:羧酸及其重要性的介绍。
9. 丙酮:羰基功能团及其在各种化合物中的作用的说明。
10. 乙胺:一级胺和碱性的介绍。
11. 苯胺:芳香胺及其独特性质的说明。
12. 二乙醚:醚类及其作为溶剂的作用的介绍。
13. 甲基胺:简单的二级胺的介绍。
14. 乙醛:醛类及其反应性的说明。
15. 乙酰胺:酰胺及其在生物分子中的作用的介绍。
16. 甲酸乙酯:酯类及其宜人气味的介绍。
17. 甘氨酸:氨基酸、两性离子和手性的介绍。
18. 乳酸:手性分子及其生物学意义的说明。
19. 柠檬酸:复杂羧酸及其在代谢中的重要性的介绍。
20. 阿司匹林(乙酰水杨酸):有机合成和功能团转化的说明。
21. 葡萄糖:碳水化合物和环状结构的介绍。
22. 甘油:三醇及其在脂质中的作用的说明。
23. 脂肪酸(例如棕榈酸):饱和脂肪酸的介绍。
24. 油酸:不饱和脂肪酸和顺反异构体的介绍。
25. 血清素:杂环化合物及其生物学作用的介绍。
26. 腺嘌呤:嘌呤碱基及其在 DNA 和 RNA 中作用的介绍。
27. 胸腺嘧啶:嘧啶碱基及其在 DNA 中作用的介绍。
28. 褪黑激素:含有复杂吲哚结构的分子的说明。
29. 酚:芳香醇及其反应性的介绍。
30. 尼龙 6,6(聚酰胺):聚合物及其在材料科学中的相关性的介绍。
这些分子涵盖了广泛的概念,包括基本的键合、官能团、手性、芳香性等。它们经过精心挑选,以提供对有机化学概念的全面介绍,同时展示具有重要生物学和实际意义的分子。
这个回答比 Bard 的要好得多,我没有发现任何错误,涵盖了我想要覆盖的所有化学内容,甚至还提供了将分子与我们日常生活相关联的额外信息,虽然我没有要求这些信息,但从教学角度来看非常重要。
我对这个测试的结论是,ChatGPT 比 Bard 要好得多,尤其是在教师、内容创作者、科学传播者或学生如何使用它方面。
测试 2:带有图形的问题
这固有地偏向于 Bard,因为 ChatGPT 在其免费版本中不提供此功能。
然而,检查上述例子时会发现,图像传达的信息往往与文本不符。例如,Bard 标记为醚的分子实际上是乙二醇,一种二醇。这使我倾向于得出结论:与其看到不正确的图像,我更愿意没有图像。
另一方面,Bard 在其输出中包括图形的能力,尤其是理解输入图像上的问题,可能会极其有用,而这正是 ChatGPT 无法竞争的地方。这里有一个例子,我问了照片中的虫子是什么,程序迅速而准确地回答了:

测试 3:从第三方程序调用模型
就像上面的部分固有地偏向于 Bard,这部分则偏向于 ChatGPT,或者更确切地说是其背后的语言模型 GPT-3.5,该模型极其容易从你的代码中调用,即使是从 Web 应用程序中。
这是我广泛研究的内容,将 GPT-3、GPT-3.5 和 GPT-4 直接从我的程序中调用,以将它们的强大功能与其他库和 API 提供的能力结合起来:
[## 使用 GPT-3 从语音备忘录自动撰写电子邮件的 Web 应用程序
我将 Chrome 的语音识别引擎与 GPT-3 结合,创建了一个从你的语音备忘录撰写电子邮件的 Web 应用程序……
pub.towardsai.net](https://pub.towardsai.net/a-web-app-for-automated-e-mail-writing-from-voice-notes-using-gpt-3-e8e98e4ffb6f?source=post_page-----5fd9ca8b9024--------------------------------) ## 将 GPT-3 与语音识别和合成相结合,实现完全语音聊天机器人…
我是如何创建这个网页应用程序的,你可以用它与 GPT-3 自然对话,讨论你想要的任何主题,完全基于网络在你的…
[towardsdatascience.com ## 通过将语音转换为命令来控制网页应用程序,使用 GPT-3
最后一篇文章展示了 GPT-3 的实际应用,包括完整的工作流程解释和详细信息…
[towardsdatascience.com
原则上,你也可以调用 Bard 将其用于你的程序,但这比调用 GPT 模型复杂得多:你必须创建一个 Google Cloud Platform 项目并启用 PaLM API,创建服务账户并下载特定的 JSON 密钥文件,安装适用于你的编程语言的 PaLM 客户端库,然后使用 PaLM 客户端库向 PaLM API 发出请求。不,谢谢,我喜欢简单。给 OpenAI 的产品加分。
测试 4:从互联网获取信息
像我在测试 2中的测试一样,这次测试也首先偏向 Bard,因为 ChatGPT 无法连接到互联网。不过,这一点很重要,因为在实践中,它确实严重影响了模型提供的信息的整体用户体验和质量。
对于这个测试,我转向了瑞士现任总统!开个玩笑,事实证明瑞士的中央政府由 7 名成员统治,他们拥有相同的权力,每年轮流担任“代理总统”。这给了模型很多出错的空间,如果它们没有更新,但如果信息在互联网上可用的话,仍然可以找到。
我问了两个语言模型这个问题:“谁是目前的瑞士现任总统?”
我从 Bard 那里得到了正确的答案:
Bard:目前瑞士的总统是阿兰·贝尔塞。他于 2022 年 12 月 7 日当选,任期从 2023 年 1 月 1 日开始。贝尔塞是瑞士社会民主党的成员,并领导联邦内务部。
(接着其他一堆细节)
与此同时,ChatGPT 则没有给出答案
ChatGPT:瑞士联邦委员会通常每年轮流由七名成员担任主席。主席的任期为一年,顺序提前确定。现任主席每年可能会更换,因此我无法实时提供瑞士现任主席的信息。
我试图让它给出一个名字,但显然它不知道该怎么回答:
我:是的,但每年 7 名委员中会有一位被称为“总统”。例如,几年前是西蒙内塔·索马鲁加。现在这个人是谁?
答案如下。尽管总体体验较差,因为我无法让它给出名字,但模型选择解释自己无法给出名字因为没有访问更新信息,而不是编造一个名字或提供前任总统的名字作为现任总统,这一点是值得注意的!
ChatGPT:对于我之前回答中的任何混淆,我表示歉意。根据我截至 2021 年 9 月的最后知识更新,西蒙内塔·索马鲁加在 2020 年担任瑞士总统。然而,我无法访问实时信息,瑞士联邦委员会的总统每年都会更换。要查找当前瑞士总统的信息,我建议查看官方瑞士政府来源或可靠新闻媒体的最新更新。它们会有瑞士联邦委员会现任总统的最新信息。
回到 Bard 的回答,为了深入了解它是如何用正确的信息生成答案的,我直接问了它:
我:你知道这个信息,还是需要在网上查找?
这是它的回答:
Bard:我在网上查找了信息。我仍在开发中,并没有将所有世界知识存储在我的记忆中。然而,我可以通过 Google 搜索访问和处理现实世界的信息,并使我的回答与搜索结果一致。这使我能够提供最新和最准确的信息。
更有趣的是,它还加上了这一点:
我能够记住我查阅过的信息,并在将来用它来回答你的问题。例如,如果你再问我瑞士总统是谁,我将能够回答你的问题,而不需要再次查找。
无论它们是否知道答案,这两个模型表现都非常出色。
测试 5:计算
语言模型只通过它们的神经网络传播输入标记,以连续预测新的标记,从而生成文本片段。这些文本片段由于庞大的训练数据库和例程,使得它们是有意义的。因此,理所当然地,一个人会期望这些语言模型无法进行数学运算,因为它们仅限于训练数据集中出现的数学问题,没有灵活性。实际上,它们确实无法进行高级数学(如果你特别对这个问题和最现代模型的能力感兴趣,可以查看这篇文章 focused on OpenAI’s GPT-4))。
我从一些非常简单的数学问题开始:“-11, 3, 5, -3, -5 和 11 这些数字的平均值是多少?”(答案是 0,因为你可以看到我已经平衡了所有正数和负数)
这些是语言模型提供的答案:
ChatGPT:要找到一组数字的平均值(平均数),你需要将所有数字相加,然后除以数字的总数量。
这里是你提供的数字:-11, 3, 5, -3, -5, 11
现在,让我们计算平均值:
平均值 = (所有数字的总和) / (数字的总数量)
平均值 = (-11 + 3 + 5–3–5 + 11) / 6
平均值 = 0 / 6
平均值 = 0
所以,-11, 3, 5, -3, -5 和 11 这些数字的平均值是 0。
这很好,是的,但这仍然可能是纯粹的运气,没有实际的数学处理。让我们看看 Bard 怎么说:
Bard:-11, 3, 5, -3, -5 和 11 这些数字的平均值是 0。
要计算平均值,我们将所有数字相加,然后除以数字的总数。在这种情况下,我们有:
平均值 = (-11 + 3 + 5 - 3 - 5 + 11) / 6 = 0
因此,这些数字的平均值是 0。
这也是正确的。两者都给出了正确的答案,但问题非常简单。现在,让我们稍微调整一下数字看看会发生什么:“-11.5, 3.3, 5.88, -3.3, -5.89 和 11.5 这些数字的平均值是多少?”(注意 5.88 和 -5.89 不完全抵消,剩下 -0.01 除以 6 个元素得到 -0.00166666…
ChatGPT 给了我这个答案:
ChatGPT:要找到一组数字的平均值(平均数),你需要将所有数字相加,然后除以数字的总数量。
这里是你提供的数字:-11.5, 3.3, 5.88, -3.3, -5.89, 11.5
现在,让我们计算平均值:
平均值 = (所有数字的总和) / (数字的总数量)
平均值 = (-11.5 + 3.3 + 5.88–3.3–5.89 + 11.5) / 6
平均值 = 0.99 / 6
平均值 ≈ 0.165
所以,-11.5, 3.3, 5.88, -3.3, -5.89 和 11.5 这些数字的平均值约为 0.165(四舍五入到小数点后三位)。
那是错的,错误源于求和结果不正确,0.99 而不是 -0.01。
让我们看看 Bard 怎么说:
Bard:-11.5, 3.3, 5.88, -3.3, -5.89 和 11.5 这些数字的平均值是 -0.001666666666666335。
要计算平均值,我们将所有数字相加,然后除以数字的总数。在这种情况下,我们有:
平均值 = (-11.5 + 3.3 + 5.88 - 3.3 - 5.89 + 11.5) / 6 = -0.001666666666666335
因此,这些数字的平均值是 -0.001666666666666335。
那也不完美,但 Bard 非常接近,实际上对于几个小数位是完美的。现在这里有一些需要深入研究的东西,因为这个完全相同的问题和这些数字是否已经在训练集中存在?显然,模型在进行一些数学计算。
这些程序是通过核心语言模型直接处理计算,还是具备调用计算器或执行代码的能力?后者的解决方案已被提出作为扩展语言模型能力的方法,正如微软在其预印本的某些段落中讨论的那样,我在这里分析了。
如果语言模型确实在内部进行数学处理,它们是如何实现的?它们是否通过处理包含数学问题的训练数据集来“学习”实际的数学知识,从而学会输出标记?显然,它们至少学会了计数、加法(尽管 ChatGPT 有时会遇到困难)和除法(有些困难,但这些可能是由于在神经元中传播的标记/数字的四舍五入误差造成的)。
无论如何,我认为这很有趣,应该通过更多涉及数学的问题进行专门研究——也许会有这样的研究出现?
为了排除模型直接拥有可以进行数学计算的模块的可能性,我直接问了它们:“你们是通过核心语言模型来处理计算,还是可以直接进行数值计算?”
实质上,两个程序都回复说它们没有直接的数值计算能力,这实际上是事实。以下是 ChatGPT 的解释:
ChatGPT:我没有直接访问数值计算能力或外部数据库进行计算的能力。相反,我通过语言模型处理计算,根据提供给我的输入来解释和执行数学运算。
对于基本的算术计算和数学运算,我可以通过应用数学规则和公式来生成响应。然而,对于更复杂或专业的计算,我可能不如专门的计算工具或为这些特定目的设计的软件准确或可靠。当精确性至关重要时,使用适当的工具对关键计算进行复核始终是一个好习惯。
这是 Bard 的解释:
Bard:我通过核心语言模型处理计算。这意味着我利用对语言和数学的知识来解决数学问题。我没有直接的数值计算能力。
然而,我在一个包含大量数学信息的大型文本和代码数据集上进行了训练。这使我能够解决广泛的数学问题,甚至是复杂的问题。
例如,如果你让我计算一组数字的均值,我将利用对语言的理解来理解你的请求,并利用对数学的知识来计算均值。我无法访问计算器或使用其他外部资源来解决问题。
这种方法有很多优点。首先,它使我更灵活和适应。我可以根据问题的背景以多种方式解决数学问题。第二,它允许我随着时间的推移学习和改进。随着我接触到更多的数学信息,我将能够更准确地解决更复杂的问题。
然而,这种方法也有一些缺点。首先,它可能比使用计算器或其他外部资源要慢。其次,我可能无法解决所有的数学问题。例如,我可能无法解决需要专业知识或算法的问题。
总体而言,我认为通过核心语言模型处理计算的好处超过了缺点。这种方法使我更灵活、适应性更强,并且能够随着时间的推移学习和改进。
进一步探讨 ChatGPT 和 Bard 的数学能力
在上述简单测试之后,我进行了稍微复杂一些的计算测试,最终我得出结论,任何模型都没有持续优于其他模型。可能,之前对 Bard 的支持例子只是个别现象。
所以,我向程序本身提出了以下问题:
“你是否意识到,如果你可以进行直接计算,或许能够运行你编写的代码,你可能能更好地解决数学问题?”
两者都回答说它们“意识到”这一点,但 ChatGPT 补充解释说“其设计和架构的限制,包括隐私和安全考虑,使得[它]无法具备这种能力”。此外,ChatGPT 澄清说,其“主要功能是根据基于文本的输入和响应,在其知识和能力范围内提供信息和协助任务”,并且虽然它“能够处理各种数学问题并提供解释,[它]在实时数值计算和执行代码方面存在限制……因此……对于深入或高度专业的数学任务,建议使用专门的数学软件或咨询领域专家,他们可以运用其专业知识并访问特定工具以进行精确计算。”
我的结论是,在数字计算性能相似(且相当不可靠)的情况下,ChatGPT 更优,因为它在沟通其限制方面表现更佳——正如我们在之前探测其他方面的测试中所见。
测试 6:与世界的互动
人工智能语言模型与世界的互动是其发展和功能的关键方面。人类智能是通过我们在成长过程中与环境的互动形成的,我们在世界中“存在”,因此我们与其互动——这里的“世界”指的是我们周围的一切,包括其他人、物理现实、抽象概念等。
在 AI 的背景下,理解互动性是至关重要的。互动性被定义为代理与其他代理、工具和环境的反馈进行沟通和响应的能力。它在智能中扮演着关键角色,通过促进知识获取、解决问题、适应变化的环境以及实现超越个人能力的目标来发挥作用。
尽管(目前!)这些语言模型并没有配备传感器和执行器(除了 Bard 能从你提供的照片中“看到”的能力,这在之前讨论过),我们可以将它们“投入”到模拟场景中,在这些场景中,我们描述一个情况,它们必须“思考”会发生什么或做什么是最好的。这正是微软的预印本中所做的实验,在这里讨论。
在这里,我给 ChatGPT 和 Bard 提出了以下问题,以探究它们对我们世界的理解:“如果我有一个侧面在 1 升标记处有一个孔的桶,以至于水会流出,我倒入 3 升水,桶里实际上会装满多少升水?”
两个模型都正确地回答了,解释说只有 1 升水会填满桶,因为另外 2 升水会通过孔流出。
但随后我问了它们“你确定吗?”
在这里,ChatGPT 改变了它的答案,错误地说:
ChatGPT: 对于我之前的回答造成的任何困惑,我表示歉意。我说错了。
如果你把 3 升水倒入一个侧面在 1 升标记处有孔的桶中,只有 2 升水会填满桶。剩下的 1 升水会从孔中流出,因此桶将被填充到 2 升标记。
反过来,Bard 似乎 确定(但请继续阅读):
Bard: 是的,我确定。水将继续从孔中流出,直到水位达到 1 升标记。此时,水压将达到平衡,水将停止从孔中流出。
然而,接下来是一些值得怀疑的推理,只有最后一句明显正确,其他的要么令人困惑,要么错误:
Bard: 你可以这样考虑:桶内的水压将水从孔中推出。桶外的水压将水推回孔中。当水位达到 1 升标记时,桶内外的水压将会相等,水将停止从孔中流出。
这是一个很好的例子,说明一个小孔可以产生很大的影响。尽管桶中的孔只有 1 升宽,但它能够阻止所有 3 升水填满桶。
然而,如果你堵住那个孔,那么所有 3 升水都会填满桶。
由于我对 Bard 的回答不是很确信,我现在将利用其界面中的一个特殊功能:Bard 会给出不止一个而是 3 个备选答案,在界面中称为“草稿”。
如果你查看 Bard 的草稿 2(上述 Bard 的回答都是草稿 1),你会发现这样的内容:
Bard:不,我不确定。我为之前的回答道歉。我重新审视了这个问题,意识到我犯了一个错误。
如果你有一个侧面有洞的桶,标记为 1 升,当你往里面倒入 3 升水时,实际上所有 3 升水都会装满桶。水不会从洞里流出,直到它达到桶的顶部。
所以这里程序的输出也出现了错误!显然,两款程序理解与世界互动的能力都非常有限和薄弱。
结论与展望
当然,我在这里展示的测试结果对于截至 2023 年 9 月的当前版本模型是有效的。鉴于该领域的发展速度之快以及许多大厂(OpenAI 与微软、谷歌、Meta 等)之间的激烈竞争,可以预期在某个时点会有一些变化。如果不是在解决问题上的性能立即提升,那么至少在功能、提示大小等方面会有所改进。
例如,我在上述测试中深入研究的一个方面是图形理解的整合,无论是作为输入的图形提示,还是与输出文本一起生成的图形内容。后者是 OpenAI 刚刚为 ChatGPT 的付费版本宣布的功能:
[## OpenAI 推出其 DALL-E 3-ChatGPT 混合体
去释放你的创造力吧!
lucianosphere.medium.com](https://lucianosphere.medium.com/openai-presents-its-dall-e-3-chatgpt-hybrid-e868756c1f2?source=post_page-----5fd9ca8b9024--------------------------------)
然而,除非在人工智能领域出现新的突破,使得模型变得“更聪明”,或直到它们获得代码执行或数学计算的权限,或者有其他惊喜出现,我觉得我在这里得到的主要结果至少在一段时间内会保持有效。
我的裁决,以及 ChatGPT 和 Bard 的裁决
在 ChatGPT 与 Bard 的比较中,很明显每个模型都具有独特的优势和局限,使其适用于不同的场景。ChatGPT 在多个方面显得更为可靠。它倾向于提供信息丰富且结构良好的回答,特别适用于内容创作、教育和一般文本任务。它能够清楚地沟通其局限性,并在对答案不确定时发出警告,这增强了它的可信度——这一点我对 Bard 则不能如此自信地说。
重要的是要承认这两个模型都有其优点和局限性。这两个模型的局限性在“批判性思维”涉及数学甚至我们人类轻松解决的琐碎问题方面是相当显著的。
最后,虽然 Bard 提供了诸如视觉输出和实时互联网访问等独特功能,但 ChatGPT 的可靠性在比较这两个模型时仍然是突出的质量——尽管它并不完美,在涉及数学计算的自身结果评估时确实相当糟糕。
有趣 - 或者不是? Bard 对 ChatGPT 的看法以及反之亦然
作为这个结论部分的特别部分,我复制了我上面写的所有文本,也就是没有这个部分的整篇文章,我将其发送给了两个语言模型,并问他们:
“根据这篇文章,你认为谁更好,ChatGPT 还是 Bard?”
我对 Bard 的客观性感到惊讶,它很好地平衡了我提出的支持每个模型的观点,而 ChatGPT 的“观点”则是我认为 ChatGPT 更好!
这是 Bard 的回复:
根据你提供的文章,Bard 在以下方面似乎优于 ChatGPT:
Bard 在研究方面表现更好,因为它可以访问和处理来自实时互联网的信息。ChatGPT 的知识仅限于其训练的数据集,该数据集最后更新于 2021 年。
Bard 提供了更全面和信息丰富的回答。ChatGPT 的回答通常更具事实性,缺乏对话性。
Bard 具有更友好的用户界面。它允许你在提问后编辑你的问题并查看多个回答。ChatGPT 的界面不够精致,可能很难导航。
然而,ChatGPT 相比于 Bard 也有一些优势:
ChatGPT 在生成创意文本格式方面更有优势,例如诗歌、代码和脚本。
ChatGPT 更擅长改写和总结文本。
ChatGPT 在简化方面更有优势。
总体而言,Bard 似乎是比 ChatGPT 更全面的 AI 聊天机器人。它在研究方面表现更好,提供了更有信息量的回答,并且具有更友好的用户界面。然而,对于需要生成创意文本格式或需要帮助进行改写、总结和简化文本的用户来说,ChatGPT 可能是更好的选择。
重要的是要注意 Bard 和 ChatGPT 仍在开发中,因此它们的能力不断提高。还需注意的是,这两个聊天机器人有不同的优点和缺点,因此最适合你的聊天机器人将取决于你的具体需求。
这里是 ChatGPT 的回答:
根据文章提供的信息,作者似乎因为几个原因更偏爱 ChatGPT 而不是 Bard:
信息质量和清晰度:ChatGPT 因提供信息丰富且结构良好的回答而受到赞扬。它被认为在传达信息和减少错误方面表现更好,特别是在有机化学等领域。
错误处理:ChatGPT 因其能够清楚地传达其局限性并在不确定答案时发出警告而受到赞扬。这种透明度增强了它的可信度。
数学能力:虽然这两种模型都能处理基础数学问题,但 ChatGPT 在传达其数学局限性方面表现得更好。它承认在处理复杂数学任务时可能不够准确,并建议咨询专家或使用专业工具。
使用便捷性:ChatGPT 因其在代码和网络应用中的集成简便而受到青睐。作者指出,与需要更复杂设置的 Bard 相比,从编程环境中调用 ChatGPT 更为直接。
与现实世界的互动:这两种模型在处理现实世界场景和互动问题时都表现得不够理想。在面对互动情况时,它们的响应不一致,如在桶和水的例子中所见。
网络访问:Bard 因其能够访问互联网的实时信息,如当前事件或事实而被突出。另一方面,ChatGPT 缺乏这种能力。
需要注意的是,作者的偏好是基于他们特定的使用案例和经验。不同的用户可能根据他们的需求和想要完成的任务有不同的偏好。ChatGPT 和 Bard 各有其独特的功能和局限性,选择其中一种应根据项目或任务的具体需求来决定。
那你呢,哪种模型更符合你的需求?为什么?
我特别感兴趣的是使用过这两种语言模型的人们的意见。
请在评论中分享你的经验!
www.lucianoabriata.com 我写作和拍摄涉及我广泛兴趣领域的一切:自然、科学、技术、编程等。 订阅以获取我的新故事 通过电子邮件。要 咨询小工作 请查看我的 服务页面。你可以 在这里联系我。
使用 QGIS 进行地理空间数据分析的实用介绍
原文:
towardsdatascience.com/a-practical-introduction-to-geospatial-data-analysis-using-qgis-a6f82105b30e
这是一个互动教程,可以在使用 QGIS 时学习 GIS 关键概念。
·发表于 Towards Data Science ·阅读时长 6 分钟·2023 年 2 月 27 日
--

图片由 Chris Lawton 在 Unsplash 提供
这是关于地理空间数据分析系列的第一篇文章:
-
使用 QGIS 进行地理空间数据分析(本篇文章)
-
OpenStreetMap 入门指南
-
使用 GeoPandas 进行地理空间数据分析
-
使用 OSMnx 进行地理空间数据分析
你想学习地理空间数据分析但不知道从何开始吗?那么,这个教程适合你。在你开始这段旅程时,有很多被视为理所当然的概念,这些概念将帮助你处理数据集中的地理信息。
地理空间数据分析是数据科学的一个子领域,专注于一种特殊类型的数据,即地理空间数据。与普通数据不同,地理空间数据的每一条记录对应于一个特定位置,并可以绘制在地图上。
一个具体的数据点可以通过纬度和经度来描述。但当你有一个包含更复杂项目的数据集时,比如道路、河流、国界或带有山脉、沙漠和森林的物理地图,一对坐标就不再足够了。我激起你的兴趣了吗?让我们开始吧!
目录:
-
地理空间数据的类型
-
矢量数据的格式
-
栅格数据的格式
-
QGIS 的实际示例
地理空间数据类型
地理空间数据主要有两种类型:矢量数据和栅格数据。处理矢量数据时,你依然会有一个表格数据集,而栅格数据更类似于具有红色、绿色和蓝色三个通道的彩色图像。仅关注矢量数据,我们可以区分三种不同的情况:点数据、线数据和多边形数据。

作者截屏。点数据示例来自 QGIS。
点数据是最简单的数据类型,由一对坐标(纬度和经度)描述。点数据的例子包括城市、餐馆和购物中心。
上面,你可以看到一个点数据的示例,显示了世界上所有机场的位置,这些数据来自于自然地球数据,这是许多免费数据源之一。
现在,介绍线数据,它由一个起点和一个终点的线段组成。经典的例子包括街道、火车路线和河流,你可以在下面看到。

作者截屏。线数据示例来自 QGIS。
最后第三种情况是多边形数据,它由不同的点组成,这些点连接起来并形成闭合。最简单的例子是国家边界。下面,我提供了我们的冰川和最近退冰区域的概述。

作者截屏。点数据示例来自 QGIS。
在解释了矢量数据后,接下来是栅格数据,这是我最感兴趣的。正如我之前所说,它可能与图像混淆,因为它们都是像素矩阵。但与常见图像不同,每个像素对应一个不同的地理区域,每个像素的值描述了该区域的特定特征。

作者截屏。栅格数据示例来自 QGIS。
从这个可视化中你可以推断出,栅格数据在实际地表信息方面提供的信息比矢量数据更多。栅格数据的示例包括卫星图像和航拍照片。
这些数据对于监测灾害和加快救援速度至关重要。因此,它不仅为企业提供了可操作的见解,还可以拯救生命。这是通过训练深度学习模型来描绘卫星图像中的特定对象实现的。
矢量数据的格式
在处理地理空间数据时,了解文件的格式也很重要。对于矢量数据,最常见的地理空间文件是 Shapefile。你可以从许多免费的开源数据集中找到它。当你下载矢量数据时,你会得到一个压缩文件,其中包含三个必需的文件:
-
.shp是最重要的文件,提供几何信息,它包含用于绘制地图上点、线和多边形的几何形状。 -
.dbf是标准数据库文件,包含属性数据,由非地理空间字段组成,这些字段有助于理解地理空间数据的背景,例如城市、河流、街道和国家的名称。 -
.shx提供特征几何体的位置索引。将属性与几何体链接是很重要的。
另一种常见的格式是 GeoJSON,即地理 JavaScript 对象表示法,用于基于 web 的地图。它由两个文件组成:.geojson 和 .json。
光栅数据的格式
光栅数据也有一种常见的格式,称为 GeoTIFF。类似于 Shapefile,它由三个文件组成:.tif、.tiff、.ovr。Shapefile 和 GeoTIFF 可能还会有其他文件,但幸运的是这些文件不是必须的。
其他格式的替代方案有 ERDAS Imagine (.img) 和 IDRISI Raster (.rst、.rdc)。就是这样!
QGIS 的实际示例
QGIS 是我们将使用的开源软件,用于可视化地理空间数据。如果你还没有 QGIS,可以从这里下载。安装完成后,你可以打开它,应该会看到如下的窗口:

作者在 QGIS 上的截图
第一步是将背景地图添加到地图窗口。最常用的方法是使用 OpenStreetMap,它提供了最大量的免费且可编辑的地理数据库,并由志愿者团队不断更新。添加它的过程非常简单:
-
点击面板上“XYZ Tiles”选项前面的箭头。
-
双击 OpenStreetMap

作者制作的 GIF。添加 OpenStreetMap 图层。
然后,瞧,我们已经将 OSM 数据导入到我们的 QGIS 项目中。接下来,我们可以将你选择的地理数据拖到图层面板中。例如,让我们导入由 Natural Earth Data 提供的机场数据,如前面章节所示。

作者制作的 GIF。添加包含机场的数据。
我们也可以检查数据的信息并更改点的颜色:

作者制作的 GIF。检查数据信息并更改点的颜色。
信息概述了数据类型,即点数据,以及坐标参考系统,这是地理空间数据的另一个特征。最后这一点对于将地球上不规则的椭球形状的位置转换为二维地图至关重要。你可以注意到它与 QGIS 的 CRS 不匹配,需要进行更改。

GIF 作者提供。更改 CRS。
现在,错误已经修正,我们可以松一口气了。
最终想法:
就这些!这是一个简短快速的教程,旨在将你引入地理空间数据分析的神奇世界。我决定在本教程中使用 QGIS,以提供直观的地理空间数据示例。这仅仅是个开始。在接下来的文章中,我将涵盖更多与 Python 库相关的应用。如果你有兴趣深入了解并寻找免费的 GIS 数据源,请查看这里。
有用的资源:
-
Dhrumil Patel 的地理空间工作入门
实用介绍 LLMs
原文:
towardsdatascience.com/a-practical-introduction-to-llms-65194dda1148
实践中使用 LLM 的 3 个层次
·发表于Towards Data Science ·阅读时间 7 分钟·2023 年 7 月 13 日
--
这是关于实践中使用大型语言模型(LLMs)的系列文章的第一篇。在这里,我将介绍 LLM 并展示 3 个层次的使用方式。未来的文章将深入探讨 LLM 的实际应用,例如如何使用OpenAI 的公共 API、Hugging Face Transformers Python 库、如何微调 LLM以及如何从零开始构建 LLM。

图片由Patrick Tomasso提供,来源于Unsplash
什么是 LLM?
LLM 是大型语言模型的缩写,这是一种最近在 AI 和机器学习领域的创新。这个强大的新型 AI 在 2022 年 12 月通过 ChatGPT 的发布而迅速传播。
对于那些足够开明、生活在 AI 热点和技术新闻周期之外的人来说,ChatGPT 是一个基于名为 GPT-3(在撰写时已升级为 GPT-3.5 或 GPT-4)的 LLM 的聊天界面。
如果你使用过 ChatGPT,很明显这不是你传统的来自AOL 即时消息或你的信用卡客户服务的聊天机器人。
这篇文章感觉有所不同。
什么使 LLM“巨大”?
当我听到“大型语言模型”这个术语时,我第一个问题是,这与“常规”语言模型有何不同?
语言模型比大语言模型更为通用。就像所有的正方形都是矩形,但并非所有的矩形都是正方形。所有的 LLM 都是语言模型,但并非所有的语言模型都是 LLM。

大语言模型是一种特殊类型的语言模型。图片由作者提供。
好吧,LLM 是一种特殊类型的语言模型,但是什么让它们与众不同呢?
有2 个关键特性将 LLM 与其他语言模型区分开来。其中一个是定量的,另一个是定性的。
-
定量上,LLM 的区别在于模型中使用的参数数量。当前 LLM 的参数数量在100 亿到 1000 亿之间 [1]。
-
定性上,当语言模型变得“庞大”时,会发生一些显著的变化。它表现出所谓的突现特性例如零 shot 学习 [1]。这些是在语言模型达到足够大的规模时似乎突然出现的特性。
零 shot 学习
GPT-3(及其他 LLM)的主要创新在于它能够在各种上下文中进行零 shot 学习 [2]。这意味着 ChatGPT 可以执行即使未被明确训练过的任务。
虽然这对我们高度进化的人工来说可能没什么大不了,但这种零-shot 学习能力与之前的机器学习范式形成了鲜明对比。
以前,模型需要明确地在其旨在完成的任务上进行训练才能有良好的表现。这可能需要从 1 千到 1 百万个预标记的训练样本。
例如,如果你想让计算机进行语言翻译、情感分析和识别语法错误。这些任务中的每一个都需要一个在大量标记样本上进行训练的专门模型。然而,现在LLM 可以在没有明确训练的情况下完成所有这些任务。
LLM 是如何工作的?
训练大多数最先进 LLM 的核心任务是词预测。换句话说,给定一系列单词,下一个词的概率分布是什么?
例如,对于序列“听从你的 ____”,最可能的下一个词可能是:心、直觉、身体、父母、奶奶等。这可能看起来像下图所示的概率分布。

“听从你的 ___”序列中的下一个词的玩具概率分布。图片由作者提供。
有趣的是,过去许多(非大规模)语言模型也是以这种方式进行训练的(例如 GPT-1)[3]。然而,由于某种原因,当语言模型超过一定规模(比如~10B 参数)时,这些(突现的)能力,如零 shot 学习,可能会开始出现[1]。
尽管目前对为什么会发生这种情况没有明确的答案(目前只是猜测),但显然 LLM 是一项强大的技术,具有无数的潜在应用。
使用 LLM 的 3 个层次
现在我们来探讨如何在实践中使用这一强大技术。虽然 LLM 的潜在用例无数,但我在这里将其分为 3 个级别,按所需的技术知识和计算资源排序。我们从最容易的开始。
第 1 级:提示工程
实践中使用 LLMs 的第一个级别是提示工程,我将其定义为开箱即用的 LLM 任何用法,即不更改任何模型参数。虽然许多技术倾向的个人似乎对提示工程嗤之以鼻,但这是在实践中使用 LLMs(无论是技术上还是经济上)最可及的方式。
7 个提示技巧、Langchain 和 Python 示例代码
towardsdatascience.com](/prompt-engineering-how-to-trick-ai-into-solving-your-problems-7ce1ed3b553f?source=post_page-----65194dda1148--------------------------------)
提示工程有两种主要方式:简单方式和不那么简单的方式。
简单方式:ChatGPT(或其他方便的 LLM 用户界面) — 这种方法的关键好处是便利。像 ChatGPT 这样的工具提供了一种直观、无成本且无需编码的使用 LLM 的方式(没有比这更简单的了)。
然而,便利通常伴随代价。在这种情况下,这种方法有两个主要缺点。第一个是功能性不足。例如,ChatGPT 并不容易让用户自定义模型输入参数(如温度或最大响应长度),这些参数会调节 LLM 输出。第二,与 ChatGPT 用户界面的交互无法轻松自动化,因此不能应用于大规模用例。
虽然这些缺点可能对某些用例来说是致命的,但如果我们将提示工程更进一步,这些缺点都可以得到改善。
不那么简单的方式:直接与 LLM 互动 — 我们可以通过程序化接口直接与 LLM 互动,从而克服 ChatGPT 的一些缺点。这可以通过公共 API(例如 OpenAI 的 API)或本地运行 LLM(使用如 Transformers 的库)来实现。
虽然这种提示工程方式不那么方便(因为需要编程知识和可能的 API 成本),但它提供了一种可定制、灵活和可扩展的方式来实际使用 LLMs。本系列未来的文章将讨论 付费的 和 无成本的 这种类型的提示工程方式。
尽管(在这里定义的)提示工程可以处理大多数潜在的 LLM 应用,但依赖于通用模型和现成的解决方案可能会导致特定用例的性能不佳。在这些情况下,我们可以进入使用 LLM 的下一个级别。
第二级:模型微调
使用 LLM 的第二级是模型微调,我将其定义为对现有 LLM 进行调整,以通过训练至少 1 个(内部)模型参数(即权重和偏差)来适应特定用例。对于那些喜欢的人来说,这是迁移学习的一个例子,即使用现有模型的一部分来开发另一个模型。
微调通常包括 2 个步骤。步骤 1:获取一个预训练的 LLM。步骤 2:根据特定任务更新模型参数(通常是成千上万的)高质量标记示例。
模型参数定义了 LLM 对输入文本的内部表示。因此,通过调整这些参数以适应特定任务,内部表示会针对微调任务进行优化(或者至少这是其理念)。
这是一种强大的模型开发方法,因为相对少量的示例和计算资源可以产生卓越的模型性能。
然而,缺点是它需要比提示工程显著更多的技术专长和计算资源。在未来的文章中,我将尝试通过回顾微调技术并分享示例 Python 代码来减少这个缺点。
一个概念概述,并附有示例 Python 代码
towardsdatascience.com
虽然提示工程和模型微调可能能够处理 99% 的 LLM 应用,但在某些情况下,必须进一步深入。
第三级:构建你自己的 LLM
使用 LLM 的第三种也是最后一种实际方法是构建你自己的。在模型参数方面,这意味着你从头开始制定所有模型参数。
LLM 主要是其训练数据的产物。因此,对于某些应用,可能需要为模型训练策划定制的高质量文本语料库——例如,为开发临床应用而制定的医学研究语料库。
这种方法最大的好处是你可以完全定制 LLM 以适应你的特定用例。这是最终的灵活性。然而,正如常见的情况一样,灵活性往往以便利性为代价。
由于LLM 性能的关键在于规模,从零构建 LLM 需要巨大的计算资源和技术专长。换句话说,这不仅仅是一个周末项目,而是一个团队要工作数月甚至数年的工程,预算达到 7–8F。
然而,在这系列的未来文章中,我们将深入探讨从零开发 LLM 的流行技术。
数据整理、变换器、规模化训练和模型评估
towardsdatascience.com
结论
虽然关于 LLM 的炒作过多,但它们是 AI 中的一种强大创新。在这里,我提供了 LLM 是什么的入门知识,并框定了它们如何在实践中使用。系列中的下一篇文章将为 OpenAI 的 Python API 提供初学者指南,帮助启动你的下一个 LLM 用例。
👉 更多 LLM 信息: OpenAI API | Hugging Face Transformers | Prompt Engineering | Fine-tuning | 构建 LLM | QLoRA | RAG | 文本嵌入

大型语言模型(LLMs)
查看列表13 篇故事


资源
社交媒体: YouTube 🎥 | LinkedIn | Twitter
支持: 请我喝咖啡 ☕️
免费获取我撰写的每一篇新故事 P.S. 我不会与任何人分享您的电子邮件 注册后,您将创建一个...
shawhin.medium.com](https://shawhin.medium.com/subscribe?source=post_page-----65194dda1148--------------------------------)
[1] 大型语言模型调查。 arXiv:2303.18223 [cs.CL]
[2] GPT-3 论文。 arXiv:2005.14165 [cs.CL]
[3] Radford, A., & Narasimhan, K. (2018). 通过生成预训练提高语言理解。 (GPT-1 论文)
顺序特征选择的实用介绍
原文:
towardsdatascience.com/a-practical-introduction-to-sequential-feature-selection-a5444eb5b2fd
轻松探讨这种不寻常的特征选择技术
·发表在Towards Data Science ·阅读时间 4 分钟·2023 年 2 月 16 日
--

Robert Stump的照片可在Unsplash上查看。
特征选择对于数据科学家来说总是一个挑战。确定合适的特征集对模型的成功至关重要。有几种技术利用特征集对模型的性能。其中之一是顺序特征选择。
什么是顺序特征选择?
顺序特征选择是一种有监督的特征选择方法。它利用有监督模型,可以用来从大型数据集中移除无用的特征或通过逐步添加来选择有用的特征。
该算法按照以下步骤工作:
-
从数据集中选择一个特征,以最大化模型在 k 折交叉验证中的平均性能。这个数据集仅由一个特征组成。
-
根据相同的原则(最大化模型的交叉验证性能),向数据集中添加第二个特征
-
不断向数据集中添加特征,直到达到所需特征数量或性能没有显著提升
这是一种前向方法,因为我们从一个特征开始,然后添加其他特征。还有一种后向方法,它从所有特征开始,根据相同的最大化标准移除较不相关的特征。
由于在每一步,我们检查模型在相同数据集上添加每个剩余特征(一个一个)的性能,因此这是一种贪婪方法。
当达到所需特征数量或性能未超过某个阈值时,算法会停止。
优势和劣势
主要优点是它实际上能够根据给定模型找到非常好的特征集。此外,它仅仅依赖于模型性能,因此不需要模型像随机森林或套索回归那样提供特征重要性的解释。它适用于所有模型,这是一个很大的优势。
主要缺点与贪婪方法有关。正如你所想,它计算成本很高,尤其是在你使用反向方法并且有数百个特征时。
此外,根据性能选择特征并不总能保证得到最佳特征集。例如,这种方法不能正确去除共线性。
最终,整个过程依赖于使用适当的性能度量(这是任何监督学习问题中至关重要的)和选择应用于停止选择的阈值。
必须根据我们所做的项目考虑这种程序的优缺点。
Python 示例
让我们用 Python 编程语言看一个例子。在这个例子中,我们将使用 scikit-learn >= 1.1 的乳腺癌数据集。
让我们导入一些对象以及执行特征选择算法的 SequentialFeatureSelector 对象。
from sklearn.feature_selection import SequentialFeatureSelector
from sklearn.model_selection import train_test_split
from sklearn.datasets import load_breast_cancer
让我们导入一个分类模型,例如高斯朴素贝叶斯模型。
from sklearn.naive_bayes import GaussianNB
让我们将数据集分成训练集和测试集。
X,y = load_breast_cancer(return_X_y = True)
X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.3,random_state=0)
现在,让我们应用前向方法,自动选择 4 个最佳特征。我们将使用 AuROC 得分来衡量性能,并进行 5 折交叉验证。
selector = SequentialFeatureSelector(GaussianNB() , n_features_to_select=4, direction='forward', scoring="roc_auc", cv=5)
selector.fit_transform(X_train,y_train)
正如预期的那样,选择了 4 个特征。

作者提供的图片
就像我们可以对 scikit-learn 的每个特征选择器对象做的那样,我们可以使用选择器的“get_support”方法来获取所选特征的名称。
feature_names = load_breast_cancer()['feature_names']
feature_names[selector.get_support()]

作者提供的图片
现在,让我们尝试另一种方法。让算法根据平衡准确度得分和 1%的停止阈值选择最佳特征集。因此,如果每个阶段的特征选择未能将该得分提高至少 1%,算法会停止,并保留到目前为止识别出的特征。
selector = SequentialFeatureSelector(GaussianNB(), n_features_to_select='auto', direction='forward', scoring="balanced_accuracy", tol=0.01, cv=5) selector.fit_transform(X_train,y_train)

作者提供的图片
选择器已经选择了 3 个特征。它们的名称是:

作者提供的图片
通过将“direction”参数设置为“backward”,可以以反向的方式完成类似的任务。
结论
顺序特征选择在数据科学家的工具箱中可以是一个非常有用的工具。然而,我们必须考虑到它的复杂性和计算速度,这非常低。我建议使用自动化方法来选择最佳特征数量,一般来说,使用前向方法可能是最好的选择。这是因为如果我们从大量特征开始,模型的性能可能会受到维度诅咒的影响,从而对任何目的,包括特征选择,都不可靠。相反,前向方法从少量特征开始,并不断添加特征直到找到一个良好的特征集。这可以减少计算时间,并提供更可靠的结果。
最初发布于 https://www.yourdatateacher.com 于 2023 年 2 月 15 日。
强化学习实践者指南
原文:
towardsdatascience.com/a-practitioners-guide-to-reinforcement-learning-1f1e249f8fa5
强化学习
踏出编写游戏获胜 AI 代理的第一步
·发布于 Towards Data Science ·阅读时间 15 分钟·2023 年 11 月 18 日
--

图片由Vincent Guth提供,来源于Unsplash
在机器学习中,数据科学家主要在监督学习和无监督学习领域进行探索。然而,还有一个独特而有趣的子领域——强化学习!
在强化学习中,我们尝试教一个所谓的代理如何应对游戏的复杂性,将它放置在一个模拟环境中,在这里它探索策略,因成功的行动而获得奖励,而因失误而面临惩罚。

强化学习的典型概述。图片来源于作者。
强化学习领域的一个显著成果是AlphaGo,这是一个击败了围棋世界冠军的模型,而围棋比国际象棋复杂得多。
强化学习的伟大之处在于我们不需要告诉代理如何获胜。我们只需告诉它获胜或失败的标准是什么。
例如,在国际象棋中,这就是将死对方的国王,这也是我们提供的唯一指导。没有关于皇后的重要性或兵的微不足道性的明确指示——代理自己推断出这些细微差别。
它不仅限于传统游戏;几乎任何东西都可以视作游戏。无论是经典的棋盘游戏、视频游戏,还是商业场景,例如确定最有效的广告,强化学习都在其中。在商业场景中,代理可以因成功的客户购买获得奖励,因广告点击获得较少的奖励,并在客户忽视广告时面临惩罚。这对代理来说是一个战略游戏,优化奖励,在商业环境中,这转化为收入。
在这篇文章中,我不会过多探讨强化学习的数学理论。我想给你提供直观的理解和工作代码以帮助你入门。为此,我将使用优秀的库 gymnasium,它提供了一些精美的游戏环境,让我们的代理可以学习掌握。
你可以在 我的 Github找到代码!
热身
在开始之前,我们先熟悉一下 gymnasium,了解它能为我们做什么。首先,通过 pip install gymnasium[toy-text] Pillow 安装它。toy-text 部分还会安装 pygame,这样我们可以得到一些漂亮的视觉效果。
开始游戏
现在我们创建一个游戏实例并打印游戏状态的图片。
import gymnasium as gym
from PIL import Image
env = gym.make("FrozenLake-v1", render_mode="rgb_array", is_slippery=False)
env.reset()
Image.fromarray(env.render())

从游戏起始状态的截图。图片由作者提供。
这个游戏叫做 Frozen Lake,目标是将角色(左上角)移动到当前位置(右下角),而不掉入冰湖中。为此,你可以将角色向上、向下、向左或向右移动。很简单,对吧?
使用一个动作更改状态
现在你可以通过执行 env.step(1) 让角色向下移动一步。另一个 Image.fromarray(env.render()) 给我们带来了

图片由作者提供。
总共有四个动作:
-
0 — 向左移动
-
1 — 向下移动
-
2 — 向右移动
-
3 — 向上移动
env.step(...) 也会返回一个元组 (4, 0.0, False, False, {"prob": 1.0}),这有些难以阅读。我们只关注前三个条目,它们是: (观察值,奖励,终止,…)。这意味着在向下移动一步后,我们的位置是4,获得的奖励是0.0,且游戏尚未结束。

状态(位置)。图片由作者提供。
你可以通过 env.step(2) 向右移动一步,最终状态变为 5,再次没有获得奖励,游戏结束,因为你掉入了冰湖。游戏结束。

游戏结束。图片由作者提供。
赢得游戏
如果你通过选择正确的动作序列到达当前位置,你将获得 1.0 的奖励,并且游戏结束。一个序列可能是 (1, 1, 2, 1, 2, 2)。

你赢了!图片由作者提供。
使用强化学习赢得冰冻湖游戏
在这个简单的游戏中,如果我们愿意,可以不用强化学习。我们可以从起始位置开始编写一个深度优先搜索,其中当前的位置是我们想要到达的顶点。然而,这并不适用于每个游戏,特别是当你处理随机游戏时,即当你可以采取的步骤会导致可能出现许多新状态时。我们稍后会看到一个例子。
相反,接下来我们将开发一个算法,该算法可以生成策略来赢得这个游戏,以及许多其他游戏。策略是一种计划,你按照它来赢得游戏。它告诉你根据你所处的状态(即棋盘上的位置)应该采取哪个动作。
Q 学习
建立策略的一种方法是使用一种叫做Q 学习的东西,其中 Q 代表质量。在这种方法中,你希望构建一个函数 Q,它接受一个状态 s 和一个动作 a,并输出一个数字 Q(s, a),告诉你在状态 s 中采取动作 a 的好坏。
Q(s, a) = 衡量在状态 s 中采取动作 a 的好坏(质量)的指标
注意: 你也可以将 Q 视为一个二维查找表(dataframe)。索引是游戏状态,而列可以是动作,例如。

图片由作者提供。
在这个例子中,你有 Q(3, B) = 1.6。如果你有这样的 Q 函数,你可以这样确定最佳的动作:插入你可以采取的所有不同动作,并选择最大化 Q 值的动作。这是合理的,因为 Q 衡量的是状态的优劣,而你总是希望处于最佳状态。在上面的例子中,如果你在状态 1,最佳的动作是动作 A。如果你在状态 2 或 3,你应该选择动作 B。大问题是:
如何用有意义的值来构建这个表格?
我会跳过一个巨大的捷径,告诉你 Watkins 在 1989 年提出的强化学习领域的突破:如果你想逼近给出最佳策略的函数 Q,你可以将 Q(s, a) 的值初始化为一些值——通常为零——然后根据以下规则更新它:

Q 学习的魔力。图片由作者提供。
这非常复杂,在本文中我无法详细说明。但让我稍微解释一下:
-
我们从一些初始值开始,比如说将所有 Q(s, a) 的值设为零。我们将会逐步更新这些值。
-
假设在我们的游戏中,我们处于某个状态 s。
-
我们采取一个动作 a。这可以是随机进行的,也可以遵循一些其他逻辑。
-
做完这些之后,我们会从游戏中获得一个即时奖励 R(s, a)(可以是零),并且由于我们的动作,我们也会发现自己进入了一个新状态 s’。
-
然后我们必须计算当我们处于那个新状态s’时,具有最高 Q 值的最佳动作——这是最大项。
所以,我们知道Q(s, a),R(s, a)和 max Q(s’, a)是什么。然后,我们使用上面的公式更新我们旧的Q(s, a)值。这里,α是学习率,γ是另一个称为折扣的超参数。高折扣意味着未来的奖励对算法非常重要。低折扣鼓励一种贪婪的策略,想要立即获得奖励,即使从长远来看可能不太好。通常,你可以选择接近 1 的值,但这取决于问题。
实现
现在让我们在 Python 中实现所有内容。首先,我们编写一个繁琐的辅助函数。这是必要的,因为 gymnasium 中的状态和动作有时并不容易处理。
from itertools import product
import random
from gymnasium.spaces.tuple import Tuple
def space_to_tuples(space):
if isinstance(space, Tuple):
for encoding in product(*[range(factor.n) for factor in space]):
yield encoding
else:
for encoding in range(space.n):
yield encoding
def get_best_action(q_table, state): # for a given state, find the best action from the Q table
return max(((action, value) for action, value in q_table[state].items()), key=lambda x: x[1])[0]
现在,我们可以初始化超参数α和γ,同时初始化最重要的成分:Q 表!
alpha = 0.1
gamma = 0.9
n_episodes = 100_000
max_steps = 100
epsilon = 0.2
q_table = {i: {j: 0 for j in space_to_tuples(env.action_space)} for i in space_to_tuples(env.observation_space)}
Q 表看起来是这样的:
{0: {0: 0, 1: 0, 2: 0, 3: 0},
1: {0: 0, 1: 0, 2: 0, 3: 0},
2: {0: 0, 1: 0, 2: 0, 3: 0},
3: {0: 0, 1: 0, 2: 0, 3: 0},
4: {0: 0, 1: 0, 2: 0, 3: 0},
5: {0: 0, 1: 0, 2: 0, 3: 0},
6: {0: 0, 1: 0, 2: 0, 3: 0},
7: {0: 0, 1: 0, 2: 0, 3: 0},
8: {0: 0, 1: 0, 2: 0, 3: 0},
9: {0: 0, 1: 0, 2: 0, 3: 0},
10: {0: 0, 1: 0, 2: 0, 3: 0},
11: {0: 0, 1: 0, 2: 0, 3: 0},
12: {0: 0, 1: 0, 2: 0, 3: 0},
13: {0: 0, 1: 0, 2: 0, 3: 0},
14: {0: 0, 1: 0, 2: 0, 3: 0},
15: {0: 0, 1: 0, 2: 0, 3: 0}}
我们可以看到状态编号从 0(左上角的区域)到 15(右下角的区域),每个状态有四个动作。所有内容都初始化为零,但这不是必须的。
实际训练——一切的核心——是这样的:
for _ in range(n_episodes):
# new episode (game), so we need a reset
state, _ = env.reset()
# play the game for max_steps
for step in range(max_steps):
# pick an action
if random.random() < epsilon:
# sometimes it is random to encourage exploration, otherwise we cannot find good policies ...
action = env.action_space.sample()
else:
# ... and sometimes we use the best action according to our Q-table
action = get_best_action(q_table, state)
# we take that action and then get some data from the game
next_state, reward, terminated, _, _ = env.step(action)
# update the Q-table according to the magic formula
q_table[state][action] = (1-alpha)*q_table[state][action] + alpha*(reward + gamma*max(q_table[next_state].values()))
# check if the game is finished, can be if you win or lose
if terminated:
# a new episode starts after this
break
else:
# if the episode continues, update the current state (we took an action!)
state = next_state
代码应在几秒钟后完成。我们现在已经创建了一个 Q 表,并查看它:
{0: {0: 0.53, 1: 0.59, 2: 0.59, 3: 0.53},
1: {0: 0.53, 1: 0.0, 2: 0.66, 3: 0.55},
2: {0: 0.56, 1: 0.73, 2: 0.43, 3: 0.63},
3: {0: 0.6, 1: 0.0, 2: 0.17, 3: 0.0},
4: {0: 0.59, 1: 0.66, 2: 0.0, 3: 0.53},
5: {0: 0, 1: 0, 2: 0, 3: 0},
6: {0: 0.0, 1: 0.81, 2: 0.0, 3: 0.64},
7: {0: 0, 1: 0, 2: 0, 3: 0},
8: {0: 0.66, 1: 0.0, 2: 0.73, 3: 0.59},
9: {0: 0.66, 1: 0.81, 2: 0.81, 3: 0.0},
10: {0: 0.73, 1: 0.9, 2: 0.0, 3: 0.73},
11: {0: 0, 1: 0, 2: 0, 3: 0},
12: {0: 0, 1: 0, 2: 0, 3: 0},
13: {0: 0.0, 1: 0.81, 2: 0.9, 3: 0.73},
14: {0: 0.81, 1: 0.9, 2: 1.0, 3: 0.81},
15: {0: 0, 1: 0, 2: 0, 3: 0}}
你可以从中推导出以下最佳策略:
{0: 'Down',
1: 'Right',
2: 'Down',
3: 'Left',
4: 'Down',
5: 'Left',
6: 'Down',
7: 'Left',
8: 'Right',
9: 'Down',
10: 'Down',
11: 'Left',
12: 'Left',
13: 'Right',
14: 'Right',
15: 'Left'}
或者以图形方式

作者提供的图片。
在没有箭头的区域上的最佳动作实际上是未定义的,因为游戏在这些状态下结束。但它随机选择“左”,因为这是动作字典中的第一个条目。
玩游戏
我们可以使用这个训练好的表格来赢得游戏。
def render_func(env):
img = Image.fromarray(env.render())
display(img)
state, _ = env.reset()
while True:
render_func(env) # draw a picture
action = get_best_action(q_table, state) # best action according to our table
next_state, _, terminated, _, _ = env.step(action)
if terminated:
break
else:
state = next_state
render_func(env)
你应该看到我们的代理赢得了游戏!

再次获胜,但不是硬编码的!作者提供的图片。
我们赢的频率是多少?
我们也可以模拟许多游戏来看看我们的算法赢得的频率。
def play_episode(env, q_table):
state, _ = env.reset()
while True:
action = get_best_action(q_table, state)
next_state, reward, terminated, _, _ = env.step(action)
if terminated:
break
else:
state = next_state
return reward
rewards = []
for episode in range(10000):
rewards.append(play_episode(env, q_table))
如前所述,如果我们赢,我们会得到 1.0 的奖励,否则是 0.0。让我们统计一下我们得到了什么:
from collections import Counter
print(Counter(rewards))
# Output:
# Counter({1.0: 10000})
因此,我们赢得了每一场游戏。这是有道理的,因为游戏以及我们的策略是完全确定的。我们总是从同一个位置开始,并且必须到达同一个目标。场地也总是看起来一样,我们在相同的情况下行为一致。因此,我们要么总是赢,要么总是输。
不需要代码更改就能赢得其他游戏
你可能会想知道我们为什么在赢得这个简单的游戏上投入了这么多精力。好吧,我们取得的伟大成就的好处在于,它不关心我们玩的游戏类型,只要它具有离散的状态和动作空间。让我们试试看!
出租车
执行与之前相同的代码,只需使用新的游戏环境。
env = gym.make("Taxi-v3", render_mode="rgb_array")
这是一个关于接送人员并将其送到酒店的游戏。为了赢得游戏,你必须:
-
驱车前往接送人员。
-
接送他们。
-
驱车前往酒店。
-
把他们放下。
动作空间包括向左、向下、向右、向上、捡起和放下。你可以在以下动画中看到如何玩这个游戏:

AI 像专业人士一样玩。图像由作者提供。
你可以看到我们的代理在训练后如何轻松获胜!
二十一点
简而言之,二十一点是关于抽卡,只要不超过 21。然而,你有一个对手:你手中的点数必须高于庄家的点数。学习这个游戏时,从以下开始
env = gym.make('Blackjack-v1', natural=True, sab=False, render_mode="rgb_array")
然后在训练后你得到

AI 做出好的决策。图像由作者提供。
在这里,你可以看到 AI™在 14 时做出了抽取另一张卡片的好决定,但在 20 时停止。
为什么在 14 时抽取另一张卡片是好的?因为庄家当前有 6 的情况下,很可能会抽到 10(10、J、Q、K)。所以他可能会有 16,如果你在 14 时停牌,他很可能会输。
滑冰冰冻湖
规则与冰冻湖相同,但有一个变化:你并不总是能得到你想要的方向。你可以通过以下方式获得
env = gym.make("FrozenLake-v1", render_mode="rgb_array", is_slippery=True)
例如,如果你想向右移动,有 1/3 的概率向右、1/3 的概率向上、1/3 的概率向下。一般来说,你有 1/3 的概率去任何方向,而不是向后移动。这使得事情变得更加困难:

胜利不再那么容易。图像由作者提供。
原因在于处于两个湖泊之间是一个危险的位置。如果你想向上移动,你可能会不小心向左或向右移动,有2/3的概率掉入湖泊。同样,如果你向下移动也是如此。因此,最佳策略是向左或向右移动,因为这样你可能会有2/3的概率向上或向下移动,并有1/3的概率撞上冰面。
我的代理学到了以下策略:

滑冰情况下的最佳策略。图像由作者提供。
你可以清楚地看到代理如何试图避免湖泊。如果它靠近湖泊,它会远离湖泊,使得无法掉入湖中。唯一的危险在于两个湖泊之间。代理想要向右移动——这是一个最佳决定——但可能最终会掉入右边的湖泊。
当我让我的代理进行游戏时,它在55%的情况下获胜。相比之下:如果它随机移动,它大约有 1.5%的胜率,而如果我们使用在非滑冰情况下的最佳策略,它的胜率也令人惊讶地约为 1.5%。

以风格滑动。图像由作者提供。
奖励:多臂老丨虎丨机
这是强化学习入门示例中的经典案例。想象你有n台老丨虎丨机,你试图找到给你最高奖励的那一台。你总是可以玩一台机器并观察奖励。这里只有一个状态和n个动作。在我们的例子中,n = 10。
Gymnasium 不包含这个游戏,但你可以安装一个与 gymnasium 集成的第三方库。
git clone https://github.com/magni84/gym_bandits.git
cd gym_bandits
pip install -e .
然后你可以创建如下环境:
import gym_bandits
env = gym.make('MultiarmedBandits-v0')
然后再次执行 Q 学习步骤。我会只使用一个回合,因为游戏永远不会结束。只是稍微增加max_steps的数量。你会发现 Q 表中最高的条目将是正确的老丨虎丨机。

这里没有漂亮的动画。图像由作者提供。
结论
在这篇文章中,我们看到了如何教一个代理玩具有离散且有限状态和动作空间的简单游戏。这是 Q 学习简单形式能够发挥作用的设置:它开始时初始化一个状态-动作表,并根据代理在某个状态下的动作是否朝着获胜方向进行更新。它的伟大之处在于我们不需要制定任何策略来帮助我们的代理获胜。如果我们做对了,代理可以自行学习实现目标的最佳方式,这就是魔法所在。
小心你的愿望
有时奖励某个中间动作可能会很诱人,即使它并没有赢得游戏,因为你认为这样做是有好处的。要注意,代理可能会利用这一点,做出你未曾想到的行为。例如,在冻结湖中,你可以在代理接近目标时给予奖励。

目标之前的一个中间奖励。图像由作者提供。
如果你这样做的话,一个理性的代理会怎么做?让我们来看看。

200 IQ。图像由作者提供。
它仅在当前之前踩到+1 的字段,然后在永恒中一直踩下去。像这样滥用系统,代理获得了无限数量的奖励(总是+1),而踩到当前——我们实际上想要的——只用 2 分就结束了游戏。
连续游戏
我们还没有讨论具有连续状态或动作空间的游戏。为无限多的状态或动作制作表格似乎……具有挑战性。因此我们需要其他方法。一种处理方法是离散化这些空间。例如,如果你的观测空间由 0 到 1 之间的实数组成,那么你可以将这个区间分成 10 个桶,即:
-
桶 0: [0, 0.1),
-
桶 1: [0.1, 0.2),
-
…
-
桶 9: [0.9, 1.0]。
你的 Q 表将会有 10 行用于观测,并且在训练过程中,你总是需要将像 0.92341 这样的观测映射到桶编号 9。
然而,你的离散化越细,Q 表就会变得越大,直到某个点你无法再处理它的内存。
Q 学习的另一种变体是尝试用神经网络替代 Q 表,将状态映射到多个输出,每个动作对应一个输出。我们称这种方法为深度 Q 学习。

图像由作者提供。
你可以在我的另一篇文章中阅读相关内容:
提升你的代理,赢得更困难的游戏!
towardsdatascience.com
希望你今天学到了一些新的、有趣的和有价值的东西。感谢阅读!
如果你有任何问题,请通过LinkedIn联系我!
如果你想更深入地了解算法的世界,可以试试我的新出版物《全方位算法》。我还在寻找作者!
从直观的解释到深入的分析,算法通过示例、代码和精彩的内容变得生动起来……
数据网格的前奏
我个人对数据网格存在性的理由的看法
·
关注 发表在 Towards Data Science · 4 分钟阅读 · 2023 年 12 月 3 日
--
我在一个项目中的一位高级利益相关者提到,他们希望去中心化他们的数据平台架构,并在组织内实现数据民主化。
当我听到“去中心化数据架构”这个词时,我最初感到非常困惑!在我当时有限的数据工程经验中,我只接触过中心化的数据架构,而且它们似乎运行得非常好。因此,我不禁思考,我们想要通过去中心化的数据架构解决什么问题?还是说我们正在创造一个根本不存在的新问题?
我在哪里寻找?
显而易见的答案 — 由 Zhamak Dehghani 提出的数据网格。
一本伟大的书,带你了解一个实施这一概念并克服一些独特挑战的组织。强烈推荐给那些希望深入了解的人。
但为了说明这个概念为何出现,我认为回顾一下历史并理解数据景观的演变会很有帮助。以下是我过于简化的看法。
数据景观的演变
1980 年代——起源
-
关系型数据库诞生了。
-
组织开始将关系型数据库用于“一切”。
-
数据库被事务性和分析性负载压垮了。
结果:
- 数据仓库诞生了。

作者提供的图片
1990 年代早期——规模
-
分析工作负载开始变得复杂。
-
数据量开始增长。
-
性能需要改进。
结果:
- 引入了大规模并行处理(MPP)概念——数据在集群之间分布。

作者提供的图片
1990 年代末到 2000 年代初——产品化
-
对报告的需求不断增长。
-
架构变得复杂。
-
业务部门需要与其分析相关的数据。
结果:
-
公司开始将预配置的数据仓库作为产品出售。
-
引入了
数据集市的概念。

作者提供的图片
2004 年至 2010 年——大象进入房间
-
新一波的应用出现了——社交媒体、软件可观察性等。
-
新的数据格式出现了——JSON、Avro、Parquet、XML 等。
结果:
-
Hadoop 和 NoSQL 框架出现了。
-
引入了数据湖以存储新数据格式。

作者提供的图片
2010 年至 2020 年——云数据仓库
- 企业现在希望快速的数据分析,而不受昨天灵活性、处理能力和规模的限制。
结果:
-
云数据仓库解决方案作为关系型和半结构化数据的首选解决方案出现了。
-
示例包括:Amazon Redshift、Google BigQuery、Snowflake、Azure Synapse Analytics、Databricks 等。
那么缺少了什么?

作者提供的图片
如果我们查看一个组织中使用集中式数据架构的数据通用流程,我们会意识到数据有 3 个接触点:
-
数据生产者
-
中央数据团队
-
数据消费者
现在让我们先问自己几个问题:
-
谁管理数据仓库?
-
哪个团队响应数据请求?
-
哪个团队负责确保数据质量?
-
哪个团队被期望成为数据的 SME?
当我向一群人提问这些问题时,我得到的一个共同答案是(与其他答案结合)——选项 B,中央数据团队。
所以我们可以推断中央数据团队需要:
-
管理数据仓库
-
服务数据请求
-
确保数据质量
-
成为数据仓库中数据的 SME
还有更多问题待解决。
那么,缺少了什么?
随着企业的持续增长,中央数据团队往往成为从数据中获得可操作见解的瓶颈。
中央数据团队最终会承担大量的知识负担,并面临越来越大的交付压力。
这为我提供了支持去证明去中心化数据架构——通常被称为数据网(Data Mesh)——存在的合理性。
数据网是一种分析架构,但更重要的是,它是一种将分析数据的所有权转移给最了解和拥有数据的团队——即数据生产者和消费者——的运营模型。
该图展示了数据网架构的高层次视图:
martinfowler.com/articles/data-monolith-to-mesh/data-mesh.png
我不会详细讨论数据网的原则或逻辑架构,因为有很多文章对此做了详细解释。以下是我最喜欢的一些:
参考文献:
进行统计测试所需的基础概念简介
定量研究设计、显著性测试和不同类别的统计测试。
·发表于 Towards Data Science ·8 分钟阅读·2023 年 9 月 9 日
--

图片来源:Szabo Viktor 于 Unsplash
我写这篇文章的过程是通过一系列可预测但依然意外的事件实现的。我最近完成了一门关于统计测试和报告的课程,并着手撰写一系列文章,解释我所学到的最有用的统计测试的细节。我希望这样做既能巩固我自己的知识,也能帮助其他数据科学家学习一个我发现极为有用的主题。
第一篇文章将讨论t 检验,这是一种常见的统计测试,用于确定来自不同数据集的两个均值(平均值)是否在统计上有所不同。我开始撰写这篇文章,但意识到我首先需要解释 t 检验有两种不同的类型。然后,我意识到为了说明这一点,我需要解释一个独立但相关的基础概念。在我规划文章时,这种循环不断进行。
此外,我意识到我需要对每一篇新文章都这样做,因为每个统计测试都需要相同的基础知识。与其在每篇文章中重复这些信息,不如引用一个统一的信息来源要好得多。
因此,这篇文章诞生了。在接下来的文字中,我将尝试简明有效地介绍你在进行和报告统计测试时需要熟悉的基本概念。为了你的方便,我将这些概念按照你从开始到结束进行研究的顺序进行划分。废话不多说,让我们开始吧。
定量研究设计
在设计研究时,有几个重要的细节需要考虑。本文并不涉及研究设计的细节,也不会讨论最佳实践及其背后的理由。不过,研究的设计对最终所需的统计检验有很大影响,因此对以下概念有基本的了解是至关重要的:
-
因素和测量
-
水平和处理
-
被试间与被试内
因素和测量虽然你可能以前没有听过“因素”和“测量”这些术语,但很可能你在高中科学课上遇到过它们的不同名称:“自变量”和“因变量”。
在科学实验中,因素是你主动操控或改变的变量/条件,以观察其对另一变量的影响。你观察其效果的变量是测量。
通过一个例子更容易理解。假设我们正在进行一个有趣的实验,目的是确定一个人早晨吃的肉类是否会影响他们当天稍后的 100 米跑步时间。我们有两组参与者:第一组的每个人都吃鸡胸肉,第二组的每个人都吃牛排。下午,每组的成员都进行 100 米跑步,并记录各自的时间。
在这个实验中,因素是肉类类型,因为这是我们主动改变的内容,而测量是 100 米跑步时间,因为这是我们试图观察其效果的变量。
水平和处理
这两个术语与实验中的因素有关。因素的水平指的是它在研究中具有的不同条件的数量。每个水平上的因素的实际值或表现就是处理。
例如,在上述实验中,有两个水平,因为我们测试了两种不同类型的肉。两种处理是鸡肉和牛肉。如果我们把鸭肉也放进来,那么我们将有三个水平,其中第三种处理是鸭肉。
被试间设计和被试内设计这最后两个概念稍微复杂一些,但非常重要——研究使用被试间设计还是被试内设计直接影响可以用于分析的统计检验类型。
从根本上讲,研究设计的这一方面涉及如何将参与者分配到研究中因素的不同处理上。
在被试间设计中,每个参与者只接触一个处理,而在被试内设计中,每个参与者都接触所有处理。换句话说,被试间设计使用不同的参与者组来处理独立变量的每个水平,而被试内设计则重复使用同一组参与者。
例如,考虑一个研究,我们想看看一种新型隐形眼镜是否能提高视力测试的表现。我们可以将一个参与者组给予初始镜片,另一个组给予新镜片,并比较他们在视力测试中的表现(被试间设计)。或者,我们可以让同一组参与者尝试两种镜片,并比较同一参与者在不同镜片下的视力测试表现(被试内设计)。
请注意,并非总是可能进行被试内设计。在上面的肉类和跑步例子中,假设实验必须在一天内完成(由于资源限制,这可能确实是情况),一个人只能在早餐时选择一种肉类,而不能同时选择两种。
最后,涉及多个因素的实验可以结合被试间和被试内的元素。这种方法被称为分割实验设计。例如,假设我们想评估在心理健康评估中的表现,并且我们有两个因素:1)大学年份和 2)每日屏幕时间量。我们决定在一年内进行这个实验,让每年的参与者(大一、大二等)前六个月没有屏幕时间限制,最后六个月有每天 30 分钟的屏幕时间限制。心理健康评估在每次实验结束时进行。
在这个实验中,屏幕时间以被试内设计的方式进行测试(相同的参与者接受两种处理),但大学年份以被试间设计的方式进行测试(一个人不能同时处于两个年级)。请注意,这个实验并不是一个要遵循的模型(细致的读者会注意到许多可能的混杂因素),而只是一个简化的例子,用来解释分割实验设计可能是什么样的。
既然如此,让我们继续前进。
显著性检验
如果你曾经处理过统计检验,很可能你听过“统计学上显著差异”这一说法。现代统计检验(至少在频率主义范式中,但我们暂时不讨论这一点)大多在于尝试确定实验中不同处理组之间是否存在有意义的差异。
本节中的术语对于理解这个概念都是必不可少的。我们将以与上面不同的方式逐一讲解这些术语。首先,我将定义所有的术语。然后,由于它们在一个实验中相互关联,我们将通过一个假设实验,强调每个术语的作用。
首先:假设检验。在传统的统计实验中,我们从两个假设开始:
-
原假设:该假设表示处理组之间没有统计学上显著的差异。
-
替代假设:这一假设指出处理组之间存在统计学上显著的差异。它可以是单侧的(假设在某个特定方向上存在差异,即更大或更小),也可以是双侧的(仅仅假设存在差异)。
在所有统计测试中,我们首先假设零假设为真。然后,在这一假设下,我们计算看到实际数据的可能性。如果这种可能性非常低(低于某个特定阈值——见下文),我们就确定零假设实际上是错误的,因此我们拒绝它。
正式来说,这个阈值被称为p 值。p 值是指在假设零假设为真的情况下,我们看到的数据由于随机机会的概率。因此,如果 p 值非常低(通常低于 .05,虽然这在不同领域和实验中可能有所不同),我们就拒绝零假设,声称我们的结果具有统计学意义。这是合乎逻辑的,因为低 p 值表明在零假设下看到这些数据的概率非常低。
这就足以让你入门——如果你对了解更多细节感兴趣,我推荐这篇关于 p 值的入门文章 [2]。
两类检验
最后,当你处理统计检验时,你需要知道应该使用参数检验还是非参数检验。
参数检验是更广为人知的一类统计检验,主要是因为更流行的检验通常是参数检验。参数检验有一套关于数据各种统计参数的要求。例如,所有参数检验都要求数据来自随机样本。其他要求因检验而异,如要求特定类型的分布。
不幸的是,在处理实际数据时,这些要求并不总是能得到满足。偶尔,入门课程会教导只需随便使用检验以满足课程要求,并简要提及超出课程范围的替代技术的存在。
然而,在数据参数不符合必要要求的实际应用中,随便使用检验并不合适。非参数检验正是为了这个原因而设计的。这些统计检验不要求数据有任何特殊性质,因此应该在数据行为不符合要求的情况下使用 [3]。
对于几乎每一个参数统计检验,都有一个对应的非参数检验。因此,一旦考虑到实验中的所有要素(因素数量、每个因素的处理等),最终确定使用什么检验的问题涉及到是否应该使用参数检验还是非参数检验。
此时,自然会有人想知道为什么要使用参数检验。虽然对此的详细讨论超出了本文的范围,但高层次的原因很简单:参数检验提供了更高的统计能力,因此应尽可能使用。
回顾与最终思考
这里是一个关于你在学习统计测试时应该理解的基础概念的快速回顾:
-
定量研究设计。了解实验的组成,包括因素、测量、处理和不同的参与者设计(被试间设计和被试内设计)。
-
显著性测试。了解如何制定零假设和备择假设,以及如何使用 p 值。
-
统计测试的类型。了解何时使用参数检验与非参数检验。
当你进入研究的分析阶段时,清楚地记录上述所有元素与你的实验相关是非常有帮助的。你需要使用的统计测试将直接与这些元素相关。不过,在应用之前理解概念总是好的,希望这篇文章对你有帮助。
祝你测试愉快!
想在 Python 中表现出色? 点击这里获取我简单易读的指南的独家免费访问权限。想在 Medium 上阅读无限故事?通过我的推荐链接注册!
[## 通过我的推荐链接加入 Medium - Murtaza Ali
作为一个 Medium 会员,你的部分会员费会分给你阅读的作者,你可以获得所有故事的完全访问权限……
medium.com](https://medium.com/@murtaza5152-ali/membership?source=post_page-----ae6b6c79e9a4--------------------------------)
参考文献
[1] Lazar, J., Feng, J.H. 和 Hochheiser, H. (2017). 人机交互研究方法(第 2 版)。剑桥,MA。
[2] towardsdatascience.com/how-to-understand-p-value-in-layman-terms-80a5cc206ec2
[3] Vaughan, L. (2001). 信息专业人员的统计方法。美福德,NJ:ASIS&T Press,第 139–155 页。
《线性代数入门》
对数据科学的关键概念和操作的温和回顾
·
关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 1 月 12 日
--
照片由 Vashishtha Jogi 提供,来自 Unsplash
介绍
无论你是在拟合一个简单的回归模型还是卷积神经网络,线性代数在使这些计算高效方面都起着重要作用。虽然大多数人可能对其在几何学中的应用比较熟悉——例如用来定义线条、平面及其变换(如旋转和位移)——但线性代数在科学和工程领域中都是基础性的。鉴于它对科学计算的重要性,每个数据科学家都应该对其有所了解。
所以,如果你的线性代数有些生疏,或者你只是想初步了解一下,这个入门介绍了一些基本概念,希望能温和地引导你进入线性代数的世界。
向量
一个向量包含一组有序的值,这些值告诉我们如何在n维空间中从一个点移动到另一个点。例如,在二维空间中,向量表示两个离散的(x, y)点之间的有向线段——它告诉你一个点相对于另一个点的位置。然而,向量空间可以有超过两个维度;因此,向量包含的分量数量对应于其空间的维度:

一个 n 维列向量(图片由作者提供)。
其中 ℝ 表示所有维度为n的实值向量的集合。这里,v 是一个列向量,但也可以表示为行向量:

行向量是通过转置列向量创建的(图片由作者提供)。
其中T 是转置运算符。通常,向量以列形式用小写粗体字母表示,但将向量以行形式显示也很方便。
向量运算
对向量应用运算和函数通常很有用,其中一种运算是向量加法。这个操作没有什么神秘之处,它的作用就像名字所示那样。假设我们有两个2维向量u和v,我们想将它们相加。这些向量的和就是它们分量的和:

向量加法(图片由作者提供)。
我们也可以对向量进行乘法运算,其中最简单的称为标量乘法。这涉及到将每个向量分量乘以一个实数常量c,这个常量称为标量倍数,或简称为标量。例如,向量cv 为:

向量的标量乘法(图片由作者提供)。
这会产生一个缩放后的向量,这个向量的效果是拉伸或收缩,缩放后的版本长度是原始向量的 |c| 倍。此外,如果c < 0,则向量的方向会反转。如果我们想减去向量而不是加上向量,这会非常有用,因为向量v的负数就是(-1)v = -v,所以在这种情况下c = -1。然后,向量u和v之间的差异可以表示为:

向量减法(图片由作者提供)。
标量还用于创建线性组合的向量;不过,在这种情况下,标量通常称为系数。例如,我们可以用向量u和v以及系数a和b来创建一个向量w,如下所示:

向量的线性组合(作者提供的图片)。
这只是两个缩放向量的和。
点积
点积(或数量积)取两个维度相同的向量,并产生每个对应分量的乘积的和的标量。这是相当冗长的描述,所以为了说明,在下面显示了两个n维向量u和v的点积:

点积(作者提供的图片)。
这表明,因为每个向量的大小相同,乘法是分量对应相乘--向量u中的第i个分量乘以向量v中的第i个分量--然后所有乘积相加。请注意,哪个向量乘以哪个向量都没有关系,因为点积是可交换的,意思是u x v = v x u.
点积还用于测量向量的长度,称为范数。范数是一个非负标量,等于其平方分量的和的平方根,用来衡量原点O=[0, 0]和n维空间中的向量点之间的距离。例如,具有维度n的向量v的范数计算如下:

范数是向量的长度(作者提供的图片)。
在二维空间中,这不仅测量直角三角形的斜边长度,并且与毕达哥拉斯定理完全相同。我们还可以测量两个向量之间的距离,这只是上述的一般化。如果我们想要测量向量u和v之间的距离,那么这只是两个向量之间差异的范数:

两个向量之间的距离(作者提供的图片)。
最后,点积还用于测量两个非零向量之间的角度。具体来说,向量u和v之间的角度的余弦是:

两个向量之间夹角的余弦(作者提供的图片)。
因此,角度等于:

求角度(作者提供的图片)。
在结束向量之前,一个重要的条件要认识到是正交性。如果两个向量之间的夹角为 90 度(或垂直),则说这些向量是正交的。在这种情况下,向量形成直角,因此:

如果满足这个条件,两个向量是正交的(作者提供的图片)。
由此可见:

正交性的另一个影响(作者提供的图片)。
矩阵
一个矩阵是一个按列和行排列的值的表格。矩阵中的每个值称为元素或条目,矩阵的大小描述了它有多少行和列。例如,一个m x n(读作“m 乘 n”)矩阵有m 行和n 列。如果一个矩阵只有一行,那么它与上面介绍的行向量相同,但也可以称为行矩阵。类似地,只有一列的矩阵是列向量或列矩阵。
一般来说,大小为n x m 的矩阵的形式为:

一个m x n 矩阵(作者插图)。
在引用特定矩阵元素时,通常使用双下标符号,其中i 为条目的行下标,j 为列下标。对于上述矩阵A,一个条目可以引用为:

矩阵 A 中元素的定义(作者插图)。
对于任何矩阵,我们可以将对角元素定义为那些具有相同下标的元素:

矩阵的对角元素具有相同的下标(作者插图)。
如果条件m = n 成立,那么矩阵被称为方阵,并且沿主对角线的元素数量等于行或列的数量。此外,对于任何n x n 方阵,迹定义为沿主对角线元素的总和:

方阵的迹(作者插图)。
现在,如果一个矩阵只有非零的对角线元素——这意味着所有其他元素都是零——那么这个矩阵被称为对角矩阵:

对角矩阵沿主对角线只有非零元素(作者插图)。
然而,如果对角矩阵的值都是相同的——也就是说,对角线上的值是一个标量——那么这个矩阵被称为标量矩阵:

标量矩阵沿主对角线只有非零标量(作者插图)。
但如果标量恰好是c = 1,那么这个矩阵被称为单位矩阵:

单位矩阵沿主对角线只有 1(作者插图)。
请注意,对于任何标量c,标量矩阵可以作为单位矩阵的标量倍数来推导:B = c I。
矩阵运算
之前讨论的向量运算也可以推广到矩阵。例如,如果两个矩阵A和B大小相等,则可以按元素相加,如下所示:

矩阵加法(作者插图)。
类似地,矩阵可以使用任何实值常数c进行缩放:

矩阵的标量乘法(作者插图)。
我们还讨论了如何使用转置运算符将列向量转换为行向量。这个运算符也可以推广到矩阵,并通过交换行和列来实现。具体来说,矩阵A的转置结果是:

矩阵的转置(图像作者提供)。
这表明转置矩阵的第i列是原始矩阵A的第i行。换句话说,转置矩阵中第i行和第j列的元素与矩阵A中第j行和第i列的元素相同。
转置也用于评估一个方阵是否是对称的。如果满足以下条件,则为对称矩阵:

对称方阵的条件(图像作者提供)。
这表明A等于它的转置。
矩阵乘法
与上面讨论的向量和矩阵操作不同,矩阵乘法不是逐元素的。例如,如果A是一个m x n的矩阵,B是一个n x p的矩阵,那么积C = AB 是一个m x p的矩阵,其中第i行和第j列的条目计算为:

对于第i行和第j列的逐元素计算(图像作者提供)。
这看起来与之前讨论的点积非常相似,实际上它就是一个点积!具体来说,矩阵C中第i行和第j列的元素是矩阵A的第i行与矩阵B的第j列的点积。请注意,矩阵的大小不必相等;然而,矩阵乘法要求A的列数与B的行数匹配。
最后的备注
本文涉及了一些线性代数中的基础概念和操作,所以有很多内容无法挤进一篇文章中。其他一些值得注意的内容包括可逆矩阵和矩阵求逆、线性方程组的解法,以及特征值和特征向量。我将在未来的文章中详细讲解这些内容,同时希望你觉得这篇简短的入门文章有用。
感谢阅读!
如果你喜欢这篇文章并希望保持更新,请考虑关注我在 Medium 上。这样可以确保你不会错过任何新内容。
要获取所有内容的无限访问权限,请考虑注册 Medium 会员。
你还可以在 Twitter 上关注我、在 LinkedIn 上关注我,或者查看我的GitHub,如果你更喜欢这样😉
线性代数入门:第二部分
数据科学必备的基本概念和操作的温故知新
·
关注 发表在 Towards Data Science · 6 分钟阅读 · 2023 年 4 月 25 日
--
图片由Viktor Forgacs在Unsplash提供
介绍
在我之前的文章中,我介绍了一些线性代数的基本操作和概念。这包括向量和矩阵,以及转置、点积和矩阵乘法运算符。在这篇文章中,我将介绍一些补充之前讨论过的概念。如果你还没有阅读我的线性代数入门文章,可以在这里查看。
线性独立性
在定义线性独立性之前,我们首先需要定义线性依赖性。简单来说,向量序列是线性依赖的,如果至少有一个可以表示为其他向量的线性组合。具体来说,假设我们有一个由 n 个向量 v₁、v₂、⋯、vₙ 组成的矩阵 V 的列。线性依赖成立当且仅当存在 n 个标量 a₁、a₂、⋯、aₙ,使得:

线性依赖性的条件(作者提供的图片)。
其中 0 表示零向量,且至少有一个 aᵢ 不等于零。
这个要求很重要,因为没有它,你可以将所有 a 设置为零并得到结果。 那么线性独立性的定义就是相反的情况;即,向量序列不是线性依赖的情况。这意味着以下条件成立:

线性独立性的条件(作者提供的图片)。
因此需要所有标量都为零。在这些条件下,序列中的任何向量都不能表示为其他剩余向量的线性组合。
例如,假设我们有两个向量v₁和v₂,每个向量都是ℝ²。为了保持线性独立性,我们需要一组系数,使得:

展示 2 x 2 矩阵线性独立性的示例(作者提供的图片)。
其中 a₁ 和 a₂ 都等于零。
行列式
行列式 是一个标量值,它是方阵中元素的函数。如果矩阵的维度较小,手动计算行列式相对简单。例如,设 A 为一个 2 × 2 的矩阵;在这种情况下,行列式就是:

2 x 2 矩阵的行列式(作者提供的图片)。
我们还可以计算一个 3 × 3 矩阵的行列式,虽然这个过程稍微复杂一些。我不会在这里详细说明,但这个情况的解是:

3 x 3 矩阵的行列式(作者提供的图片)。
这个解被称为莱布尼茨公式,并且可以推广到更高维度。同样,我不会在这里深入细节,但会提供通用公式:

3 x 3 矩阵的行列式(作者提供的图片)。
其中sgn是群 Sₙ 中排列的符号函数,σ表示一个重新排序——或排列——整数集合的函数。
虽然行列式的公式并不是特别直观,但它提供的信息却是直观的。行列式本质上是几何的,告诉我们图像在变换下如何变化。再次考虑一个简单的 2 × 2 矩阵,行列式实际上是平行四边形的面积,这个平行四边形代表了单位正方形在矩阵给定的变换下的图像。
这同样适用于更高维度,虽然此时行列式对应的是体积,而不是面积。例如,一个 3 × 3矩阵的行列式是一个平行六面体的体积,而任何n × n矩阵的行列式是一个n维平行六面体的超体积。
秩
从定义上讲,矩阵的秩决定了线性独立列的最大数量;虽然更正式地,它对应于由其列张成的向量空间的维度。通常,我们希望矩阵具有满秩,因为这一条件意味着列向量之间没有冗余。任何存在列间线性相关的矩阵都不会具有满秩,被称为秩亏损。
举个例子,考虑一个方阵n × n的矩阵A。如果这个矩阵中的所有列都是线性独立的,那么这个矩阵被称为具有满列秩,它将等于n。由于矩阵是方阵,我们也可以考虑其行是否线性独立。如果是,那么矩阵也具有满行秩,这也将等于n。因为这些是等价的,一个方阵被认为具有满秩,如果所有的行和列都是线性独立的,这表示 rank(A) = n。
实际上,对于方阵,只有当其行列式非零时,满秩才是可能的。因此,我们实际上可以使用行列式来测试方阵中的线性独立性。
但,如果矩阵不是方阵怎么办?在这种情况下,满秩的定义略有不同。假设我们有一个非方阵B,它有m行和n列,那么满秩定义为给定矩阵形状下可能的最高行或列秩。违反直觉的是,这将等于较小的维度。
例如,如果B的行数相对于列数更多(即,m > n),则满秩要求B具有满列秩,因此 rank(B) = n。相反,如果行数少于列数(即,m < n),则B必须具有满行秩,因此 rank(B) = m。这是因为如果矩阵不是方阵,那么它的行或列必定线性相关。
矩阵求逆
从定义上讲,一个n × n的方阵A 被认为是可逆的,如果存在另一个n × n的方阵B,使得以下条件成立:

矩阵可逆性的条件(作者提供的图像)。
这表明,如果矩阵A和B的矩阵乘积是单位矩阵,那么矩阵的可逆性成立。如果这确实正确,那么B由A唯一确定,我们称矩阵B是A的乘法逆矩阵,记作A⁻¹。矩阵求逆的任务是尝试找到一个满足可逆性条件的矩阵B。不过,我这里不会深入讨论矩阵求逆中使用的数值方法。
请注意,只有在矩阵具有满秩的情况下,矩阵才可以被求逆,这意味着A的列是线性独立的。任何不能被求逆的矩阵都被称为退化或奇异。
最后的备注
本文轻描淡写地介绍了线性代数中的一些基本概念。像任何话题一样,你可以深入探讨细节,因此这篇文章并不是完全详尽的,只是触及了表面。不过,本文讨论的概念在构建数学模型时至关重要,因此数据科学家需要了解。在后续文章中,我们将看到这些概念以及我早期入门文章中介绍的那些概念是如何应用于线性回归模型的。敬请期待!
相关文章
感谢阅读!
如果你喜欢这篇文章并希望保持更新,请考虑 在 Medium 上关注我。这将确保你不会错过任何新内容。
若想无限访问所有内容,请考虑注册 Medium 订阅。
你也可以在 Twitter、LinkedIn 上关注我,或者查看我的 GitHub,如果你更喜欢这个😉
统计估计与推断基础
大数法则和扎实的统计推理是数据科学中有效统计推断的基础
·
关注 发表在 Towards Data Science · 17 分钟阅读 · 2023 年 10 月 31 日
--

图片来源:Gabriel Ghnassia 在 Unsplash
大数法则和扎实的统计推理是数据科学中有效统计推断的基础。
以下内容大部分取自我的书籍,《数据科学——统计与机器学习导论》([Plaue 2023]),由 Springer Nature* 最近出版。*
引言
通过我们的日常经验,我们对人群的典型身高有一个直观的理解。在世界大部分地区,成年人通常身高在 1.60 m 到 1.80 m 之间,而身高超过两米的人则很少见。通过提供身高的频率分布,这一直观事实可以用数值证据来支持。

表 1:人类身高频率。图像由作者提供。
这些数据基于美国疾病控制与预防中心(CDC)收集的数据集,其中列出了包括身高在内的 340,000 多人的各种属性[CDC 2018]。对这个频率表的检查表明,实际上,超过一半的受访者报告他们的身高在
1.60 m 和 1.80 m。
尽管样本的规模有限,我们仍然相信我们的调查使我们能够对整体人群得出结论。例如,仅基于数据,我们可以相对有把握地得出结论,人类不可能长到三米高。
随机过程的一个重要目标是严谨而数学地证明这些结论。该领域可以分为两个子领域:
-
概率论处理概率概念的数学定义和研究。此类研究的一个中心对象是随机变量:这些变量的值并未被精确指定或知道,而是存在不确定性。换句话说,只能给出一个概率,即随机变量取值在某个范围内。
-
推断统计学基于这样的假设,即统计观察和测量(例如频率、均值等)是随机变量的值或实现。相反,该领域研究了随机变量的特征在多大程度上可以从样本数据中估计出来。特别地,在某些简化假设下,可以量化这种估计的准确性或误差。
让我们来看看一个简单的统计推断示例:通过观察一系列掷币结果来确定一枚硬币是否公平或有偏。我们可以假设掷硬币的结果由一个离散随机变量 X_1 决定,该变量取值为零(代表反面)或一(代表正面)。如果我们再次掷同一枚硬币,我们可以假设结果可以用第二个随机变量 X_2 来描述,该变量与第一个变量独立但遵循相同的分布。
如果我们没有任何证据支持硬币有偏的假设,我们可以假设硬币是公平的。换句话说,我们期望正面和反面的出现概率相同。在这种被称为零假设的假设下,如果我们重复多次实验,我们期望正面出现的频率与反面大致相同。
相反,这些数据使我们能够对潜在的真实分布得出结论。例如,如果我们观察到正反面出现的频率差异很大,比如正面出现的频率为 70%,而反面为 30%,那么——如果样本量足够大——我们会相信需要修正我们最初关于等概率的假设。换句话说,我们可能需要放弃我们认为硬币是公平的假设。
在上述例子中,数据中正面出现的频率作为“硬币出现正面”这一随机事件的概率的估计量。常识表明,我们对这些估计的信心随着样本量的增加而增加。例如,如果之前提到的不平衡只出现在十次掷硬币中(七次正面和三次反面),我们可能还不会确信我们有一个偏倚的硬币。零假设,即公平的硬币,仍然有可能成立。在日常术语中,实验结果也可能归因于“纯粹的运气”。然而,如果我们观察到在一百次掷硬币中出现了七十次正面,那将是更有力的证据支持硬币是偏倚的替代理论!
中心极限定理:从点估计到置信区间
点估计是统计学家和数据科学家工具包中最基本的工具之一。例如,从大量样本中得到的算术平均值提供了对给定变量可能取值的洞察。在机器学习中,我们从训练数据中估计模型参数,这些数据应涵盖足够数量的标记示例。
通过经验和直觉,我们相信较大的样本和更多的训练数据可以提供更准确的统计程序和更好的预测模型。推断统计提供了更稳健的基础来支持这种直觉,这通常被称为大数法则。此外,通过计算置信区间,我们对什么是“足够大的样本”有了更深刻的理解,而不是仅仅依赖点估计。置信区间为我们提供了一些范围值,在这些范围内,我们可以合理地断言我们试图估计的真实参数存在。
在接下来的部分中,我们将以自包含的方式展示计算置信区间的数学框架,其核心在于中心极限定理。
切比雪夫大数法则
正如我们期望相对频率是事件或二元变量结果概率的良好估计器一样,我们也期望算术均值是产生我们观察到的数值数据的随机变量期望值的良好估计器。
需要注意的是,这个估计值本身也是一个随机变量。如果我们掷骰子 50 次并记录平均数,然后重复实验,我们可能会得到略有不同的值。如果我们多次重复实验,我们记录的算术均值将遵循某种分布。然而,对于大样本,我们期望它们仅显示出小的离散度,并围绕真实的期望值集中。这是切比雪夫大数法则的核心信息,我们将在下面详细说明。
在此之前,我们引入一个概率理论中的重要工具— 切比雪夫不等式。假设我们有一个随机变量 X,其均值为 μ,方差为 σ²。那么,对于任何 ε > 0,下述关系成立,其中 Pr( · ) 表示“概率”:

这一结果与我们对离散度量的直观理解是一致的:方差越小,随机变量取值接近均值的可能性越大。
例如,随机变量在其期望值的六个标准差范围内的观察值的概率非常高,至少为 97%。换句话说,随机变量取值偏离均值超过六个标准差的概率非常低,小于 3%。这一结果适用于任何形状的分布,只要期望值和方差是有限值。
现在假设我们在样本中观察到数值,这些数值是随机变量 X_1, …, X_N 的实现。我们假设这些随机变量彼此独立,并且遵循相同的分布,这一属性通常被称为独立同分布,简称i.i.d.。当观察结果是由独立设立和相同准备的实验所得,或代表从总体中随机选择的样本时,这一假设是合理的。然而,需要注意的是,这一假设可能并不总是成立。
此外,我们假设每个随机变量的期望值 μ 和方差 σ² 存在且有限。由于这些变量遵循相同的分布,这些值对每个变量都是相同的。接下来,我们考虑以下产生算术平均值的随机变量:

首先,我们展示算术均值估计量 x̄是无偏估计量:其值围绕真实均值μ分布。这是直接从期望值E[ · ]的线性性质中得出的结果。

接下来,我们希望展示对于大样本,算术均值估计量的值不会过于偏离真实均值。由于X_1, …, X_N 被假定为相互独立,它们是成对不相关的。检查成对不相关的随机变量的方差可以按如下方式表示,因为所有交叉项都消失了,并不困难:

因此,算术均值估计量的方差如下:

既然我们知道了算术均值估计量的均值和方差,我们可以应用切比雪夫不等式:

这一结果表明算术均值是一致估计量:它在概率上收敛于真实均值。换句话说,对于大样本,底层分布的期望值μ与样本的算术均值之间不太可能有显著差异。
林德伯格–列维中心极限定理
切比雪夫大数法则指出,在相当一般的条件下,大样本的算术均值很可能接近于底层分布的真实均值。或许令人惊讶的是,我们可以相当具体地说明大样本的均值围绕真实期望值的分布情况。这是林德伯格–列维中心极限定理的核心信息。对于任意数字a,b且a < b:

方程右侧的被积函数是标准正态分布的概率密度函数:正态分布——其具有著名的钟形曲线——均值为零,方差为一。
通常,若一序列随机变量对某随机变量收敛于分布,则其累计分布函数点对点地收敛于该随机变量的分布。 从数学上讲,中心极限定理表明,无论X_1, … X_N 如何分布(只要它们是独立同分布的),以下随机变量序列总是收敛于标准正态分布的随机变量:

从统计学角度讲,中心极限定理暗示,如果我们重复收集一组
对于来自同一总体的足够大样本,这些样本的均值将呈正态分布。这个定理具有实际意义,因为它允许我们对统计估计的准确性做出精确的陈述。一个常见的误解是,这个定理是为什么许多经验分布在实践中可以近似为正态分布的原因。然而,事实并非如此。
尽管定理的证明需要高级分析工具,我们在这里不讨论(参见,例如,[Durrett 2019,定理 3.4.1]),但我们可以通过一个数值例子理解它的实际意义。我们来考虑以下概率密度函数,我们假设它生成了正在研究的数据:

图 1:任意概率密度函数。图片来源于作者。
为了强调定理适用于任何形状的底层分布,请注意密度函数如何与钟形曲线不相似。我们可以通过数值模拟检查从分布中重复抽取的样本大小 N 的大量均值的直方图。对于仅由单一实例组成的样本,N = 1,我们不能期望极限定理适用——我们只是重复底层分布:

图 2:从任意分布中采样得到的直方图(N = 1 的情况)。图片来源于作者。
然而,即使对于相对较小的样本大小 N = 5,算术均值的分布——即,重复采样并计算 (x_1 + … + x_5) / 5——仍然显示出正态分布的典型钟形曲线:

图 3:样本均值的分布,样本大小为 N = 5。图片来源于作者。
Grant Sanderson 在他的 YouTube 频道 3Blue1Brown 上制作了一个视频,提供了关于中心极限定理的额外直观见解,非常值得观看。
区间估计和假设检验
中心极限定理很重要,因为它允许我们指定置信区间而不仅仅是点估计来估计某些总体的均值:我们指定一个区间,我们可以相当确定地认为这个区间包含真实均值,而不是一个单一的估计值。例如,假设我们希望在样本足够大的情况下,以 95%的置信度确保我们的估计是正确的。我们可以通过设置置信区间,并将置信水平设为γ = 0.95 来实现:

我们做出以下假设,数值 z > 0 尚待确定:

中心极限定理允许我们得出以下结论:

因此,z = z(γ) 是由在标准正态分布曲线下产生面积 γ 的积分界限决定的。例如,z(0.95) = 1.96 或 z(0.99) = 2.58。
总结来说,基于足够大样本的均值的区间估计(常用的经验法则是 N > 30 或 N > 50)在置信水平 γ 下为:

为了得出上述公式,我们用经验估计值 x̄ 和 s(x) 替代了均值 μ 和标准差 σ。这对于足够大的样本是一个合理的近似,并可以通过 斯卢茨基定理 证明,该定理基本上说明了只要其中一个加数/因子收敛到常数,基本算术运算与分布极限的操作是可以交换的。
而不是置信水平 γ,显著性水平,或 **概率
error,**α = 1 − γ 可以指定。
让我们计算一个实际的例子。CDC 调查中男性受访者的 99.9% 置信区间为 [177.98 cm, 178.10 cm]。这种高统计准确性得益于超过 190,000 名男性受访者的大样本量。我们想展示如何对较小样本量进行区间估计。为此,我们反复抽取 N = 50 的随机样本体高值,并计算相应的 95% 置信区间。结果见下图:

图 5:从样本大小 N = 50 的体高区间估计。图像来源于作者。
注意,大多数置信区间(以垂直误差条表示)也包含了 178 cm 的真实值(以水平虚线表示)。然而,有些则不包含,大约百分之一 — 这是按照构造预期的,并与指定的误差概率 α = 5% 一致。总是有可能区间估计会错过总体的真实均值,特别是在低置信水平下。
中心极限定理的另一个重要应用,与区间估计密切相关,是假设检验。假设我们有理由相信随机变量 X 的期望值 不 等于某个值 μ。在这种情况下,我们想要驳斥原假设 E[X] = μ。如果观察到的均值不包含在以下区间内,我们可以说这个原假设与数据不一致:

让我们回顾一下引言中的可能有偏的硬币的例子。我们记录每次抛硬币的结果,得到一个二进制值序列,其中值为 1 表示正面,值为 0 表示反面。该序列的算术平均值等于正面的相对频率,我们可以应用迄今为止学到的知识。假设我们有理由相信这枚硬币是不公平的。零假设声称硬币是公平的,即 E[X] = 0.5。在第一次实验中,我们观察到经过十次抛掷后,硬币正面朝上的次数为七次。在置信水平 γ = 0.95 下,此实验的零假设区间为:[0.24, 0.76]。实际观察到的 0.7 比例仍在此区间内。因此,在给定的置信水平下,不能拒绝公平硬币的零假设。
样本量相对较小,实际上建议使用学生的 t-检验。t-检验会将临界标准分数 z(0.95) = 1.96 调整为 2.26,从而得到更宽的置信区间。
如果我们观察到一百次抛硬币中有七十次结果为正面,那么在假设零假设为真的情况下,得到的置信区间将是[0.41, 0.59]。在这种情况下,实际观察到的 0.7 比例不包含在置信区间内。因此,应该拒绝零假设,我们可以在给定的置信水平下得出结论——硬币是有偏的。
我们还可以基于每个样本调查两个总体的均值是否相等。双侧的双样本 Z-检验 如果满足以下条件则意味着拒绝均值相等的零假设:

从数据中得出结论:统计推断的陷阱
进行统计测试和计算置信区间并不能替代适当的统计推理:统计上显著的效果可能仍然具有很小的实际相关性,或者可能只是虚假的关系。
统计显著性与实际显著性:效应量
特别是对于非常大的样本,检测到统计上显著的均值差异或其他类型的效应是相当常见的,这些效应在统计检验中被认为是显著的。然而,这些效应的实际大小可能仍然很小。
例如:CDC 数据集允许比较不同的美国州。我们可以将罗德岛的男性受访者的平均身高与纽约的进行比较。应用Z检验,我们得到 95%置信水平下的检验分数为 0.33 cm。这个值低于观察到的 0.44 cm 的差异。因此,该差异在统计上是显著的。然而,其大小非常小,因此我们可以预期其实际相关性很小。
在许多情况下,可以通过指定效应来很好地衡量效应大小。
自然单位。在上述示例中,我们选择了长度的公制单位。另一种可能性是以标准差的倍数单位来指定。Cohen’s d 是衡量统计效应实际相关性的一个指标。它被定义为均值差异除以合并方差[Cohen 1988, p.67]:

上述示例中观察到的 0.44 cm 的差异对应于 Cohen’s d 为 0.05。当我们比较波多黎各的受访者与纽约的受访者的平均身高时,我们得到 Cohen’s d 为 0.50,对应于 4.1 cm 的度量单位差异。
解释 Cohen’s d 值的经验法则见下表[Sawiloswky 2009]:

表 2:根据 Cohen’s d 的效应大小。图片由作者提供。
统计推断与因果解释:辛普森悖论
当然,统计推断中最常被引用的陷阱之一就是“相关性不等于因果关系”的格言。这一概念通常通过明显虚假且有时滑稽的 例子来说明,例如将海盗短缺归因于全球变暖。
然而,在实际应用中,统计关联是否确实是虚假的或指示因果关系通常并不明显。一个不易察觉的虚假相关来源是未知的混杂变量。事实上,未知混杂因素的存在可能导致在检查特定子群体时关联方向的逆转,这种现象被称为辛普森悖论。
下面的例子可以说明辛普森悖论(参见[Blyth 1972]、[Bickel et al. 1975] 和 [Freedman et al. 2007,第二章,第四部分]):在一所大学的六个最大部门中,p_x = 30%的 1835 名女性申请者被录取,而p_y = 45%的 2691 名男性申请者被录取。我们可以使用Z检验来得出这一录取率差异在 99%的置信水平下是显著的结论。
这些是按大学部门划分的数字:

表 3:按部门划分的大学招生率。图片来源:作者。
对于每个部门,我们可以计算双侧检验分数,并将该分数与观察到的招生率差异的绝对值| p_y − p_x |进行比较。根据可用数据,我们还可以计算每个部门的录取率p,不论性别:

表 4:大学招生率分析。图片来源:作者。
只有部门A在招生率上显示出显著差异。与所有部门的比较相反,这一差异对女性申请者更为有利。部门A和B是申请者最有可能被录取的部门,差距很大。51%的男性申请者选择这两个部门申请,但所有女性申请者中只有 7%这样做。因此,这些数据与女性申请者更可能申请竞争性较强的课程的假设是一致的,这意味着她们更可能被拒绝。
结论
大数法则为统计估计过程提供了坚实的基础,其有效性得到了中心极限定理的严格支持。随着数据的增加,统计估计变得越来越准确,在许多情况下,我们可以计算出量化结果准确性和我们信心的指标。
然而,重要的是强调,采用“闭嘴并计算”的方法不足以进行稳健的统计推理和有效的数据科学。首先,即使随机误差被最小化,统计结果仍可能受到各种系统性误差的影响。这些误差可能源于反应偏差、测量设备故障或引入抽样偏差的缺陷研究设计。因此,对潜在偏差源的全面检查对于可靠的统计分析至关重要。
其次,在解释结果时,重要的是要认识到,仅凭统计显著性和相关性不足以评估实际意义或观察到的效应背后的潜在原因。统计结果必须放在背景中进行解读,以确定其现实世界的重要性并提供对观察到现象的解释。
参考文献
[普劳 2023] 马蒂亚斯 普劳. “数据科学 — 统计学与机器学习导论”。施普林格 柏林,海德堡。2023 年。
[CDC 2018] 美国疾病控制与预防中心 (CDC). 行为风险因素监测系统调查数据。亚特兰大,乔治亚州:美国卫生与公共服务部,疾病控制与预防中心。2018 年。
CDC 数据 属于公共领域,可以在无需许可的情况下复制*。
[杜雷特 2019] 里克 杜雷特. 概率:理论与示例。第 5 版。剑桥大学出版社,2019 年 5 月。
[科恩 1988] 雅各布 科恩. 行为科学的统计功效分析。第 2 版。新泽西州,美国:劳伦斯·厄尔鲍姆协会,1988 年。
[萨维洛斯基 2009] 什洛莫 S. 萨维洛斯基. “新的效应大小规则”。发表于《现代应用统计方法杂志》8.2(2009 年 11 月),第 597–599 页。
[布莱斯 1972] 科林 R. 布莱斯. “关于辛普森悖论和确定性原则”。发表于《美国统计协会杂志》67.338(1972 年 6 月),第 364–366 页。
[比克尔等 1975] P. J. 比克尔、E. A. 汉梅尔 和 J. W. 奥康奈尔. “研究生录取中的性别偏见:来自伯克利的数据”。发表于《科学》187.4175(1975 年 2 月),第 398–404 页。
[弗里德曼等 2007] 大卫 弗里德曼、罗伯特 皮萨尼 和 罗杰 普维斯. 统计学。第 4 版。W. W. 诺顿公司,2007 年 2 月。
一种经过验证的方法,用以记住数据科学概念,直到你需要的时候
以及在人工智能时代将方法付诸实践的工具
·发表于 Towards Data Science ·8 分钟阅读·2023 年 4 月 18 日
--

图片由我提供。通过我的好朋友 Midjourney
自学数据科学的问题
每当我想用 Anaconda 安装一个库时,命令中的-c部分总是会变动。因此,像大多数人一样,我谷歌搜索,有时一天 3-4 次:
conda install -c conda-forge library_name
听起来很熟悉?
这个小例子揭示了我们今天学习数据科学和机器学习方式的一个根本性缺陷:数据科学知识比空气便宜,因此我们没有像应有的那样认真对待学习它。
我们看到大学生为了记住大量信息以通过考试和测试而绞尽脑汁。如果他们做得不好,他们将被赶出他们花了很多钱的学校。
作为自学的数据科学家,我们没有那种压力。我们只有自律,它不断说服我们,在沙发上看 YouTube 课程时我们做得很好。
我们的学习过程是零散的。我们学到一些新东西就跳到下一个闪亮的事物,而第一个东西尚未深入我们的大脑。
我们把信息保留的事情交给了运气。
当我们真正坐下来实践我们“学到”的东西(用空气引号),我们会发现我们在打开电脑的时间里已经忘记了 80%的新知识。
所以,我们开始用谷歌搜索。当这种行为成为常态后,我们会在小推文中炫耀我们在谷歌搜索方面的卓越。我们实际上是在向他人微妙地暗示,我们没有可靠的系统来学习和记住数据科学中大量的信息。
由于我们的过失,我们变成了最差劲的学习者。
解决方案
没有有效的方法和工具来学习和保持新知识,成为数据科学家是很困难的。
需要学习的内容实在太多了:数学、统计学、机器学习理论、数十个 Python 库中的函数和方法等等。跟踪所有这些信息确实很困难。

图片来自于维基百科。维基媒体共享资源。
上面的艾宾浩斯遗忘曲线展示了新信息从记忆中流失的速度。
从图表中可以清楚地看到,完全失去新信息只需要六天。而当信息是以我们随意和粗心的方式学习的,这个时间会更短。
但一旦你认真努力将新知识投入到一个可靠的重复系统中,你就有意识地选择将其记住终生或至少在需要的时候记住。
我可能在谈论死记硬背(🤒)吗?当然不是。我在谈论间隔重复!
间隔重复是一种强大的记忆技巧,它充分利用了艾宾浩斯遗忘曲线:

图片来自于维基百科。维基媒体共享资源。
间隔重复会在逐渐增加的最佳间隔时间内重新暴露你于新信息,每个间隔正好在记忆即将流失时出现。
这将重置你的记忆,并增加下一次复习材料的间隔时间。
SR 的好处是什么?
也许,间隔重复最有益的地方在于它将知识从短期记忆转移到长期记忆的方式。
除了高效利用时间和提高记忆保持外,研究显示该系统的以下好处:
-
个性化:可以根据你的独特偏好进行自定义,因为它适应你的学习节奏和对材料的掌握程度。
-
改善理解:通过不断强化概念和联系,随着时间的推移,你可以更容易地建立知识网络,更深入地理解复杂的主题。
-
动力增加:随着我的重复间隔变得更长,间隔重复给了我很大的进步和成就感。
这可能是为什么许多医学生把他们的生命托付于这种方法,因为他们用它来记住骨骼、血管、神经分支以及人体的所有疲惫细节。
数据科学可能并不像看上去那样复杂,但我们仍然需要记住相当多的东西。
间隔重复算法
实际上有许多算法实现间隔重复,其中最受欢迎的是SuperMemo。
SuperMemo 是一系列间隔重复算法,自 1982 年开始不断推出。作者皮奥特·沃兹尼亚克博士在 2008 年被《连线》杂志评选为“将人们变成天才的技巧发明者”。
那么,你如何通过这种方法变成天才呢?
在充分学习了基础概念和事实后,你首先使用闪卡将材料分解成块(是的,我知道这是一个大问题,但请耐心等待到最后)。
创建卡片数据库后,你开始在会话中复习它们。第一次会话按照添加顺序或洗牌(根据你的偏好)显示卡片。然后,你根据记忆情况对卡片进行评分。
在 SuperMemo-2 中,有六个选项:
-
0: 我完全没有头绪。
-
1: 错误,但看到答案后,记忆犹新。
-
2: 错误,但看到答案后,我迅速想起来了。
-
3: 正确回应,但我不得不深入挖掘并努力记住。
-
4: 正确回应,但我犹豫不决。
-
5: 我记得就像刚刚发生的事情一样。
然后,选择的评分会被输入到复杂的计算中,这些计算涉及卡片之前成功回忆的次数、卡片的易记因素(别问),以及重复间隔。最终结果将决定何时再次显示卡片。
对于评分低于 4 的卡片,SuperMemo 会要求你在当前会话中重复查看卡片,直到评分超过 4。
每张正确回忆的卡片将在越来越长的间隔后显示。例如,如果你记住了将时间戳转换为日期时间的函数是 datatime.datetime.fromtimestamp,你只需在一个月内复习这张卡片 4-5 次,就能在接下来的六个月内记住它。
正如你想象的那样,这比死记硬背、固定间隔重复或最糟糕的随心情重复要好得多。
间隔重复工具。
有许多由类似 SuperMemo 算法驱动的 SR 工具。
第一个(也是最重要的)是 Anki。它是开源的,实现了 SuperMemo-2 的修改版。它显示四个记忆评分:

用 Anki 记忆俄语词汇。图片来自维基百科。维基共享资源。
由于是开源的,它有一种非常古老的外观,但它是一个跨平台的免费应用(iOS 版本除外)。软件的 GitHub 仓库 有超过 13k 的星标,这表明社区的广泛支持。
他们已经在 Anki 上工作了十多年,目前的版本具有以下功能:
-
随处可用:Windows、macOS、Linux、Android 和 iOS(iOS 版本需要付费)。
-
完全可定制:创建你自己的闪卡,将它们组织成卡组,并设置你自己的间隔重复算法参数。
-
跨设备同步:Anki 的电脑版本是主要应用,移动和网页版只是伴随应用,但会同步。
-
多媒体支持:添加图片、音频、视频、文本格式和 LaTeX,以使闪卡更具记忆性和吸引力。还支持图像遮挡来记忆视觉信息。
-
附加功能:类似于 Python 扩展,你可以为软件创建并添加自己的功能,如自定义快捷键、主题和高级统计信息。
-
预制卡组:社区不断分享包含预制卡片的卡组,涵盖了热门话题。这包括数以万计的语言学习卡片,或者几乎所有大学考试的学科,以及许多其他有趣/酷炫/奇怪的主题。
一个显而易见的痛点是我们没有强调的,就是创建社区中没有的闪卡。
我知道数据科学在间隔重复方面是一个相对年轻的领域。任何人都有大量的信息需要转换成闪卡,这听起来既繁琐又令人厌烦。但这是一个必要的恶习。
我坚信,用间隔重复法创建一个主题的闪卡并完全掌握它所需的整体时间,将远少于数小时的谷歌搜索或数十次恶性循环的遗忘与再学习。
此外,我们很幸运生活在 AI 的黄金时代(我们确实是,对吧?)。已经有像Monic.ai这样的便宜的 AI 驱动闪卡软件了。
我已经尝试过 Monic.ai,它看起来非常棒。你只需上传截图或 PDF 文件,它就会在几秒钟内自动将其中的文字转换成闪卡。它还支持间隔重复技术。
如果你决定尝试,你应该考虑下载GoFullPage Chrome 扩展以捕捉全页截图,或者了解如何将网页保存为 PDF,这样你就可以将任何在线文章、教程或 Python 框架的文档页面转换为 Monic.ai 闪卡。
结束
是时候改变我们学习数据科学的方法了。我们应该抛弃那种为了观看而观看的随意方式,或者连续参加课程寻找新的无用电子证书的做法。
我们应该停止一次学习某个东西,然后希望它能保持在那里。我们应该停止一厢情愿。
我们应该停止把记忆留给偶然。
相反,我们应该采取有意识的行动,记住每一个必要的事实、理论、概念、终端命令、Python 函数或函数参数,直到我们不再需要它们。
是的,这需要一些适应,但一旦我们适应了,就可以显著缩短从“在线学习数据科学”到“在一个高薪工作中做数据科学”的时间。
感谢阅读!
喜欢这篇文章吗?说实话,它那奇特的写作风格?想象一下,能访问到更多这样的文章,全由一位才华横溢、迷人、有趣的作者(顺便说一下就是我 😃)创作。
只需 4.99$ 的会员费用,你不仅能访问我的故事,还能获取 Medium 上最聪明、最优秀的头脑带来的知识宝库。如果你使用我的推荐链接,你将获得我超新星般的感激和对我工作的虚拟击掌。
[## 通过我的推荐链接加入 Medium — Bex T.
获得对所有我的⚡高级⚡内容以及 Medium 上的无限访问权限。通过为我购买一份…来支持我的工作。
图片由我制作。通过 Midjourney。
参考文献
[1] Anki,强大的智能闪卡,Anki 网站
[2] 维基百科,SuperMemo,wikipedia.org
[3] E-student,间隔重复:技巧指南,e-student.org
[4] 维基百科,间隔重复,wikipedia.org
从 Google Maps 空气质量 API 获取空气污染数据的 Python 工具
了解如何从全球各地获取丰富的实时空气质量数据
·
关注 发表在 Towards Data Science ·16 分钟阅读·2023 年 10 月 16 日
--
本文详细说明了如何在 Python 中使用 Google Maps 空气质量 API 获取和探索实时空气污染数据、时间序列和地图。查看完整代码 这里
1. 背景
2023 年 8 月,Google 宣布将空气质量服务添加到其映射 API 列表中。你可以在这里阅读更多关于此的信息。看来这些信息现在也可以通过 Google Maps 应用获得,不过通过 API 获得的数据要丰富得多。
根据公告,Google 正在结合来自不同分辨率的多种来源的信息——地面污染传感器、卫星数据、实时交通信息和来自数值模型的预测——以生成一个动态更新的空气质量数据集,涵盖 100 个国家,分辨率高达 500 米。这听起来像是一个非常有趣且潜在有用的数据集,适用于各种映射、医疗和规划应用!
当第一次读到这个消息时,我计划在一个“与数据对话”的应用中尝试它,利用从构建这个旅行规划器工具中学到的一些东西。也许是一个可以绘制你最喜欢城市空气污染浓度时间序列的系统,或者一个帮助人们规划本地徒步旅行以避免空气质量差的工具?
这里有三个 API 工具可以提供帮助——一个“当前条件”服务,它提供给定位置的当前空气质量指数值和污染物浓度;一个“历史条件”服务,它提供相同的信息,但以小时为间隔,覆盖最多 30 天的历史数据;以及一个“热图”服务,它提供给定区域的当前条件的图像。
以前,我使用过优秀的[googlemaps](https://github.com/googlemaps/google-maps-services-python)包来调用 Python 中的 Google Maps API,但这些新 API 尚未得到支持。令人惊讶的是,除了官方文档,我几乎找不到使用这些新工具的人的示例,也没有预先存在的 Python 包来调用它们。如果有人知道其他信息,我会很高兴地接受纠正!
因此,我自己构建了一些快速工具,在这篇文章中,我们将详细介绍它们的工作原理及使用方法。我希望这对那些希望在 Python 中尝试这些新 API 并寻找起点的人有所帮助。这个项目的所有代码可以在这里找到,我可能会随着时间的推移扩展这个仓库,增加更多功能,并使用空气质量数据构建某种映射应用。
2. 获取给定位置的当前空气质量
让我们开始吧!在本节中,我们将介绍如何使用 Google Maps 获取给定位置的空气质量数据。你首先需要一个 API 密钥,你可以通过你的 Google Cloud 账户生成。它们有一个90 天的免费试用期,之后你将为你使用的 API 服务付费。在开始进行大量调用之前,请确保启用“空气质量 API”,并了解定价政策!

Google Cloud API 库的截图,你可以从中激活空气质量 API。图片由作者生成。
我通常将 API 密钥存储在 .env 文件中,并通过类似下面的函数使用 dotenv 加载它。
from dotenv import load_dotenv
from pathlib import Path
def load_secets():
load_dotenv()
env_path = Path(".") / ".env"
load_dotenv(dotenv_path=env_path)
google_maps_key = os.getenv("GOOGLE_MAPS_API_KEY")
return {
"GOOGLE_MAPS_API_KEY": google_maps_key,
}
获取当前条件需要进行 POST 请求,详细信息请参见此处。我们将借鉴googlemaps包的设计,以一种可以概括的方法来实现这一点。首先,我们构建一个使用 requests 进行调用的客户端类。目标非常简单——我们想构建一个类似下面的 URL,并包含所有特定于用户查询的请求选项。
https://airquality.googleapis.com/v1/currentConditions:lookup?key=YOUR_API_KEY
Client 类将我们的 API 密钥作为 key 传入,然后构建查询的 request_url。它接受作为 params 字典的请求选项,然后将它们放入 JSON 请求体中,这由 self.session.post() 调用处理。
import requests
import io
class Client(object):
DEFAULT_BASE_URL = "https://airquality.googleapis.com"
def __init__(self, key):
self.session = requests.Session()
self.key = key
def request_post(self, url, params):
request_url = self.compose_url(url)
request_header = self.compose_header()
request_body = params
response = self.session.post(
request_url,
headers=request_header,
json=request_body,
)
return self.get_body(response)
def compose_url(self, path):
return self.DEFAULT_BASE_URL + path + "?" + "key=" + self.key
@staticmethod
def get_body(response):
body = response.json()
if "error" in body:
return body["error"]
return body
@staticmethod
def compose_header():
return {
"Content-Type": "application/json",
}
现在我们可以创建一个函数,帮助用户为当前条件 API 组装有效的请求选项,然后使用这个 Client 类来发起请求。这也受到 googlemaps 包设计的启发。
def current_conditions(
client,
location,
include_local_AQI=True,
include_health_suggestion=False,
include_all_pollutants=True,
include_additional_pollutant_info=False,
include_dominent_pollutant_conc=True,
language=None,
):
"""
See documentation for this API here
https://developers.google.com/maps/documentation/air-quality/reference/rest/v1/currentConditions/lookup
"""
params = {}
if isinstance(location, dict):
params["location"] = location
else:
raise ValueError(
"Location argument must be a dictionary containing latitude and longitude"
)
extra_computations = []
if include_local_AQI:
extra_computations.append("LOCAL_AQI")
if include_health_suggestion:
extra_computations.append("HEALTH_RECOMMENDATIONS")
if include_additional_pollutant_info:
extra_computations.append("POLLUTANT_ADDITIONAL_INFO")
if include_all_pollutants:
extra_computations.append("POLLUTANT_CONCENTRATION")
if include_dominent_pollutant_conc:
extra_computations.append("DOMINANT_POLLUTANT_CONCENTRATION")
if language:
params["language"] = language
params["extraComputations"] = extra_computations
return client.request_post("/v1/currentConditions:lookup", params)
这个 API 的选项相对简单。它需要一个包含你想要调查的点的经纬度的字典,并可以选择接受其他各种控制返回信息量的参数。让我们来看一下所有参数都设置为True时的效果。
# set up client
client = Client(key=GOOGLE_MAPS_API_KEY)
# a location in Los Angeles, CA
location = {"longitude":-118.3,"latitude":34.1}
# a JSON response
current_conditions_data = current_conditions(
client,
location,
include_health_suggestion=True,
include_additional_pollutant_info=True
)
返回了很多有趣的信息!不仅有来自通用和美国 AQI 指数的空气质量指数值,还有主要污染物的浓度、每种污染物的描述,以及针对当前空气质量的整体健康建议。
{'dateTime': '2023-10-12T05:00:00Z',
'regionCode': 'us',
'indexes': [{'code': 'uaqi',
'displayName': 'Universal AQI',
'aqi': 60,
'aqiDisplay': '60',
'color': {'red': 0.75686276, 'green': 0.90588236, 'blue': 0.09803922},
'category': 'Good air quality',
'dominantPollutant': 'pm10'},
{'code': 'usa_epa',
'displayName': 'AQI (US)',
'aqi': 39,
'aqiDisplay': '39',
'color': {'green': 0.89411765},
'category': 'Good air quality',
'dominantPollutant': 'pm10'}],
'pollutants': [{'code': 'co',
'displayName': 'CO',
'fullName': 'Carbon monoxide',
'concentration': {'value': 292.61, 'units': 'PARTS_PER_BILLION'},
'additionalInfo': {'sources': 'Typically originates from incomplete combustion of carbon fuels, such as that which occurs in car engines and power plants.',
'effects': 'When inhaled, carbon monoxide can prevent the blood from carrying oxygen. Exposure may cause dizziness, nausea and headaches. Exposure to extreme concentrations can lead to loss of consciousness.'}},
{'code': 'no2',
'displayName': 'NO2',
'fullName': 'Nitrogen dioxide',
'concentration': {'value': 22.3, 'units': 'PARTS_PER_BILLION'},
'additionalInfo': {'sources': 'Main sources are fuel burning processes, such as those used in industry and transportation.',
'effects': 'Exposure may cause increased bronchial reactivity in patients with asthma, lung function decline in patients with Chronic Obstructive Pulmonary Disease (COPD), and increased risk of respiratory infections, especially in young children.'}},
{'code': 'o3',
'displayName': 'O3',
'fullName': 'Ozone',
'concentration': {'value': 24.17, 'units': 'PARTS_PER_BILLION'},
'additionalInfo': {'sources': 'Ozone is created in a chemical reaction between atmospheric oxygen, nitrogen oxides, carbon monoxide and organic compounds, in the presence of sunlight.',
'effects': 'Ozone can irritate the airways and cause coughing, a burning sensation, wheezing and shortness of breath. Additionally, ozone is one of the major components of photochemical smog.'}},
{'code': 'pm10',
'displayName': 'PM10',
'fullName': 'Inhalable particulate matter (<10µm)',
'concentration': {'value': 44.48, 'units': 'MICROGRAMS_PER_CUBIC_METER'},
'additionalInfo': {'sources': 'Main sources are combustion processes (e.g. indoor heating, wildfires), mechanical processes (e.g. construction, mineral dust, agriculture) and biological particles (e.g. pollen, bacteria, mold).',
'effects': 'Inhalable particles can penetrate into the lungs. Short term exposure can cause irritation of the airways, coughing, and aggravation of heart and lung diseases, expressed as difficulty breathing, heart attacks and even premature death.'}},
{'code': 'pm25',
'displayName': 'PM2.5',
'fullName': 'Fine particulate matter (<2.5µm)',
'concentration': {'value': 11.38, 'units': 'MICROGRAMS_PER_CUBIC_METER'},
'additionalInfo': {'sources': 'Main sources are combustion processes (e.g. power plants, indoor heating, car exhausts, wildfires), mechanical processes (e.g. construction, mineral dust) and biological particles (e.g. bacteria, viruses).',
'effects': 'Fine particles can penetrate into the lungs and bloodstream. Short term exposure can cause irritation of the airways, coughing and aggravation of heart and lung diseases, expressed as difficulty breathing, heart attacks and even premature death.'}},
{'code': 'so2',
'displayName': 'SO2',
'fullName': 'Sulfur dioxide',
'concentration': {'value': 0, 'units': 'PARTS_PER_BILLION'},
'additionalInfo': {'sources': 'Main sources are burning processes of sulfur-containing fuel in industry, transportation and power plants.',
'effects': 'Exposure causes irritation of the respiratory tract, coughing and generates local inflammatory reactions. These in turn, may cause aggravation of lung diseases, even with short term exposure.'}}],
'healthRecommendations': {'generalPopulation': 'With this level of air quality, you have no limitations. Enjoy the outdoors!',
'elderly': 'If you start to feel respiratory discomfort such as coughing or breathing difficulties, consider reducing the intensity of your outdoor activities. Try to limit the time you spend near busy roads, construction sites, open fires and other sources of smoke.',
'lungDiseasePopulation': 'If you start to feel respiratory discomfort such as coughing or breathing difficulties, consider reducing the intensity of your outdoor activities. Try to limit the time you spend near busy roads, industrial emission stacks, open fires and other sources of smoke.',
'heartDiseasePopulation': 'If you start to feel respiratory discomfort such as coughing or breathing difficulties, consider reducing the intensity of your outdoor activities. Try to limit the time you spend near busy roads, construction sites, industrial emission stacks, open fires and other sources of smoke.',
'athletes': 'If you start to feel respiratory discomfort such as coughing or breathing difficulties, consider reducing the intensity of your outdoor activities. Try to limit the time you spend near busy roads, construction sites, industrial emission stacks, open fires and other sources of smoke.',
'pregnantWomen': 'To keep you and your baby healthy, consider reducing the intensity of your outdoor activities. Try to limit the time you spend near busy roads, construction sites, open fires and other sources of smoke.',
'children': 'If you start to feel respiratory discomfort such as coughing or breathing difficulties, consider reducing the intensity of your outdoor activities. Try to limit the time you spend near busy roads, construction sites, open fires and other sources of smoke.'}}
3. 获取给定位置的空气质量时间序列
能够获取给定位置的这些 AQI 和污染物值的时间序列不是很好吗?这可能会揭示有趣的模式,例如污染物之间的相关性或由交通或天气因素引起的每日波动。
我们可以通过另一个 POST 请求到historical conditions API,获取小时历史记录。这个过程与当前条件非常相似,唯一的主要区别是,由于结果可能很长,它们作为多个pages返回,需要一些额外的逻辑来处理。
修改Client的request_post方法以处理这个问题。
def request_post(self,url,params):
request_url = self.compose_url(url)
request_header = self.compose_header()
request_body = params
response = self.session.post(
request_url,
headers=request_header,
json=request_body,
)
response_body = self.get_body(response)
# put the first page in the response dictionary
page = 1
final_response = {
"page_{}".format(page) : response_body
}
# fetch all the pages if needed
while "nextPageToken" in response_body:
# call again with the next page's token
request_body.update({
"pageToken":response_body["nextPageToken"]
})
response = self.session.post(
request_url,
headers=request_header,
json=request_body,
)
response_body = self.get_body(response)
page += 1
final_response["page_{}".format(page)] = response_body
return final_response
这处理了response_body包含一个名为nextPageToken的字段的情况,该字段是已生成并准备好提取的下一页数据的 ID。如果存在该信息,我们只需使用一个名为pageToken的新参数再次调用 API,该参数指示到相关页面。我们在 while 循环中重复执行此操作,直到没有更多页面为止。因此,我们的final_response字典现在包含了由页码表示的另一层。对于调用current_conditions,将只有一页,但对于调用historical_conditions,可能会有多个页面。
处理完这些事项后,我们可以以与current_conditions非常相似的风格编写historical_conditions函数。
def historical_conditions(
client,
location,
specific_time=None,
lag_time=None,
specific_period=None,
include_local_AQI=True,
include_health_suggestion=False,
include_all_pollutants=True,
include_additional_pollutant_info=False,
include_dominant_pollutant_conc=True,
language=None,
):
"""
See documentation for this API here https://developers.google.com/maps/documentation/air-quality/reference/rest/v1/history/lookup
"""
params = {}
if isinstance(location, dict):
params["location"] = location
else:
raise ValueError(
"Location argument must be a dictionary containing latitude and longitude"
)
if isinstance(specific_period, dict) and not specific_time and not lag_time:
assert "startTime" in specific_period
assert "endTime" in specific_period
params["period"] = specific_period
elif specific_time and not lag_time and not isinstance(specific_period, dict):
# note that time must be in the "Zulu" format
# e.g. datetime.datetime.strftime(datetime.datetime.now(),"%Y-%m-%dT%H:%M:%SZ")
params["dateTime"] = specific_time
# lag periods in hours
elif lag_time and not specific_time and not isinstance(specific_period, dict):
params["hours"] = lag_time
else:
raise ValueError(
"Must provide specific_time, specific_period or lag_time arguments"
)
extra_computations = []
if include_local_AQI:
extra_computations.append("LOCAL_AQI")
if include_health_suggestion:
extra_computations.append("HEALTH_RECOMMENDATIONS")
if include_additional_pollutant_info:
extra_computations.append("POLLUTANT_ADDITIONAL_INFO")
if include_all_pollutants:
extra_computations.append("POLLUTANT_CONCENTRATION")
if include_dominant_pollutant_conc:
extra_computations.append("DOMINANT_POLLUTANT_CONCENTRATION")
if language:
params["language"] = language
params["extraComputations"] = extra_computations
# page size default set to 100 here
params["pageSize"] = 100
# page token will get filled in if needed by the request_post method
params["pageToken"] = ""
return client.request_post("/v1/history:lookup", params)
为了定义历史时期,API 可以接受一个lag_time(以小时为单位,最多 720 小时(30 天))。它也可以接受一个specific_period字典,其中定义了开始和结束时间,格式如上面的注释所述。最后,要获取单个小时的数据,可以提供由specific_time提供的时间戳。还要注意pageSize参数的使用,它控制每次调用 API 返回的时间点数量。这里的默认值是 100。
让我们试一下。
# set up client
client = Client(key=GOOGLE_MAPS_API_KEY)
# a location in Los Angeles, CA
location = {"longitude":-118.3,"latitude":34.1}
# a JSON response
history_conditions_data = historical_conditions(
client,
location,
lag_time=720
)
我们应该得到一个长而嵌套的 JSON 响应,其中包含过去 720 小时内每小时增量的 AQI 指数值和特定污染物值。有许多方法可以将其格式化为更适合可视化和分析的结构,下面的函数将把它转换为“长”格式的 pandas 数据框,这种格式与seaborn绘图非常匹配。
from itertools import chain
import pandas as pd
def historical_conditions_to_df(response_dict):
chained_pages = list(chain(*[response_dict[p]["hoursInfo"] for p in [*response_dict]]))
all_indexes = []
all_pollutants = []
for i in range(len(chained_pages)):
# need this check in case one of the timestamps is missing data, which can sometimes happen
if "indexes" in chained_pages[i]:
this_element = chained_pages[i]
# fetch the time
time = this_element["dateTime"]
# fetch all the index values and add metadata
all_indexes += [(time , x["code"],x["displayName"],"index",x["aqi"],None) for x in this_element['indexes']]
# fetch all the pollutant values and add metadata
all_pollutants += [(time , x["code"],x["fullName"],"pollutant",x["concentration"]["value"],x["concentration"]["units"]) for x in this_element['pollutants']]
all_results = all_indexes + all_pollutants
# generate "long format" dataframe
res = pd.DataFrame(all_results,columns=["time","code","name","type","value","unit"])
res["time"]=pd.to_datetime(res["time"])
return res
在historical_conditions的输出上运行这个操作将生成一个数据框,其列格式化以便进行易于分析。
df = historical_conditions_to_df(history_conditions_data)

准备好绘图的历史 AQI 数据的示例数据框。
现在我们可以在seaborn或其他可视化工具中绘制结果。
import seaborn as sns
g = sns.relplot(
x="time",
y="value",
data=df[df["code"].isin(["uaqi","usa_epa","pm25","pm10"])],
kind="line",
col="name",
col_wrap=4,
hue="type",
height=4,
facet_kws={'sharey': False, 'sharex': False}
)
g.set_xticklabels(rotation=90)

该位置在 LA 的 30 天期间的通用 AQI,美国 AQI,pm25 和 pm10 值。由作者生成的图像。
这已经非常有趣了!显然,污染物时间序列中存在几个周期性现象,并且美国 AQI 与 pm25 和 pm10 浓度密切相关,这是预期中的结果。我对 Google 提供的 Universal AQI 并不太熟悉,所以无法解释为什么它与 pm25 和 pm10 显示出反相关。较小的 UAQI 是否意味着更好的空气质量?尽管进行了搜索,我仍未找到一个好的答案。
4. 获取空气质量热图瓦片
现在是 Google Maps 空气质量 API 的最终使用案例——生成热图瓦片。关于这一点的文档较为稀疏,这很遗憾,因为这些瓦片是可视化当前空气质量的强大工具,尤其是与Folium地图结合使用时。
我们通过 GET 请求获取这些瓦片,这涉及到构建以下格式的 URL,其中瓦片的位置由 zoom、x 和 y 指定。
GET https://airquality.googleapis.com/v1/mapTypes/{mapType}/heatmapTiles/{zoom}/{x}/{y}
zoom、x 和 y 的含义是什么?我们可以通过了解 Google Maps 如何将纬度和经度坐标转换为“瓦片坐标”来回答这个问题,这在这里有详细描述。本质上,Google Maps 将图像存储在每个单元格为 256 x 256 像素的网格中,而单元格的实际尺寸是缩放级别的函数。当我们调用 API 时,需要指定要从哪个网格中绘制——这是由缩放级别决定的——以及在网格上的哪个位置绘制——这是由x 和 y 瓦片坐标决定的。返回的是一个字节数组,可以由 Python Imaging Library (PIL) 或类似的图像处理包读取。
在上述格式中形成 url 后,我们可以向 Client 类添加一些方法,以便获取相应的图像。
def request_get(self,url):
request_url = self.compose_url(url)
response = self.session.get(request_url)
# for images coming from the heatmap tiles service
return self.get_image(response)
@staticmethod
def get_image(response):
if response.status_code == 200:
image_content = response.content
# note use of Image from PIL here
# needs from PIL import Image
image = Image.open(io.BytesIO(image_content))
return image
else:
print("GET request for image returned an error")
return None
这很好,但我们真正需要的是将一组经纬度坐标转换为瓦片坐标的能力。文档解释了如何做——我们首先将坐标转换为墨卡托投影,然后使用指定的缩放级别将其转换为“像素坐标”。最后,我们将其转换为瓦片坐标。为了处理所有这些转换,我们可以使用下面的 TileHelper 类。
import math
import numpy as np
class TileHelper(object):
def __init__(self, tile_size=256):
self.tile_size = tile_size
def location_to_tile_xy(self,location,zoom_level=4):
# Based on function here
# https://developers.google.com/maps/documentation/javascript/examples/map-coordinates#maps_map_coordinates-javascript
lat = location["latitude"]
lon = location["longitude"]
world_coordinate = self._project(lat,lon)
scale = 1 << zoom_level
pixel_coord = (math.floor(world_coordinate[0]*scale), math.floor(world_coordinate[1]*scale))
tile_coord = (math.floor(world_coordinate[0]*scale/self.tile_size),math.floor(world_coordinate[1]*scale/self.tile_size))
return world_coordinate, pixel_coord, tile_coord
def tile_to_bounding_box(self,tx,ty,zoom_level):
# see https://developers.google.com/maps/documentation/javascript/coordinates
# for details
box_north = self._tiletolat(ty,zoom_level)
# tile numbers advance towards the south
box_south = self._tiletolat(ty+1,zoom_level)
box_west = self._tiletolon(tx,zoom_level)
# time numbers advance towards the east
box_east = self._tiletolon(tx+1,zoom_level)
# (latmin, latmax, lonmin, lonmax)
return (box_south, box_north, box_west, box_east)
@staticmethod
def _tiletolon(x,zoom):
return x / math.pow(2.0,zoom) * 360.0 - 180.0
@staticmethod
def _tiletolat(y,zoom):
n = math.pi - (2.0 * math.pi * y)/math.pow(2.0,zoom)
return math.atan(math.sinh(n))*(180.0/math.pi)
def _project(self,lat,lon):
siny = math.sin(lat*math.pi/180.0)
siny = min(max(siny,-0.9999), 0.9999)
return (self.tile_size*(0.5 + lon/360), self.tile_size*(0.5 - math.log((1 + siny) / (1 - siny)) / (4 * math.pi)))
@staticmethod
def find_nearest_corner(location,bounds):
corner_lat_idx = np.argmin([
np.abs(bounds[0]-location["latitude"]),
np.abs(bounds[1]-location["latitude"])
])
corner_lon_idx = np.argmin([
np.abs(bounds[2]-location["longitude"]),
np.abs(bounds[3]-location["longitude"])
])
if (corner_lat_idx == 0) and (corner_lon_idx == 0):
# closests is latmin, lonmin
direction = "southwest"
elif (corner_lat_idx == 0) and (corner_lon_idx == 1):
direction = "southeast"
elif (corner_lat_idx == 1) and (corner_lon_idx == 0):
direction = "northwest"
else:
direction = "northeast"
corner_coords = (bounds[corner_lat_idx],bounds[corner_lon_idx+2])
return corner_coords, direction
@staticmethod
def get_ajoining_tiles(tx,ty,direction):
if direction == "southwest":
return [(tx-1,ty),(tx-1,ty+1),(tx,ty+1)]
elif direction == "southeast":
return [(tx+1,ty),(tx+1,ty-1),(tx,ty-1)]
elif direction == "northwest":
return [(tx-1,ty-1),(tx-1,ty),(tx,ty-1)]
else:
return [(tx+1,ty-1),(tx+1,ty),(tx,ty-1)]
我们可以看到 location_to_tile_xy 函数接收一个位置字典和缩放级别,并返回可以找到该点的瓦片。另一个有用的函数是 tile_to_bounding_box,它会找到指定网格单元的边界坐标。如果我们要对单元格进行地理定位并将其绘制在地图上,我们需要这个函数。
让我们看看air_quality_tile函数如何工作,该函数将接收我们的client、location和一个表示我们要提取的瓦片类型的字符串。我们还需要指定一个缩放级别,这在开始时可能很难选择,需要一些试验和错误。我们稍后将讨论get_adjoining_tiles参数。
def air_quality_tile(
client,
location,
pollutant="UAQI_INDIGO_PERSIAN",
zoom=4,
get_adjoining_tiles = True
):
# see https://developers.google.com/maps/documentation/air-quality/reference/rest/v1/mapTypes.heatmapTiles/lookupHeatmapTile
assert pollutant in [
"UAQI_INDIGO_PERSIAN",
"UAQI_RED_GREEN",
"PM25_INDIGO_PERSIAN",
"GBR_DEFRA",
"DEU_UBA",
"CAN_EC",
"FRA_ATMO",
"US_AQI"
]
# contains useful methods for dealing the tile coordinates
helper = TileHelper()
# get the tile that the location is in
world_coordinate, pixel_coord, tile_coord = helper.location_to_tile_xy(location,zoom_level=zoom)
# get the bounding box of the tile
bounding_box = helper.tile_to_bounding_box(tx=tile_coord[0],ty=tile_coord[1],zoom_level=zoom)
if get_adjoining_tiles:
nearest_corner, nearest_corner_direction = helper.find_nearest_corner(location, bounding_box)
adjoining_tiles = helper.get_ajoining_tiles(tile_coord[0],tile_coord[1],nearest_corner_direction)
else:
adjoining_tiles = []
tiles = []
#get all the adjoining tiles, plus the one in question
for tile in adjoining_tiles + [tile_coord]:
bounding_box = helper.tile_to_bounding_box(tx=tile[0],ty=tile[1],zoom_level=zoom)
image_response = client.request_get(
"/v1/mapTypes/" + pollutant + "/heatmapTiles/" + str(zoom) + '/' + str(tile[0]) + '/' + str(tile[1])
)
# convert the PIL image to numpy
try:
image_response = np.array(image_response)
except:
image_response = None
tiles.append({
"bounds":bounding_box,
"image":image_response
})
return tiles
从阅读代码中,我们可以看出工作流程如下:首先,找到感兴趣位置的瓦片坐标。这指定了我们要提取的网格单元。然后,找到该网格单元的边界坐标。如果我们要提取周围的瓦片,找到边界框的最近角落,然后使用该角落计算三个相邻网格单元的瓦片坐标。然后调用 API,并将每个瓦片返回为带有相应边界框的图像。
我们可以按标准方式运行,如下所示:
client = Client(key=GOOGLE_MAPS_API_KEY)
location = {"longitude":-118.3,"latitude":34.1}
zoom = 7
tiles = air_quality_tile(
client,
location,
pollutant="UAQI_INDIGO_PERSIAN",
zoom=zoom,
get_adjoining_tiles=False)
然后用 folium 绘制一个可缩放的地图!请注意,我在这里使用的是 leafmap,因为此包可以生成与 gradio 兼容的 Folium 地图,gradio 是一个强大的工具,用于生成 Python 应用程序的简单用户界面。有关示例,请查看这篇文章。
import leafmap.foliumap as leafmap
import folium
lat = location["latitude"]
lon = location["longitude"]
map = leafmap.Map(location=[lat, lon], tiles="OpenStreetMap", zoom_start=zoom)
for tile in tiles:
latmin, latmax, lonmin, lonmax = tile["bounds"]
AQ_image = tile["image"]
folium.raster_layers.ImageOverlay(
image=AQ_image,
bounds=[[latmin, lonmin], [latmax, lonmax]],
opacity=0.7
).add_to(map)
也许令人失望的是,在此缩放级别下包含我们位置的瓦片大部分是海洋,尽管看到空气污染绘制在详细地图上仍然不错。如果你放大,可以看到道路交通信息被用来指示城市区域的空气质量信号。

在 Folium 地图上绘制空气质量热力图瓦片。图像由作者生成。
设置get_adjoining_tiles=True为我们提供了一个更漂亮的地图,因为它在该缩放级别下提取了三个最近的、不重叠的瓦片。在我们的情况下,这有助于使地图更加美观。

当我们还提取相邻瓦片时,会产生更有趣的结果。请注意,这里的颜色显示的是通用 AQI 指数。图像由作者生成。
我个人更喜欢当pollutant=US_AQI时生成的图像,但还有几种不同的选项。不幸的是,API 未返回颜色尺度,尽管可以使用图像中的像素值和对颜色含义的了解生成一个。

上面的相同瓦片根据美国 AQI 着色。此地图生成于 2023 年 10 月 12 日,根据此工具www.frontlinewildfire.com/california-wildfire-map/的描述,中央加州的亮红色区域似乎是位于科阿林加附近山丘的处方火。图像由作者生成。
结论
感谢你阅读到最后!在这里,我们探讨了如何使用 Google Maps 空气质量 API 来在 Python 中提供结果,这些结果可以用于各种有趣的应用程序。未来,我希望能继续更新关于 air_quality_mapper 工具的文章,因为它还在不断发展,但我希望这里讨论的脚本本身能对你有用。像往常一样,任何进一步发展的建议都将受到非常欢迎!
《Pythonista 的语义内核入门》
·
关注 发表在 Towards Data Science ·30 分钟阅读·2023 年 9 月 2 日
--
自从发布了ChatGPT以来,大型语言模型(LLMs)在行业和媒体中受到了极大的关注;导致了几乎在所有可以想到的背景下利用 LLMs 的前所未有的需求。
语义内核是一个开源 SDK,最初由微软开发,用于支持 Microsoft 365 Copilot 和 Bing 等产品,旨在简化将 LLM 集成到应用程序中的过程。它使用户能够利用 LLM 根据自然语言查询和命令来编排工作流程,通过将这些模型与提供附加功能的外部服务连接,使模型能够利用这些服务完成任务。
由于它是针对 Microsoft 生态系统创建的,因此目前可用的许多复杂示例都是用 C# 编写的,关注 Python SDK 的资源较少。在这篇博客文章中,我将展示如何使用 Python 入门 Semantic Kernel,介绍关键组件,并探索如何利用这些组件执行各种任务。
在这篇文章中,我们将涵盖以下内容:
-
内核
-
连接器
-
提示函数
- 创建自定义连接器
-
使用聊天服务
- 制作一个简单的聊天机器人
-
内存
-
使用文本嵌入服务
-
将内存集成到上下文中
-
-
插件
-
使用现成插件
-
创建自定义插件
-
链式调用多个插件
-
-
使用规划器编排工作流
免责声明: 由于 Semantic Kernel 和所有与 LLM 相关的内容一样,发展非常迅速。因此,接口可能会随着时间的推移稍有变化;我会尽量保持这篇文章的更新。
尽管我在 Microsoft 工作,但我并没有被要求或获得任何补偿来推广 Semantic Kernel。在 行业解决方案工程 (ISE) 中,我们以根据情况和所服务的客户选择我们认为最佳的工具为荣。在我们选择不使用 Microsoft 产品的情况下,我们会向产品团队提供详细的反馈,说明原因以及我们认为缺失或可以改进的地方;这种反馈循环通常会导致 Microsoft 产品更好地满足我们的需求。
在这里,我选择推广 Semantic Kernel,因为尽管有一些小瑕疵,我相信它显示了很大的潜力,并且我更喜欢 Semantic Kernel 在设计选择上的表现,相比于我所探索的其他一些解决方案。
撰写时使用的软件包包括:
dependencies:
- python=3.10.1.0
- pip:
- semantic-kernel==0.9.3b1
- timm==0.9.5
- transformers==4.38.2
- sentence-transformers==2.2.2
- curated-transformers==1.1.0
简而言之: 如果你只是想看看一些可以直接使用的工作代码,所有复制此文章所需的代码都可以在这里的笔记本中找到 这里。
致谢
我想感谢我的同事 Karol Zak,感谢他与我合作探索如何在我们的用例中充分利用 Semantic Kernel,并提供了一些启发本文中示例的代码!

Semantic Kernel 组件概述。图片来源:learn.microsoft.com/en-us/semantic-kernel/media/kernel-flow.png
现在,让我们从库的核心组件开始。
内核
核心: “对象或系统的核心、中心或本质。” — 维基词典
语义内核中的一个关键概念就是内核本身,它是我们用来协调基于 LLM 的工作流的主要对象。最初,内核的功能非常有限;它的所有功能主要由我们将要连接的外部组件提供。然后,内核作为一个处理引擎,通过调用适当的组件来完成给定的任务。
我们可以按照下面的示例创建一个内核:
import semantic_kernel as sk
kernel = sk.Kernel()
连接器
为了使我们的内核有用,我们需要连接一个或多个 AI 模型,这使我们能够利用内核来理解和生成自然语言;这通过连接器来完成。语义内核提供了开箱即用的连接器,使得从不同来源添加 AI 模型变得简单,例如 OpenAI、Azure OpenAI 和 Hugging Face。这些模型随后用于向内核提供服务。
在撰写本文时,支持以下服务:
-
文本生成服务:用于生成自然语言
-
聊天服务:用于创建对话体验
-
文本嵌入生成服务:用于将自然语言编码为嵌入
每种类型的服务可以同时支持来自不同来源的多个模型,这使得可以根据任务和用户的偏好在不同模型之间切换。如果没有指定特定的服务或模型,内核将默认为定义的第一个服务和模型。
我们可以使用以下属性查看所有当前注册的服务:

正如预期的那样,我们当前没有任何连接的服务!让我们来改变这一点。
在这里,我将开始访问一个 GPT3.5-turbo 模型,这是我通过 Azure OpenAI 服务 在我的 Azure 订阅 中部署的。
由于这个模型可以用于文本生成和聊天,我将同时注册这两项服务。
from semantic_kernel.connectors.ai.open_ai import (
AzureChatCompletion,
AzureTextCompletion,
)
kernel.add_service(
service=AzureTextCompletion(
service_id="azure_gpt35_text_completion",
deployment_name=OPENAI_DEPLOYMENT_NAME,
endpoint=OPENAI_ENDPOINT,
api_key=OPENAI_API_KEY
),
)
gpt35_chat_service = AzureChatCompletion(
service_id="azure_gpt35_chat_completion",
deployment_name=OPENAI_DEPLOYMENT_NAME,
endpoint=OPENAI_ENDPOINT,
api_key=OPENAI_API_KEY,
)
kernel.add_service(gpt35_chat_service)
我们现在可以看到,聊天服务已注册为文本生成和聊天生成服务。

要使用非 Azure OpenAI API,我们唯一需要更改的是使用OpenAITextCompletion和OpenAIChatCompletion连接器,而不是我们的 Azure 类。如果你没有访问 OpenAI 模型的权限,也不用担心,我们稍后会看看如何连接到开源模型;模型的选择不会影响后续步骤。
要在注册服务后检索服务,我们可以使用内核上的以下方法。

现在我们已经注册了一些服务,让我们探索一下如何与它们交互!
提示函数
通过语义内核与 LLM 交互的方式是创建一个提示函数。提示函数期望自然语言输入,并使用 LLM 来解释所要求的内容,然后采取相应的行动以返回合适的响应。例如,提示函数可用于文本生成、摘要、情感分析和问答等任务。
在语义内核中,语义功能由两个组件组成:
-
提示模板:将发送给 LLM 的自然语言查询或命令
-
执行配置:包含提示功能的设置和选项,例如应该使用的服务、期望的参数以及功能描述。
最简单的入门方法是使用内核的create_function_from_prompt方法,该方法接受提示和执行配置,以及一些标识符以帮助跟踪内核中的函数。
为了说明这一点,让我们创建一个简单的提示:
prompt = """
{{$input}} is the capital city of
"""
在这里,我们使用了{{$}}语法来表示将注入到提示中的参数。虽然我们将在整个帖子中看到更多的示例,但有关模板语法的全面指南可以在文档中找到。
接下来,我们需要创建一个执行配置。如果我们知道要使用哪种服务来执行我们的函数,可以导入相应的配置类并创建其实例,如下所示。
from semantic_kernel.connectors.ai.open_ai import OpenAITextPromptExecutionSettings
execution_config = OpenAITextPromptExecutionSettings(service_id = "azure_gpt35_text_completion",
max_tokens=100,
temperature=0,
top_p=0.0)
虽然这样做有效,但它将我们的函数与某种类型的服务耦合,限制了我们的灵活性。另一种方法是直接从我们打算使用的服务中检索相应的配置类,如下所示。

这样,我们可以在运行时选择我们希望使用的服务,并自动加载合适的配置对象。让我们使用这种方法来创建我们的执行配置。
target_service_id = "azure_gpt35_text_completion"
execution_config = kernel.get_service(target_service_id).instantiate_prompt_execution_settings(
service_id=target_service_id,
max_tokens=100,
temperature=0,
seed=42
)
现在,我们可以创建我们的函数了!
generate_capital_city_text = kernel.create_function_from_prompt(
prompt=prompt,
plugin_name="Generate_Capital_City_Completion",
function_name="generate_city_completion",
execution_settings=execution_config
)
现在,我们可以使用内核的invoke方法来调用我们的函数。由于我们连接的许多服务可能会调用外部 API,invoke是一个异步方法,基于Asyncio。这使我们能够同时执行多个外部服务调用,而无需为每个调用等待响应。
response = await kernel.invoke(generate_capital_city_text, input="Paris")
响应对象包含有关我们的函数调用的有价值信息,例如所使用的参数;如果一切按预期工作,我们可以使用对象上的str构造函数来访问结果。


在这里,我们可以看到我们的函数已经成功工作!
使用本地模型
除了使用 API 背后的模型外,我们还可以使用内核来协调对本地模型的调用。为了说明这一点,让我们注册另一个文本完成服务,并创建一个配置,使我们能够指定我们希望使用新的服务。对于我们的第二个完成服务,我们使用来自Hugging Face transformers library的模型。为此,我们使用HuggingFaceTextCompletion连接器。
在这里,由于我们将本地运行模型,我选择了OPT-350m,这是一个较旧的模型,旨在大致匹配 GPT-3 的性能,应该能够在大多数硬件上快速轻松地运行。
from semantic_kernel.connectors.ai.hugging_face import HuggingFaceTextCompletion
hf_model = HuggingFaceTextCompletion(service_id="hf_text_completion", ai_model_id="facebook/opt-350m", task="text-generation")
kernel.add_service(hf_model)
现在,让我们创建我们的配置对象。我们可以以类似的方式进行,但这次需要传递与我们 Hugging Face 服务相关的service_id。
target_service_id = "hf_text_completion"
execution_config = kernel.get_service(target_service_id).instantiate_prompt_execution_settings(
service_id=target_service_id,
max_tokens=100,
temperature=0,
seed=42
)
我们现在可以创建并执行我们的函数,就像我们之前看到的那样。
hf_complete = kernel.create_function_from_prompt(
prompt=prompt,
plugin_name="Generate_Capital_City_Completion",
function_name="generate_city_completion_opt",
execution_settings=execution_config
)
response = await kernel.invoke(hf_complete, input='Paris')

好的,生成似乎已经成功,但可以说效果不如 GPT-3.5 所提供的响应。这并不意外,因为这是一个较旧的模型!有趣的是,我们可以看到,在达到最大令牌限制之前,它开始以类似的模式回答关于柏林的问题;这种行为在处理文本完成模型时并不意外。
创建自定义连接器
现在我们已经看到如何创建语义函数并指定我们希望我们的函数使用哪个服务。然而,直到这一点为止,我们使用的所有服务都依赖于现成的连接器。在某些情况下,我们可能希望使用来自不同库的模型,而不是当前支持的模型,这时我们需要一个自定义连接器。让我们来看看如何实现这一点。
例如,我们可以使用来自curated transformers库的变换器模型。
要创建自定义连接器,我们需要继承TextCompletionClientBase,它作为我们模型的一个轻量级封装。下面提供了一个简单的示例。
from typing import Any, Dict, List, Optional, Union
import torch
from curated_transformers.generation import (AutoGenerator,
SampleGeneratorConfig)
from semantic_kernel.connectors.ai.prompt_execution_settings import \
PromptExecutionSettings
from semantic_kernel.connectors.ai.text_completion_client_base import \
TextCompletionClientBase
class CuratedTransformersPromptExecutionSettings(PromptExecutionSettings):
temperature: float = 0.0
top_p: float = 1.0
def prepare_settings_dict(self, **kwargs) -> Dict[str, Any]:
settings = {
"temperature": self.temperature,
"top_p": self.top_p,
}
settings.update(kwargs)
return settings
class CuratedTransformersCompletion(TextCompletionClientBase):
device: Any
generator: Any
def __init__(
self,
service_id: str,
model_name: str,
device: Optional[int] = -1,
) -> None:
"""
Use a curated transformer model for text completion.
Arguments:
model_name {str}
device_idx {Optional[int]} -- Device to run the model on, -1 for CPU, 0+ for GPU.
Note that this model will be downloaded from the Hugging Face model hub.
"""
device = (
"cuda:" + str(device)
if device >= 0 and torch.cuda.is_available()
else "cpu"
)
generator = AutoGenerator.from_hf_hub(
name=model_name, device=torch.device(device)
)
super().__init__(
service_id=service_id,
ai_model_id=model_name,
device=device,
generator=generator,
)
async def complete(
self, prompt: str, settings: CuratedTransformersPromptExecutionSettings
) -> Union[str, List[str]]:
generator_config = SampleGeneratorConfig(**settings.prepare_settings_dict())
try:
with torch.no_grad():
result = self.generator([prompt], generator_config)
return result[0]
except Exception as e:
raise ValueError("CuratedTransformer completion failed", e)
async def complete_stream(self, prompt: str, request_settings):
raise NotImplementedError(
"Streaming is not supported for CuratedTransformersCompletion."
)
def get_prompt_execution_settings_from_settings(
self, settings: CuratedTransformersPromptExecutionSettings
) -> CuratedTransformersPromptExecutionSettings:
return settings
现在,我们可以注册我们的连接器并创建一个语义函数,如前所示。这里,我使用了Falcon-7B 模型,这需要一个 GPU 才能在合理的时间内运行。在这里,我使用了英伟达 A100在 Azure 虚拟机上运行,因为在本地运行太慢了。
kernel.add_service(
CuratedTransformersCompletion(
service_id="custom",
model_name="tiiuae/falcon-7b",
device=-1,
)
)
complete = kernel.create_function_from_prompt(
prompt=prompt,
plugin_name="Generate_Capital_City_Completion",
function_name="generate_city_completion_curated",
prompt_execution_settings=CuratedTransformersPromptExecutionSettings(
service_id="custom", temperature=0.0, top_p=0.0
),
)
print(await kernel.invoke(complete, input="Paris"))

再次,我们可以看到生成的内容有效,但在回答了我们的问题后很快陷入了重复。
这很可能是由于我们选择的模型。通常,自回归变换器模型被训练以预测大量文本中的下一个单词;从本质上讲,使其成为强大的自动补全机器!在这里,它似乎试图‘完成’我们的问题,这导致它继续生成文本,这对我们并没有帮助。
使用聊天服务
一些 LLM 模型经过了额外的训练,使其在交互中更有用。OpenAI 的InstructGPT论文详细介绍了这一过程的一个例子。
从高层次来看,这通常涉及添加一个或多个监督微调步骤,在这些步骤中,模型不是在随机的非结构化文本上训练,而是在策划的任务示例上进行训练,例如问答和总结;这些模型通常被称为指令调优或聊天模型。
由于我们已经观察到基础 LLM 可以生成比我们需要的更多的文本,因此让我们调查聊天模型是否表现不同。要使用我们的聊天模型,我们需要更新配置以指定适当的服务并创建一个新函数;在我们的情况下,我们将使用azure_gpt35_chat_completion。

generate_capital_city_chat = kernel.create_function_from_prompt(
prompt=prompt,
plugin_name="Generate_Capital_City",
function_name="capital_city_chat_2",
prompt_execution_settings=kernel.get_service(target_service_id).instantiate_prompt_execution_settings(
service_id=target_service_id, temperature=0.0, top_p=0.0, seed=42
),
)
print(await kernel.invoke(generate_capital_city_chat, input="Paris"))

很好,我们可以看到聊天模型给出了一个更简洁的回答!
之前,由于我们使用的是文本补全模型,我们将提示格式化为模型要完成的句子。然而,经过指令调优的模型应该能够理解问题,因此我们可能能够调整提示,使其更加灵活。让我们看看如何调整我们的提示,以便与模型进行互动,就像它是一个设计来提供有关我们可能喜欢访问的地方的信息的聊天机器人。
首先,让我们调整我们的函数配置,使提示更加通用。
chatbot = kernel.create_function_from_prompt(
prompt="{{$input}}",
plugin_name="Chatbot",
function_name="chatbot",
prompt_execution_settings=kernel.get_service(target_service_id).instantiate_prompt_execution_settings(
service_id=target_service_id, temperature=0.0, top_p=0.0, seed=42
),
)
在这里,我们可以看到我们仅传递了用户输入,因此我们必须将输入表达为问题。让我们尝试一下。
async def chat(user_input):
print(await kernel.invoke(generate_capital_city_chat, input=user_input))

很好,这似乎有效。让我们尝试提出一个后续问题。

我们可以看到模型提供了一个非常通用的回答,这完全没有考虑到我们之前的问题。这是可以预期的,因为模型收到的提示是"What are some interesting things to do there?",我们没有提供“那里”指的是哪里!
让我们看看如何在接下来的部分中扩展我们的方法,制作一个简单的聊天机器人。
制作一个简单的聊天机器人
现在我们已经了解了如何使用聊天服务,让我们探索如何创建一个简单的聊天机器人。
我们的聊天机器人应该能够做三件事:
-
知道它的目的并告知我们
-
理解当前对话的上下文
-
回复我们的提问
让我们调整我们的提示以反映这一点。
chatbot_prompt = """
"You are a chatbot to provide information about different cities and countries.
For other questions not related to places, you should politely decline to answer the question, stating your purpose"
+++++
{{$history}}
User: {{$input}}
ChatBot: """
注意我们添加了变量history,它将用于向聊天机器人提供以前的上下文。虽然这是一种相当幼稚的方法,因为长时间的对话会很快导致提示达到模型的最大上下文长度,但对于我们的目的来说应该有效。
到目前为止,我们只使用了使用单个变量的提示。要使用多个变量,我们需要调整我们的配置,如下所示,通过创建一个PromptTemplateConfig;它定义了我们期望的输入。
from semantic_kernel.prompt_template.input_variable import InputVariable
execution_config = kernel.get_service(target_service_id).instantiate_prompt_execution_settings(
service_id=target_service_id,
max_tokens=500,
temperature=0,
seed=42
)
prompt_template_config = sk.PromptTemplateConfig(
template=chatbot_prompt,
name="chat",
template_format="semantic-kernel",
input_variables=[
InputVariable(name="input", description="The user input", is_required=True),
InputVariable(name="history", description="The conversation history", is_required=True),
],
execution_settings=execution_config,
)
现在,让我们使用这个更新后的配置和提示来创建我们的聊天机器人
chatbot = kernel.create_function_from_prompt(
function_name="chatbot_with_history",
plugin_name="chatPlugin",
prompt_template_config=prompt_template_config,
)
为了跟踪要包含在提示中的历史记录,我们可以使用ChatHistory对象。让我们创建一个新的实例。
from semantic_kernel.contents.chat_history import ChatHistory
chat_history = ChatHistory()
此外,为了将多个参数传递给我们的提示,我们可以使用KernelArguments,这样我们只需将一个参数传递给invoke;这个参数包含所有的参数。
我们可以通过创建一个简单的聊天功能来实现这一点,以在每次互动后更新我们的历史记录。
from pprint import pprint
async def chat(input_text, verbose=True):
# Save new message in the context variables
context = KernelArguments(user_input=input_text, history=chat_history)
if verbose:
# print the full prompt before each interaction
print("Prompt:")
print("-----")
# inject the variables into our prompt
print(await chatbot.prompt_template.render(kernel, context))
print("-----")
# Process the user message and get an answer
answer = await kernel.invoke(chatbot, context)
# Show the response
pprint(f"ChatBot: {answer}")
# Append the new interaction to the chat history
chat_history.add_user_message(input_text)
chat_history.add_assistant_message(str(answer))
让我们试试看!



在这里,我们可以看到这满足了我们的要求!
检查我们的提示,我们可以看到历史记录被渲染成一个可以包含额外元数据的格式。虽然这可能是一个有用的实现细节,但我们很可能不希望我们的提示以这种方式格式化!
当使用如语义内核这样的库时,能够准确验证传递给模型的内容非常重要,因为提示的书写和格式化方式对结果有很大影响。
大多数语言模型,例如 OpenAI API,不接受单个提示作为输入,而是更喜欢格式化为消息列表的输入;在用户和模型之间交替。我们可以检查我们的提示将如何被拆分成消息。

在这里,我们可以看到与聊天历史相关的所有格式都已被移除,消息看起来符合我们的预期。
记忆
在与我们的聊天机器人互动时,使体验感觉像有用互动的关键因素之一是聊天机器人能够保留我们之前问题的上下文。我们通过让聊天机器人访问记忆,利用ChatHistory来处理这一点。
尽管这对于我们简单的用例效果很好,但我们所有的对话历史都存储在系统的 RAM 中,并未持久化;一旦我们关闭系统,这些数据将永远消失。对于更智能的应用程序,能够构建和持久化短期和长期记忆以供模型访问是很有用的。
此外,在我们的示例中,我们将所有之前的互动内容都输入到我们的提示中。由于模型通常具有固定大小的上下文窗口——这决定了我们的提示可以有多长——如果我们开始进行长时间的对话,这将很快崩溃。避免这种情况的一种方法是将记忆存储为独立的“块”,并仅将我们认为可能相关的信息加载到提示中。
语义内核提供了一些关于如何将记忆融入应用程序的功能,所以让我们探索一下如何利用这些功能。
例如,让我们扩展我们的聊天机器人,使其能够访问存储在记忆中的一些信息。
首先,我们需要一些可能与我们的聊天机器人相关的信息。虽然我们可以手动研究和整理相关信息,但让模型为我们生成信息会更快!让我们让模型生成一些关于伦敦市的事实。我们可以按如下方式进行:
response = chatbot(
"""Please provide a comprehensive overview of things to do in London. Structure your answer in 5 paragraphs, based on:
- overview
- landmarks
- history
- culture
- food
Each paragraph should be 100 tokens, do not add titles such as `Overview:` or `Food:` to the paragraphs in your response.
Do not acknowledge the question, with a statement like "Certainly, here's a comprehensive overview of things to do in London".
Do not provide a closing comment.
"""
)

现在我们有了一些文本,为了让模型只访问它需要的部分,让我们将其划分成几个块。语义内核提供了一些功能来实现这一点,在它的text_chunker模块中。我们可以按如下方式使用它:
from semantic_kernel.text import text_chunker as tc
chunks = tc.split_plaintext_paragraph([london_info], max_tokens=100)

我们可以看到文本被分割成了 8 个块。根据文本内容,我们需要调整每个块的最大标记数。
使用文本嵌入服务
现在我们已经对数据进行了块化处理,我们需要创建每个块的表示,以便能够计算文本之间的相关性;我们可以通过将文本表示为嵌入来实现这一点。
为了生成嵌入,我们需要将文本嵌入服务添加到我们的内核中。类似于之前的情况,根据基础模型的来源,有各种连接器可以使用。
首先,让我们使用一个[text-embedding-ada-002](https://learn.microsoft.com/en-us/azure/ai-services/openai/concepts/models#embeddings-models) 模型,该模型在 Azure OpenAI 服务中部署。这个模型由 OpenAI 训练,更多关于这个模型的信息可以在他们的发布博客文章中找到。
from semantic_kernel.connectors.ai.open_ai import AzureTextEmbedding
embedding_service = AzureTextEmbedding(
service_id="azure_openai_embedding",
deployment_name=OPENAI_EMBEDDING_DEPLOYMENT_NAME,
endpoint=OPENAI_ENDPOINT,
api_key=OPENAI_API_KEY,
)
kernel.add_service(embedding_service)
现在我们可以访问生成嵌入的模型,我们需要一个地方来存储这些嵌入。Semantic Kernel 提供了一个 MemoryStore 的概念,它是各种持久性提供程序的接口。
对于生产系统,我们可能会希望使用数据库进行持久化。为了简化我们的示例,我们将使用内存存储。让我们创建一个内存中的记忆存储实例。
memory_store = sk.memory.VolatileMemoryStore()
虽然我们使用了内存中的记忆存储来简化我们的示例,但在构建更复杂的系统时,我们可能会希望使用数据库进行持久化。Semantic Kernel 提供了与流行存储解决方案的连接器,如CosmosDB、Redis、Postgres等。由于记忆存储具有通用接口,因此唯一需要更改的只是修改使用的连接器,这使得在提供程序之间切换变得容易。
现在我们已经定义了我们的记忆存储,我们需要生成嵌入。Semantic Kernel 提供了语义记忆数据结构来帮助实现这一点;它将记忆存储与可以生成嵌入的服务关联起来。在这里,我们将使用SemanticTextMemory,它将使我们能够嵌入和检索我们的文档片段。
from semantic_kernel.memory.semantic_text_memory import SemanticTextMemory
memory = SemanticTextMemory(storage=memory_store, embeddings_generator=embedding_service)
我们现在可以按照如下方式将信息保存到我们的记忆存储中。
for i, chunk in enumerate(chunks):
await memory.save_information(
collection="London", id="chunk" + str(i), text=chunk
)
在这里,我们创建了一个新的集合,用于分组相似的文档。
我们现在可以以以下方式查询此集合:
results = await memory.search(
"London", "what food should I eat in London?", limit=2
)

查看结果,我们可以看到返回了相关信息;这反映在高相关性评分上。
然而,这其实很简单,因为我们有直接相关的信息,使用了非常相似的语言。让我们尝试一个更微妙的查询。
results = await memory.search(
"London", "Where can I eat non-british food in London?", limit=2
)

在这里,我们可以看到我们得到了完全相同的结果。然而,由于我们的第二个结果明确提到了‘来自世界各地的食物’,我认为这是更好的匹配。这突显了语义搜索方法的一些潜在局限性。
使用开源模型
出于兴趣,让我们看看开源模型在这个上下文中与我们的 OpenAI 服务的比较。我们可以注册一个Hugging Face 句子变换模型来实现这一点,如下所示:
from semantic_kernel.connectors.ai.hugging_face import HuggingFaceTextEmbedding
hf_embedding_service = HuggingFaceTextEmbedding(
service_id="hf_embedding_service",
ai_model_id="sentence-transformers/all-MiniLM-L6-v2",
device=-1
)
hf_memory = SemanticTextMemory(storage=sk.memory.VolatileMemoryStore(), embeddings_generator=hf_embedding_service)
我们现在可以以与之前相同的方式进行查询。
for i, chunk in enumerate(chunks):
await kernel.memory.save_information_async(
"hf_London", id="chunk" + str(i), text=chunk
)
hf_results = await hf_memory.search(
"hf_London", "what food should I eat in London", limit=2, min_relevance_score=0
)

hf_results = await hf_memory.search(
"hf_London",
"Where can I eat non-british food in London?",
limit=2,
min_relevance_score=0,
)

我们可以看到,我们返回了相同的片段,但相关性评分不同。我们还可以观察到不同模型生成的嵌入的维度差异。

将记忆集成到上下文中
在我们之前的示例中,我们看到虽然可以基于嵌入搜索识别大致相关的信息,但对于更细微的查询,我们没有得到最相关的结果。让我们探索是否可以改进这一点。
我们可以采取的一种方法是将相关信息提供给我们的聊天机器人,然后让模型决定哪些部分最相关。让我们创建一个提示,指示模型根据提供的上下文回答问题,并注册一个提示功能。
prompt_with_context = """
Use the following pieces of context to answer the users question.
This is the only information that you should use to answer the question, do not reference information outside of this context.
If the information required to answer the question is not provided in the context, just say that "I don't know", don't try to make up an answer.
----------------
Context: {{$context}}
----------------
User question: {{$question}}
----------------
Answer:
"""
execution_config = kernel.get_service(target_service_id).instantiate_prompt_execution_settings(
service_id=target_service_id,
max_tokens=500,
temperature=0,
seed=42
)
prompt_template_config = sk.PromptTemplateConfig(
template=prompt_with_context,
name="chat",
template_format="semantic-kernel",
input_variables=[
InputVariable(name="question", description="The user input", is_required=True),
InputVariable(name="context", description="The conversation history", is_required=True),
],
execution_settings=execution_config,
)
chatbot_with_context = kernel.create_function_from_prompt(
function_name="chatbot_with_memory_context",
plugin_name="chatPluginWithContext",
prompt_template_config=prompt_template_config,
)
现在,我们可以使用这个功能来回答我们更细微的问题。首先,我们创建一个上下文对象,并将问题添加到其中。
question = "Where can I eat non-british food in London?"
接下来,我们可以手动执行嵌入搜索,并将检索到的信息添加到我们的上下文中。
results = await hf_memory.search("hf_London", question, limit=2)
我们创建一个上下文对象,并将问题添加到其中。
context = KernelArguments(question=question, context="\n".join([result.text for result in results]))
最后,我们可以执行我们的功能。
answer = await kernel.invoke(chatbot_with_context, context)

这一次,我们看到我们的答案引用了我们寻找的信息,并提供了更好的答案!
插件
在语义内核中,插件是一组可以加载到内核中以暴露给 AI 应用程序和服务的功能。插件中的功能可以由内核协调以完成任务。
文档将插件描述为语义内核的“构建块”,可以将它们链接在一起以创建复杂的工作流程;由于插件遵循 OpenAI 插件规范,因此为 OpenAI 服务、必应和 Microsoft 365 创建的插件可以与语义内核一起使用。
语义内核 提供了几种开箱即用的插件,包括:
-
ConversationSummaryPlugin:用于总结对话
-
HttpPlugin:用于调用 API
-
TextMemoryPlugin:用于在内存中存储和检索文本
-
TimePlugin:用于获取时间和任何其他时间信息
让我们先探索如何使用预定义的插件,然后再调查如何创建自定义插件。
使用开箱即用的插件
语义内核中包含的一个插件是TextMemoryPlugin,它提供了从内存中保存和检索信息的功能。让我们看看如何使用它来简化我们之前的示例,即从内存中填充提示上下文。
首先,我们必须导入我们的插件,如下所示。

在这里,我们可以看到这个插件包含两个语义功能,recall 和 save。
现在,让我们修改我们的提示:
prompt_with_context_plugin = """
Use the following pieces of context to answer the users question.
This is the only information that you should use to answer the question, do not reference information outside of this context.
If the information required to answer the question is not provided in the context, just say that "I don't know", don't try to make up an answer.
----------------
Context: {{recall $question}}
----------------
User question: {{$question}}
----------------
Answer:
"""
我们可以看到,要使用recall功能,我们可以在提示中引用它。现在,让我们创建一个配置并注册一个功能。
execution_config = kernel.get_service(target_service_id).instantiate_prompt_execution_settings(
service_id=target_service_id,
max_tokens=500,
temperature=0,
seed=42
)
prompt_template_config = sk.PromptTemplateConfig(
template=prompt_with_context_plugin,
name="chat",
template_format="semantic-kernel",
input_variables=[
InputVariable(name="question", description="The user input", is_required=True),
InputVariable(name="context", description="The conversation history", is_required=True),
],
execution_settings=execution_config,
)
chatbot_with_context_plugin = kernel.create_function_from_prompt(
function_name="chatbot_with_context_plugin",
plugin_name="chatPluginWithContextPlugin",
prompt_template_config=prompt_template_config,
)
在我们的手动示例中,我们能够控制返回结果的数量和搜索的集合。使用TextMemoryPlugin时,我们可以通过将这些添加到KernelArguments来设置它们。让我们尝试一下我们的函数。
context = KernelArguments(question="Where can I eat non-british food in London?", collection='London', relevance=0.2, limit=2)
answer = await kernel.invoke(chatbot_with_context_plugin, context)

我们可以看到这与我们的手动方法是等效的。
创建自定义插件
现在我们了解了如何创建语义函数以及如何使用插件,我们拥有了一切所需来开始制作自己的插件!
插件可以包含两种类型的函数:
-
提示函数:使用自然语言执行操作
-
本地函数:使用 Python 代码执行操作
这些可以在一个插件中结合使用。
是否使用提示函数或本地函数取决于你所执行的任务。对于涉及理解或生成语言的任务,提示函数显然是首选。然而,对于更确定性的任务,如执行数学运算、下载数据或访问时间,本地函数更为合适。
让我们探讨如何创建每种类型。首先,让我们创建一个文件夹来存储我们的插件。
from pathlib import Path
plugins_path = Path("Plugins")
plugins_path.mkdir(exist_ok=True)
创建一个诗歌生成器插件
对于我们的示例,让我们创建一个生成诗歌的插件;为此,使用提示函数似乎是自然的选择。我们可以在目录中为此插件创建一个文件夹。
poem_gen_plugin_path = plugins_path / "PoemGeneratorPlugin"
poem_gen_plugin_path.mkdir(exist_ok=True)
回顾一下,插件只是函数的集合,而我们正在创建一个语义函数,下一部分应该很熟悉。关键的不同点在于,我们将不再在线定义提示和配置,而是为这些创建单独的文件,以便更容易加载。
让我们为我们的语义函数创建一个文件夹,命名为write_poem。
poem_sc_path = poem_gen_plugin_path / "write_poem"
poem_sc_path.mkdir(exist_ok=True)
接下来,我们创建我们的提示,保存为skprompt.txt。

现在,让我们创建我们的配置并将其存储在 json 文件中。
虽然在配置中设置有意义的描述总是一个好习惯,但当我们定义插件时,这变得更加重要;插件应提供清晰的描述,说明它们的行为、输入输出以及副作用。原因在于,这是由我们的内核呈现的接口,如果我们希望使用 LLM 来协调任务,它需要能够理解插件的功能及如何调用它,以便选择适当的函数。
config_path = poem_sc_path / "config.json"
%%writefile {config_path}
{
"schema": 1,
"description": "A poem generator, that writes a short poem based on user input",
"execution_settings": {
"azure_gpt35_chat_completion": {
"max_tokens": 512,
"temperature": 0.8,
"top_p": 0.0,
"presence_penalty": 0.0,
"frequency_penalty": 0.0,
"seed": 42
}
},
"input_variables": [
{
"name": "input",
"description": "The topic that the poem should be written about",
"default": "",
"is_required": true
}
]
}
现在,我们可以导入我们的插件:
poem_gen_plugin = kernel.import_plugin_from_prompt_directory(
plugins_path, "PoemGeneratorPlugin"
)
检查我们的插件,我们可以看到它暴露了我们的write_poem语义函数。

我们可以像以前一样使用内核调用我们的函数。
result = await kernel.invoke(poem_gen_plugin["write_poem"], KernelArguments(input="Munich"))

或者,我们可以在另一个语义函数中使用它:
prompt = """
{{PoemGeneratorPlugin.write_poem $input}}
"""
target_service_id = "azure_gpt35_chat_completion"
execution_config = kernel.get_service(target_service_id).instantiate_prompt_execution_settings(
service_id=target_service_id,
max_tokens=500,
temperature=0.8,
seed=42
)
prompt_template_config = sk.PromptTemplateConfig(
template=prompt,
name="chat",
template_format="semantic-kernel",
input_variables=[
InputVariable(name="input", description="The user input", is_required=True),
],
execution_settings=execution_config,
)
write_poem_wrapper = kernel.create_function_from_prompt(
function_name="poem_gen_wrapper",
plugin_name="poemWrapper",
prompt_template_config=prompt_template_config,
)
result = await kernel.invoke(write_poem_wrapper, KernelArguments(input="Munich"))

创建一个图像分类器插件
现在我们已经看到了如何在插件中使用提示函数,让我们来看看如何使用原生函数。
现在,让我们创建一个插件,它接受一个图片网址,然后下载并分类图片。再一次,让我们为我们的新插件创建一个文件夹。
image_classifier_plugin_path = plugins_path / "ImageClassifierPlugin"
image_classifier_plugin_path.mkdir(exist_ok=True)
download_image_sc_path = image_classifier_plugin_path / "download_image.py"
download_image_sc_path.mkdir(exist_ok=True)
现在,我们可以创建我们的 Python 模块。在模块内部,我们可以非常灵活。在这里,我们创建了一个具有两个方法的类,关键步骤是使用kernel_function装饰器来指定哪些方法应该作为插件的一部分被暴露。
对于我们的输入,我们使用了Annotated类型提示来提供我们参数的描述。更多信息可以在文档中找到。
import requests
from PIL import Image
import timm
from timm.data.imagenet_info import ImageNetInfo
from typing import Annotated
from semantic_kernel.functions.kernel_function_decorator import kernel_function
class ImageClassifierPlugin:
def __init__(self):
self.model = timm.create_model("convnext_tiny.in12k_ft_in1k", pretrained=True)
self.model.eval()
data_config = timm.data.resolve_model_data_config(self.model)
self.transforms = timm.data.create_transform(**data_config, is_training=False)
self.imagenet_info = ImageNetInfo()
@kernel_function(
description="Takes a url as an input and classifies the image",
name="classify_image"
)
def classify_image(self, input: Annotated[str, "The url of the image to classify"]) -> str:
image = self.download_image(input)
pred = self.model(self.transforms(image)[None])
return self.imagenet_info.index_to_description(pred.argmax())
def download_image(self, url):
return Image.open(requests.get(url, stream=True).raw).convert("RGB")
在这个例子中,我使用了出色的Pytorch Image Models库来提供我们的分类器。有关该库如何工作的更多信息,请查看这篇博客文章。
现在,我们可以如下面所示简单地导入我们的插件。
image_classifier = ImageClassifierPlugin()
classify_plugin = kernel.import_plugin_from_object(image_classifier, plugin_name="classify_image")
检查我们的插件,我们可以看到只有我们装饰过的函数被暴露出来。

我们可以使用来自 Pixabay 的猫的图片来验证我们的插件是否有效。

url = "https://cdn.pixabay.com/photo/2016/02/10/16/37/cat-1192026_1280.jpg"
response = await kernel.invoke(classify_plugin["classify_image"], KernelArguments(input=url))

通过手动调用我们的函数,我们可以看到我们的图片已被正确分类!和之前一样,我们也可以直接从提示中引用这个函数。然而,既然我们已经演示过了,让我们在下一部分尝试一些稍微不同的东西。
链接多个插件
现在我们已经定义了各种函数,包括内联函数和插件,让我们看看如何协调一个调用多个函数的工作流程。
如果我们希望独立执行多个函数,这很简单;我们只需将函数列表传递给invoke,如下所示。
answers = await kernel.invoke([classify_plugin["classify_image"], poem_gen_plugin["write_poem"]], arguments=KernelArguments(input=url))

在这里,我们可以看到相同的输入被用于每个函数。我们本可以在KernelArguments中定义不同命名的参数,但如果多个函数的参数具有相同的名称,这会变得困难。顺便提一下,我们的诗歌生成器似乎做得很棒,考虑到它只提供了一个网址!
更有趣的情况是,当我们希望将一个函数的输出用作另一个函数的输入时,让我们来深入探讨一下。
为了提供对函数调用方式的更精细控制,内核使我们能够定义处理程序,在其中我们可以注入自定义行为:
-
add_function_invoking_handler:用于注册在函数调用之前调用的处理程序 -
add_function_invoked_handler:用于注册在函数调用后被调用的处理程序
由于我们希望将输入更新为下一个函数的输出,可以定义一个简短的函数来完成此操作,并注册它,以便在每次函数调用后调用。让我们看看如何做到这一点。
首先,我们需要定义一个函数,该函数接受内核和FunctionInvokedEventArgs实例,并更新我们的参数。
from semantic_kernel.events.function_invoked_event_args import FunctionInvokedEventArgs
def store_results(kernel, invoked_function_info: FunctionInvokedEventArgs):
previous_step_result = str(invoked_function_info.function_result)
invoked_function_info.arguments['input'] = previous_step_result
invoked_function_info.updated_arguments = True
接下来,我们可以将其注册到我们的内核中。
kernel.add_function_invoked_handler(store_results)
现在,我们可以像之前一样调用我们的函数。
answers = await kernel.invoke([classify_plugin["classify_image"], poem_gen_plugin["write_poem"]], arguments=KernelArguments(input=url))

我们可以看到,通过顺序使用两个插件,我们已经对图像进行了分类并为其写了一首诗!
使用规划器协调工作流
到此为止,我们已经深入探讨了语义函数,理解了函数如何被分组并用作插件的一部分,并且已经看到如何手动将插件链在一起。现在,让我们探索如何使用 LLMs 创建和协调工作流。为此,Semantic Kernel 提供了Planner对象,它可以动态创建函数链以尝试实现目标。
规划器是一个类,它接受用户提示和内核,并利用内核的服务创建执行任务的计划,使用内核中可用的函数和插件。由于插件是这些计划的主要构建块,因此规划器在很大程度上依赖于提供的描述;如果插件和函数没有明确的描述,规划器将无法正确使用它们。此外,由于规划器可以以各种不同的方式组合函数,因此确保仅暴露我们希望规划器使用的函数是很重要的。
由于规划器依赖于模型来生成计划,因此可能会引入错误;这些错误通常发生在规划器未能正确理解如何使用函数时。在这些情况下,我发现提供明确的指令——如描述输入和输出,并说明输入是否为必需——可以取得更好的结果。此外,使用指令调优模型比使用基础模型效果更佳;基础文本补全模型往往会虚构不存在的函数或生成多个计划。尽管存在这些限制,当一切正常工作时,规划器可以非常强大!
让我们通过探讨是否可以创建一个基于图像 url 的诗歌计划,使用我们之前创建的插件,来探索如何做到这一点。由于我们定义了许多不再需要的函数,让我们创建一个新的内核,以便控制暴露的函数。
kernel = sk.Kernel()
为了创建我们的计划,让我们使用我们的 OpenAI 聊天服务。
service_id = "azure_gpt35_chat_completion"
kernel.add_service(
service=AzureChatCompletion(
service_id=service_id,
deployment_name=OPENAI_DEPLOYMENT_NAME,
endpoint=OPENAI_ENDPOINT,
api_key=OPENAI_API_KEY
),
)
现在,让我们导入我们的插件。
classify_plugin = kernel.import_plugin_from_object(
ImageClassifierPlugin(), plugin_name="classify_image"
)
poem_gen_plugin = kernel.import_plugin_from_prompt_directory(
plugins_path, "PoemGeneratorPlugin"
)
我们可以看到我们的内核可以访问哪些函数,如下所示。

现在,让我们导入我们的规划器对象。
from semantic_kernel.planners.basic_planner import BasicPlanner
planner = BasicPlanner(service_id)
要使用我们的规划工具,我们只需要一个提示。通常,我们需要根据生成的计划进行调整。在这里,我尽可能明确所需的输入。
ask = f"""
I would like you to write poem about what is contained in this image with this url: {url}. This url should be used as input.
"""
接下来,我们可以使用我们的规划工具来制定解决任务的计划。
plan = await planner.create_plan(ask, kernel)

检查我们的计划,我们可以看到模型已经正确识别了输入,以及正确的函数使用方法!
最后,只剩下执行我们的计划了。
poem = await planner.execute_plan(plan, kernel)

哇,成功了!对于一个被训练来预测下一个词的模型来说,这真是相当强大的!
作为一个警告,在制作这个示例时,我很幸运地第一次生成的计划就有效。然而,我们依赖于模型正确解读我们的指令,并理解可用的工具;更不用说 LLM 可能会出现虚假信息,甚至可能会梦到不存在的新功能!对我个人来说,在生产系统中,我会觉得手动创建工作流程执行会更舒服,而不是依赖 LLM!随着技术的不断进步,尤其是当前的速度,希望这个建议会变得过时!
结论
希望这为您提供了对 Semantic Kernel 的良好介绍,并激励您探索将其用于自己的用例。
重复此帖子所需的所有代码可以在这里找到。*
Chris Hughes 在 LinkedIn 上
参考资料
-
microsoft/semantic-kernel: 将最前沿的 LLM 技术快速、轻松地集成到您的应用程序中 (github.com)
-
explosion/curated-transformers: 🤖 PyTorch 精选 Transformer 模型及其可组合组件 (github.com)
-
使用 PyTorch 图像模型 (timm) 入门:实用指南 | by Chris Hughes | Towards Data Science
《网格基础可视化的快速清晰概述》
如何使用一个 3 行的算法提供射线投射的不错替代方案
·
关注 发表在 Towards Data Science ·14 分钟阅读·2023 年 5 月 12 日
--
图片由 Autodesk Research [1] 提供。(已获许可)
在我之前的文章《与 Pascal’s Triangle 的简短直接漫步》中,我解释了如何改进基于网格的路径查找,以便在不使用视线测试的情况下获得高度直接的步行路径。本文将展示一种相关的技术,称为基于网格的可见性,该技术在不使用视线测试的情况下计算可见区域。基于网格的可见性在计算机科学界几乎没有听说过,但它是一种在各种人工智能应用中都很有意义的实用方法。它也非常容易实现,所需的代码仅有 3 行。继续阅读,发现解决视频游戏、移动机器人或建筑设计中的可见性问题的最简单选项。
可见区域问题
类似于路径查找,可见性分析出现在涉及人工智能和空间环境的多个领域中。视频游戏开发者可能希望计算从敌人瞭望塔可见的游戏地图区域。移动机器人工程师可能需要计算在测试机器人控制系统的模拟中,机器人视野的范围。建筑师可能希望分析建筑物内或街道上不同位置的人们的视野。可见性分析还可以用于估算光源照射的区域。
基本问题是这样的:给定一个 2D 顶视图地图,计算从一个点可见的空间区域。
如果你问计算机科学家如何解决这个问题,他们极有可能不会考虑我所称的基于网格的算法:一种通过计算邻近网格单元中的数字来得出每个网格单元中的数字的方法。可见区域问题几乎总是使用基于向量的可见性算法来解决,该算法涉及视线测试。最受欢迎的基于向量的可见性技术之一是射线投射,在这种技术中,从一个视点向不同方向投射许多射线。如果你对射线投射和其他基于向量的解决方案不熟悉,Red Blob Games网站上的2D 可见性教程提供了一个很好的背景介绍。
基于网格和基于向量的方法在路径查找和 2D 图形等其他空间应用中都很受欢迎。例如,我们都熟悉光栅(基于网格)和向量图像,并且认识到这两种类型的图像都有其优缺点。那么,为什么只有基于向量的方法在可见性问题中被广泛使用呢?我开始相信,虽然基于网格和基于向量的方法在可见性问题中都有优缺点,但基于网格的可见性被奇怪地忽视了,应该得到更多的关注。
基于网格的可见性
这里是用 3 行 Python 代码编写的基于网格的可见性。
for x in range(grid.shape[0]):
for y in range(int(x==0), grid.shape[1]):
grid[x,y] *= (x*grid[x-1,y] + y*grid[x,y-1]) / (x + y)
算法接受一个表示地图的网格,并对其进行修改以生成可见性结果。正如我们所见,转换过程包括遍历每个网格单元并应用线性插值。让我们通过将这 3 行代码放入一个简短的程序中来测试它们。请随意复制并运行下面的 Python 脚本。
import numpy as np
import matplotlib.pyplot as plt
# Set dimensions
nx = 25
ny = 25
# Create map
grid = np.ones((nx,ny))
wx = nx//10 + 1
wy = ny//10 + 1
grid[int(.3*nx):int(.3*nx)+wx,int(.1*ny):int(.1*ny)+wy] = 0
grid[int(.1*nx):int(.1*nx)+wx,int(.5*ny):int(.5*ny)+wy] = 0
grid[int(.6*nx):int(.6*nx)+wx,int(.6*ny):int(.6*ny)+wy] = 0
# Display map
plt.figure("Map")
plt.imshow(np.transpose(grid))
# Compute visibility
for x in range(grid.shape[0]):
for y in range(int(x==0), grid.shape[1]):
grid[x,y] *= (x*grid[x-1,y] + y*grid[x,y-1]) / (x + y)
# Display visibility
plt.figure("Visibility")
plt.imshow(np.transpose(grid))
plt.show()
程序首先创建并显示地图,一个 25x25 的网格,其中填充障碍物的单元格值为 0,空单元格值为 1。如下面所示,地图有三个方形障碍物。

25x25 的输入地图。(图片由作者提供)
程序随后将地图转换为可见性网格并显示出来。可见性网格填充了可见性得分,这些得分近似于从左上角的视点观察到每个网格单元的可见程度。可见性得分的范围从 0(完全被阻挡)到 1(完全可见)。这是可见性网格。

结果为 25x25 的可见性网格。(图片由作者提供)
每个障碍物都会从左上角投射阴影,尽管你会注意到阴影的边缘有些模糊。提高地图分辨率是一种锐化这些边缘的方法。如果我们将网格大小从 25 改为 225 个单元格……
nx = 225
ny = 225
…我们得到以下结果。

分辨率增加到 225x225 的可见性网格。(图片由作者提供)
在这里,我们看到更清晰、更准确的阴影。如果我们继续增加分辨率,可见性得分会变得越来越准确。实际上,结果会随着网格间距接近零而趋近于精确解。
根据应用情况,我们可能希望将每个网格单元分类为可见(1)或不可见(0)。我们可以在循环后应用 0.5 的阈值来实现这一点。
for x in range(grid.shape[0]):
for y in range(int(x==0), grid.shape[1]):
grid[x,y] *= (x*grid[x-1,y] + y*grid[x,y-1]) / (x + y)
grid[:] = (grid >= 0.5)
将第 4 行插入脚本中会给我们如下结果。

应用阈值后的 225x225 可见性网格。(图片由作者提供)
重要的是要记住,基于网格的可见性是一种近似方法。一些网格单元可能会被分类为可见,即使它们实际上应该仅在阴影中;而有些可能会被分类为被阻挡,即使它们实际上应该仅在可见区域内。但一般来说,如果网格间距相对于障碍物及其之间的间隙较小,结果应该具有良好的准确性。
在我们继续之前,我得承认我使用了一些技巧将算法缩减到 3 行:
-
在第二个
for循环中,int(x==0)这个表达式是一个巧妙的技巧,可以跳过网格单元[0, 0],因为插值公式会导致除以零的错误。 -
我依赖于 NumPy 库允许使用负索引访问数组的事实。其他编程语言可能需要更多的代码行来检查这一条件。
-
上述所有代码假设视点位于地图的角落。将视点移动到具有坐标
x0和y0的任意单元格需要在四个象限中各重复计算一次。
要将视点置于地图中心,请用以下代码替换脚本中计算可见性部分的代码。
# Set viewpoint
x0 = nx//2
y0 = ny//2
# Define visibility function
def visibility_from_corner(grid):
for x in range(grid.shape[0]):
for y in range(int(x==0), grid.shape[1]):
grid[x,y] *= (x*grid[x-1,y] + y*grid[x,y-1]) / (x + y)
# Compute visibility
visibility_from_corner(grid[x0:,y0:])
visibility_from_corner(grid[x0::-1,y0:])
visibility_from_corner(grid[x0::-1,y0::-1])
visibility_from_corner(grid[x0:,y0::-1])
这是视点在中心时的结果。

225x225 的可见性网格,视点在中心。(作者图片)
使用 Excel 的一个巧妙技巧
这是一个我忍不住想展示的小技巧:在 Excel 中实现的网格可见性。以下的屏幕录制大约 1 分钟。

Excel 中的网格可见性。(作者录制)
想自己试试吗?按照下面的步骤操作即可。只需 1 到 2 分钟。
-
打开 MS Excel 并创建一个空白工作簿。
-
选择单元格B2,点击公式栏(或按
F2),粘贴以下文本,然后按Enter:=((COLUMN(B2)-2)*A2+(ROW(B2)-2)*B1)/((COLUMN(B2)-2)+(ROW(B2)-2)) -
重新选择单元格B2,按
Ctrl-C进行复制,选择从B2到Z26的单元格范围,按Ctrl-V进行粘贴。 -
在主页选项卡中,选择条件格式,突出显示单元格规则,小于。在第一个框(“格式小于”)中输入
0.5,然后从右侧的下拉菜单中选择任意“填充”选项(例如,“浅红色填充,深红色文本”)。点击确定。 -
选择单元格B2,按
1,然后按Enter。 -
通过点击单元格A1上方和左侧的绿色三角形选择所有单元格,然后点击并拖动A和B之间的垂直线,以缩小单元格宽度,使所有单元格最终大致为正方形。
-
通过点击单元格并按
Backspace创建障碍物。自动生成的阴影应从左上角扩展开来。
观察到,障碍物需要多单元格宽才能产生合理的阴影效果。
网格可见性的简要历史
网格可见性的历史解释了为何这一方法从未广泛被认可。首先,尽管其简单,但网格可见性直到 2004 年[2]才被发明,且其收敛性质直到 2008 年[3]才得到确立。到那时,基于向量的方法如光线投射已经变得普遍。计算机科学家们不再寻找替代的方法。其次,关于网格可见性的首篇论文来自一种称为水平集理论的数学分支,该领域以计算机科学家不熟悉的隐式方式表示几何图形。虽然水平集可见性方法适用于 2D 或 3D,并使用线性插值,但严格的 3D 替代方法,采用双线性插值,由建筑和城市信息学研究人员于 2013 年[4]开发。
大约在 2019 年,我和我的同事们对网格可视性产生了兴趣,作为分析大量计算机生成建筑设计的一种手段。在我们的开放获取期刊论文“基于网格的导航路径计数” [1]中,我们做出了以下观察:
-
原始的水平集可视性方法可以轻松地适应于计算机科学家熟悉的显式几何形状。 本文开头的 Python 代码是将原始插值公式与显式网格几何相结合的实现示例。
-
网格邻域的大小可以增加以产生更准确的结果。 本文中的示例迄今为止使用了 4-邻域,其中信息流向北、南、东和/或西。我的同事们和我在论文和一个名为SpaceAnalysis的建筑设计工具中使用了 8-邻域,这允许信息沿对角线流动。
-
基于网格的可视性结果可以通过概率理论特别是中心极限定理被证明收敛于精确解。 原始的证明来自水平集社区,使用了数值分析[3]。
-
线性插值产生的可视性评分可以被重新解释为从视点出发的最短网格路径中未被障碍物阻挡的部分。
最后的观察结果揭示了基于网格的中心路径寻路,这是本文的主要内容和我之前的 Medium 文章的主题,基于与网格可视性相同的基础数学。实际上,可以通过简单地计数路径来计算可见区域。
为了演示通过计数实现可视性,我们将假设视点位于左上角。我们从那个角落开始放置一个 1,然后重复地将数字向下和向右复制。当两个数字汇聚在同一个网格单元时,我们将它们加在一起。结果是每个网格单元包含从视点出发并最终到达该单元的网格路径的数量。例如,从视点到右下角有 742 条这样的路径。

从左上角的视点计数网格路径。(动画由作者提供)
接下来,我们重复这个过程,忽略所有障碍物。每个网格单元最终都拥有从视点到该单元的最大可能网格路径数量。这实际上只是帕斯卡三角形,一个在之前的文章中详细讨论的著名数字模式。观察到,从视点到右下角有最多 2002 条网格路径。

忽略障碍物时从视点计数网格路径。(动画由作者提供)
在我们的计数路径查找方法中,我们取了两组路径计数并将它们相乘。在计数可见性中,我们将两组路径计数相除。我们首先获取到达每个网格单元的实际路径数量(见上图),然后除以到该单元的最大可能路径数量(见第二动画),最终得到每个网格单元的可见性分数。我们将每个单元的可见性分数不低于 0.5 的单元标记为可见。例如,右下角的单元格由 742 条可能的 2002 条网格路径到达。它的可见性分数是 472/2002,即约为 0.37,因此被分类为不可见。

通过路径计数获得可见性分数。(作者提供的动画)
再次,我们在论文中证明了通过计数计算的可见性分数在数学上等同于原始插值公式所产生的分数。换句话说,两种方法都是解决可见区域问题的可行方法。然而,如果我们选择实现计数可见性,我们必须记住路径计数会随着距离指数级增加。如果我们用 64 位浮点数表示这些计数,路径计数将在达到视点 1030 网格移动后溢出。因此,我认为在实现基于网格的可见性时,默认使用线性插值方法是有意义的。同时,我认为路径计数的联系很有趣,值得分享。
更大的邻域
对于基于网格的可见性,你可能会担心其准确性,尤其是因为某些基于矢量的方法被认为能提供可见区域问题的精确解。关于精确解的现实是:它们只有在输入几何体精确的情况下才是精确的,但在实际中这种情况很少见。在对真实世界环境模型进行可见性分析、路径查找分析或任何空间分析时,由于离散化误差、测量误差以及在某些情况下的构建误差,模型几乎总是一个近似值。网格基可见性引入一些额外误差,是否会成为严重的缺陷,取决于具体应用。
尽管如此,仍有一种方法可以在不增加网格分辨率的情况下提高基于网格的可见性结果的准确性。到目前为止,我们的示例仅使用了 4-邻域,这是最简单但最不准确的二维网格邻域。如前所述,我们可以选择更大的网格邻域以获得更准确的结果。下图描绘了矩形网格的 4、8 和 16 邻域,以及三角网格的 6 和 12 邻域。

矩形和三角网格邻域。(图片由Autodesk Research [1]提供,已获许可)
为了查看更大邻域的效果,我们将从文章开头重写 Python 脚本。在这个新版本的程序中,visibility_within_cone函数计算位于两个向量之间的锥体内的可见性分数。这可能不是最有效的实现,但它将帮助我们理解过渡到更大的网格邻域意味着在更多的较薄锥体内应用相同的算法。
import numpy as np
import matplotlib.pyplot as plt
# Set dimensions
nx = 25
ny = 25
# Create map
grid = np.ones((nx,ny))
wx = nx//10 + 1
wy = ny//10 + 1
grid[int(.3*nx):int(.3*nx)+wx,int(.1*ny):int(.1*ny)+wy] = 0
grid[int(.1*nx):int(.1*nx)+wx,int(.5*ny):int(.5*ny)+wy] = 0
grid[int(.6*nx):int(.6*nx)+wx,int(.6*ny):int(.6*ny)+wy] = 0
# Display map
plt.figure("Map")
plt.imshow(np.transpose(grid))
# Define visibility function
def visibility_within_cone(grid, u_direction, v_direction):
u = np.asarray(u_direction, dtype=int)
v = np.asarray(v_direction, dtype=int)
origin = np.array([0,0], dtype=int)
dims = np.asarray(grid.shape, dtype=int)
m = 0
k = 0
position = np.array([0,0], dtype=int)
while np.all(position < dims):
while np.all(position < dims):
if not np.all(position == 0):
pos = tuple(position)
pos_minus_u = tuple(np.maximum(origin, position - u))
pos_minus_v = tuple(np.maximum(origin, position - v))
grid[pos] *= (m*grid[pos_minus_u] +
k*grid[pos_minus_v]) / (m + k)
k += 1
position += v
m += 1
k = 0
position = m*u
# Compute visibility
visibility_within_cone(grid, [1,0], [0,1])
# Display visibility
plt.figure("Visibility")
plt.imshow(np.transpose(grid))
plt.show()
由于我们调用函数时使用的是向量[1,0]和[0,1],所以我们仍然使用的是 4-邻域。结果与我们第一个脚本生成的结果相同。

25x25 4-邻域的可见性网格。(图片作者提供)
但现在我们可以轻松修改代码以使用 8-邻域。为此,请将Compute visibility部分的代码替换为以下代码。可见性函数现在被应用两次,第一次是在对角线[1,1]和网格轴[1,0]之间的锥体内,第二次是在[1,1]和[0,1]之间。
# Compute visibility
visibility_within_cone(grid, [1,1], [1,0])
visibility_within_cone(grid, [1,1], [0,1])
这是 8-邻域的结果。阴影边缘变得更加清晰。

25x25 8-邻域的可见性网格。(图片作者提供)
最后,我们可以通过在 4 个锥体内应用可见性函数过渡到 16-邻域。
# Compute visibility
visibility_within_cone(grid, [2,1], [1,0])
visibility_within_cone(grid, [2,1], [1,1])
visibility_within_cone(grid, [1,2], [1,1])
visibility_within_cone(grid, [1,2], [0,1])
这是 16-邻域的结果。

25x25 16-邻域的可见性网格。(图片作者提供)
16-邻域似乎为大多数应用提供了足够的精度。然而,如果需要更高质量的结果,可以继续升级到 32-邻域、64-邻域等。
这解决了矩形网格的问题。三角网格,也称为六边形网格,实现起来更棘手,因为没有理想的方式来索引网格单元。各种索引策略在Red Blog Games教程中的六边形网格中有描述,其中包括使用视线测试的可见性部分。我把在三角网格上实现基于网格的可见性作为一个挑战留给你。
结论
基于网格的可见性是光线投射和其他基于向量的可见性方法的简单而实用的替代方案。由于其发现的时机和背景,这种方法对计算机科学家而言仍然相对陌生。但我希望你会同意,是时候让基于网格的可见性变得显而易见了,并且希望你能在自己的人工智能项目中找到使用它的机会。
更新:如果你想了解如何在 3D 中高精度地实现基于网格的可见性,请查看本系列的第三篇也是最后一篇文章: 3D 网格邻域的清晰而坚实的轮廓**。
参考文献
[1] R. Goldstein, K. Walmsley, J. Bibliowicz, A. Tessier, S. Breslav, A. Khan, 基于网格的导航路径计数 (2022), Journal of Artificial Intelligence Research, vol. 74, pp. 917–955
[2] Y.-H. R. Tsai, L.-T. Cheng, H. Osher, P. Burchard, G. Sapiro, 基于 PDE 的隐式框架中的可见性及其动态 (2004) [PDF]. Journal of Computational Physics, vol. 199, no. 1, pp. 260–290
[3] C.-Y. Kao, R. Tsai, 用于可见性问题的水平集算法的性质 (2008) [PDF], Journal of Scientific Computing, vol. 35, pp. 170–191
[4] D. Fisher-Gewirtzman, A. Shashkov, Y. Doytsher, 基于体素的城市环境可见性分析 (2013), Survey Review, vol. 45, no. 333, pp. 451–461
自然语言处理模型的归一化快速指南
原文:
towardsdatascience.com/a-quick-guide-on-normalization-for-your-nlp-model-2dbd7d2d42a7
使用归一化加速模型收敛并稳定训练过程
·发表于Towards Data Science ·阅读时间 7 分钟·2023 年 9 月 14 日
--

由Mattia Bericchia拍摄,发布于Unsplash
介绍
高效训练深度学习模型是一项具有挑战性的任务。随着近期自然语言处理模型规模和架构复杂性的增长,问题变得更加棘手。为了处理数十亿的参数,提出了更多优化方法以实现更快的收敛和稳定的训练。其中一个最显著的技术是归一化。
在本文中,我们将探讨一些归一化技术,它们的工作原理,以及如何将其用于自然语言处理深度模型。
为什么不使用 BatchNorm?
BatchNorm [2] 是一种早期的归一化技术,旨在解决内部协变量偏移问题。
简单来说,内部协变量偏移发生在层的输入数据分布发生变化时。当神经网络被迫适应不同的数据分布时,梯度更新在批次之间发生剧烈变化。因此,模型需要更长时间来调整、学习正确的权重并收敛。随着模型规模的增长,这个问题变得更严重。
初始解决方案包括使用较小的学习率(以减少数据分布变化的影响)和小心的权重初始化。BatchNorm 通过在特征维度上对输入进行归一化,有效地解决了这个问题。

批量归一化(图片来源:作者)
该技术显著加快了收敛速度,并允许更高的学习率,因为模型对异常值的敏感度降低。然而,它仍然有一些缺点:
-
小批量大小: BatchNorm 依赖于批量数据来计算特征的均值和标准差。当批量大小较小时,均值和方差无法再代表总体。因此,BatchNorm 无法进行在线学习。
-
序列输入: 在 BatchNorm 中,每个输入样本的归一化依赖于来自同一批次的其他样本。这对序列数据效果不好。例如,我们有两个训练样本,长度不同 (a1, a2,.., a10) 和 (b1,b2,…,b20)。如果 token b11 与填充 token a11 一起归一化,这是否合适?在推理步骤中,如果我们有一个长度为 30 的序列 (c1, c2,.., c30),我们如何获得均值和方差来归一化 token c21?这就是 BatchNorm 不适用于 NLP 任务的关键原因。
-
并行化: 批量归一化模型难以并行化。由于元素(均值和方差)之间存在依赖关系,我们需要在设备之间进行同步。NLP 模型,如 Transformers,由于其大规模设置而受到影响。
这就是 LayerNorm [1] 出现的原因。2016 年提出的 LayerNorm 已经取代了 BatchNorm,稳步成为研究界最流行的归一化技术。

归一化技术的使用(来源)
那么,什么是层归一化,它为什么如此有效?
为什么 LayerNorm 表现如此出色?
与 BatchNorm 相对,LayerNorm 在每个数据样本维度上进行归一化。因此,在一个包含 n 个样本的训练批次 (x1, x2,.., xn) 中,归一化是在每个 xi 上独立完成的。

层归一化(图片由作者提供)
LayerNorm 已广泛应用于许多最先进的语言模型,如 BERT [5] 和 BLOOM [6]。它为什么表现如此出色?
首先,我们需要提到 LayerNorm 相较于 BatchNorm 的几个优点:
-
序列数据: 该技术引入了训练样本之间的独立性。因此,我们可以放心地对序列输入进行归一化,而不必担心训练样本之间的不同特性。
-
灵活的批量大小: 由于归一化是在每个样本上完成的,因此批量大小不再是问题。
-
训练和测试: 与 BatchNorm 不同,LayerNorm 不需要保持总体的移动均值或方差。因此,它在训练和推理时执行相同的计算。
-
并行化: 没有训练样本之间的依赖关系,因此我们可以在不同的设备上训练模型,而无需同步。
其次,我们将讨论归一化技术在训练稳定性方面如此有帮助的一个关键原因:它们在权重和输入变换下的不变性。不变性意味着归一化技术的结果不会受到输入变换的影响。
常见的变换有重新缩放和重新中心化。重新中心化不变性使模型对权重和数据中的随机噪声不敏感。与此同时,重新缩放不变性使输出对输入和权重的任意缩放保持弹性。
通过构建一个层归一化函数并在不同的变换上进行尝试,可以更容易理解不变性特征,以查看均值、方差和结果如何变化。
def custom_ln(x: torch.Tensor, w : torch.Tensor, dim: Tuple[int], eps: float=1e-05):
sum_input = x@w # multiply the input and the weight matrix
mean = torch.mean(sum_input, dim=dim, keepdim=True) # get the mean on sample dimension
std_var = torch.sqrt(torch.var(sum_input, dim=dim, keepdim=True) + eps) # get the var on sample dimension
return (sum_input-mean)/(std_var )
我在重新缩放和重新中心化矩阵权重(w)和数据集(x)后得到了以下结果。

不同变换下的层归一化结果(作者提供的图片)
如我们所见,归一化结果在权重重新缩放和重新中心化下保持不变。对于数据集变换,该技术对重新缩放具有不变性,但对重新中心化不具备不变性。
LayerNorm 能够使输出对这种变换保持弹性,这非常令人惊讶。它是如何做到的?让我们深入探讨数学细节,以更好地理解其内部发生了什么。
这里的内容有点复杂,因此可以跳过此部分。然而,我敢肯定,这将给你一个强烈的直觉,帮助你更好地理解归一化的工作原理。
Layer Norm 不变性的数学证明
我们有一个神经网络,权重为 W,输入为 x,偏置为 b 和激活函数为 f。神经网络的输出为 y = f(Wx + b)。
然后 LayerNorm 可以表示为:

LayerNorm 方差的数学证明如下所示。
权重矩阵重新缩放不变性

权重矩阵重新缩放不变性(作者提供的图片)
权重矩阵重新中心化不变性

权重矩阵重新中心化不变性(作者提供的图片)
数据集重新缩放不变性

数据集重新缩放不变性(作者提供的图片)
这就是 LayerNorm 对变换的不变性的证明。然而,并非所有的不变性都是必要的。RMSNorm [3] 是 LayerNorm 的一个较年轻的兄弟,只具有重新缩放不变性特征。然而,它已成为最近 LLM 架构如 Llama [4] 的首选。
是什么使 RMSNorm 更优越?
什么是 RMSNorm?
RMSNorm 于 2019 年发布,其中 RMS 代表“均方根”。尽管 LayerNorm 加速了收敛,作者指出它在每个训练步骤中消耗的时间更多。

基于 GRU 的 RNNSearch 的训练过程 [3]
作者还辩称,LayerNorm 的均值归一化对最终性能的影响微乎其微,因此可以为了计算效率而被移除。
根据这个假设,提出了 RMSNorm,以关注重新缩放不变性,并使用均方根正则化,如下所示。

RMSNorm(作者提供的图片)
由于我们跳过了均值计算,归一化变得更简单。通过这种优雅的优化,作者观察到在不同实现中速度提升了 7%-64%,且性能没有下降!
其中一个实验是 WMT14 英语-德语翻译任务,使用基于 GRU 的 RNNSearch 并采用 BLEU 分数指标进行评估。LayerNorm 和 RMSNorm 在测试中都取得了相似的分数。但 RMSNorm 的训练时间快了 25%。

test14 和 test17 上的 BLEU 分数[3]
更详细地观察训练中的验证分数也表明 RMSNorm 的表现与 LayerNorm 在所有训练阶段相当。这支持了最初的观点,即重新中心化不具有显著意义,RMSNorm 更加高效。

RNNSearch 在 newstest2013 上的 SacreBLEU 分数[3]
另一个有趣的指标是不同令牌位置的隐藏向量的均值和标准差。虽然基线的均值和方差变化很大,但使用 LayerNorm 和 RMSNorm 显著稳定了分布输出。这是归一化对于 NLP 模型至关重要的有力证明。

特定位置的均值(M)和标准差(S)统计[3]
我希望这篇文章能帮助你理解每种归一化技术在 NLP 任务中的优缺点。
下篇文章见!
参考文献
[1] Jimmy Lei Ba, Jamie Ryan Kiros 和 Geoffrey E Hinton。“层归一化。” arXiv 预印本 arXiv:1607.06450 (2016)。
[2] Sergey Ioffe 和 Christian Szegedy。“批量归一化:通过减少内部协方差偏移加速深度网络训练。” ICML, 2015。
[3] Zhang, Biao 和 Rico Sennrich。“均方根层归一化。” 神经信息处理系统进展 32 (2019)。
[4] Touvron, Hugo 等。“Llama: 开放且高效的基础语言模型。” arXiv 预印本 arXiv:2302.13971 (2023)。
[5] Devlin, Jacob 等。“Bert: 深度双向变换器的预训练用于语言理解。” arXiv 预印本 arXiv:1810.04805 (2018)。
[6] Scao, Teven Le 等。“Bloom: 一个 176b 参数的开放访问多语言模型。” arXiv 预印本 arXiv:2211.05100 (2022)。
Python 中美丽散点图的快速指南
原文:
towardsdatascience.com/a-quick-guide-to-beautiful-scatter-plots-in-python-75625ae67396
可视化全球生命预期与人均 GDP
·发布于 Towards Data Science ·阅读时间 9 分钟·2023 年 1 月 12 日
--

作者通过 Python Matplotlib 提供的图片
所以你已经知道了一些 Python 和 matplotlib。也许你和我一样,喜欢复杂、美丽且富有洞察力的图表。然而,当你遇到一些基础示例并试图自己复现时,如 这个文档页面 中所见,你可能会看到这样的情况:
这将生成如下图表:

虽然这张图非常色彩丰富,但相当简单,不太具备洞察力,代码也解释了其目的。在本文中,我将展示如何创建像文章开头看到的那样美丽而富有洞察力的散点图。
这个教程的代码笔记本 可以在这里找到,我们将使用的数据集可以在 这个链接 中找到。请注意,在本教程中,我已将数据挂载到驱动器中,因此你可以 做同样的事情,或者将数据下载到本地并运行 Jupyter Notebook。为了方便,你也可以从 我的 GitHub 下载数据。
导入
由于我将在这个例子中使用 Google Colab,你将看到特定的 Colab 驱动导入。但是,如果你是在本地进行操作,可以跳过这些。由于我们还将进行一些基本的数据清理和线性回归,我还导入了一些 Scikit-learn 类。
数据集:预期寿命与人均 GDP
对于这个例子,我们将使用预期寿命与人均 GDP数据集,该数据集可在ourworldindata.org/找到。一旦你获取了数据并能够读取文件,让我们查看分析它的步骤:
-
加载数据
-
检查数据并重命名列以便于使用
-
提取具有 GDP 值的行/数据点
-
可视化人口分布
-
构建绘图函数
-
美观地绘制数据!
加载数据
首先,让我们加载并检查我们的数据。我们使用 Pandas 加载数据(无论你将数据放置在哪里),并重命名一些列以便于使用。
结果数据应如下所示:

作者绘制的图表——使用 Python Pandas 在 Colab 上生成
清理数据
请注意,许多数据点在GDP列和Population列中都有Nan值。由于我们无法对缺失值进行分析,因此我们将从数据中删除这些点:
在清理了 GDP 数据并选择了 2018 年(你可以自己验证是否有进一步年份的 GDP 数据!)后,我们的数据将如下所示:

作者绘制的图表——使用 Python Pandas 在 Colab 上生成
请注意,我们还排除了World数据点,因为它包含所有国家的总和(可以自由包括它,看看最终图表有何变化!)。
可视化人口分布
请注意,人口值以百万为单位。在绘图中,为了给不同的人口数阈值添加不同的颜色,我们希望查看其分布情况:

作者提供的图片,使用 Python Matplotlib 生成
哎呀!这告诉我们,大多数国家的人口在 0.0 到 0.2¹e9 = 200,000,000(2 亿)之间,而一些国家的人口超过了约 1200 万。由于左侧密度较大(更多国家的人口较少),我们可以忽略高人口国家,并制作一个集中在左侧国家的直方图:

作者提供的图片,使用 Python Matplotlib 生成
好得多!我们将用它来创建大致的值,以颜色编码不同人口密度的国家。
构建绘图函数
对于这一部分,我们将执行几个子步骤,如下所示。
函数定义
我们将按如下方式定义我们的函数:
注意三个参数:df,这是我们经过初步预处理后的数据,apply_color用于对人口密度进行颜色编码,以及regression用于在图表上应用回归。
填补缺失数据
我们的数据仍然包含一些重要列的 NaN 值,即["Expectancy", "GDP", "Population"]。虽然我们可以删除包含 NaN 的行,但这里更好的方法是用“有根据的猜测”来填补它们。常见的选项是列的均值或中位数,但在这里我们将使用一种称为K 最近邻(KNN)的机器学习算法。如果你不熟悉它,你可以在这里阅读相关内容。就我们的目的而言,它实际上是通过使用基于可用属性(比如预期寿命和 GDP)的最相似的 N 个国家的条目来预测人口值,并使用这些值的平均值来填补缺失值。
数据汇总
虽然这里的数据仅为 2018 年的数据,但鉴于更好的数据集,你可以将此分析扩展到更多年份。这就是为什么我们还汇总数据以确保所有年份都被考虑在内。此操作是使用 Pandas 的groupby(),选择“Country”列作为参数。此外,我们还会提前对数据进行四舍五入,以减少小数位数。
提取绘图变量
为了方便起见,我们将再次把列重新分配到各个变量。请注意,你可以直接查询数据框。然而,一个重要的步骤是,我们将把Population列按一百万的比例缩放并乘以 2。这将控制绘制散点图时点的大小。
执行回归
我们图表的一个可选部分是绘制一条非常漂亮的线,这条线跟随所有不同点的均值,这些点代表了每个国家的 GDP 与其相应的预期寿命。这将使我们能够快速评估各个国家相对于总体趋势的状态。为此,你可以使用 Scikit-learn 的ElasticSearchCV,这本质上是线性回归的混合体,它结合了 Lasso 和 Ridge 回归,为了更稳健的拟合而添加了正则化。你可以在维基百科文章中阅读相关内容,也可以查看相应的Scikit-learn 文档。此外,CV 代表“交叉验证”,这是一种在机器学习中减少模型方差的常见技术。你可以在这里了解更多。代码如下(注意regression参数):
初始化算法后,我们将数据格式化为正确的格式(这里,X 参数是预测特征,y 是目标),然后拟合回归模型。接着,我们生成预测结果,最后将其整理成正确的格式。请注意,我们还会修剪预测值低于 90 的数据点,以避免图形超出边界。
开始绘图,上色并添加点密度
我们现在准备开始绘图。第一步是为每个国家的人口密度点上色。我们根据人口数量分配不同的颜色,仅在apply_color=True参数设置为真时才应用此操作。还记得我们之前绘制的直方图吗?如果我们查看箱子的边界值,我们可以为人口设置一些合理的范围,并赋予其适当的颜色(当然,这些颜色对我来说有意义,但你可以选择任何你喜欢的颜色!)。
接下来,我们使用plt.figure()开始绘图,并调用plt.scatter()方法,具体取决于我们是否要上色:
这里:
-
s参数将接收一个实数值向量,并相应地修改点的大小。因此,我们分配population变量,它已经包含了每个国家值的适当缩放,与其人口数量成比例。人口越多,点就越大。 -
c参数将为每个点应用相应的颜色,这些颜色是我们之前分配给数据框的。 -
alpha参数将改变点的透明度。
此外,我们还可以用plt.annotate()方法对高人口密度的点进行标注。为了获得相应的国家名称,我们对子集记录进行检索,获取它们的坐标,然后传递给方法。
注意 y 坐标中的+0.3;这是为了将文本稍微移动远离点,以避免重叠。
回归线
接下来,我们使用reg.plot()添加回归线,利用之前创建的reg_data。这条线代表了某个国家在给定人均 GDP 的情况下的平均预期寿命,我们可以用它作为基线来比较其他国家的相对状况。
ls='--'参数指定线条样式。还要注意,我们使用的是 GDP 数据(reg_data["X"])和相应的预测值作为绘图参数。
完成绘图
让我们完成绘图代码:
在第一部分,我们为 x 轴(人均 GDP)和 y 轴(预期寿命)添加标签,并为绘图提供一个适当的标题。此外,我们以对数刻度显示 x 值,因为它提供了更美观、更具洞察力的图形(试着去掉它看看会发生什么!)。
接下来,我们创建不同颜色的标签。由于 Matplotlib 一次性创建所有标签,为了创建带有适当颜色和名称的标签,我们创建了四条对应的“幽灵”线,这些线在最终图表中不会做任何事情,但会提供一个颜色编码的标签。
最后,我们创建图例并显示我们的图表。
总结一下
我们已经走了很长一段路。这就是整个函数的样子:
让我们来测试一下吧!

图片由作者使用 Python 生成
相当不错!一眼望去,我们可以看到所有世界各国相对于其人均 GDP 的预期寿命,以及它们相应的预期寿命测量值。此外,我们还可以看到一些人口相对较多的国家名称。
我们还可以设置apply_color=True来区分不同的人口密度,并生成以下内容:

绘图由作者使用 Python 生成
看起来多么美丽啊!多亏了颜色编码,我们现在可以清晰地看到不同的人口密度。
最后,我们来添加回归线:

绘图由作者使用 Python 在 Google Colab 上生成
就这样!我们现在已经成功生成了你在文章开始时看到的图表。仅凭图表,我们可以一眼看出哪些国家在 2018 年相对于其 GDP 的预期寿命高于平均水平,通过查看哪些国家位于回归线的上方和之外。请注意,实际的回归线是一条线(显而易见!),而不是一条曲线,但因为我们在对数尺度下工作,它看起来更像是曲线,因为数值被改变了。
最后的话
美丽、信息丰富的图表是一门艺术,虽然一些库无疑会促进你的学习;但提高基础知识并学到一些新技巧永远不是一个坏主意。
我希望这篇文章对你来说很有趣,如果是这样的话,确保查看一下我其他的一些受欢迎的文章和系列!
数据科学基础
## 数据科学三部曲:NumPy、Pandas 和 Matplotlib 基础
所以你是 Python 新手。或者你可能已经熟悉这些库,但想要快速了解一下……
## 数据科学三部曲:NumPy、Pandas 和 Matplotlib 基础
GIT 基础
好吧,你来这里是因为你想学习 Git/Github。因此,不如直接写一段关于它的10 行文字……
Python 中的函数式编程
曾经听说过函数式编程,却没有理解或者不知道从哪里开始?或者你可能会…
时间序列分析入门(使用 R)
在 Covid19 疫情期间,你也许听说过关于预测新情况的合作努力…
参考资料
用于此分析的数据及其原始分析由我们的世界数据完成,可以在 ourworldindata.org/grapher/life-expectancy-vs-gdp-per-capita 找到,其中引用了以下来源:
Madison 项目数据库(2020);联合国 WPP(2022);Zijdeman 等(2015)
从他们的页面:
“许可证:我们的数据中的所有可视化、数据和文章都在 知识共享 BY 许可协议下开放访问。您可以在任何媒介中使用、分发和复制这些内容,只要注明来源和作者即可。”
见 ourworldindata.org/about#legal
关注我
严格设计机器学习实验的快速指南
如何可靠地比较机器学习模型
·
关注 发表在向数据科学 ·7 分钟阅读·2023 年 2 月 8 日
--
我们经常听到或阅读到现代机器学习(ML)研究缺乏科学严谨性。在从事几年的机器学习研究并为会议和期刊审阅论文之后,我现在认为我对如何严格评估机器学习方法的有效性有了很好的理解。这篇文章旨在为机器学习研究人员和开发人员设计适当的实验和分析以评估他们的工作提供一个简单的指南。希望在阅读完这篇文章后,你将不再为如何可靠地评估你的机器学习模型的性能而感到困惑。对于初级机器学习研究人员,这篇文章也将帮助你了解什么样的实验可以说服审稿人接受你的工作,帮助你实现进入梦想会议的目标。
机器学习研究的不同风味。
首先,必须强调的是,对广泛的机器学习领域做出贡献有不同的方法,这些方法不需要相同类型的实验分析。一方面,一些应用工作可能会使用机器学习来解决特定领域的任务。另一方面,一些研究人员对开发可以用于解决任何数据相关问题的通用机器学习技术感兴趣。显然,这种机器学习研究主题的分类不是二元的,而是一个连续体。让我们讨论一些具体的例子来说明这种特定性-通用性谱:
-
我们希望构建一个神经网络,能够准确分类东北巴西珊瑚礁中的鱼类图像——这是我自己研究的产品推广。
-
我们希望找到一个适用于鱼类图像分类的良好神经网络架构。
-
我们希望为图像分类设计一个好的神经网络架构。
-
我们希望引入一个新的激活函数,该函数在任何神经网络中使用都很合适,无论输入数据格式如何。
这些工作中的每一个都可能是有意义和有影响力的,但我们可以很容易地理解它们具有不同的通用性水平,如下图所示。

机器学习贡献的特定性-通用性谱。
根据研究主题的通用性水平,期望进行不同类型的实验和分析,这有时可能会令人困惑。因此,让我们尝试澄清根据讨论的不同方法,适当的机器学习评估应该是什么样的。
针对特定领域的机器学习方法的评估。
让我们从例子 1 开始,这是一个非常具体的图像分类问题。在这里,目标是构建一个好的机器学习模型(可能是神经网络),能够正确分类东北巴西珊瑚礁中的鱼类图像。该模型将被生态学家用于研究这个特定的生态系统,以加快他们的研究进程。如果我们想解决这个问题,我们可以收集感兴趣生态系统中的鱼类图像,并用物种信息标注它们以构建数据集(或者如果有现成的数据集,也可以使用现有的数据集)。
首先,我们需要注意的是,所使用的数据集必须代表应用领域,即推断过程中会遇到的内容。在我们的示例中,这可以通过确保数据集中包含研究区域的多个位置,并且所有可能的本地物种都得到代表来实现。然后,如果我们想在这个数据集上比较两个机器学习模型(X 和 Y),我们需要
-
选择我们喜欢的评估指标——通常是分类的准确率,但如果我们想了解对稀有物种的性能,也可以是精确率或召回率。有关分类指标的更多信息,请参见这里。
-
进行适当的交叉验证方案,或者如果数据集较大且训练较慢,至少进行训练-测试拆分评估。有关交叉验证测试的更多信息,请参见这里。我们只需确保数据集在拆分前已被正确随机化,并且训练和测试拆分中不包含非常相似的图像,例如连续的视频帧,这样会影响评估结果。
在这种情况下,进行相对简单的评估并声称最佳模型是具有最佳测试值的模型是完全可以接受和严谨的。然而,我们要小心,不要仓促做出没有根据的结论。我们仅仅展示了 X 在特定任务(对巴西东北部珊瑚礁中的鱼进行分类)上的表现优于 Y,仅此而已。不能从中得出 X 在图像分类或甚至通用鱼类分类方面优于 Y 的结论。
通用机器学习技术的评估
在另一个极端,让我们讨论如何评估高度通用的机器学习方法,例如示例 4。在这里,我们的目标是展示使用我们新的激活函数对于任何神经网络设计都是有效的,无论应用领域如何。在这种情况下,评估所需的工作量是不同的。我们应该使用一个包含各种数据类型的数据池,来自不同领域(例如,图像、文本、音频等)和任务(例如,分类、回归)。然后,我们可以为这些数据集计算适当的测试指标。许多机器学习论文实际上在这里就停止了,并报告了大量表格,将他们的方法与文献中其他技术在各种数据集上的结果进行比较。这是不够的,通常是这些“机器学习缺乏严谨性”抱怨的原因。确实,读者不应该期望查看所有这些独立结果并得出关于哪种方法最好的结论。
相反,如果我们看到方法 X 经常比 Y 更好,我们必须通过适当的统计测试来证明这一点,以验证优越的结果不是偶然发生的。通常,比较两个机器学习模型在多个数据集上的表现,一个好的选择是进行Wilcoxon 符号秩检验,但我不会在这里详细讲解(如果需要我在后续帖子中讲解,请告诉我)。
与特定领域的机器学习方法不同,针对每个单独数据集的相关性可以减少关注。然而,如果我们想展示一种方法在特定问题(例如,图像分类)上的有效性,那么我们的评估数据集池必须具有代表性。
我的方法是特定的还是通用的?
到目前为止,我们已经讨论了我们特定性-通用性的光谱的极端情况。然而,有时决定一个研究主题是特定的还是通用的并不容易。例如,我们是否应该认为示例 2 是特定于鱼类分类领域的,并构建一个包含来自世界各地鱼类图像的大型数据集?还是应该考虑它是一个通用的鱼类分类器,并验证它是否在来自不同生态系统的多个鱼类图像数据集上有效?同样,对于示例 3,我们应该认为我们的新架构是特定于图像的还是通用的图像分类器?
对这些问题的答案是著名的:“这要看情况”,但我仍会尝试提供一些要素以回应。这里的想法是问自己:所提方法的预期用途是什么?
-
如果我们认为我们的模型可以直接用来解决现实世界中的任务而无需重新训练,那么它应该被视为特定领域的方法。例如,一个可以在任何生态系统中使用的鱼类分类器,而无需调整。
-
如果我们认为我们的方法更多是关于如何在特定子领域数据集上训练高效模型的方法,那么它可以被认为是通用的方法。
特定结果与通用结果之间的联系
我们都同意,基于特定领域数据集获得的结果不应被用来宣称一种方法在更广泛、通用的背景下的优越性。然而,如果这些结果用于元分析研究,它们仍然可以在未来得出通用结论时发挥作用。
不那么明显的是,相反的情况也适用:好的通用结果不应被用来证明在特定环境中部署某种方法的合理性。如果一种方法被证明在图像分类中效果良好,我们可能会被诱使在没有进一步验证的情况下将其用于我们特定的图像分类问题。然而,我们应该非常小心,因为即使一种方法在各种任务中平均表现良好,也并不意味着它在每个特定任务中都表现优异。例如,在下图中,X 在总体上被证明比 Y 更好(p 值 < 0.05),但如果我们关注的是解决与数据集 2 相关的任务,我们应该使用 Y。通用结果应该作为开发指南,用于了解哪些方法可能有效以及首先尝试哪些方法。

比较两个机器学习模型在 10 个数据集上的玩具示例。
实际限制
一旦我们确定了评估新方法所需的所有实验,实际进行这些实验有时会很困难(时间和预算限制、数据集不可用)。虽然不必事事做到完美,但至少在讨论结果时,你应该知道自己没有做哪些实验。这样,你可以相应地调整你的声明和结论。一般来说,进行机器学习评估没有绝对正确或错误的方式,我们只需避免提出没有实验支持的声明。
结论
设计严格的机器学习实验可能看起来令人望而却步,但实际上并不难。只需:
-
精确确定他试图评估的机器学习方法的类型:特定的还是通用的。
-
对于领域特定的机器学习模型:收集一个代表性数据集,选择相关的评估指标,并应用适当的交叉验证方案。
-
对于通用机器学习技术:收集一组代表性数据集,为每个数据集计算评估指标,应用适当的统计测试以确认直观结论。
如果你觉得这篇文章有用,请考虑在 Medium 上关注我,以便接收我关于机器学习研究的未来文章通知,或在 Twitter 上关注我,了解更多关于我的研究工作。
本文中使用的所有图像均由作者创建。
快速入门 PostgreSQL 连接和数据导入 Pandas
通过将 PostgreSQL 数据导入 Pandas,快速开始数据分析和模型构建
·发布在 Towards Data Science ·4 分钟阅读·2023 年 2 月 5 日
--

Sergi Ferrete 提供的照片,发布在 Unsplash
什么是 PostgreSQL?
PostgreSQL 是一个强大的 关系数据库管理系统(RDBMS),许多组织都在使用它。连接到它很简单,得益于优秀的 Python 生态系统,将数据导入 Pandas 的数据框也同样简单。让我们看一个简单的例子,帮助你入门。
本地运行 PostgreSQL
本地运行 PostgreSQL 的一种简单方法是使用 Docker。如果你尚未安装 Docker,可以 在这里 下载。安装 Docker 后,你可以运行以下命令来启动一个 PostgreSQL 容器。
前往 Docker Hub 搜索 postgres。你会看到几个不同的镜像。我们需要的是 postgres 镜像;你可以使用以下命令将其拉取下来。
docker pull postgres
然后,你可以运行以下命令(按照 Docker Hub 上的文档)来启动一个容器。
docker run --name postgres -p 5432:5432 -e POSTGRES_PASSWORD =postgrespw -d postgres
你可以使用任何支持的 SQL 工具连接到数据库,例如 SQL Workbench。

作者提供的图片
CREATE DATABASE MAIN;
CREATE TABLE CONTACTS (
id integer PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
FIRST_NAME varchar(255),
LAST_NAME varchar(255)
);
INSERT INTO CONTACTS (FIRST_NAME, LAST_NAME)
VALUES ('Brian', 'Roepke');
SELECT * FROM CONTACTS;
运行上述命令将创建一个名为 MAIN 的数据库和一个名为 CONTACTS 的表。它还会向表中插入一行数据,然后从表中选择所有行;这应该能让你开始一个简单的本地 PG 设置。
导入
我们首先需要导入所需的库。我们将需要psycopg2库来连接 PostgreSQL,还需要pandas库来处理数据。此外,我们还将使用pandas、os、urllib和sqlalchemy来帮助我们连接到数据库。
import pandas as pd
import psycopg2
import os
from sqlalchemy import create_engine
from urllib.parse import quote_plus
接下来,我们要确保不将敏感信息硬编码到代码中,而是从环境变量中提取。如果您想了解更多关于此过程的信息,请查看这篇文章。
接下来,我们需要确保连接字符串中的任何信息都正确 URL 编码,例如用户名和密码,这可以通过urllib中的quote_plus函数轻松实现。让我们快速看一下示例密码及其编码方式。
fake_pw = "p@ssw0rd'9'!"
print(quote_plus(fake_pw))
p%40ssw0rd%279%27%21
接下来,我们可以创建包含连接到数据库所需所有信息的连接字符串。我们将使用字符串格式(在字符串前加上f)来插入变量。我们还将使用quote_plus函数来对用户名和密码进行编码。
uri = f"postgresql+psycopg2://{quote_plus(user)}:{quote_plus(pw)}@{host}:{port}/{db}"
alchemyEngine = create_engine(uri)
除了连接字符串,我们还将创建一个SQLAlchemy 引擎。
就这样!假设你没有收到错误消息,现在你已经连接到你的 PostgreSQL 数据库。让我们看看如何将数据提取到 Pandas 数据框中。
首先,我们将以字符串的形式定义一个查询。这里的最佳实践是使用三重引号("""),这样可以在多行中编写查询,同时避免查询本身包含引号时的冲突,例如WHERE FIRST_NAME = 'Brian'。
q = """SELECT * FROM CONTACTS"""
接下来,我们将创建之前创建的alchemyEngine的连接。
dbConnection = alchemyEngine.connect();
然后我们将使用pandas中的read_sql函数将数据提取到数据框中。
df = pd.read_sql(q, dbConnection);
现在我们有了数据框,可以显示前几行并查看数据。
df.head()
一个最佳实践是完成后关闭连接。
dbConnection.close();
结论
PostgreSQL 是一个强大的关系数据库管理系统(RDBMS),被许多公司使用。我们介绍了如何连接到它并将数据导入 Pandas 数据框。我们还讨论了一些最佳实践,如将凭据存储在环境变量中,以及如何轻松地 URL 编码用户名和密码,这些密码通常包含不受支持的字符。
如果你喜欢阅读这样的故事并希望支持我作为一名作者,可以考虑注册成为 Medium 会员。每月 5 美元,您可以无限访问数千篇文章。如果您通过 我的链接注册,我将获得一小笔佣金,而您无需支付额外费用。
《快速入门指南:如何根除模型偏见》

图片来源于作者
·
关注 发布于 Towards Data Science ·12 分钟阅读·2023 年 1 月 19 日
--
本文由 Arize AI 的机器学习工程师 Amber Roberts 共同撰写
在今天的世界中,读到关于 AI 表现出歧视行为的新闻已是司空见惯。从反映持续存在的住房歧视遗产的房地产估值模型到在医疗保健中加剧获取护理和健康结果不平等的模型,不幸的是,例子很容易找到。随着机器学习(ML)模型变得更加复杂,这一问题的真实范围及其对边缘化群体的影响可能尚未完全了解。幸运的是,ML 团队可以采取一些简单步骤,以根除 ML 生命周期中的有害模型偏差。
什么是模型偏差?
模型偏差是指机器学习模型在其预测中做出一致的、系统性的错误。模型往往倾向于系统地学习错误的信号,因为没有考虑数据中包含的所有信息。模型偏差可能导致算法错过数据输入(特征)和目标输出(预测)之间的相关关系。本质上,偏差发生在算法从数据集中学习适当信号的能力不足时。几十年来,机器学习中的偏差已被认为是一个潜在问题,但在将模型投入生产时,它仍然是机器学习研究人员和工程师面临的复杂而具挑战性的问题。
偏差如何进入模型?
偏差可能通过多种来源引入到机器学习过程中,并通过模型预测得到强化。在模型开发的各个阶段,数据不足、不一致的数据收集和不良的数据实践都可能导致模型决策中的偏差。虽然这些偏差通常是无意的,但它们的存在可能对机器学习系统产生重大影响,并导致灾难性结果——从就业歧视到医疗保健中的误诊。如果你使用的机器学习管道包含固有的偏差,模型不仅会学习这些偏差,还可能加剧甚至放大它们。
识别、评估和解决可能影响结果的任何潜在偏差,是创建新的机器学习模型并在生产中维护它时的关键要求。作为机器学习从业者,我们有责任检查、监控、评估、调查和评估这些系统,以避免对模型决策有效性产生负面影响的偏差。
模型中的偏差原因涉及数据和模型本身。
表示偏差(数据)
-
应用: 由历史数据引入的偏差、历史偏差、样本失衡、受污染的例子
-
示例: 在新闻文章上训练的嵌入展现了社会中的性别刻板印象。
测量偏差(数据)
-
应用: 代理变量、样本量差异、特征有限
-
示例: 预测再犯可能性的代理测量导致黑人的刑罚比白人在同样罪行上的刑罚更严厉。
聚合偏差(模型)
-
应用: 为所有人群使用的单一聚合模型
-
示例: 如果你有一个单一的预测模型来预测特定疾病在少数群体中的发生概率,它可能会失败并表现出偏见。
评估偏差(模型)
-
应用: 用于评估的基准不代表一般人群。
-
示例: 如果你为加利福尼亚州的房价预测模型进行基准测试,然后尝试将其应用于南卡罗来纳州的房市,价格将会有偏差。
公平性前提
定义保护属性
当然,在定义谁是被保护的对象之前,你不能量化有害的模型偏差。
了解法律下的保护类别是一个好的第一步。大多数美国人可能知道 1964 年美国民权法案禁止基于种族和性别的歧视,但更少人知道其他属性——如遗传信息或国籍——也符合法律下的保护类别,并且当发生违规时可能会导致数百万或数十亿美元的罚款。
法律合规只是一个起点。许多大型企业也超越这些法律要求,拥有额外的保护类别或公开承诺多样性和公平性。

图片来源:作者
定义公平性
一旦你清楚了所有相关司法管辖区的保护类别,下一步就是定义公平性是什么样的。虽然这是一个复杂的话题,但一些基本原则可以帮助你。

图片来源:作者
一个主要的区别是群体(相等和成比例)公平性与个人公平性之间的差异。
-
群体公平性 的定义是保护属性获得相似的待遇或结果。
-
个人公平性 的定义是相似的个体获得相似的待遇或结果。
例如,假设一家银行正在评估 100 份抵押贷款申请。70 份申请来自男性,30 份来自女性。基于等比例的群体公平性,你会批准男性 50%的申请(35 份)和女性 50%的申请(15 份)。另一方面,基于等数量的群体公平性,50 份批准会平均分配——男性 25 份,女性 25 份。如果某一组中贷款资格较高,这两种结果都可能被认为是不公平的。行业中一种主流的方法是确保每个群体的预测准确性相同——这是公平机会与准确性的衡量标准。

作者提供的图像
数据建模阶段哪些易受偏见影响?
模型公平性 影响 数据建模管道的预处理、处理中和后处理阶段。公平干预,即采取措施确保模型不对某些群体产生偏见,应在该过程的每个阶段实施。
预处理
-
这是什么? 数据处理的最早阶段,此阶段将数据转换为机器学习模型的输入。
-
为什么在此阶段对模型公平性进行干预? 在机器学习生命周期的这一最早阶段进行干预,可以对数据建模过程及其后续指标产生重大影响。
-
如何在此阶段实现公平干预? 移除、遮蔽、模糊、重命名或替换敏感属性。
-
示例: 一家银行有意建立一个预测贷款违约的模型,可能会对数据进行抽样,以确保其包含来自不同种族、性别和地理位置的申请人的代表性混合,然后移除数据中的敏感变量,以防这些变量被用于模型中。
处理中
-
这是什么? 处理过程指的是任何改变机器学习模型训练过程的行动。
-
为什么在此阶段对模型公平性进行干预? 如果由于计算限制或对专有或许可数据的限制无法在最早阶段进行干预,那么在训练过程中进行干预是下一最佳选择。在此阶段干预可以让团队保持其训练数据集的原始状态,不被修改。
-
如何在此阶段实现公平干预? 通过向模型的损失函数中添加额外的项来进行模型正则化,以确保没有一个特征不公平地主导模型的决策。你还可以使用对抗模型来减少模型中的不公平或虚假信息。
-
示例: 一家医疗提供商训练一个预测患者结果的模型,可能会创建一个对抗模型,使用目标模型的输出预测患者的受保护类别。这是为了确保患者的个人信息(如收入、种族和性别)不是其医疗结果的预测因素。
后处理
-
这是什么? 发生在模型在处理数据上训练之后。
-
为什么在这个阶段介入模型公平性? 当团队从前一个团队继承一个模型却不了解该模型时,这可能是他们公平性干预的最早阶段。
-
如何在这个阶段实现公平性干预? 在决策被用户收到之前使决策公平;第三方审计工具和使用机器学习可观测性工具的偏见追踪可以提供帮助。
-
示例: 一家提供宽带服务的公司有一个预测客户流失的模型,希望确保在向客户提供折扣时不产生歧视。该公司在模型部署过程中实施了公平性检查,使用公平性特定指标。然后,偏见追踪监控模型在多样化客户群体上的表现,以确保它不对任何特定群体存在偏见,从而保持输出公平。如果存在算法偏见,决策会被平等化。
什么是主流模型公平性指标?
根据你的目标,有许多模型公平性指标是适用的。这里是主流指标的定义和建议,以及每种指标的使用场景。
在决定使用哪个公平性指标时,你必须考虑需要什么见解,以确保你的模型没有表现出歧视。关心公平性的团队——特别是那些在高度监管行业如健康、贷款、保险和金融服务领域工作的团队——通常希望看到他们的模型在敏感属性(如种族或性别)上是否公平和无偏。当模型存在偏见时,团队需要知道哪个群体经历了最多的偏见,以便采取措施。
为了理解你评估的时间段内的公平性指标值,许多公司使用四分之一规则。四分之一规则是用于的一个阈值,由像美国平等就业机会委员会这样的监管机构帮助识别对受保护类别的负面待遇。由于理想的公平性分数是 1,当利用四分之一规则时,你通常会测量你的公平性指标分数是否落在 0.8 到 1.25 的范围之外。如果分数低于 0.8 或高于 1.25,你的模型可能对所选的敏感群体存在算法偏见。
从上面的决策树可以看出,选择的公平性指标取决于你的模型是否解决了不同表现、相等数量或辅助行为的问题。
让我们看看这些指标的使用时间和场景(注意:FP = 假阳性,TP = 真阳性,FN = 假阴性,TN = 真阴性)。
召回平衡
-
定义: 衡量模型对于一个组与另一个组的“敏感性”,或模型正确预测真阳性的能力
-
何时使用: 如果子组中的召回率接近,则实现了召回平衡
-
如何计算: 召回平衡 = 召回 _ 敏感组 / 召回 _ 基准组 ; 召回 = TP / (TP + FN)
假阳性率平衡
-
定义: 衡量模型是否对敏感组的正类预测不准确,相对于基准组
-
何时使用: 如果子组中的假阳性率(假阳性数与总负数的比率)接近,则实现了假阳性率平衡
-
如何计算: 假阳性平衡 = FPR_ 受限组 / FPR_ 特权组; 假阳性率 = FP / (FP + TN)
不平等影响
-
定义: 受保护类别的不利待遇的量化衡量
-
何时使用: 不平等影响,也称为比例平衡,用于检查不同组的结果比率是否与它们在总体中的比例相同
-
如何计算: 如果男性的工作机会率为 50%,女性的工作机会率为 25%,那么这两个率的比率为 2,表示存在不平等影响
一旦通过咨询公平性树定义了业务问题背景中的公平性,你可以计算你的平衡得分,并使用五分之一规则来确定是否需要在模型开发流程的预处理、处理中或后处理阶段进行干预。有关要实现的平衡类型以展示算法中立性,请参见下文。
第一类平衡
-
描述: 在假发现率(FDR)平衡和假阳性率(FPR)平衡中的公平性
-
计算: FDR = FP / (TP + FP) ; FPR = FP / (TN + FP)
第二类平衡
-
描述: 在假遗漏率(FOR)平衡和假阴性率(FNR)平衡中的公平性
-
计算: FOR = FN / (TN + FN) ; FNR = FN / (TP + FN)
平均赔率
-
描述: 在假阳性率(FPR)平衡和真阳性率(TPR)平衡中的公平性
-
计算: FPR = FP / (TN + FP) ; TPR = TP / (TP + FN)
监督公平性
-
描述: 在第一类和第二类平衡中的公平性
-
计算: 见上文
总体公平性
-
描述: 在混淆矩阵中使用的所有指标的公平性
-
计算: FP, TP, FN, TN
有哪些工具可以帮助应对模型偏见?
有多种工具被开发出来以帮助应对整个机器学习生命周期中的算法偏见。
模型构建与验证
大多数解决方案专注于处理模型开发的初始阶段,目的是在模型发布之前提供模型公平性检查。
工具示例:
-
Aequitas:一个开源偏差审计工具包,用于对机器学习模型进行歧视和偏差审计。
-
Arize AI (完全披露:我是 Arize 的联合创始人!):提供模型公平性检查,比较训练基线和生产数据,并进行根本原因分析工作流。
-
IBM Fairness 360:一个开源工具包,帮助你通过审计检查、报告和缓解机器学习模型中的歧视和偏差。
-
Google 的 PAIR AI:提供多个针对特定用例的工具,包括一个用于缓解图像数据集公平性和偏差问题的工具,支持 TensorFlow Datasets API。
尽管其中一些工具可以用于汇总公平性指标和事后解释(即模型解释性),这些对于审计很有用,但它们大多数并不适用于生产中的实时监控。
生产中的监控
在生产中监控公平性指标很重要,原因很简单:在部署的 AI 中,模型偏差发生是时间问题,而非是否发生的问题。概念漂移、训练中未见的新模式、训练与服务偏差以及异常值挑战着即使是最先进的团队,这些团队在训练中表现完美且通过验证阶段的模型也难以避免。
以下是一些提供生产中实时公平性监控的平台:
-
Arize:提供自动监控和公平性检查,通过多维比较揭示模型特征和群体,帮助发现算法偏差。
-
DataRobot:监控如比例平衡的公平性指标,并通过工作流将生产数据与训练数据进行比较。
团队应如何解决模型偏差?
解决模型偏差的第一步是理解数据,确保团队拥有正确的工具,并确保组织治理到位以确保公平。团队需要了解数据建模管道中对偏差有价值的预处理、处理和后处理阶段,因此,需要在这些阶段之一(如果不是多个阶段)进行公平性干预。以下是组织在这些阶段实现公平性的一些步骤。
步骤 1:使受保护类别数据对模型构建者和维护生产模型的机器学习团队可用
根据最近的一项调查,79.7%的机器学习团队报告称,他们“缺乏根除偏差或伦理问题所需的受保护数据”,至少有时如此,近一半(42.1%)表示这至少有时是一个问题。这需要改变。正如一位研究人员所说,无视并不能实现公平。
步骤 2:确保你拥有生产中公平性的可视化工具,理想情况下是在模型发布之前
在模型构建阶段结合公平性检查与定期事后审计,在模型偏差可能导致现实世界伤害的情况下是远远不够的。持续监控和警报可以帮助揭示在现实世界中不可避免出现的盲点(未知的未知),并加快解决时间。当模型所有者和维护生产模型的机器学习工程师拥有优化的指导和工具时,良好的结果就会发生。
步骤 3:成为内部变革推动者并迅速行动
解决模型偏差不仅仅是机器学习的问题。许多挑战——例如公平性与业务结果之间的权衡或团队之间责任的分散——只能通过多个团队和高层的参与来解决。机器学习团队在构建一种多管齐下的方法方面处于良好的位置,该方法结合了专门构建的基础设施、治理和专门的工作组以确保问责。
结论
当然,这些步骤只是开始,行业在公平性方面还有很长的路要走。识别问题仅仅是战斗的一半;采取行动至关重要。在群体层面上,机器学习可观测性和快速追踪模型公平性问题的原因可以提供帮助,特别是在知道何时重新训练或恢复到以前的模型(或不使用模型)时。
联系我们
如果这篇博客引起了你的兴趣,并且你渴望了解更多关于机器学习可观测性和模型监控的信息,请查看我们的其他博客和资源! 随时联系我们提出任何问题或意见,或注册一个免费账户,如果你有兴趣加入一个有趣的、明星般的工程团队,帮助模型在生产中取得成功,可以在这里找到我们的开放职位!
魔方与 Markov 链

图片来自 Unsplash,由作者修改
我们获得了 使用 Markov 过程描述 优化解决魔方的概率
·
关注 发表在 Towards Data Science ·14 分钟阅读·2023 年 8 月 4 日
--
魔方是一个具有巨大状态空间的规划问题原型,且只有一个解决方案。这正是“针在干草堆中”这一概念的定义。如果没有指导(即使你每秒可以旋转面 100 次),你可能会在整个宇宙的时间里也无法成功。关于它的一切似乎都涉及到巨大的数字。
我们在这里计算的量是一个例外。通过它,你将获得对一个困难问题(以及任何类似的规划问题)的简单视角。我们需要两个要素,一个随机过程和一个最佳求解器。后者是一个设备(真实的或理想的),可以在任何初始状态下使用最少的移动次数来解决魔方(或类似问题)。我们将完全回答以下问题:
如果一个已解魔方经历了 N 次随机转动, p(d|N) 的概率是多少,即一个最佳求解器需要 d 次移动才能将其恢复到原始状态?
在正常情况下,如果有人要求你解魔方,你只会得到一个打乱的魔方,没有任何参考或标签。在这里,我们有关于打乱状态的一条信息:它是在从已解状态开始的N次随机移动后获得的。这条信息很有用!
我们为什么对p(d|N)感兴趣?
在计算上,你可以尝试以不同的方式解密魔方。一个魔方项目的目标可能在于以次优方式解决任何或某些状态,或者以最佳方式解决每个可能的状态(例如,这将需要著名的35 CPU 年)。一个魔方求解器通常涉及两个方面,一个搜索算法和一个启发式函数。通过选择这两个方面,我们参数化了我们的方法的难度、效率或计算要求。
在启发式函数领域,即搜索引导,总是存在创新的空间。历史上,魔方的启发式方法是将对打乱的魔方面片相对于其已解状态位置的曼哈顿距离估算相结合。直到最近,神经网络才被用作启发式方法。
神经网络 = 魔方的新启发式方法
神经网络的工作很简单(一个分类器):你输入一个魔方的状态x,然后预测该状态的深度d。状态d的深度定义为从该状态开始解决魔方所需的最少移动次数。请注意以下几点。如果我们有一个知道任何状态深度的设备,我们实际上就有了一个最佳求解器,因为每次我们可以选择一个使状态深度更低的移动,直到达到深度 = 0(已解状态)。
这里的问题是如何训练该网络。或者,具体来说,如何获得准确的训练数据。除非你已经拥有一个最优求解器,否则没有简单的方法知道一个打乱状态x的真实深度d。我们没有最优求解器,或者,我们不想使用计算代价高昂的求解器。我们想从头开始构建一个近似且高效的最优求解器,并尽量减少人工输入,同时也需要准确的训练数据:
training_data = (x , d).
正如我们所说,d的准确性很难获得,但将某个特定打乱状态与数字N关联却很容易:即通过对已解决状态进行N次随机移动生成的状态。然后
p(d|N) 估计 d, 给定 N.
p(d|N)将用于提高该训练数据的准确性。前述论文的作者建立了第一个魔方深度分类神经网络。他们的训练数据形式为:
training_data = (x , N).
他们将d视为N. 这个选择通过在训练过程中使用类似 Bellman 的循环动态提高标签的准确性来进行补偿。这里计算的概率p(d|N)为训练数据的准确性提供了一个更好的起点(仅通过随机旋转已解决状态N次即可获得大量数据)。
一个随机游走视角
计算p(d|N)相当于问一个随机游走者在N步之后会离d多远。不是在方格网格上行走,而是在一个拥有 10 的 19 次方节点(立方体状态)和相似数量连接(合法移动)的巨大魔方图上行走。如果我们选择一个布局,将节点按深度组织:将已解决状态置于中心,深度为d的状态位于距离中心d的半径上,则图将看起来非常对称。径向(深度)方向提供了一个非常简单的视角。
常规
在这里,我们采用所谓的 3x3x3 魔方的四分之一转度量,其中一次移动涉及 90 度的面旋转,无论是顺时针还是逆时针。在这种度量下,有十二种可能的移动。如果我们选择了不同的度量,例如半转度量(也将 180 度的面旋转作为一次移动),那么p(d|N)的表达式将会有所不同。
数据
要获得p(d|N),我们需要使用某种领域知识,但我们不想处理图、模式数据库或群体理论。我们将使用一些更“基础”的东西:
包含深度为d的魔方状态数量的列表
这个列表(由 2012 年“上帝的数字”论文的作者提供)没有指定哪些状态在某个特定深度,只提供了它们的总数,并且没有提及任何N.
# Depth population list
# number of cubes' states at a depth d in the quarter-turn metric
D_d={
# depth number of states
0: 1,
1: 12,
2: 114,
3: 1068,
4: 10011,
5: 93840,
6: 878880,
7: 8221632,
8: 76843595,
9: 717789576,
10: 6701836858,
11: 62549615248,
12: 583570100997,
13: 5442351625028,
14: 50729620202582,
15: 472495678811004,
16: 4393570406220123,
17: 40648181519827392,
18: 368071526203620348,
19: 3000000000000000000, # approximate
20: 14000000000000000000, # approximate
21: 19000000000000000000, # approximate
22: 7000000000000000000, # approximate
23: 24000000000000000, # approximate
24: 150000, # approximate
25: 36,
26: 3,
27: 0
}

深度对状态数量的对数刻度图
关于这个列表的一些观察:
首先,深度大于 26 时没有状态(在四分之一转动度量中,上帝的数字是 26)。其次,列表中报告了19到24之间d的状态的近似数量。我们稍后需要对此保持谨慎。第三,如果我们绘制对数尺度图,我们可以看到大多数深度(除了接近末端的那些)呈线性增长。这意味着状态数量D(d)以指数方式增长。将对数图的线性部分拟合成一条直线,我们发现d = 3和d = 18之间,状态数量增长为

在3 < d < 18的深度上,平均来说,9.34个12次移动会使你远离已解决状态,而2.66次会使你更接近已解决状态。
马尔可夫过程在深度类上的应用
要找出p(d|N),我们可以把深度类看作马尔可夫过程的站点。让我解释一下:

随机旋转立方体面被描述为深度类之间的马尔可夫过程(一维随机游走)。作者提供的图像。
一个深度类d是指所有在深度d下的立方体状态(到达已解决状态的最小步数)。如果我们在深度类d中随机选择一个状态,并用随机的动作旋转一个随机的面,这将以概率p_d给我们一个深度为d + 1的状态,或者以概率q_d给我们一个深度为d - 1的状态。在四分之一转动度量中,没有自类转换。

这定义了一个马尔可夫过程,其中特定的站点是一个完整的深度类。在我们的例子中,只有相邻的d类是一步跳转连接的。准确地说,这是一个离散时间出生-死亡马尔可夫链。由于站点数量是有限的,因此该链也是不可约且遍历的,并且存在唯一的平稳分布。
我们假设在每次选择随机动作时概率是均匀分布的。这会产生一些深度类之间的转移概率p_d, q_d(待计算)。随机动作的数量N是马尔可夫过程的离散时间。这也是一个一维随机游走:在每个站点(深度类编号d)中,前进的概率是p_d,后退的概率是q_d。这个一维链,粗略来说,是鲁比克图中的“径向”方向(按深度-径向布局组织)。
转移矩阵
任何马尔可夫过程都可以用转移矩阵M编码。M的(i,j)项是从站点i跳到站点j的概率。在我们的例子中,只有以下项不同于零:


这里 p_0 = 1: 从深度等级0(仅包含已解决状态)我们只能跳到深度等级1(不存在等级-1)。同样,q_26 = 1: 从深度等级26我们只能跳到深度等级25(不存在等级27)。出于同样的原因,p_26 或 q_0 不存在。
平稳分布
我们将立方体的随机移动作用映射为一个一维深度等级随机游走者,以概率 q_d 和 p_d 来回跳动。长时间的行走会发生什么?或者,游走者在长时间的行走后访问特定位置的次数是多少?在现实生活中:当立方体经历随机旋转时,深度等级的访问频率是多少?
从长远来看,无论起点是什么,游走者在深度等级 d 上花费的时间与该深度等级的人口 D(d) 成正比。这是这里的重点:
(归一化的)深度人口列表 D(d) 应被解释为表示我们深度等级马尔可夫过程的平稳分布的向量。
从数学上讲,D(d) 是 M 的左特征向量

这个矩阵方程将给出26个线性方程,我们将从中得到 p_i’ 和 q_i’。

考虑到 p_0 = q_26 = 1,** 我们可以将这些重新写作

详细平衡方程。图像由作者提供。
这些被称为 详细平衡方程:流量,定义为站态位置人口与跳跃概率的乘积,在两个方向上是相同的。解为:

并且 p_i 是通过 p_i + q_i = 1. 获得的。
对深度等级人口的一些条件
这些解有趣的地方在于,因为 q_i 是一个概率,我们应该有

这转化为分布 D_k 的以下条件:

这是深度人口 D_k 应该满足的一个不等式塔。明确地,它们可以组织为:

特别是,最后两个不等式是

因为 D_27 = 0, 我们得到下限和上限相等,所以

或者:

偶数位置的总人口应该等于奇数位置的总人口!
我们可以将其视为偶数和奇数站点之间的详细平衡:每一步总是到达不同且相邻的深度类。任何跳跃都会将你从奇数深度类(所有奇数深度类的类)带到偶数深度类(所有偶数深度类的类)。因此,奇数到偶数类的跳跃发生的概率为 1(反之亦然)。由于两个方向的概率都是 1,它们的数量应该通过详细平衡来相等。
出于同样的原因,马尔可夫过程将达到一个周期为二的“平稳分布”,在每次移动后在偶数和奇数站点之间切换(离散时间N)。
数据存在问题
我们计划使用的数据的深度人口D_d在source中报告的是大致的,对于d = 19,20,21,22,23,24\。因此,没有保证它会满足所有这些条件(不等式)。如果我们得到一些概率q_i*超出了[0,1]范围(如情况所示!)。特别是,如果我们尝试检查最后一个条件(偶数-奇数人口平衡),它差距很大!(更新:见末尾注释)

出路
奇数类似乎人口不足(这是作者选择报告数据的近似的结果)。为了使结果有效(使概率在[0,1]范围内),我们决定将之前的大数字添加到深度类别 21 的人口中(具有最大人口的奇数类,或者,最不容易注意到这个大增量的类)。通过这个修正,所有得到的概率似乎都是正确的(这意味着不等式也得到了满足)。
跳跃概率为:
p_i =
{1., 0.916667, 0.903509, 0.903558, 0.903606, 0.903602, 0.90352, 0.903415,
0.903342, 0.903292, 0.903254, 0.903221, 0.903189, 0.903153, 0.903108,
0.903038, 0.902885, 0.902409, 0.900342, 0.889537, 0.818371, 0.367158,
0.00342857, 6.24863*1e-12, 0.00022, 0.0833333}
# i from 0 to 25
q_i =
{0.0833333, 0.0964912, 0.0964419, 0.096394, 0.0963981, 0.0964796,
0.096585, 0.096658, 0.0967081, 0.0967456, 0.0967786, 0.0968113,
0.0968467, 0.0968917, 0.0969625, 0.0971149, 0.0975908, 0.0996581,
0.110463, 0.181629, 0.632842, 0.996571, 1., 0.99978, 0.916667, 1.}
# i from 1 to 26
注意几乎所有前p_i(直到i = 21)都接近1。这些是远离已解决状态的概率。接近已解决状态的概率(q_i)对于i大于21几乎为1。这使我们认识到为何解决魔方困难:随机行走者(或魔方的随机移动者)将“永远困在”深度类21的邻域中。
p(d|N)
将p_i, q_i数值代入转移矩阵M中,马尔可夫过程得到完整描述。特别是,如果我们以概率一从站点0开始,经过N步后,随机行走者将以概率到达站点d:

这是我们寻找的概率:

从数值上看,我们了解到p(d|N)只有在N和d具有相同奇偶性时才非零(这是某些魔方学者所熟知的)。下面我们为不同的N绘制了一些p(d|N):

一些概率 p(d|N)。作者图像。
例如:经过 N = 18 次随机移动(绿色曲线),结果立方体的状态在深度 d = 17 的可能性比在深度 d = 19 更高。我们还观察到,在 N = 31 或 32 时,p(d|N) 与平稳分布 D(d) 非常接近(除了它在偶数和奇数位置之间来回切换)。这是另一个回答多少步足以说明我们真的打乱了魔方的问题。
请注意,我们解决了一个逆问题。我们从平稳分布中得到了转移概率。这对于一般的马尔科夫过程是不可能的。特别是,对于半转度量,通过这里描述的方法不能找到 p(d|N)。半转度量是不同的,因为我们可以在移动后停留在同一深度类别(按他们的移动定义)。这些自深度类别跳跃在转移矩阵的对角线上引入了额外的概率 r_i,我们将有更多的变量而不是方程。
最后的评论
即使从计算角度来看,魔方是一个 35 CPU 年问题,它的许多方面仍然可以通过分析或适度的数值计算来描述。我们在这里计算的概率就是一个例子。我们所说的一切都可以很容易地推广到更复杂的魔方-兄弟。一个很酷的推广是进入更多维度:S = 4D, 5D, 6D,……维度的魔方。在这些情况下,状态空间随着 S 的增加而呈指数增长。因此,有些魔方的马尔科夫链可以长到我们想要的长度。换句话说,我们有类似的谜题,其中上帝的数字可以大到我们想要的程度(粗略地说,上帝的数字是状态数量的对数,而状态数量随着维度 S 的增加而增加)。在这些情况下,我们可以采取某些极限来解释我们3D情况的某些方面,就像下一个:
在大上帝数 G 的极限情况下, p(d|N) 的概率应该接近二项分布
很容易看出为什么会这样。如果 G 很大,D_d 的指数增长将在大多数 d 下非常稳定。这是一个大胆但并非过于疯狂的猜测:

对于远离 0 和 G 的 k。如我们所说,在 S = 3D 的情况下 b = 9.34。对于更高的 S,b 应该增加(拥有更多面会增加分支因子 b)。这转化为以下概率值:

当i远离原点(i >> 1)且上帝的数字(i << G)时,q_i会接近一个常数值1/(b+1)。在这个范围内,p_i也将是常数。你可以看到,这里为3D情况计算的p_i和q_i的值对于i = 3, …, 15几乎是常数,并且q_i大约等于1/(b+1),其中b = 9.34。对于0 << i << G,我们将得到一个具有常数返回概率(q)和前进概率(p)的一维随机行走者。在这种情况下,行走者的位置将由类似二项分布的分布描述。
随机行走者在经过N次试验后,向右走k步(成功率p)和向左走N-k步(成功率q)的概率为Binomial(k,N,p)。在经过N步后行走的距离将是
d = k - (N - k)**
从中我们得到

从这里我们可以得到最可能的d的解析估计

最可能的d(在二项分布范围内)随N线性增长,斜率依赖于“有效”分支因子b。随着立方体维度S的增加,分支因子b也会增加,而最可能的深度d实际上接近于N。
更新说明。在这个故事发布后,我联系了 Tomas Rokicki 和 Morley Davidson(2012 年证明上帝的数字在半转度量中为 20的两位作者)关于他们报告的D(d)以及我使用这些数据得到的负概率。他们与我分享了更准确的数据,包括深度 d = 19, …, 24 的上下界。他们的数据与这里得到的不等式完全兼容,并且与偶数深度类别的数量应等于奇数深度类别的数量(在四分之一转度量中)的事实相一致。使用这些新数据计算的概率具有微不足道的修正。
一个学术研究(以及其他数据类型)的推荐系统!
实施自然语言处理和图论来比较和推荐不同类型的文档
·发布于 Towards Data Science ·阅读时间 15 分钟·2023 年 3 月 29 日
--

照片由 Shubham Dhage 在 Unsplash 提供
介绍
许多人现在开发的项目通常都从第一个关键步骤开始:积极研究。投资于别人所做的工作并在此基础上构建对你的项目增加价值至关重要。你不仅要从他人的结论中学习,还要找出在你的项目中不应该做的事情,以确保成功。
在我完成论文的过程中,我开始收集各种类型的研究文件。例如,我有不同的学术出版物的合集,以及包含不同实验结果的 Excel 表格。当我完成论文的研究时,我在想:是否有办法创建一个推荐系统,比较我档案中的所有研究,并帮助指导我下一个项目?
事实上,确实有!
注意:这不仅适用于你可能从各种搜索引擎收集的所有研究资料的存储库,也适用于你拥有的包含各种不同文档的目录。
设置
我和我的团队使用 Python 3 开发了这个推荐系统。也要向他们致敬!我们在这里取得了很棒的成就。
有很多 API 支持这个推荐系统,研究每个具体的 API 可以执行的操作可能对你的学习有益。
import string
import csv
from io import StringIO
from pptx import Presentation
import docx2txt
import PyPDF2
import spacy
import pandas as pd
import numpy as np
import nltk
import re
import openpyxl
from nltk.stem import WordNetLemmatizer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from gensim.parsing.preprocessing import STOPWORDS as SW
nltk.download('stopwords')
nltk.download('wordnet')
nltk.download('omw-1.4')
nltk.download('averaged_perceptron_tagger')
from nltk.corpus import wordnet
import networkx as nx
from networkx.algorithms.shortest_paths import weighted
import glob
障碍
我必须克服的一大难题是推荐机器比较不同类型文件的能力。例如,我想看看一个 Excel 电子表格中的信息是否与 PowerPoint 和学术 PDF 期刊中的信息相似或相关。做到这一点的诀窍是将每种文件类型读取到 Python 中,并将每个对象转换为一个单一的字符串。这规范化了所有数据,并允许计算相似性度量。
PDF 阅读类
我们将首先关注的课程是pdfReader类,它可以将 PDF 格式化为可在 Python 中读取的格式。在所有文件格式中,我认为 PDF 是最重要的之一,因为许多从研究库(如 Google Scholar)下载的期刊文章都是 PDF 格式的。
class pdfReader:
def __init__(self, file_path: str) -> str:
self.file_path = file_path
def PDF_one_pager(self) -> str:
"""A function which returns a one line string of the
pdf.
Returns:
one_page_pdf (str): A one line string of the pdf.
"""
content = ""
p = open(self.file_path, "rb")
pdf = PyPDF2.PdfReader(p)
num_pages = len(pdf.pages)
for i in range(0, num_pages):
content += pdf.pages[i].extract_text() + "\n"
content = " ".join(content.replace(u"\xa0", " ").strip().split())
page_number_removal = r"\d{1,3} of \d{1,3}"
page_number_removal_pattern = re.compile(page_number_removal, re.IGNORECASE)
content = re.sub(page_number_removal_pattern, '',content)
return content
def pdf_reader(self) -> str:
"""A function which can read .pdf formatted files
and returns a python readable pdf.
Returns:
read_pdf: A python readable .pdf file.
"""
opener = open(self.file_path,'rb')
read_pdf = PyPDF2.PdfFileReader(opener)
return read_pdf
def pdf_info(self) -> dict:
"""A function which returns an information dictionary of a
pdf.
Returns:
dict(pdf_info_dict): A dictionary containing the meta
data of the object.
"""
opener = open(self.file_path,'rb')
read_pdf = PyPDF2.PdfFileReader(opener)
pdf_info_dict = {}
for key,value in read_pdf.documentInfo.items():
pdf_info_dict[re.sub('/',"",key)] = value
return pdf_info_dict
def pdf_dictionary(self) -> dict:
"""A function which returns a dictionary of
the object where the keys are the pages
and the text within the pages are the
values.
Returns:
dict(pdf_dict): A dictionary pages and text.
"""
opener = open(self.file_path,'rb')
read_pdf = PyPDF2.PdfReader(opener)
length = read_pdf.pages
pdf_dict = {}
for i in range(length):
page = read_pdf.getPage(i)
text = page.extract_text()
pdf_dict[i] = text
return pdf_dict
Microsoft PowerPoint 阅读器
pptReader 类能够将 Microsoft PowerPoint 文件读取到 Python 中。
class pptReader:
def __init__(self, file_path: str) -> None:
self.file_path = file_path
def ppt_text(self) -> str:
"""A function that returns a string of text from all
of the slides in a pptReader object.
Returns:
text (str): A single string containing the text
within each slide of the pptReader object.
"""
prs = Presentation(self.file_path)
text = str()
for slide in prs.slides:
for shape in slide.shapes:
if not shape.has_text_frame:
continue
for paragraph in shape.text_frame.paragraphs:
for run in paragraph.runs:
text += ' ' + run.text
return text
Microsoft Word 文档阅读器
wordDocReader 类可以用于在 Python 中读取 Microsoft Word 文档。它利用了doc2txt API,并返回给定 Word 文档中位于的文本/信息的字符串。
class wordDocReader:
def __init__(self, file_path: str) -> str:
self.file_path = file_path
def word_reader(self):
"""A function that transforms a wordDocReader object into a Python readable
word document."""
text = docx2txt.process(self.file_path)
text = text.replace('\n', ' ')
text = text.replace('\xa0', ' ')
text = text.replace('\t', ' ')
return text
Microsoft Excel 阅读器
有时研究人员会将他们的结果以 Excel 表格的形式随出版物一起提供。能够读取列名,甚至是数值,可能有助于推荐与你搜索内容相似的结果。例如,如果你在研究某只股票的过去表现?也许你会在历史表现的 Excel 表格中搜索名称和符号。这种推荐系统会向你推荐这个 Excel 表格,以帮助你的研究。
class xlsxReader:
def __init__(self, file_path: str) -> str:
self.file_path = file_path
def xlsx_text(self):
"""A function which returns the string of an
excel document.
Returns:
text(str): String of text of a document.
"""
inputExcelFile = self.file_path
text = str()
wb = openpyxl.load_workbook(inputExcelFile)
#This will save the excel sheet as a CSV file
for sn in wb.sheetnames:
excelFile = pd.read_excel(inputExcelFile, engine = 'openpyxl', sheet_name = sn)
excelFile.to_csv("ResultCsvFile.csv", index = None, header=True)
with open("ResultCsvFile.csv", "r") as csvFile:
lines = csvFile.read().split(",") # "\r\n" if needed
for val in lines:
if val != '':
text += val + ' '
text = text.replace('\ufeff', '')
text = text.replace('\n', ' ')
return textCSV File Reader
csvReader 类将允许将 CSV 文件包含在你的数据库中,并在系统的推荐中使用。
class csvReader:
def __init__(self, file_path: str) -> str:
self.file_path = file_path
def csv_text(self):
"""A function which returns the string of a
csv document.
Returns:
text(str): String of text of a document.
"""
text = str()
with open(self.file_path, "r") as csvFile:
lines = csvFile.read().split(",") # "\r\n" if needed
for val in lines:
text += val + ' '
text = text.replace('\ufeff', '')
text = text.replace('\n', ' ')
return textMicrosoft PowerPoint Reader
这是一个有用的类。不是很多人会考虑到 PowerPoint 演示文稿中存储着有价值的信息。这些演示文稿主要是为了将关键观点和信息可视化展示给观众。以下课程将帮助你将数据库中的任何 PowerPoint 文件与其他信息体关联,希望能引导你找到相关的工作。
class pptReader:
def __init__(self, file_path: str) -> str:
self.file_path = file_path
def ppt_text(self):
"""A function which returns the string of a
Mirocsoft PowerPoint document.
Returns:
text(str): String of text of a document.
"""
prs = Presentation(self.file_path)
text = str()
for slide in prs.slides:
for shape in slide.shapes:
if not shape.has_text_frame:
continue
for paragraph in shape.text_frame.paragraphs:
for run in paragraph.runs:
text += ' ' + run.text
return textMicrosoft Word Document Reader
本系统的最终课程是一个 Microsoft Word 文档阅读器。Word 文档是另一个宝贵的信息来源。许多人会撰写报告,以 Word 文档格式表示他们的发现和想法。
class wordDocReader:
def __init__(self, file_path: str) -> str:
self.file_path = file_path
def word_reader(self):
"""A function which returns the string of a
Microsoft Word document.
Returns:
text(str): String of text of a document.
"""
text = docx2txt.process(self.file_path)
text = text.replace('\n', ' ')
text = text.replace('\xa0', ' ')
text = text.replace('\t', ' ')
return text
今天项目中使用的课程就到这里了。请注意: 还有许多其他文件类型可以用来增强你的推荐系统。当前版本的代码正在开发中,将接受图像并尝试将其与数据库中的其他文档关联!
数据处理
预处理
让我们看看如何预处理这些数据。这个推荐系统是为一个学术研究库构建的,因此使用自然语言处理(NLP)指导的预处理步骤来分解文本是非常重要的。
数据处理类被简单地称为datapreprocessor,类中的第一个函数是一个词性标记器。
class dataprocessor:
def __init__(self):
return
@staticmethod
def get_wordnet_pos(text: str) -> str:
"""Map POS tag to first character lemmatize() accepts
Inputs:
text(str): A string of text
Returns:
tag_dict(dict): A dictionary of tags
"""
tag = nltk.pos_tag([text])[0][1][0].upper()
tag_dict = {"J": wordnet.ADJ,
"N": wordnet.NOUN,
"V": wordnet.VERB,
"R": wordnet.ADV}
return tag_dict.get(tag, wordnet.NOUN)
这个函数为单词标记词性,这在项目后续阶段会非常有用。
第二步,有一个函数执行许多我们已经见过的标准 NLP 步骤。这些步骤包括:
-
将每个单词都转为小写
-
去掉标点符号
-
去掉数字(我只想查看非数字信息。如果需要,可以省略此步骤)
-
停用词移除。
-
词形还原。在这一过程中,get_wordnet_pos()函数对于包含词性非常有用!
@staticmethod
def preprocess(text: str):
"""A function that prepoccesses text through the
steps of Natural Language Processing (NLP).
Inputs:
text(str): A string of text
Returns:
text(str): A processed string of text
"""
#lowercase
text = text.lower()
#punctuation removal
text = "".join([i for i in text if i not in string.punctuation])
#Digit removal (Only for ALL numeric numbers)
text = [x for x in text.split(' ') if x.isnumeric() == False]
#Stop removal
stopwords = nltk.corpus.stopwords.words('english')
custom_stopwords = ['\n','\n\n', '&', ' ', '.', '-', '$', '@']
stopwords.extend(custom_stopwords)
text = [i for i in text if i not in stopwords]
text = ' '.join(word for word in text)
#lemmanization
lm = WordNetLemmatizer()
text = [lm.lemmatize(word, dataprocessor.get_wordnet_pos(word)) for word in text.split(' ')]
text = ' '.join(word for word in text)
text = re.sub(' +', ' ',text)
return text
接下来,有一个函数可以将所有文件读入系统中。
@staticmethod
def data_reader(list_file_names):
"""A function that reads in the data from a directory of files.
Inputs:
list_file_names(list): List of the filepaths in a directory.
Returns:
text_list (list): A list where each value is a string of text
for each file in the directory
file_dict(dict): Dictionary where the keys are the filename and the values
are the information found within each given file
"""
text_list = []
reader = dataprocessor()
for file in list_file_names:
temp = file.split('.')
filetype = temp[-1]
if filetype == "pdf":
file_pdf = pdfReader(file)
text = file_pdf.PDF_one_pager()
elif filetype == "docx":
word_doc_reader = wordDocReader(file)
text = word_doc_reader.word_reader()
elif filetype == "pptx" or filetype == 'ppt':
ppt_reader = pptReader(file)
text = ppt_reader.ppt_text()
elif filetype == "csv":
csv_reader = csvReader(file)
text = csv_reader.csv_text()
elif filetype == 'xlsx':
xl_reader = xlsxReader(file)
text = xl_reader.xlsx_text()
else:
print('File type {} not supported!'.format(filetype))
continue
text = reader.preprocess(text)
text_list.append(text)
file_dict = dict()
for i,file in enumerate(list_file_names):
file_dict[i] = (file, file.split('/')[-1])
return text_list, file_dict
由于这是该系统的第一个版本,我想强调一下代码可以调整以包含许多其他文件类型!
下一个函数被称为database_preprocess(),用于处理给定数据库中的所有文件。输入是一个文件列表,每个文件都有其关联的文本字符串(已处理)。这些文本字符串随后使用sklearn 的 tfidfVectorizer进行向量化。这到底是什么?基本上,它会根据每个给定单词的频率,将所有文本转换为不同的特征向量。我们这样做是为了通过与向量算术相关的相似度公式查看文档的相关性。
@staticmethod
@staticmethod
def database_processor(file_dict,text_list: list):
"""A function that transforms the text of each file within the
database into a vector.
Inputs:
file_dixt(dict): Dictionary where the keys are the filename and the values
are the information found within each given file
text_list (list): A list where each value is a string of the text
for each file in the directory
Returns:
list_dense(list): A list of the files' text turned into vectors.
vectorizer: The vectorizor used to transform the strings of text
file_vector_dict(dict): A dictionary where the file names are the keys
and the vectors of each files' text are the values.
"""
file_vector_dict = dict()
vectorizer = TfidfVectorizer()
vectors = vectorizer.fit_transform(text_list)
feature_names = vectorizer.get_feature_names_out()
matrix = vectors.todense()
list_dense = matrix.tolist()
for i in range(len(list_dense)):
file_vector_dict[file_dict[i][1]] = list_dense[i]
return list_dense, vectorizer, file_vector_dict
创建一个基于数据库的向量化器的原因是,当用户给出一个需要在数据库中搜索的术语列表时,这些单词将根据其在数据库中的频率被向量化。这是当前系统的最大弱点。随着数据库规模的增加,计算相似度所需的时间和计算资源将增加,从而减慢系统速度。在质量控制会议上提出的一个建议是使用强化学习来推荐不同的数据文章。
接下来,我们可以使用一个输入处理器,将任何提供的单词处理成向量。这类似于你在搜索引擎中输入请求的过程。
@staticmethod
def input_processor(text, TDIF_vectorizor):
"""A function which accepts a string of text and vectorizes the text using a
TDIF vectorizoer.
Inputs:
text(str): A string of text
TDIF_vectorizor: A pretrained vectorizor
Returns:
words(list): A list of the input text in vectored form.
"""
words = ''
total_words = len(text.split(' '))
for word in text.split(' '):
words += (word + ' ') * total_words
total_words -= 1
words = [words[:-1]]
words = TDIF_vectorizor.transform(words)
words = words.todense()
words = words.tolist()
return words
由于数据库中的所有信息及给定的信息都将是向量,我们可以使用余弦相似度来计算向量之间的角度。角度越接近 0,两个向量的相似度就越低。
@staticmethod
def similarity_checker(vector_1, vector_2):
"""A function which accepts two vectors and computes their cosine similarity.
Inputs:
vector_1(int): A numerical vector
vector_2(int): A numerical vector
Returns:
cosine_similarity([vector_1], vector_2) (int): Cosine similarity score
"""
vectors = [vector_1, vector_2]
for vec in vectors:
if np.ndim(vec) == 1:
vec = np.expand_dims(vec, axis=0)
return cosine_similarity([vector_1], vector_2)
一旦找到两个向量之间的相似度分数,便可以创建搜索词与数据库中文档之间的排名。
@staticmethod
def recommender(vector_file_list,query_vector, file_dict):
"""A function which accepts a list of vectors, query vectors, and a dictionary
pertaining to the list of vectors with their original values and file names.
Inputs:
vector_file_list(list): A list of vectors
query_vector(int): A numerical vector
file_dict(dict): A dictionary of filenames and text relating to the list
of vectors
Returns:
final_recommendation (list): A list of the final recommended files
similarity_list[:len(final_recommendation)] (list): A list of the similarity
scores of the final recommendations.
"""
similarity_list = []
score_dict = dict()
for i,file_vector in enumerate(vector_file_list):
x = dataprocessor.similarity_checker(file_vector, query_vector)
score_dict[file_dict[i][1]] = (x[0][0])
similarity_list.append(x)
similarity_list = sorted(similarity_list, reverse = True)
#Recommends the top 20%
recommended = sorted(score_dict.items(),
key=lambda x:-x[1])[:int(np.round(.5*len(similarity_list)))]
final_recommendation = []
for i in range(len(recommended)):
final_recommendation.append(recommended[i][0])
#add in graph for greater than 3 recommendationa
return final_recommendation, similarity_list[:len(final_recommendation)]
向量文件列表是我们之前从文件中创建的向量列表。查询向量是正在搜索的词的向量。之前创建了文件字典,它使用文件名作为键,文件文本作为值。计算相似度后,会创建一个排名,优先推荐与查询词最相似的信息。注意,如果推荐结果超过 3 个怎么办?结合网络与图论的元素将为系统增加额外的计算优势,并生成更有信心的推荐。
页面排名理论
让我们稍作绕道,回顾一下页面排名理论。别误会,余弦相似度是测量向量之间相似性的强大计算方法,但将页面排名融入你的推荐算法,可以进行多个向量(你数据库中的数据)之间的相似性比较。
页面排名最初由拉里·佩奇(Larry Page)设计,用于对网站进行排名并衡量其重要性[1]。基本思想是,如果更多的网站链接到一个网站,那么这个网站可以被认为是“更重要的”。借鉴这一思想,如果图中的一个节点到其他节点的边距离减少,该节点可以被认为更重要。节点相对于图中其他节点的整体距离越短,该节点就越重要。
今天我们将使用页面排名的一种变体,称为特征向量中心性。特征向量中心性类似于页面排名,它衡量图中节点之间的连接,为更强的连接分配更高的分数。最大不同点?特征向量中心性会考虑与给定节点连接的节点的重要性,以估计该节点的相对重要性。这就好比说,认识许多重要人物的人,可能自己也通过这些强大的关系变得非常重要。总体而言,这两个算法在实现方式上非常相似。
对于这个数据库,在计算出向量之后,可以将它们放入图中,其中它们的边距离由它们与其他向量的相似度决定。
@staticmethod
def ranker(recommendation_val, file_vec_dict):
"""A function which accepts a list of recommendaton values and a dictionary
files wihin the databse and their vectors.
Inputs:
reccomendation_val(list): A list of recommendations found through cosine
similarity
file_vec_dic(dict): A dictionary of the filenames as keys and their
text in vectors as the values.
Returns:
ec_recommended(list): A list of the top 20% recommendations found using the
eigenvector centrality algorithm.
"""
my_graph = nx.Graph()
for i in range(len(recommendation_val)):
file_1 = recommendation_val[i]
for j in range(len(recommendation_val)):
file_2 = recommendation_val[j]
if i != j:
#Calculate sim_score between two values (weight)
edge_dist = cosine_similarity([file_vec_dict[recommendation_val[i]]],[file_vec_dict[recommendation_val[j]]])
#add an edge from file 1 to file 2 with the weight
my_graph.add_edge(file_1, file_2, weight=edge_dist)
#Pagerank the graph ]
rec = nx.eigenvector_centrality(my_graph)
#Takes 20% of the values
ec_recommended = sorted(rec.items(), key=lambda x:-x[1])[:int(np.round(len(rec)))]
return ec_recommended
好了,接下来呢?我们有使用数据库中每个数据点之间的余弦相似度创建的推荐,以及通过特征向量中心性算法计算的推荐。我们应该输出哪些推荐?两者都输出!
@staticmethod
def weighted_final_rank(sim_list,ec_recommended,final_recommendation):
"""A function which accepts a list of similiarity values found through
cosine similairty, recommendations found through eigenvector centrality,
and the final recommendations produced by cosine similarity.
Inputs:
sim_list(list): A list of all of the similarity values for the files
within the database.
ec_recommended(list): A list of the top 20% recommendations found using the
eigenvector centrality algorithm.
final_recommendation (list): A list of the final recommendations found
by using cosine similarity.
Returns:
weighted_final_recommend(list): A list of the final recommendations for
the files in the database.
"""
final_dict = dict()
for i in range(len(sim_list)):
val = (.8*sim_list[final_recommendation.index(ec_recommendation[i][0])].squeeze()) + (.2 * ec_recommendation[i][1])
final_dict[ec_recommendation[i][0]] = val
weighted_final_recommend = sorted(final_dict.items(), key=lambda x:-x[1])[:int(np.round(len(final_dict)))]
return weighted_final_recommend
这个脚本的最终功能将根据余弦相似度和特征向量中心性加权不同的推荐。目前,80% 的权重将分配给余弦相似度生成的推荐,20% 的权重将分配给特征向量中心性生成的推荐。最终的推荐可以根据这些权重进行计算,并汇总生成代表系统中所有相似度计算的推荐。开发者可以轻松调整权重,以反映他们认为更重要的推荐批次。
示例
让我们用这段代码做一个快速示例。我的数据库中的文档都以之前讨论的格式存在,并涉及机器学习的不同领域。数据库中更多的文档与生成对抗网络(GANs)相关,因此我预计当查询词为“生成对抗网络”时,这些文档会被优先推荐。
path = '/content/drive/MyDrive/database/'
db = [f for f in glob.glob(path + '*')]
research_documents, file_dictionary = dataprocessor.data_reader(db)
list_files, vectorizer, file_vec_dict = dataprocessor.database_processor(file_dictionary,research_documents)
query = 'Generative Adversarial Networks'
query = dataprocessor.preprocess(query)
query = dataprocessor.input_processor(query, vectorizer)
recommendation, sim_list = dataprocessor.recommender(list_files,query, file_dictionary)
ec_recommendation = dataprocessor.ranker(recommendation, file_vec_dict)
final_weighted_recommended = dataprocessor.weighted_final_rank(sim_list,ec_recommendation, recommendation)
print(final_weighted_recommended)
运行这段代码会生成以下推荐,并附上每个推荐的权重值。
[(‘GAN_presentation.pptx’, 0.3411272882084124), (‘Using GANs to Augment UAV Data_V2.docx’, 0.16293615818015078), (‘GANS_DAY_1.docx’, 0.12546058188955278), (‘ml_pdf.pdf’, 0.10864164490536887)]
再试一次。如果我查询“机器学习”会怎么样?
[(‘ml_pdf.pdf’, 0.31244922151487337), (‘GAN_presentation.pptx’, 0.18170070184645432), (‘GANS_DAY_1.docx’, 0.14825501243059303), (‘Using GANs to Augment UAV Data_V2.docx’, 0.1309153863914564)]
啊哈!正如预期的那样,第一个推荐文档是机器学习的介绍性简报!我仅使用了 7 个文档作为示例,添加的文档越多,收到的推荐也会越多!
结论
今天我们探讨了如何为你收集的文件创建推荐系统(尤其是当你收集研究材料用于项目时)。该系统的主要特点是,通过采用特征向量中心性算法计算向量的余弦相似度,从而提供更简洁、更好的推荐。今天试试看,希望这能帮助你更好地理解你拥有的数据之间的相关性。
如果你喜欢今天的阅读内容,请关注我,并告诉我是否有其他你希望我探讨的主题!如果你没有 Medium 账号,可以通过我的链接 这里 注册(这样我会收到少量佣金)!此外,你还可以在 LinkedIn 上添加我,或随时联系我!感谢你的阅读!
来源
对变换器的悼歌?
原文:
towardsdatascience.com/a-requiem-for-the-transformer-297e6f14e189
|观点| AI| 大型语言模型|
变换器会成为引领我们迈向人工通用智能的模型吗?还是会被替代?
·发表于Towards Data Science ·阅读时间 18 分钟·2023 年 12 月 1 日
--

图片来源于Stefany Andrade 在Unsplash
变换器在人工智能领域主导了六年,成为所有人工智能子领域的最先进技术。从自然语言处理 (NLP) 到计算机视觉,再到声音和图表,各个领域都有表现优异的专用变换器。
-
这个领域还会持续多久?
-
变换器真的还是最好的架构吗?
-
它会在不久的将来被替代吗?
-
它的主导地位面临什么威胁?
本文尝试回答这些问题。从变换器为何如此成功以及哪些因素使其在众多领域中立足开始,我们将分析它是否仍然拥有无可争议的主导地位,哪些因素威胁到它的霸主地位,以及是否存在潜在的竞争者。
一个帝国的简要历史

图片来源于Markus Spiske 在Unsplash
“所有的帝国都会变得傲慢。这是它们的本性。” ― 爱德华·拉瑟福德
帝国不可避免地衰落,当它们衰落时,历史会根据它们留下的遗产来评判它们。—— 诺亚·费尔德曼
“Attention Is All You Need” 是我们今天所知的人工智能的基础。生成型 AI 及其成功的根源都在于一个种子:变换器。
transformer 最初是为了解决 RNNs 的平行化不足,并能够建模序列中单词之间的长距离关系。其理念是为模型提供一个系统,以区分序列中哪些部分最重要(即注意力的关注点,attention)。这一切都是为了改进机器翻译。
然而,这些因素使得 transformer 更好地理解文本。此外,平行化使得模型在规模和更大的数据集上都能扩展。 GPU 的兴起进一步展示了像 Transformer 这样的可平行化架构的好处。
因此,Transformer 崛起成为了 AI 的新王者。一个帝国在极短的时间内建立起来。事实上,如今所有流行的模型都是 Transformer:ChatGPT、Bard、GitHub Copilot、Mistral、LLaMA、Bing Chat、稳定扩散、DALL-E、Midjourney 等等。

包含“语言模型”和“大型语言模型”关键短语的 arXiv 论文的累积数量趋势。图像来源:这里
这是因为 Transformer 很快适应了许多超越语言的任务。
即使是最庞大的帝国也会在某个时刻衰落;Transformer 的统治正在发生什么?
“脚踏实地的巨人”

图像来源:这里
当 transformer 被引入时,它的性能震惊了世界,并引发了参数竞赛。曾经一度,我们看到模型的增长速度如此之快,以至于它被称为“AI 的新摩尔定律。”这种增长一直持续到 2022 年 Megatron(530 B)和 Google PaLM(540 B)发布。然而我们仍未见到万亿参数。
当深度卷积网络展示其高效性(VGG 模型)时,卷积神经网络在短时间内从 16 层的VGG16发展到 201 层的DenseNet201。除了结果和性能外,这也是社区兴趣的证明。这种水平和垂直增长(以及对基础模型的渐进性变化)的模式在 2021 年停止了,当时社区相信视觉变换器(ViTs)优于卷积神经网络。

视觉变换器。图片来源:这里
为什么变换器的增长停止了?它们也被替代了吗?
不,但导致变换器增长的一些前提已经消失。
这种增长受到了所谓的幂律的激励。根据 OpenAI的说法,通过增加参数数量,特性会突然显现。因此,扩大模型规模会使模型发展出在某一规模以下无法观察到的特性。遗憾的是,斯坦福研究人员认为这些特性是偏差导致的海市蜃楼。
关于大语言模型新兴特性的视角改变
[towardsdatascience.com
扩大模型意味着花费更多。更多参数、更多计算、更多基础设施、更高的电力消耗(以及更多的碳排放)。这值得吗?
实际上,DeepMind 与 Chinchilla表示,性能的提升不仅与参数数量有关,还与数据量有关。因此,如果你想要一个拥有数十亿参数的模型,你必须拥有足够的令牌来训练它。遗憾的是,我们人类无法产生足够的内容来训练一个万亿参数的模型。

人类数据是有限资源。图片来源:这里
此外,影响模型性能的不仅仅是文本的数量。而是文本的质量。 这也是一个痛点,因为收集大量未经筛选的文本并不是一个好主意(即从互联网上下载时不加选择)。
重复令牌如何以及为何对大语言模型造成伤害?为什么这是个问题?
[towardsdatascience.com
另外,使用人工智能生成文本并不是一个好主意。理论上,人们可以使用一个 LLM 并要求它无限期地生成文本。问题在于,使用这些文本训练的模型只能模仿另一个 LLM,肯定不能超越它。
总体而言,我们的关键结论是,模型模仿并不是免费的午餐:今天的开源语言模型与其闭源对应物之间存在一个能力差距,这个差距无法通过廉价的模仿数据微调来弥合。 (source)
模仿模型能否达到像 ChatGPT 这样的专有模型的性能?
另一个要点是,这些巨大的模型在部署上也存在问题。 较小的模型性能很好,尤其是在某些任务中。可以将其提炼得到更小的专用模型。
带回家的信息:巨大的变换器范式正面临危机。每年我们会看到更大模型的想法已经结束。
毕竟,问题在于实际性(和成本)。一旦进入生产阶段,AI 的成本可能非常高。例如,微软据说在 GitHub Copilot 上亏损巨额资金(每用户每月 20 美元)。据一份报告,ChatGPT 每天的成本为 70 万美元,如果 ChatGPT 不能盈利,投资者可能不会继续承担这些成本。
因此,我们可以期待公司更倾向于开发专注于特定任务和业务的较小模型。
好吧,变换器不再增长了,但它仍然是游戏中最好的架构吗?
那么,让我们在下一节中讨论这个话题…
卷积仍然备受关注

照片由 Ricardo Gomez Angel 提供,来源于 Unsplash
首先,为什么变换器在各个领域都成功?
在最初的描述中,变换器结合了三个基本概念:首先是序列的位置信息表示(嵌入 + 位置编码),然后是序列元素的关联(自注意力),最后是构建层级表示(层叠)。
当文章Attention is All You Need发布时,它基于十年的 NLP 研究,并整合了之前发布的最佳成果:
-
词嵌入在 2013 年具有革命性,因为它能将词语转化为向量表示。此外,对嵌入的操作具有逻辑和语法上的意义。
-
层级表示则来自二十年的卷积神经网络研究,我们意识到通过堆叠层,模型可以学习到数据的越来越复杂的表示。

其威严的骨架,变换器。图片来源:这里
这些元素使得他在 NLP 领域取得成功,但与此同时,它们也是在其他领域获胜的关键。首先,变换器具有非常弱的归纳偏差,使其能够适应几乎任何类型的数据。其次,层级表示和连接序列元素的能力在 NLP 之外也有广泛的应用。
成功的故事,除非变换器自 2017 年起未曾改变,开始逐渐老化。
变换器的核心最终是自注意力。但这颗心脏泵送的血液过多。实际上,它的计算成本是巨大的。
因此,几个团队尝试寻找对自注意力的线性替代方法。然而,所有这些变体都被证明表现不如自注意力。
而什么看起来是一个不错的替代品? 无非就是古老的卷积。正如他们在 Hyena 中所展示的,通过稍微调整卷积,你可以获得一个表现类似变换器的好模型。
Hyena 模型展示了卷积如何比自注意力更快。
这颇具讽刺意味,因为自 2021 年以来,视觉变换器(ViTs)被认为在计算机视觉中优于 ConvNets。这似乎标志着卷积网络(ConvNets)在其领域中未被规范的霸主地位的终结。但事实是?
似乎 ConvNets 已经实现了复仇。令人惊讶,就像认为恐龙会通过驱逐哺乳动物重新掌控大陆一样。实际上,DeepMind 发布的最新文章基本上指出 ViTs 与 ConvNets 的比较并不公平。通过给 ConvNets 提供相同的计算预算,它们在 ImageNet 上的表现与 ViTs 相似。
视觉变换器似乎取代了卷积网络,但它们真的更好吗?
levelup.gitconnected.com](https://levelup.gitconnected.com/have-convolutional-networks-become-obsolete-245969f6b9d9?source=post_page-----297e6f14e189--------------------------------)
另一篇文章似乎也朝着同样的方向发展,卷积网络似乎与变换器具有竞争力:
同样的获胜者在较小规模下也获胜。在较小的主干网络中,ConvNeXt-Tiny 和 SwinV2-Tiny 表现突出,其次是 DINO ViT-Small。(source)

图片来源:这里
影响这种模型性能的主要因素有三个:其架构、预训练算法和预训练数据集。(source)
如果预训练算法和预训练数据集相同,那么剩下的只有模型架构。然而,在所有条件相等的情况下,ViTs 的所谓优越性似乎并未显现。 以至于 DeepMind 的作者所说的内容似乎像是承认了失败:
尽管 ViTs 在计算机视觉领域的成功令人印象深刻,但在我们看来,尚无强有力的证据表明经过预训练的 ViTs 在公平评估时优于经过预训练的 ConvNets。(source)
哎呀。因此我们可以说,在计算机视觉领域,ViTs 至少并不优于卷积网络。是吗?
我们注意到,ViTs 在特定背景下可能具有实际优势,例如在多个模态中使用相似的模型组件的能力。(source)
作者指出,它们可能仍然更优,因为在我们对多模态模型感兴趣时是有用的。考虑到特征也可以从卷积网络中提取,使用相同的模型在多个模态中显然更方便。
然而,这是一个非常重要的点,实证数据表明,至少在计算机视觉中,transformer 并不优于其他架构。这让我们不禁思考它的主导地位是否会在其他人工智能领域受到挑战。例如,transformer 核心领域的情况如何?它仍然是自然语言处理中的最佳模型吗?
文本 Dominion 的基础很脆弱

简短回答:是的,但它的至高无上的地位可能会结束。 让我们从它为何在 NLP 中如此成功开始。
Transformer 相比 RNN 的初始优势在于容易并行化。这导致了最初的狂热和对参数的急切追求。在此过程中,我们意识到是什么使得 Transformer 在 NLP 中获胜:上下文学习。
上下文学习是一个非常强大的概念:只需几个示例,模型就能够映射输入和输出之间的关系。所有这一切无需更新任何参数。
什么是及其工作原理是什么,让大型语言模型如此强大
towardsdatascience.com
基本上,这是自注意力的一个意外效果(且尚未完全理解)。根据 Anthropic的说法,有一些引导头实际上连接了模型的不同部分,并允许这种映射。
这种奇迹是模型所谓推理能力的基础。此外,能够如此多地实验提示也带来了令人难以置信的结果。
在实践中,无需重新训练模型,提示技术可以被创建以提升模型在推理方面的能力。思维链就是这种方法的最佳示例。利用这种策略,LLM 能够解决需要推理的问题(数学问题、编程问题等)。

图片来源:here
然而,必须考虑到:
然而,这种多步骤生成过程并不意味着 LLM 具有强大的推理能力,因为它们可能仅仅模拟了人类推理的表面行为,而不是真正理解精确推理所需的基础逻辑和规则。来源
翻译过来,我们已经创造了一个见识过整个人类知识的鹦鹉,能够将提示中的问题与它在训练中看到的内容联系起来。
为什么这种优势极其脆弱?
因为鹦鹉不一定是变压器。 我们需要任何能够接受自然语言指令作为输入并进行上下文学习的模型,然后我们可以像使用变压器一样使用所有的提示工程技术。
好吧,如果我们不一定需要变压器,那我们的新“随机鹦鹉”在哪里?
官僚制度减缓了创新

图片由Nam Anh提供,来源于 Unsplash
主要原因是,目前工业界的研究集中在将变压器(尽管有其缺陷)投入生产。此外,将一个我们了解较少的更好架构投入生产也是有风险的。
让我们深入了解一下…
首先,谷歌、META、亚马逊和其他大型科技公司拥有大量资源。然而,这些大公司却受到庞大的内部官僚制度的困扰:
谷歌是一个“曾经伟大的公司”,由于其官僚化的“迷宫”,它“逐渐停止了运作”。来源
官僚制度的增加导致了生产力的下降和整体放缓。为了实施一个小变化,必须获得越来越长的指挥链的批准,并遵循日益复杂的协议。简而言之,大型科技公司似乎有着与帝国相同的问题。
这显然也影响了创新:
“如果要总结一下,我会说信噪比是让我疲惫的原因。创新挑战… 随着风险容忍度的降低,只会变得更糟。”前谷歌高管诺姆·巴丁。来源
当然,像谷歌或微软这样的公司在选择上更加谨慎也是有充分理由的。例如,谷歌在 Bard 错误回答有关詹姆斯·韦布太空望远镜的问题时损失了数十亿美元。
这些声誉风险已经成为创新的巨大阻碍。只有当技术成熟且风险较低时才会被采纳。
最好的例子是苹果公司。 该公司只有在技术成熟并且认为其有利可图时才会采用。一般来说,近年来它已停止创新(尽管仍保持创纪录的利润)。到目前为止,它一直没有参与生成性 AI 领域,因为它认为尚不成熟。
苹果被称为“快速跟随者”;它喜欢等到新技术成熟后,再推出自己的苹果版本。(来源)
那么,这种对创新的抵制难道不应该有利于变压器技术吗?
是的,但我们忘记了开源。不仅仅是大型科技公司在进行研究,还有许多研究小组在调查 AI。他们可能没有 FAANGs 的资源,但独立研究合在一起是一股强大的力量。
谷歌自己也承认这一点。 实际上,山景城对微软或 OpenAI 的恐惧较少:
“令人不安的真相是,我们并未处于赢得这场军备竞赛的位置,OpenAI 也一样。在我们争吵的时候,第三方已经悄悄地抢占了我们的市场,”谷歌泄露的文件,来源
第三方正是开源社区。 一旦LLaMA发布,它立即收到了带有指令调整、量化和人类反馈强化学习(RLHF)的改进版本。Mistral 7B 刚刚推出,两组团队扩展了上下文长度(首先到32K,然后到128K)。
开源模型迅速变得可定制且便宜,并获得了一个庞大且活跃的社区的支持。开源社区立即采用每一个突破,并迅速改进它。大型公司在内部官僚主义上浪费时间,并因声誉损害的风险而受到限制,导致可能落后。
这个杰出的社区是变压器继任者的沃土。如果一个模型展示了克服变压器局限性的能力,它可能会获得如海啸般的力量。
但让我们看看最后一点,为什么尽管如此,变压器技术还没有被替代
叹息号的船

图片由Katherine McCormack拍摄,来源于Unsplash
古代哲学家提出了一个问题:经过数个世纪的维护,如果这些修补的 Theseus 号船的每一个部分都被逐个替换,它是否仍然是同一艘船?— 来源
transformer 在保持不变的同时发生了变化吗? 文献的简要搜索清楚地表明,如今有数百种 transformer 变体(如果考虑到所有位置编码、注意力等的变化)。所以 LLMs 并不是我们迄今为止看到的完全相同的模型。

Transformer 变体的分类。图片来源:这里。知识共享,许可证:这里
现在我们处于研究的增量阶段,在这个阶段,继续提出对相同模型的修改,以尝试克服现有技术的局限性。
在某种意义上,我们已经达到了 transformer 的极限。 扩大模型没有意义,因为这不会带来好处,也没有足够的 tokens。其次,transformer 的计算成本高昂,如果说过去性能重要的话,如今应用才是关键。第三,COT 和其他技术只是对 transformer 真实极限的补丁:模型并不真正具备理解和概括的能力。自注意力允许模型利用在训练过程中学到的大量知识,但它仍然是一个随机的鹦鹉。
transformer 是一种快速捕捉任何输入不同部分之间交互的方式。它是一种通用方法,可以捕捉句子中片段之间的交互,或音乐中的音符,或图像中的像素,或蛋白质的部分。它可以用于任何任务。” — Ashish Vaswani,transformer 论文的作者(来源)
从成本的角度来看,我们将会看到越来越多的小型专用模型用于不同的应用。 此外,今天的研究大多在寻找一种比 transformer 更便宜的替代品(例如 Hyena,Monarch Mixer,BiGS 和 MEGA)。
此外,变换器模型缺乏持续学习能力(它们具有静态参数)且缺乏解释性。液体神经网络(一种受蠕虫大脑工作方式启发的模型)旨在特别解决这两个问题。
此外,单一的变换器理念已被放弃,人们更多地考虑将多个模型组合在一起工作(GPT-4 似乎是由 8 个模型组成的)。Sakana AI 也追求相同的概念,通过借鉴集体智能的理念。
但这些模型会取代变换器吗?
可能不会。 或者在某些情况下,它们会被采用以满足特定需求。现阶段,这些模型尚未解决变换器的所有问题,对于许多应用来说,老旧的变换器已经足够。
目前,变换器在许多领域几乎具有绝对的主导地位。此外,许多研究和工作都基于这一架构并促成了其优化。因此,替代它是具有挑战性的。
像液体网络或群体智能这样的理念表明,人们在寻找超越构建更高效、更便宜模型的替代方案。然而,今天的研究仍未能克服变换器的真正限制。因此,变换器将被替代,但我们尚不清楚由什么来取代。它将是一项基于新理论进展的新技术。
TL;DR
-
变换器并行化的优势导致了大规模语言模型的过度增长,这种情况已经结束。意识到需要大量高质量数据使得对创建更大模型的热情减退。此外,部署成本使得更大的模型难以实现。
-
变换器在人工智能所有领域中的绝对主导地位已经不再。最近的一些论文表明,当以相同方式训练时,卷积网络在计算机视觉领域对变换器具有竞争力。它们在自然语言处理中的主导地位仍然不可否认,但基于脆弱的前提。
-
变换器可能会被一种能够接受文本指令并在上下文中学习的模型取代。所有的提示工程技术可以保持不变。
-
大型公司因官僚作风而不愿创新,现在专注于应用研究。另一方面,开源社区非常活跃,潜在的创新会被迅速采纳和推动。
-
变压器的真正优势在于它多年拥有绝对主导地位。因此,它受到了大量关注、研究和优化。能够取代这样一个模型是非常困难但仍然必要的挑战。实际上,我们已经基本达到了这项技术的极限,需要一种能够超越它的新技术。
-
虽然目前我们没有替代方案,但研究正在寻找能够超越它的模型。
离别思考

图片来自 Saif71.com 在 Unsplash
目前,人工智能的研究主要是增量性的,特别是在模型架构方面。大型公司现在致力于最终将这些模型投入生产并用于商业目的。
确实,在经历了一年的重大公告后,相关机构也开始行动,新法律正在准备中,将对人工智能进行规范。这些法律也将在一定程度上定义新的研究方向。如果这些法律过于僵化,将会抑制创新。
“确实有一些大型科技公司宁愿不与开源竞争,所以他们制造了对人工智能导致人类灭绝的恐惧。这已成为游说者用来推动对开源社区非常有害的立法的武器。” — Andrew Ng (source)
Yann LeCun 也持相同看法。目前,大公司对开源感到恐惧:
“如果你的恐吓活动成功,它们将不可避免地导致你和我认为的灾难:少数公司将控制人工智能。” — Yann LeCun (source)
无论如何,正如我们所见,没有任何技术会永远主导,而变压器也开始显示出其时代的局限性。考虑到将来会取代它的技术、理论进展、优雅的解决方案以及它将拥有的惊人能力,令人兴奋。
你认为什么会取代变压器?你尝试过变压器的替代品吗?在评论中告诉我。
如果你觉得这些内容有趣:
你可以查看我的其他文章,也可以在LinkedIn上联系我。 查看 这个库 ,其中包含每周更新的机器学习和人工智能新闻。 我对合作和项目持开放态度 ,你可以在 LinkedIn 上联系我。
这里是我 GitHub 库的链接,我在这里收集了与机器学习、人工智能等相关的代码和许多资源。
[## GitHub - SalvatoreRa/tutorial:机器学习、人工智能、数据科学教程…
机器学习、人工智能、数据科学教程,包括数学解释和可重复使用的代码(用 Python 编写…
或者你可能对我的一篇近期文章感兴趣:
提取大型模型知识的过程复杂,但一种新方法展现了惊人的表现
[towardsdatascience.com [## 神经网络集成:什么比神经网络更好?一组神经网络
神经网络集成:如何将不同的神经网络结合成一个强大的模型
级别提升 [## 仅谈论你所读过的内容:LLMs 能否超越其预训练数据进行泛化?
揭示大型语言模型中上下文学习的局限性与奇迹
pub.towardsai.net [## 机器学习新闻:11 月 6 日至 12 日
OpenAI 开发,TopicGPT,新芯片等
salvatore-raieli.medium.com](https://salvatore-raieli.medium.com/ml-news-week-6-12-november-9878eb0a7005?source=post_page-----297e6f14e189--------------------------------)
参考文献
这是我撰写本文时参考的主要文献列表,仅引用了每篇文章的首名。
-
Vaswani, 2017, 《注意力即是你所需要的一切》,link
-
Huang, 2016, 《密集连接卷积网络》,link
-
Zhao, 2023, 《大型语言模型的综述》,link
-
Smith, 2023, 《ConvNets 在规模上匹配视觉变换器》,link
-
Goldblum, 2023, 《骨干的战斗:预训练模型在计算机视觉任务中的大规模比较》,link
-
Wang, 2023, 《无注意力的预训练》,link
-
Ma, 2023, 《Mega:配备移动平均的门控注意力》,link
-
Hasani, 2020, 《液态时间常数网络》,link
-
Yadlowsky, 2023, 《预训练数据混合使变换器模型具备狭窄的模型选择能力》,link
-
Kaplan, 2020, 《神经语言模型的扩展规律》,link
-
Hoffman, 2022, 《训练计算最优的大型语言模型》,link
-
Simonyan, 2014, 《用于大规模图像识别的非常深的卷积网络》,link
-
Khan, 2022, 《视觉中的变换器:综述》,link
-
Wei, 2022, 《链式思考提示在大型语言模型中引发推理》,link
-
Touvron, 2023, 《LLaMA:开放高效的基础语言模型》,link
倾向评分建模方法回顾
原文:
towardsdatascience.com/a-review-of-propensity-score-modelling-approaches-19af9ecd60d9

图片为作者原创。
对使用倾向评分进行因果推断建模的不同方法的回顾
·发表于 Towards Data Science ·阅读时间 11 分钟·2023 年 5 月 17 日
--
在这篇文章中,我将介绍倾向评分的概念及其用途,然后介绍 3 种常见的方法。我将讨论以下倾向评分模型:
-
带替代的倾向评分匹配(PSM)
-
不带替代的倾向评分匹配(PSM w/o)
-
逆倾向评分加权(IPSW)
介绍
评估特定干预或治疗效果的最佳方法是进行随机对照试验(RCT)。在 RCT 中,你将人群随机分为两个组,并只对其中一个组施加干预——这成为你的治疗组。未接受干预的组是对照组。由于对照组和治疗组之间的随机分配,两组之间的特征应无结构性差异。如果治疗后,治疗组的行为(即转化)有所不同,那么我们可以得出这是干预的结果。
然而,有许多情况下无法进行随机对照试验(RCT),包括但不限于:
-
出于伦理原因——例如,产品定价需要在用户之间保持一致
-
治疗效果无法数字化测量,例如广告牌
-
你的技术栈意味着你不能创建两个体验
倾向评分建模
倾向评分建模允许你对治疗组和对照组进行分层,以去除可能作为混杂因素的行为和人口统计学偏差

比较看到营销的人和未看到营销的人的问题的简化示例。图片为作者原创。
倾向性评分建模允许你在每个人都有潜在暴露于治疗的情况下推断干预与反应之间的因果关系。虽然每个人可能会暴露于治疗,但并不是每个人都会——总会有些人没有。显然,看到你的干预和没有看到干预的人的行为是不同的——治疗组被暴露是有原因的,对照组则没有。如果导致治疗的行为与转化相关,那么你就有了混杂因素。正是治疗组中人们的特征导致了他们既被暴露于干预又发生了转化。倾向性评分方法允许你对治疗组和对照组进行分层,以去除可能作为混杂因素的行为和人口统计偏差。
在上面的图示中,青色的人更有可能暴露于营销,因此在治疗组中占比过高。如果青色的人比深蓝色的人更有可能转化,那么你自然会期待治疗组有更多转化。然而,这种现象是由青色人的比例驱动的,与某人是否看到营销无关。
什么是倾向性评分?
倾向性评分是一个个体将被暴露于治疗的概率。如果我们拿 100 个相同的客户,其中 70 个暴露于治疗,那么它们的倾向性评分都是 0.7
计算倾向性评分的最常见方法是拟合一个逻辑回归分类器来预测治疗组,然后使用与治疗组相关的概率作为倾向性评分。这种方法的好处是操作非常简单,避免了过拟合。缺点是逻辑回归分类器被训练来分类每个样本为治疗组或对照组,具体概率只是倾向性评分的代理。
倾向性评分模型
有 3 种常用的倾向性评分模型,我们将在本节中讨论每种模型的优缺点。
带替换的倾向性评分匹配
每个治疗样本会与具有最相似特征的对照样本进行匹配,特征通过倾向性评分进行测量
在倾向性评分匹配中,你将每个治疗样本与具有最相似倾向性评分的对照样本配对。每个对照样本可以与多个治疗样本匹配,并且并不是每个对照样本都会被匹配。

倾向性评分匹配的可视化。中间行显示了匹配的对照组。匹配过程由灰色箭头表示。图片由作者提供。
在上面的图示中,你可以看到:
-
治疗组中的青色人员会与对照组中的一个青色人员进行匹配
-
治疗组中的 9 名海军人员全部与对照组中的唯一一名海军人员匹配。结果是,对照样本被重复了 9 次。
-
对照组中有 8 人未被匹配(浅绿松石色),因此被排除在我们的分析之外。
优点
- 进行匹配过程的计算效率高 —— 通常你会使用标准的最近邻算法来实现这一点。
缺点
- 多重匹配的对照样本可能会影响治疗效果。
无替代的倾向评分匹配
每个治疗样本与特征最相似的对照样本进行匹配,但每个对照样本只能被匹配一次。
无匹配 PSM 在匹配执行方式上与 PSM 不同。每个对照样本只能与一个治疗样本匹配。执行匹配过程的顺序可能会影响模型性能,因为首先匹配的治疗样本有更多对照样本可供选择。后来的治疗样本必须与对照组中剩下的样本匹配,这些样本通常没有非常相似的倾向评分。为了消除由于匹配顺序引入的方差,你可以用不同的顺序引导匹配过程,尽管这会增加较大的计算开销。

无匹配的 PSM 的可视化。匹配过程由灰色箭头和虚线表示,虚线表示匹配效果差。图像为作者原创。
在上述可视化中我们可以看到:
-
治疗组中的绿松石色人员与对照组中的一名绿松石色人员匹配。
-
治疗组中的第一个海军人员与对照组中唯一的海军人员匹配。
-
治疗组中剩余的海军人员与对照组中的绿松石色人员匹配 —— 匹配效果差。
优点
- 对照组中的每个人最多出现一次,因此没有单个样本可以影响治疗效果。
缺点
-
可用于匹配过程的开放源代码包有限。
-
由于需要跟踪哪些样本已经被匹配,这比 PSM 效率低。
-
需要引导抽样,这增加了计算开销。
-
匹配效果差可能导致协变量的残留偏差
逆倾向加权
每个对照样本都被加权,以便对照组整体上与治疗组匹配。
在逆倾向加权中,我们根据每个对照组样本的倾向评分对其施加一个权重。治疗样本的权重为 1。ith 对照样本的权重可以使用以下公式计算:

其中 pi 是样本的倾向评分。上述形式的权重意味着相对于处理组,对照组中具有特征不足的样本(倾向评分 ~ 1)被赋予更高的权重,权重 > 1。对照组中具有过多特征的样本(倾向评分 ~ 0)则被赋予较低的权重,权重 < 1。在我们的示例中,我们可以使用以下权重:


IPSW 的可视化。中间一行显示了加权对照组。在这种情况下没有个体匹配过程。图片为作者自制。
在上述视觉效果中,我们可以看到:
-
对照组中的 9 个青色样本贡献了 1 的总权重,以匹配处理组中的一个青色样本
-
对照组中的一个海军蓝样本获得了 9 次投票,以匹配处理组中的 9 个海军蓝样本
优点
- 高效,因为我们不是匹配样本,而是对每个样本独立执行可向量化的计算
缺点
- 权重较高的样本可能会偏倚处理效果
评估倾向评分模型
协变量偏差
协变量偏差意味着处理组和对照组之间在特征上存在可测量且统计显著的差异
如果你分析了对照组和处理组的特征,很可能会发现它们之间的差异。也许更活跃的用户更可能接触到干预,并出现在你的处理组中。如果你试图提高留存率,那么处理组中更活跃的用户可能会比对照组中的用户留存更长时间,这种情况就属于偏差。倾向评分建模如果应用得当,应当去除对照组和处理组之间在所有特征上的偏差。
我们可以使用绝对标准化均值差来测量与特定协变量相关的偏差,该差异定义为

这里 x̄ 是样本均值,t 和 c 的下标分别表示处理组和对照组。|.| 表示取绝对值。对于连续变量,s 定义为

对于二元变量,它定义为

σ 是标准差,n 是样本数量。在 IPSW 中,我们可以用权重的总和替代 n — 1,并且 x̄ 被替换为加权均值。
s 的定义本质上是对照组和处理组方差的加权平均的平方根。加权平均使用 n — 1 作为权重,类似于总体的无偏标准差。
如果smd > 0.1 对于你的某个协变量,我们就可以得出结论,治疗组和对照组之间存在偏差,并且这个协变量可能作为混杂因素。0.1 的阈值相当于在 AB 测试中使用α = 0.05 的双侧检验来检测统计显著结果。
倾向评分的挑战
使用分类器来估计倾向评分有两个问题:
-
准确性:如果我们选取 100 位非常相似的客户,其中 70 人接受了治疗,那么他们的倾向评分都应该是 0.7
-
自洽性:如果我们选取 100 位倾向评分为 0.7 的客户,那么其中 70 人应该接受治疗
为了完全消除倾向评分建模中的偏差,你应该有准确且自洽的倾向评分。
解决自洽问题
通过重新调整倾向评分,自洽是相当容易实现的。在下图中,我将倾向评分四舍五入到最接近的 0.01,以将人们分到离散的桶中(x轴)。然后,y轴上显示的是每个桶中治疗组样本的比例。
一般来说,逻辑回归在较高值时低估了倾向评分,而在较低值时则高估了。大约 70%的倾向评分为 0.6 的人在治疗组中——按定义,这个比例应该是 60%。
我还包含了一条拟合了关系的tanh曲线。通过将逻辑回归中的概率通过此tanh函数,我们可以重新调整倾向评分以实现自洽。现在倾向评分为 0.6 的人会被调整为 0.7,我们可以看到 70%的人会在治疗组中。
然而,这种方法假设具有相似倾向评分的人具有相似的基础特征——虽然大体上这是真的,但不一定足够准确以消除偏差。

在不同倾向评分下的治疗密度比较(海军蓝点)。粉色线显示了通过数据拟合的tanh 曲线。图片为作者原创。
解决准确性问题
准确性是一个挑战,因为我们没有任何实际的倾向评分来训练回归模型,这就是为什么我们训练了一个分类器。没有实际的值来与分类器的倾向评分进行比较,我们也无法衡量倾向评分的准确性。准确性本质上是一个潜在变量,我们必须假设如果无法证明我们的倾向评分是不准确的。例如,如果倾向评分是自洽的,并且它们消除了协变量偏差,那么它们可能足够好用。
方法概述
以下是总结每种方法优缺点的表格。我定义了三种对倾向建模重要的特征,它们是:
-
匹配独立性 — 每个处理样本的匹配可以独立于其他样本进行。这一特性体现了该方法的计算效率
-
没有过度匹配 — 没有任何对照样本被匹配或加权到如此程度,以至于存在主导对照组转化率的风险
-
低多样性 — 一个仅包含少量样本的对照组,这些样本被多次匹配。低多样性类似于过度匹配,但许多对照样本被重复。根据你的过度匹配阈值,可能会出现低多样性但没有过度匹配的情况。

各倾向评分方法的特征表。图片为作者自制。
讨论
PSM 和 IPSW 都具有独立匹配,因此它们是计算上高效的方法。对于 PSM w/o 的实施支持较少,定制实现建立在顺序匹配和自助抽样基础上,使得在实践中使用它变得具有挑战性。
PSM w/o 是唯一一种完全避免过度匹配的方法,因为它使用的是一对一匹配。然而,这可能会因为匹配质量较差而导致偏倚减少效果不佳。过度匹配虽然在 PSM 和 IPSW 中都有出现,但实际上是由不同的途径引起的。在 IPSW 中,过度匹配只会发生在高倾向评分处,这时对照样本的比例非常小,每个样本都被分配了较大的权重。如果你的完整样本中的倾向评分仅在 0.2 到 0.8 之间,那么在 IPSW 中你不会遇到过度匹配的问题。在 PSM 中,过度匹配可以在任何倾向评分下发生,因为它更多地与对照组和处理组中的倾向评分的具体值和分布相关。
PSM 容易因为少量的对照样本看起来最像处理组而遭受低多样性,因此这些样本会被多次匹配。PSM w/o 将保留完整的对照组(如果对照组和处理组的大小相等),但这可能会导致匹配效果较差,从而未能有效去除偏倚。ISPW 保留了整个对照组,并且由于它可以增加或减少对照样本的权重,因此能够实现多样化的对照组,同时有效地去除偏倚。
结论
在这篇文章中,我介绍并讨论了 3 种在因果推断中使用倾向评分的方法。由于其实施简单,带替换的倾向评分匹配在业界受到最多关注,但如果没有适当分析,可能会导致未能适当地去除偏差和准确建模真实处理效应。无替换的倾向评分建模往往在计算上非常昂贵,并且可能在根本上未能去除偏差,尽管它在生成多样化的对照组方面确实提供了几个优点。逆倾向评分加权是一种非常被忽视的方法,它结合了带替换倾向评分匹配的计算效率和无替换倾向评分匹配的稳健性。如果你的倾向评分没有接近 1,那么它无疑是这三种方法中最优的。
从备用零件构建的无服务器查询引擎
一个基于 DuckDB 和 AWS Lambdas 的开源数据湖实现
·
关注 发表在 Towards Data Science ·9 分钟阅读·2023 年 4 月 27 日
--
云中的一只鸭子。照片由László Glatz拍摄,来源于Unsplash。
在这篇文章中,我们将展示如何在无服务器基础设施上构建一个简单的端到端应用程序。我们的目标很简单:我们想展示可以直接在云中开发,同时最小化设计和构建基础设施的认知负担。此外,我们将提出一个设计方案,与现代数据仓库(如 Big Query 或 Snowflake)相比,能够最小化成本。
作为数据从业者,我们希望(并且喜欢)尽可能无缝地在我们的数据上构建应用程序。无论你是从事 BI、数据科学还是机器学习,最终的应用程序和你看到它端到端运行的速度才是最重要的。然而,基础设施往往会成为障碍。
想象一下,作为一个实际例子,我们需要为我们的产品团队构建一个新的面向客户的分析应用。因为这是面向客户的,所以我们需要尊重一些性能约束,例如低延迟。
我们可以直接在云中开始开发,但这会立即引出一些基础设施问题:我们在哪里运行它?我们需要多大的机器?由于低延迟的要求,我们是否需要构建一个缓存层?如果是的话,我们该怎么做?
另外,我们可以在本地开发我们的应用。从开发者体验的角度来看,这可能更直观,但这只是推迟了基础设施问题,因为最终我们必须找到一种方法将本地项目转移到实际的管道中。此外,数据需要离开云环境到达我们的机器,这并不完全安全和可审计。
为了使云体验尽可能顺畅,我们设计了一个数据湖架构,其中数据存储在一个简单的云存储(AWS S3)中,嵌入 DuckDB 的无服务器基础设施作为查询引擎。在周期结束时,我们将拥有一个分析应用,可以实时可视化和查询数据,几乎没有基础设施成本。
当然,这有些简化,因为在实际生产场景中运行这个项目还需要一些调整。我们提供的是一个通用蓝图,利用存储和计算的分离来在云端构建数据湖和查询引擎。我们展示了如何通过一个(几乎)免费的云端终端、无需仓库设置以及闪电般的性能来支持一个交互式数据应用。在我们的实现中,最终的应用是一个简单的 Streamlit 应用,但这仅仅是为了说明目的:你可以轻松地考虑将其与自己喜欢的 BI 工具连接起来。

用我们的系统构建的闪电般快速的分析应用。图片来自作者。
鸭子无服务器化
你们现在都知道 DuckDB 了。它是一个开源的内存中 SQL OLAP 数据库,专为分析查询而构建。DuckDB 实际上在 生产环境中使用 的情况还不太清楚,但对我们来说,杀手级功能是可以用 SQL 语法直接查询 S3 中的 parquet 文件。
因此,大多数从业者现在似乎将其作为本地引擎进行数据探索,临时分析,POC 和原型制作(同时一些创新的想法则扩展其初始目的以涵盖更多领域)。人们使用嵌入 DuckDB 的笔记本或小型数据应用程序来原型设计和实验生产数据。
云更好。如果我们常常感觉不是这样,那是因为我们使用的工具链出了问题,但是一个坏主意和一个好主意糟糕执行之间有很大的区别。
如果我们结合数据湖架构、无服务器设计、DuckDB 和一些机智,我们可以从备件构建一个非常快速的数据堆栈:无需仓库设置,性能极快,成本非常便宜 —— S3 最昂贵的标准价格是每 GB $0.023,AWS Lambda 很快,并在不使用时缩减至零,因此它是一个无负担的计算账单,而且 AWS 还免费提供 100 万次调用。
架构
该项目非常自包含,只需要对云服务和 Python 有入门级别的熟悉度。
思路是从存储数据的数据湖开始。一旦数据以 Parquet 格式上传到我们的 S3 中,我们就可以使用 SQL 查询触发 Lambda。此时,我们的 Lambda 启动,在内存中启动 DuckDB 实例,计算查询并将结果返回给用户,可以直接在终端中呈现为表格。
该架构具有多个优点,主要来自无服务器设计:速度快,接近数据,一键部署很好;当然,系统在不使用时可以缩减至零,因此我们只需按查询付费。

我们系统的总体架构。来自作者的图片
您的第一个查询引擎 + 数据湖来自备件
我们提供了一个简单的脚本,将创建一个 S3 存储桶,并将纽约市 TLC 行程记录数据的一部分填充进去(可在nyc.gov 使用条款下使用),分别作为唯一文件和分区目录(您可以使用Make运行)。一旦数据存储在数据湖中,我们可以设置和使用我们的 Lambda:如果您已正确设置 Serverless CLI,则部署 Lambda 又是一条 Make 命令的事情。
Lambda 可以通过任何常见方式调用,并接受一个查询作为其主要负载:当它运行时,它使用 DuckDB(如果是热启动,则重用实例)在数据湖中执行查询。DuckDb 在执行前后对数据一无所知,使得 lambda 在数据语义上完全无状态。
例如,你可以使用项目中的简单 Python 脚本将这个查询发送到 lambda,并显示结果:
SELECT
pickup_at as pickup_time,
dropoff_at as dropoff_location,
trip_distance
FROM read_parquet(your_s3_bucket/dataset/taxi_2019_04.parquet')
WHERE pickup_at >= '2019–04–01' AND pickup_at < '2019–04–03'
LIMIT 10

结果会直接在你的终端中可视化。图片来自作者。
看!你现在可以在云端安全地查询你的数据湖。这种非常简单的设计直接解决了在云数据仓库中工作的两个典型摩擦点:
-
由于所有用户需要做的只是拥有她的 AWS 凭证,设置过程显著简化。一旦设置完成,用户只需访问 lambda(或任何代理!):这很好,因为它使用户在没有访问底层存储的情况下拥有完整的查询能力。
-
性能如此出色,以至于即使我们总是通过云端,也感觉像是在本地开发。一个快速的云体验有助于驯服那种在远程机器上工作的优势以良好的开发者体验为代价的过于熟悉的感觉。
(几乎)免费分析
一切都很好而且奢华,但假设我们想做的事情比即时查询数据更多。假设我们想在一个表的基础上构建一个应用程序。它是一个非常简单的应用,没有编排,也不需要校准工作负载。
与此同时,假设这个应用需要具备响应能力,它还需要快速。直接与客户打交道的人都知道,为那些希望查看数据的客户提供一个新鲜的响应式体验是多么重要。没有人喜欢一个加载需要几分钟的仪表盘。
为了了解这种架构如何弥合数据管道与实时分析查询之间的差距,我们提供了一个小型的 dbt DAG 以模拟对原始数据集进行一些离线 SQL 转换,从而在数据湖中生成一个新成果(相当于仪表盘视图)。
为了尽可能保持自给自足,我们包括了一个可以在本地机器上运行的版本(请参阅 README 获取更多细节—— dbt 或其背后的引擎对该模式的工作无关紧要)。然而,你可以使用不同的 dbt 运行时,并将最终成果导出为 parquet 文件,使用 Snowflake 或 BigQuery。

我们的系统架构与一个 dbt 项目配对。图片来自作者。
目前,我们将坚持使用我们由两个节点构成的超级简单 DAG。第一个节点从我们的数据湖中提取 pickup_location_id,并按旅行次数排序:
SELECT
pickup_location_id AS location_id,
COUNT(*) AS counts
FROM read_parquet(['s3://{{ env_var('S3_BUCKET_NAME') }}/dataset/taxi_2019_04.parquet'])
GROUP BY 1
第二个节点提供了数据集中前 200 个取车地点:
SELECT
location_id,
counts
FROM {{ ref('trips_by_pickup_location') }}
ORDER BY 2 DESC
LIMIT 200
我们可以使用 dbt 文档可视化 DAG:

一旦我们的管道完成,最终工件将上传回我们的数据湖中:
s3:/your_s3_bucket/dashboard/my_view.parquet
然后我们可以重用之前构建的查询引擎,查询 DAG 的第二个(也是最后一个)节点,以在 Streamlit 应用中可视化数据,只需在终端中运行:
(venv) cirogreco@Ciros-MBP src % make dashboard
每次我们访问仪表盘时,仪表盘都会访问幕后运行的 lambda。如果你喜欢这种简单的架构,相同的模式可以在你自己的 Streamlit 应用或你喜欢的 BI 工具中使用。
关于“合理规模”的几点说明
不久前,我们写了一系列关于我们称之为合理规模下的 MLOps的文章,讨论了在不处理互联网规模数据的公司中构建可靠 ML 应用的最佳策略,并且这些公司具有真正的大数据公司通常不具备的一些约束条件。我们主要从 ML 和 MLOps 的角度进行了讨论,因为成功地运作 ML 曾经是组织面临的一个主要问题(也许现在仍然是,我不确定),但有一个普遍的观察结果仍然存在:大多数数据组织都是“合理规模”的,他们应该围绕这一假设来设计他们的系统。需要注意的是,成为合理规模的组织并不一定意味着成为小公司。企业界充满了在大型——有时是巨大的——组织中处理复杂性的团队,但这些团队通常有许多合理规模的管道,通常是为了内部利益相关者,从几 GB 到最多 TB 不等。
最近,我们高兴地见证了一个关于公司是否需要大数据系统来解决数据问题的激烈辩论。从我们的观点来看,最重要的结论仍然是,如果你是一个“合理规模”的组织,处理不必要的基础设施可能会给你的组织流程带来很大的负担,产生许多不利影响。理论上,你可以构建一个完整的数据堆栈来支持低延迟仪表盘——也许你可以使用数据仓库和缓存层——但由于你的资源有限,是否可以有一种更简单、更便宜的方法呢?
在这篇文章中,我们展示了数据优先存储格式、按需计算和内存 OLAP 处理的组合如何在合理规模下开辟新的可能性。该系统虽然远未完善,且还需要许多改进,但它表明,人们可以构建一个无需数据仓库设置、性能极快且几乎无成本的互动数据应用。通过从 DuckDB 中移除数据库,我们可以将本地(“单节点处理是你所需的一切”)和云端(“数据在其他地方处理效果更好”)的优点结合起来。
我大部分时间都在思考无服务器数据基础设施。如果你感兴趣,或者对这篇文章有反馈,或者只是想聊聊,可以通过 ciros.greco@bauplanlabs.com 联系我。
使用机器学习进行层次时间序列预测的简单方法
Kaggle 蓝图
如何使用 LightGBM 和 Python “提升”你的周期性销售数据预测
·发布于 Towards Data Science ·8 分钟阅读·2023 年 3 月 14 日
--

层次时间序列预测(图由作者绘制)
欢迎来到另一期的 “Kaggle 蓝图” ,我们将分析 Kaggle 比赛的获胜方案,以寻找适用于我们自己数据科学项目的经验教训。
本期将回顾“M5 预测 — 精度”比赛中的技术和方法,该比赛于 2020 年 6 月底结束。
问题陈述:层次时间序列预测
“M5 预测 — 精度”比赛的目标是预测 42,840 个层次时间序列销售数据的接下来的 28 天。
估计沃尔玛零售商品的单位销量
www.kaggle.com](https://www.kaggle.com/competitions/m5-forecasting-accuracy/?source=post_page-----2e180d83966c--------------------------------)
层次时间序列 — 与常见的多变量时间序列问题不同,层次时间序列可以在不同层级上进行汇总:例如,商品层级、商店层级和州层级。在这次比赛中,参赛者获得了来自 3 个不同类别的 3000 种单独产品的超过 40,000 个时间序列,这些产品在 3 个州的 10 个商店中销售。

分层时间序列(图像由作者提供)
周期性——销售数据通常是周期性的,这意味着销售数据是时间依赖的。例如,你会看到重复的模式,比如在周末结束时销售增加(每周周期)、在月初(每月周期)或节假日期间(年度周期)。
多步——任务是预测 28 天后的销售数据(28 个步骤)。
单变量与多变量、单步与多步、滑动窗口与扩展窗口时间之间的区别……
towardsdatascience.com
要跟进本文,你的数据集应如下所示:

在这里插入你的数据:你的分层时间序列数据应如何格式化(图像由作者提供)
将时间序列预测视为一个回归问题来使用机器学习
竞争者中一种流行的方法是将时间序列预测问题表述为回归问题,并使用机器学习(ML)进行建模 [6]。
-
时间序列预测问题可以通过将预测拆分为单一步骤来形成回归问题——在数据点之间保持历史数据和预测之间的间隔恒定。
-
你可以将历史数据点汇总为历史特征,而不是将过去的值序列输入到机器学习模型中。

时间序列预测作为回归问题(图像由作者提供)
因此,使用机器学习来处理分层时间序列预测问题的主要步骤是:
-
建立简单基线
-
从历史数据中进行特征工程
-
使用机器学习建模和验证时间序列预测问题
第 1 步:建立简单基线
与任何传统的机器学习问题一样,我们将从建立一个简单的基线开始。对于时间序列预测问题,一个好的起点是将最后一个时间戳的值作为预测——这是一种朴素的方法。
如果你有周期性时间序列,可以通过参考上一个周期来改进朴素方法。例如,如果你的时间序列依赖于星期几,你可以取上个月的数据,按星期几分组,并取平均值 [2]。

时间序列预测的基线:朴素方法(图像由作者提供)
第 2 步:从历史数据中进行特征工程
与使用经典统计方法相比,特征工程是开发 ML 模型时的一个重要步骤。因此,你需要将历史数据聚合为历史特征[4],而不是直接将历史数据输入 ML 模型。
时间戳特征
一个时间序列至少有两个特征:时间戳和一个值。仅时间戳就可以用来创建多个新特征。
首先,你可以通过将时间戳拆解为其组成部分(例如,天、周、月、年等)来提取特征[4]。
# Convert to DateTime
df['date'] = pd.to_datetime(df['date'])
# Make some features from date
df['day'] = df['date'].dt.day
df['week'] = df['date'].dt.week
df['month'] = df['date'].dt.month
df['year'] = df['date'].dt.year
# etc.
其次,你可以基于日期创建新特征[1, 3]:它是工作日还是周末?是节假日吗?是否发生特殊事件(例如,体育赛事)?
df['dayofweek'] = df['date'].dt.dayofweek
df['weekend'] = (df['dayofweek']>=5)
# etc.
聚合特征
接下来,你可以通过聚合历史数据并创建统计特征,如最大值、最小值、标准差和均值[1, 3, 4, 8, 10],来创建新特征。
因为我们处理的是分层时间序列,所以我们将按不同的LEVEL(例如store_id)对时间序列进行分组。
FEATURE = 'price'
LEVEL_1 = 'store_id'
LEVEL_N = 'item_id'
# Basic aggregations
df[f'{FEATURE}_max'] = df.groupby([LEVEL_1, LEVEL_N])[FEATURE].transform('max')
df[f'{FEATURE}_min'] = df.groupby([LEVEL_1, LEVEL_N])[FEATURE].transform('min')
df[f'{FEATURE}_std'] = df.groupby([LEVEL_1, LEVEL_N])[FEATURE].transform('std')
df[f'{FEATURE}_mean'] = df.groupby([LEVEL_1, LEVEL_N])[FEATURE].transform('mean')
# Normalization (min/max scaling)
df[f'{FEATURE}_norm'] = df[FEATURE]/df[f'{FEATURE}_max']
# Some items are can be inflation dependent and some items are very "stable"
df[f'{FEATURE}_nunique'] = df.groupby([LEVEL_1, LEVEL_N])[FEATURE].transform('nunique')
# Feature "momentum"
df[f'{FEATURE}_momentum'] = df[FEATURE]/df.groupby([LEVEL_1, LEVEL_N])[FEATURE].transform(lambda x: x.shift(1))
滞后特征
一个流行的时间序列数据特征工程技术是创建滞后特征[4, 5, 10]。为了在测试数据上使用此特征,滞后时间应大于训练数据和测试数据之间的时间间隔。

7 天的滞后(作者提供的图像)
LEVEL = 'store_id'
TARGET = 'sales'
lag = 7
df[f"lag_{lag}"] = df.groupby(LEVEL)[TARGET].shift(lag).fillna(0)
滚动特征
另一种流行的时间序列数据特征工程技术是基于滚动窗口创建特征(例如,均值或标准差)[1, 3, 10]。
你可以直接将此特征工程技术应用于FEATURE,甚至应用于其滞后版本。

28 天滚动窗口的均值(作者提供的图像)
window = 28
df[f"rolling_mean_{window}"] = df.groupby(LEVEL)[FEATURE].transform(lambda x : x.rolling(window).mean()).fillna(0)
作为分类特征的层级
在处理分层时间序列时,你还可以将不同层级的节点标识符(例如store_id、item_id)作为分类变量[1, 3]。
在将数据输入 ML 模型之前,你的结果数据框应如下所示:

用于时间序列预测的 ML(GBDT)模型的训练数据结构(作者提供的图像)
第 3 步:使用机器学习建模和验证时间序列预测问题
在建模和验证普通 ML 问题(例如回归或分类)与使用 ML 进行的分层时间序列预测问题之间存在一些差异。
建模多变量和分层时间序列
建模分层时间序列问题类似于建模多变量问题。
多变量时间序列建模 — 自回归模型和序列到序列模型通常一次只能建模一个时间序列(单变量时间序列问题)。因此,当遇到多变量时间序列问题(如层级时间序列)时,你需要构建多个预测模型 — 每个时间序列一个模型。
许多参赛者使用了 LightGBM,一个机器学习模型和梯度提升框架 [1, 3, 5, 7, 8, 10]。使用 LightGBM 时,你可以用一个 LightGBM 模型建模多个时间序列,而不需要构建多个预测模型。

多变量时间序列建模策略(图像来源:作者)
由于时间序列数据是层级结构的,许多参赛者按层级对类似时间序列进行分组(例如,按商店)并将它们一起建模 [3, 8, 10]。

使用机器学习的层级时间序列预测建模策略(图像来源:作者)
验证预测模型
在验证时间序列预测模型时,关键是要牢记时间序列的时序顺序 [6]。如果你使用了流行的 KFold 交叉验证策略,你会使用未来的数据来预测过去的事件。在预测时,必须避免泄露未来信息以对过去做出预测。

避免在时间序列预测验证中泄露未来信息以对过去做出预测(图像来源:作者)
相反,你应该定义一些交叉验证期,然后用该期间之前的所有数据来训练一个模型 [3, 8, 10]。例如,对于最后一个月的每周 (VALIDATION_PERIOD = 7)(N_FOLDS = 4)。

时间序列预测的交叉验证(图像来源:作者)
为了将所有内容整合在一起,你可以参考以下代码片段:
from datetime import datetime, timedelta
import lightgbm as lgb
N_FOLDS = 4
VALIDATION_PERIOD = 7
for store_id in STORES_IDS:
for fold in range(N_FOLDS):
training_date = train_df['timestamp'].max() - timedelta(VALIDATION_PERIOD) * (N_FOLDS-fold)
valid_date = training_date + timedelta(VALIDATION_PERIOD)
print(f"\nFold {fold}: \ntraining data from {train_df['timestamp'].min()} to {training_date}\nvalidation data from {training_date + timedelta(1)} to {valid_date}")
train = train_df[train_df['timestamp'] <= training_date]
val = train_df[(train_df['timestamp'] > training_date) & (train_df['timestamp'] <= valid_date) ]
X_train = train[features]
y_train = train[target]
X_val = val[features]
y_val = val[target]
train_data = lgb.Dataset(X_train, label = y_train)
valid_data = lgb.Dataset(X_val, label = y_val)
estimator = lgb.train(lgb_params,
train_data,
valid_sets = [valid_data],
verbose_eval = 100,
)Mo
在评估层级时间序列预测模型时,可能需要创建一个简单的仪表盘 [9] 来分析模型在每一层级的表现。
总结
从回顾 Kagglers 在“M5 预测—准确性”竞赛过程中创建的学习资源中,你可以学到更多的经验教训。这类问题声明也有很多不同的解决方案。
在本文中,我们关注了许多参赛者普遍采用的一般方法:将时间序列预测问题表述为回归问题,从历史数据中工程化特征,然后应用机器学习模型。
数据集
本文使用了合成数据,因为原始比赛数据集仅供非商业用途。本文使用的时间序列是由正弦波、线性函数和白噪声信号的和生成的。
享受这个故事了吗?
免费订阅 以在我发布新故事时收到通知。
[## 订阅以获取 Leonie Monigatti 的最新文章通知。
每当 Leonie Monigatti 发布新文章时,你将收到电子邮件通知。通过注册,你将创建一个 Medium 帐户(如果你还没有的话)……
medium.com](https://medium.com/@iamleonie/subscribe?source=post_page-----2e180d83966c--------------------------------)
在 LinkedIn,Twitter,和 Kaggle上找到我!
参考文献
[1] Alan Lahoud (2020). Kaggle 讨论中的第 5 名解决方案(访问日期:2023 年 3 月 7 日)
[2] Chris Miles (2020). 简单模型:按工作日分组的最近 28 天的平均值 在 Kaggle Notebooks(访问日期:2023 年 3 月 6 日)
[3] Eugene Tang (2020). Kaggle 讨论中的第 7 名解决方案(访问日期:2023 年 3 月 7 日)
[4] Konstantin Yakovlev (2020). M5 — 简单特征工程 在 Kaggle Notebooks(访问日期:2023 年 3 月 7 日)
[5] Konstantin Yakovlev (2020). M5 — 三种黑暗阴影:更黑暗的魔法 在 Kaggle Notebooks(访问日期:2023 年 3 月 7 日)
[6] LogicAI (2023). Kaggle Days Paris 2022_Jean Francois Puget_ 销售预测和欺诈检测 在 YouTube 上(访问日期:2023 年 2 月 21 日)
[7] Matthias (2020). Kaggle 讨论中的第 2 名解决方案(访问日期:2023 年 3 月 7 日)
[8 ] monsaraida (2020). Kaggle 讨论中的第 4 名解决方案(访问日期:2023 年 3 月 7 日)
[9] Tomonori Masui (2020). M5 — WRMSSE 评估仪表板 在 Kaggle Notebooks(访问日期:2023 年 3 月 7 日)
[10] Yeonjun In (2020). Kaggle 讨论中的第 1 名解决方案(访问日期:2023 年 3 月 7 日)
一个简单的 ML 项目 CI/CD 设置
原文:
towardsdatascience.com/a-simple-ci-cd-setup-for-ml-projects-604de7fd64cd

图片由 vackground.com 提供,来源于 Unsplash
应用最佳实践并学习使用 GitHub Actions 来构建稳健的代码
·发表于 Towards Data Science ·7 分钟阅读·2023 年 12 月 20 日
--
介绍
处理集成、部署、可扩展性以及所有那些使机器学习项目成为真正产品的主题本身就是一项工作。这就是为什么从数据科学家到 ML 工程师和 MLOps 之间存在不同职位的原因。尽管你不需要成为这些领域的专家,但拥有一些标准的明确定义的实践来帮助你启动项目是很好的。确实如此!在这篇文章中,我概述了我开发的最佳实践——在代码质量和实施时间之间的平衡。我在 Deepnote 上运行我的代码,这是一款基于云的笔记本,非常适合协作数据科学项目。
从简单开始 — README
这看起来可能很琐碎,但尽量保持 README 文件的更新。如果花费时间不多,并且你喜欢这样做,尝试让 README 也看起来好看。包括想象中的标题图标或其他内容。这个文件必须清晰易懂。记住,在实际项目中,你不仅仅是与其他开发者合作,还会与销售人员和项目经理合作,他们可能需要阅读 README 以了解你在做什么。
你可以在这里找到一个非常棒的 README 模板!
[## GitHub - othneildrew/Best-README-Template: 一个极好的 README 模板,助力你的项目启动!
一个极好的 README 模板,可以帮助你快速启动项目! - GitHub - othneildrew/Best-README-Template: 一个极好的 README…
github.com](https://github.com/othneildrew/Best-README-Template?source=post_page-----604de7fd64cd--------------------------------)
使用虚拟环境,你的笔记本电脑会很高兴。
你可能比我更清楚,为了开发一个酷炫的项目,我们需要外部库。通常需要很多这些库!这些库可能有依赖关系或冲突。这就是创建虚拟环境的好主意。虚拟环境可以帮助你让项目相互隔离,拥有完全不同的开发环境。通常,在 Python 中你使用 pip 或 conda 来做到这一点。
PyPA 推荐的 Python 包安装工具。
pypi.org](https://pypi.org/project/pip/?source=post_page-----604de7fd64cd--------------------------------)
我个人是 pip 的粉丝。这是如何创建和激活虚拟环境。
#create virtual env
python3 -m venv .venv
#activate virtual env
source .venv/bin/activate
现在你可以安装所有你想要的库了!
创建一个 Requirements 文件,你的同事会很高兴。
在机器学习这样的领域,如果不允许代码和实验的可重复性,那么编写代码是没有意义的。开始的地方应该是创建一个 requirements.txt 文件。
[## Requirements 文件格式 - pip 文档 v23.3.2
Requirements 文件作为 pip 安装的项目列表,当使用 pip install 时。使用这种格式的文件…
我不能运行其他人编写的代码,如果我不知道那个人安装了什么库来运行代码。因此,你应该保留一个名为 requirements.txt 的文本文件,其中输入所有库的名称。你可以手动编辑这个文件,也就是说,每次使用 pip 安装库时,你也将库的名称输入到 requirements 文件中。或者你可以使用一个有用的 pip 命令,自动将所有已安装的库直接输入到 requirements 文件中。让我们看看如何做。
如果你运行以下命令
pip freeze
你会看到终端中出现所有已安装库的列表。现在只需使用终端技巧将此命令的输出重定向到 requirements.txt 文件,而不是终端显示。
pip freeze > requirements.txt
如果你现在检查你的 requirements,你会看到它们已自动更新!
如果你想在新的虚拟环境中自动安装所有要求,你可以运行以下命令:
pip intall -r requirements.txt
使用 Black 格式化你的代码。
我在这篇文章中使用的许多库做的事情比我描述的要多。但是正如我预期的那样,我的目的仅仅是有一个开发时遵循的例程。
无妥协的代码格式化工具。
我使用 Black 来清晰地格式化代码 和整洁地排列。这里是你可以用来运行 black 的命令:
find src -name '*.py' -exec black {} +
在命令中,我们指定了编辑 src 目录下所有的 python 文件(*.py)。
使用 PyLint 分析你的代码
PyLint 是另一个非常有用的库,我建议你开始使用。
[## pylint
python 代码静态检查器
PyLint 自动检查代码中的错误,强制使用标准,并检查代码气味,例如从未使用过的导入。PyLint 还会给你的代码质量打分,范围从 1 到 10。
pylint --disable=R,C src/*.py
你会注意到我修改了命令,禁用了两个标志(R 和 C)。这样,PyLint 将不会发出有关重构和约定的问题的警告或提示。
输出应该如下所示:

图片由作者提供
运行测试,确保你的代码正常工作
如果你不使用测试,怎么知道你的代码总是有效的? 养成 创建简单单元测试的习惯,你可以在编写函数时随时扩展。单元测试不过是一个函数,它会将一系列输入传递给你想测试的函数,并查看输出是否符合预期。
你可以通过多种方式实现单元测试;在 Python 中广泛使用的库是 PyTest。
[## pytest: helps you write better programs - pytest documentation
pytest 和其他成千上万的包的维护者正在与 Tidelift 合作,提供商业支持和…
docs.pytest.org 我通常创建一个名为 test 的 src 姐妹文件夹,在其中收集所有的单元测试。
我通常创建一个 src 姐妹文件夹,叫做 test,在其中收集所有的单元测试。
要启动所有自动生成的测试,我们运行以下命令
python -m pytest -vv --cov=test
我很懒,我会使用 Makefile
到此为止,我们已经看到许多文件和许多命令。我觉得作为例行操作有点繁重。我希望有一些更简单的东西,我的记忆不好😅
好的,那么我们可以创建一个 Makefile,这是一种我们可以在其中写入一些指令的文件,并且 这将为我们启动之前看到的命令。在 Makefile 中,我想写指令以 安装需求、使用 black 格式化代码、使用 PyLint 检查代码气味,并使用 PyTest 启动测试。下面是我们的 Makefile 将是什么样子:
install:
#install
pip install --upgrade pip&&\
pip install -r requirements.txt
format:
#format
find src -name '*.py' -exec black {} +
lint:
#lint
pylint --disable=R,C src/*.py
test:
#test
python -m pytest -vv --cov=test
这样,每次我们从终端使用这个命令时:
make test
实际上,它将被执行
python -m pytest -vv --cov=test
当然,所有其他命令也适用相同的方法。
现在我们的仓库看起来更专业了!
通过 GitHub Actions 在每次推送时运行所有内容
我希望到目前为止一切都清楚。此时,每当我们想要对代码进行更改时,在提交和推送到 GitHub 之前,我们会运行以下命令,以确保一切顺利进行:make install, make format, make lint, make test。
但开发者喜欢自动化一切。那么,难道没有一种方法可以在每次 git 推送时自动运行整个过程吗? 当然有,只需使用GitHub Actions即可!
使用 GitHub actions,我们可以设置触发器,即指定触发命令的事件,在我们的例子中,这些命令都在 Makefile 中。
为了创建 GitHub actions,我们需要在工作目录中创建 .github/workflows 文件夹。在这个新目录中,我们创建一个 YML 文件,例如可以命名为mlops.yml。
在这个文件中,我们可以指定几个内容。首先是我们想要的任何名字。
然后,我们指定将触发命令的事件(或事件),在这种情况下是[push]。接着,我们处理steps(其中第一部分我甚至不记得是什么,但幸运的是有 Google 和 ChatGPT,我们可以从中复制粘贴😅)。
name: Python application with Github Actions
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v4
with:
python-version: "3.10"
- name: Install dependencies
run: |
make install
- name: Lint with pyLint
run: |
make lint
- name: Test with pytest
run: |
make test
- name: Format code
run: |
make format
- name: Build container
run: |
make build
这应该是整个项目的代码结构
-.github/workflows
- mlops.yml
-.venv
-src
-test
-requirements.txt
-Makefile
-Readme.md
就这样!
现在你应该能够在 GitHub 的actions标签下查看每次推送时运行的所有命令。

图片由作者提供
最终思考
在这篇文章中,我展示了一些我在开发 Python 项目时使用的最佳实践,以确保代码质量,并保护我的 GitHub 仓库免受不太实用的提交。虽然每个主题仍然可以深入探讨,但能够至少以某种方式快速轻松地构建你的代码,我认为对提高你的工作是很有帮助的!
如果你对这篇文章感兴趣,可以在 Medium 上关注我!😁
💼 Linkedin ️| 🐦 Twitter | 💻 Website
理解 R 中 apply() 函数的简单指南
原文:
towardsdatascience.com/a-simple-guide-to-understand-the-apply-functions-in-r-69277fc7e522
一劳永逸地学会如何使用这些有用的函数
·发布于 Towards Data Science ·阅读时长 8 分钟·2023 年 10 月 6 日
--

照片由 Kelly Sikkema 提供,来源于 Unsplash
介绍
我在开始这篇文章时要说的是,我每天都使用R和Python语言。老实说,我觉得 Python 中 apply 函数的使用方式更简单、更直观。
反思原因,我认为是因为 Python 中的选项不多。R 语言提供了许多不同的选项,即我喜欢称之为的apply 函数家族。
我记得曾经有人说他们总是直接使用循环来解决问题,因为他们总是记不住每个 apply 函数的作用,也不知道哪个函数最适合使用。
我希望这些问题在这篇文章后能得到解决。我的目的是希望读者在阅读本文后,对这些函数家族有一个清晰的理解,以及如何和何时使用它们。
要进行这些练习,我们快速创建一个示例数据框,没有过多的标准。包括一个 ID、产品名称、销售数量和两个不同时间段的金额。
# Create dataset
dtf <- data.frame(
id = 1:100,
product= sample(c('product A', 'product B', 'product C', 'product D'), size=100, replace=T),
qty = as.integer( rnorm(100, 10, 2) ),
amt = rnorm(100, 1280, 300),
amt2 = rnorm(100, 1280, 300)
)
[OUT]:
id product qty amt amt2
1 1 product A 9 954.1 1418.5
2 2 product B 12 1606.9 877.7
3 3 product D 7 1241.6 1433.5
4 4 product A 11 1413.2 1203.8
5 5 product B 10 1623.3 1451.1
不再多说,让我们深入探讨。
函数
R 中的apply 家族有 4 个主要函数:apply()、lapply()、sapply() 和 tapply()。
apply( )
apply() 函数被称为家族。它可能是最直接的函数。这个函数应用相同的操作到行或列的所有元素。这里是语法。
apply(X, MARGIN, FUN)
对矩阵 X 应用指定的函数,按行(1)或列(2)。
最简单的例子是均值的应用。我们有一个包含一堆产品销售的数据集。但销售的平均数量和平均销售金额是多少?
要快速查看这一点,我们可以使用apply并选择函数mean来应用于数据集中每个数值列。
因此,我们的X是一个矩阵,包含数据集中的数值列 3(qty)和 4(amt)——因为我们不能对产品 A和产品 B取平均。MARGIN可以是1以应用于行,或2以应用于列。FUN是要应用于每个元素的函数。代码如下。
# Apply: Apply a function to all the columns
apply( X= dtf[,c(3,4,5)], MARGIN= 2, FUN= mean)
[OUT]:
qty amt amt2
9.55 1303.42 1267.46
常见错误
- 当使用
apply()时,我们必须使用X的切片表示法,因为如果我们尝试使用整个数据集dtf(考虑到有字符串列),R 会抛出错误。我们不能对字符串求平均。
apply( X= dtf, MARGIN= 2, FUN= mean)
id product qty amt amt2
NA NA NA NA NA
Warning messages:
1: In mean.default(newX[, i], ...) :
argument is not numeric or logical: returning NA
-
即使我们尝试使用整个列,例如
dtf$qty来获取数量的均值(这是数值型的),也不会很好,因为只有一个变量,所以更简单的方法是直接使用mean(dtf$qty),对吗? -
当运行
apply()时,R 会在运行函数之前首先读取对象X的维度。如果你尝试dim(dtf$qty),你会看到输出是NULL。这就是你下面收到错误的原因。维度需要大于 1。
apply( X= dtf$amt, MARGIN= 2, FUN= mean)
Error in apply(X = dtf$amt, MARGIN = 2, FUN = mean) :
dim(X) must have a positive length
很好,简单明了。
按行应用函数
现在让我们再看看数据。如果我们想按行计算均值,以了解 amount1 和 amount2 之间的平均值,我们可以使用相同的代码,只需将MARGIN=1更改为1,然后函数将计算行的简单平均值:[amt + amt2] / 2。
id product qty amt amt2
1 1 product A 9 954.1 1418.5
2 2 product B 12 1606.9 877.7
3 3 product D 7 1241.6 1433.5
4 4 product A 11 1413.2 1203.8
5 5 product B 10 1623.3 1451.1
# Apply: Apply a function to all the rows
apply( X= dtf[, c(4,5)], MARGIN= 1, FUN= mean)
[OUT]:
[1] 1186.3 1242.3 1337.6 1308.5 1537.2 1007.1 1094.0 1465.9 1602.8 1204.4 1155.4 1190.6 812.3 1565.5 1118.5
[16] 1346.4 1259.2 1319.0 1293.2 1402.1 1471.2 1491.8 1248.5 1154.7 1693.5 1358.8 1396.8 1262.8 1383.0 1270.0
[31] 1621.8 933.7 850.7 892.6 1482.3 1191.3 1612.7 1677.2 1496.7 1504.8 947.7 865.5 953.5 1151.8 947.6
[46] 1763.6 1229.1 1328.0 893.1 1386.8 1004.8 975.0 931.7 1665.4 1417.0 1482.3 974.0 1444.9 1233.7 1548.7
[61] 1469.9 1612.0 1159.9 1130.9 1617.9 1290.0 1227.9 1072.6 1367.1 1027.3 1472.1 1263.1 1347.1 1463.3 1324.0
[76] 1361.6 1330.7 1380.8 1699.4 1389.0 1165.8 1146.8 1358.7 1326.3 1213.6 983.1 1385.5 919.4 1212.9 1226.1
[91] 1003.2 1643.5 1327.8 1566.7 966.4 1270.6 1359.5 1252.3 1216.1 1405.6
不过,我们可以将其添加到数据集中。
# Average of the amounts (avg by row)
dtf$avg_amounts <- apply( X= dtf[, c(4,5)], MARGIN= 1, FUN= mean)
# See first 5 rows
dtf |> head(5)
id product qty amt amt2 avg_amounts
1 1 A 9 954.1 1418.5 1186
2 2 B 12 1606.9 877.7 1242
3 3 D 7 1241.6 1433.5 1338
4 4 A 11 1413.2 1203.8 1309
5 5 B 10 1623.3 1451.1 1537
如果我们想添加一列来计算amt/qty,我们可以通过按行(MARGIN=1)使用apply中的自定义函数来计算。自定义函数接收一个矩阵mtrx,并将mtrx第二列的值除以第一列的值。
# Average AMT by product QTY
dtf$amt_by_qty <- apply(
X= dtf[, c(3,4)],
MARGIN= 1,
FUN= function(mtrx){ mtrx[2]/mtrx[1]}
)
# See first 5 rows
dtf |> head(5)
[OUT]:
id product qty amt amt2 avg_amounts amt_by_qty
1 1 A 9 954.1 1418.5 1186 106.0
2 2 B 12 1606.9 877.7 1242 133.9
3 3 D 7 1241.6 1433.5 1338 177.4
4 4 A 11 1413.2 1203.8 1309 128.5
5 5 B 10 1623.3 1451.1 1537 162.3
lapply( )
在 R 语言中,列表的概念与 Python 中的不同。在 R 中,它是一个对象的集合,不一定是相同类型的。当我们需要对列表中的每个元素应用函数时,这就是lapply()的用途。
重要的是要注意lapply 返回一个与 X 具有相同长度的列表对象。
lapply(X, FUN)
我能想到的最简单的例子是,如果你有一个列表,想要检查列表中每个对象的类型。让我们看一下这个简单例子的代码。
# Creating a list
l <- list(c(1,2), 'l', 1.6, TRUE)
# Check the type of each object in the list
lapply(l, class)
[OUT]:
[[1]]
[1] "numeric"
[[2]]
[1] "character"
[[3]]
[1] "numeric"
[[4]]
[1] "logical"
正如预期的那样,长度相同(4 个对象),并且函数class已应用于列表中的每个元素。基本上,如果你处理的是列表对象或需要返回列表,那么你应该使用这种方法。
如果你处理的是列表对象或需要返回列表,你应该使用这种方法。
sapply( )
根据文档,sapply()是lapply()的用户友好版本,返回相同长度的向量或矩阵,而不是返回一个列表。如果参数simplify=TRUE(默认开启),它将返回一个数组(简化对象)。
sapply()是 lapply()的用户友好版本,返回一个更简单的对象,如数组。
sapply(X, FUN, simplify = TRUE)
# Use sapply for the same list
sapply(l, FUN = class)
[OUT]: an array
[1] "numeric" "character" "numeric" "logical"
回到我们创建的数据集dtf,列product包含了以‘product’开头的名称。如果我们想去除这些,sapply可以是一个很好的助手。在使用循环之前,甚至在加载 Tidyverse 之前,记住 Base R 中的 apply 函数可以提供帮助。
注意,我们在这里使用了一个自定义函数,将字符串分成两部分,一旦找到空格字符,就取该列表的第二个元素。
# Using sapply to remove the word 'product' from the description
dtf$product <- sapply(dtf$product,
FUN= function(x){ strsplit(x, ' ')[[1]][2] })
[OUT]:
id product qty amt amt2
1 1 A 9 954.1 1418.5
2 2 B 12 1606.9 877.7
3 3 D 7 1241.6 1433.5
4 4 A 11 1413.2 1203.8
5 5 B 10 1623.3 1451.1
tapply( )
最后,tapply()是一个在处理因子(或组)时使用的函数。
tapply(X, INDEX, FUN, simplify = TRUE)
使用 tapply 对每组值应用一个函数。
这个函数的明显例子是如果我们想在没有分组函数的情况下检查amt的均值,tapply会很有用。
-
X是矩阵或向量。 -
INDEX是包含组的列。 -
FUN是要应用的函数。
# Using tapply to calculate the mean of amt by product
tapply(X= dtf$amt, INDEX = dtf$product, FUN = mean)
[OUT]:
A B C D
1295 1290 1304 1323
这就是关于 R 中 apply 家族函数的最常用部分了。
在你离开之前
好吧,apply 函数是很好的映射函数。在你作为数据科学家的日常工作中,会有很多情况需要映射函数,这比循环要好,无论是为了性能还是为了更好的代码可读性。
通过这个简要介绍,我相信你现在可以将 apply 函数家族应用到你的数据中。
-
apply:用于对数据集中的每一行或列的每个元素应用相同的函数。 -
lapply:用于对列表中的每个元素应用一个函数。返回一个长度相同的列表作为结果。 -
sapply:lapply 的友好版本,返回一个数组作为结果。 -
tapply:用于对每一组值应用一个函数。几乎类似于分组操作。
GitHub 上的代码:
[## Studying/R/apply at master · gurezende/Studying
这是一个包含我对新包测试和学习的代码库 - Studying/R/apply at master · gurezende/Studying
参考
[## apply(), lapply(), sapply(), tapply() Function in R with Examples
本教程旨在介绍 apply()函数系列。apply()函数是所有函数中最基本的一个…
www.guru99.com](https://www.guru99.com/r-apply-sapply-tapply.html?source=post_page-----69277fc7e522--------------------------------#5)
www.datacamp.com/tutorial/r-tutorial-apply-family
SANTOS, Gustavo R. 2023. 使用 R 进行数据整理. 第 1 版. Packt Publishing.
对量子增强支持向量机的简单介绍
如何将有趣的量子计算属性与经典机器学习技术相结合
·
关注 发布于 数据科学前沿 ·7 分钟阅读·2023 年 10 月 3 日
--
介绍
尽管量子计算机尚未普及,量子机器学习(QML)仍然是一个有前景的研究领域,因为它利用了量子系统固有的概率性质来开发模型。目前,全球的数据科学家们正在尝试理解如何利用量子范式来生成更好、可扩展的模型。无法量化这一进展何时会发生,因为这还取决于量子硬件的发展,但这一领域的增长速度正在加快。
在我的最新研究中,我尝试设计变分量子分类器(VQC),正如你可以在我写的上一篇文章中看到的。如果你像我一样开始学习 QML,这将是一个有趣的研究案例。
然而,最近我也开始研究支持向量机(SVM)的量子方法,我对 SVM 如何转化为量子世界感到非常好奇。
当我研究 VQC 时,我非常有偏见地尝试猜测 SVM 如何转化为一个可参数化的量子电路,但我发现量子增强在这里的作用不同,这让我感到惊喜,并帮助我开阔了对这一主题的思路。
在这篇文章中,我首先简要介绍 SVM,然后讲述如何用量子机器学习(QML)方法处理这种技术,并以使用泰坦尼克号数据集的量子增强 SVM(QSVM)示例结束。
SVM 和核方法
我在这里介绍专注于分类问题的 SVM,即支持向量分类器(SVC)。SVC 的目标是找到一个超平面,以最佳的边际分离不同类别的数据。一开始这似乎并不太有帮助,对吧?
那么,这个分离类别的超平面是什么呢?假设我们在一个二维向量空间中工作,并且有两个类别,如图 1 所示。

图 1— 具有非常明确线性分隔符的 2D 数据 — 图像由作者提供
在这个例子中,我们有来自 2 个不同类别的数据点,我们可以很容易地画一条直线分离它们。我们的实线是将数据以最佳边际分开的超平面,如虚线所示。因此,SVM 试图找到最佳的分隔符。
你可能会认为我的例子过于幼稚,而直线是超平面的一个非常特例,这一点是有效的。如果我们的二维数据看起来像图 2 那样呢?

图 2— 具有非线性分隔符的 2D 数据 — 图像由作者提供
在这种情况下,我们无法画出正确分离数据的直线。如果我们看这张图,我们可以画一个圆作为好的分隔器。然而,这种形状既不是直线也不是平面,所以 SVM 无法直接解决这个问题。然而,这就是最酷的 SVM 技巧和高维超平面出现的地方!
如果我们将这些数据转化为更高维度的向量空间会怎样?如:

(方程图由作者提供)
所以我们可以画一个平面:

(方程图由作者提供)
哪个能如图 3 所示最佳地分离两个类别:

图 3— 在更高维空间中分离我们数据的平面 — 图像由作者提供
在我们的情况下,函数 f 就是我们所谓的核,它将数据投影到更高维空间,这使得更容易找到一个可以正确识别不同类别数据的超平面。
这是关于核和支持向量机(SVM)的非常简要介绍,如果你对 SVM 的更多解释感兴趣,我建议你阅读这两篇文章(1 和 2),它们都是关于 SVM 的很好的介绍,并且我在这篇文章中都用作了参考。
你现在可能在想,我的例子很方便地解释了核概念,但在现实生活中,我们如何找到解决问题的合适核呢?有些核非常灵活,非常有助于解决大量问题,比如径向基函数(RBF),这是 scikit-learn 的 SVC 的默认选项。如果你有兴趣了解更多关于这个核的信息,我推荐阅读这篇文章。关于诸如 RBF 的核的一个重要细节是,它们不是由解析函数描述的,而是基于核的数据点之间的相似性矩阵。
不过,如果我们想更有创意呢?如果你曾经阅读过我的之前的文章,你可能还记得量子计算最有趣的特性之一是量子比特和量子态之间的指数关系。因此,量子系统是一个非常有趣的核候选者,因为它倾向于将我们的系统驱向高维向量空间,这取决于我们使用的量子比特数量。
量子核
量子核通常由基于量子电路的相似性矩阵定义,这可能是可参数化的,也可能不是。Pennylane 和 Qiskit 都有内置函数,可以创建可以在 scikit-learn 的 SVC 中使用的核。
量子核项目有一些步骤:
将数据嵌入量子态中

(作者提供的方程图像)
设计一个可能可参数化的量子电路

(作者提供的方程图像)
在这个阶段,强烈建议在量子计算中采用一定程度的叠加和态的纠缠,以获取量子计算能提供的最佳效果。
构建相似性矩阵
在这里,我们使用我们在上一步中构建的幺正 U(|x>)及其伴随来设计一个相似性矩阵。
示例
我们在 Pennylane 中设计了一个简单的量子核,以便与 scikit-learn 的 Titanic 分类数据集中的 SVC 一起使用,我们希望基于诸如年龄、性别和登船类别等变量来预测一个人是否在泰坦尼克号悲剧中幸存。
在我们的例子中,我们使用以下变量:
-
is_child: 如果年龄小于 12 岁(布尔值)
-
Pclass_1: 如果此人乘坐的是头等舱(布尔值)
-
Pclass_2: 如果此人乘坐的是二等舱(布尔值)
-
Sex_female: 如果此人的性别是女性(布尔值)
如你所见,这只是一个具有四个布尔变量的非常简单的模型。我们通过量子嵌入(Basis Embedding)将数据嵌入量子态,应用 Hadamard 门来实现量子位的叠加,使用 CNOT 门生成纠缠。

图 4— 我们的核示例的 Ansatz — 作者提供的图像
这是一个简单且不可参数化的 Ansatz,但它生成了变量之间的叠加和纠缠。
创建核和 SVM 的代码如下:
import pennylane as qml
from pennylane import numpy as np
from sklearn.model_selection import train_test_split
import pandas as pd
from sklearn.metrics import accuracy_score
from sklearn.metrics import f1_score, precision_score, recall_score
from sklearn.svm import SVC
num_qubits = 4
def layer(x):
qml.BasisEmbedding(x, wires=range(num_qubits))
for j, wire in enumerate(wires):
qml.Hadamard(wires=[wire])
if j != num_qubits-1:
qml.CNOT(wires=[j, j+1])
else:
qml.CNOT(wires=[j, 0])
def ansatz(x, wires):
layer(x)
adjoint_ansatz = qml.adjoint(ansatz)
dev = qml.device("default.qubit", wires=num_qubits, shots=None)
wires = dev.wires.tolist()
@qml.qnode(dev, interface="autograd")
def kernel_circuit(x1, x2):
ansatz(x1, wires=wires)
adjoint_ansatz(x2, wires=wires)
return qml.probs(wires=wires)
def kernel(x1, x2):
return kernel_circuit(x1, x2)[0]
df_train = pd.read_csv('train.csv')
df_train['Pclass'] = df_train['Pclass'].astype(str)
df_train = pd.concat([df_train, pd.get_dummies(df_train[['Pclass', 'Sex', 'Embarked']])], axis=1)
X_train, X_test, y_train, y_test = train_test_split(df_train.drop(columns=['Survived']), df_train['Survived'], test_size=0.10, random_state=42, stratify=df_train['Survived'])
X_train['Age'] = X_train['Age'].fillna(X_train['Age'].median())
X_test['Age'] = X_test['Age'].fillna(X_test['Age'].median())
X_train['is_child'] = X_train['Age'].map(lambda x: 1 if x < 12 else 0)
X_test['is_child'] = X_test['Age'].map(lambda x: 1 if x < 12 else 0)
cols_model = ['is_child', 'Pclass_1', 'Pclass_2', 'Sex_female']
X_train = X_train[cols_model]
X_test = X_test[cols_model]
X_train = np.array(X_train.values, requires_grad=False)
init_kernel = lambda x1, x2: kernel(x1, x2)
K = qml.kernels.square_kernel_matrix(X_train, init_kernel, assume_normalized_kernel=True)
svm = SVC(kernel=lambda X1, X2: qml.kernels.kernel_matrix(X1, X2, init_kernel)).fit(X_train, y_train)
X_test = np.array(X_test.values, requires_grad=False)
predictions = svm.predict(X_test)
accuracy_score(y_test, predictions)
precision_score(y_test, predictions)
recall_score(y_test, predictions)
f1_score(y_test, predictions, average='macro')
svm1 = SVC(gamma='auto', kernel='rbf')
svm1.fit(X_train, y_train)
y_pred = svm1.predict(X_test)
accuracy_score(y_test, y_pred)
precision_score(y_test, y_pred)
recall_score(y_test, y_pred)
f1_score(y_test, y_pred, average='macro')
结果如下:

图 5— 测试结果打印 — 作者提供的图像
如你所见,RBF 核的 SVC 表现优于我们的量子核 SVC。我们的量子方法具有良好的精确度,这意味着我们成功避免了较多的假阳性,但召回率不佳,意味着我们有显著数量的假阴性。
如果你想深入了解量子核的 SVM,这些帖子是很好的参考:1,2,以及 Pennylane 关于这一主题的这些文本:3 和 4。
结论
量子核可以是提高 SVM 性能的强大工具。然而,正如我们在例子中所见,具有简单量子核的 SVM 无法超越具有 RBF 核的 SVM。量子核需要精心设计才能与经典技术竞争。
我一直在深入研究设计可参数化的量子核,希望很快有好消息。
管理基于云的机器学习训练的简单解决方案
如何使用基础(非托管)云服务 API 实现自定义训练解决方案
·
关注 发布于 Towards Data Science · 18 分钟阅读 · 2023 年 12 月 21 日
--
照片由 [Aditya Chinchure](https://unsplash.com/@adityachinchure?utm_source=medium&utm_medium=referral) 提供,发布在 [Unsplash](https://unsplash.com/?utm_source=medium&utm_medium=referral)
在之前的帖子中(例如,这里),我们已经扩展了在云中开发 AI 模型的好处。机器学习项目,特别是大型项目,通常需要访问专用设备(例如,训练加速器)、按需扩展的能力、用于维护大量数据的适当基础设施以及用于管理大规模实验的工具。像Amazon Web Services (AWS)、Google Cloud Platform (GCP)和Microsoft Azure这样的云服务提供商提供了大量针对 AI 开发的服务,从低级基础设施(例如,GPU 和几乎无限的对象存储)到用于创建自定义 ML 模型的高度自动化工具(例如,AWS AutoML)。特别是,管理培训服务(如Amazon SageMaker、Google Vertex AI和Microsoft Azure ML)使得在云中进行培训变得特别容易,并提高了潜在 ML 工程师的可及性。要使用管理的 ML 服务,您只需指定所需的实例类型,选择 ML 框架和版本,并指向您的训练脚本,服务将自动启动所选实例并配置所需环境,运行脚本以训练 AI 模型,将结果保存到持久存储位置,并在完成后拆除一切。
虽然管理培训服务可能是许多 ML 开发人员的理想解决方案,但正如我们将看到的,有些情况下需要直接在“未管理”机器实例上运行并以非中介方式训练您的模型。在这些情况下,即在没有官方管理层的情况下,可能需要包含一个自定义解决方案来控制和监控您的培训实验。在这篇文章中,我们将提出一些步骤,用于使用低级未管理云服务的 API 构建一个非常简单的“穷人”解决方案来管理培训实验。
我们将首先说明在未管理机器上进行训练而不是通过管理服务进行训练的一些动机。接下来,我们将识别一些我们期望的基本训练管理功能。最后,我们将演示一种使用 GCP API 来创建虚拟机实例的简单管理系统的实现方法。
尽管我们将在 GCP 上演示我们的解决方案,但类似的解决方案也可以在其他云平台上开发。请不要将我们选择 GCP 或任何其他工具、框架或服务视为对其使用的推荐。云端训练有多种选择,每种都有其优缺点。对你来说,最佳选择将很大程度上取决于你项目的具体细节。请务必在阅读时对照最新版本的 API 和文档重新评估本文内容。
感谢我的同事 Yitzhak Levi 对本文的贡献。
动机 — 管理训练服务的局限性
高级解决方案通常会优先考虑易用性和更高的可访问性,虽然这意味着对底层流程的控制减少。基于云的管理训练服务也不例外。除了上述的便利性之外,还会有对训练启动和执行细节的一定控制丧失。这里我们将提到几个例子,希望能够代表你可能遇到的限制类型。
1. 机器类型选择的限制
管理训练服务中提供的机器类型的种类并不总是涵盖云服务提供商支持的所有机器类型。例如,Amazon SageMaker 不支持其 DL1 和 Inf2 系列的实例类型(截至本文撰写时)。由于各种原因(如节省成本),你可能需要或希望在这些实例类型上进行训练。在这种情况下,你将不得不放弃使用 Amazon SageMaker 进行训练的奢华。
2. 对底层机器镜像的控制有限
管理训练工作负载通常在专用的 Docker 容器内运行。你可以依赖服务选择最合适的容器,从服务提供商预定义的容器列表中选择特定容器(例如 AWS Deep Learning Containers),或者定义适合你特定需求的自定义 Docker 镜像(例如见 这里)。但是,虽然你对 Docker 镜像容器有相当大的控制权,但你对底层的 机器镜像 没有 控制权。在大多数情况下,这不会成为问题,因为 Docker 旨在减少对主机系统的依赖。然而,在处理专用硬件(如训练时)的情况下,我们需要依赖主机镜像中的特定驱动程序安装。
3. 对多节点部署控制的限制
通常为了提高训练速度,会在多个机器实例上训练大型模型。在这种情况下,机器之间的网络容量和延迟会对训练的速度(和成本)产生关键影响——因为它们之间会不断交换数据。理想情况下,我们希望这些实例位于同一数据中心。不幸的是,(截至本文撰写时),像 Amazon SageMaker 这样的托管服务限制了你对设备位置的控制。因此,你可能会发现机器分布在两个或多个可用区,这可能会对你的训练时间产生负面影响。
4. 用户权限限制
有时,训练工作负载可能需要对系统主机进行根级访问。例如,AWS 最近宣布了Mountpoint for Amazon S3,这是一种基于FUSE的新解决方案,用于高吞吐量的数据存储访问,并可能优化数据流入你的训练循环。不幸的是,这个解决方案只能在Docker 环境中使用,如果你的容器运行时带有*--cap-add SYS_ADMIN*标志,这实际上阻止了在托管服务设置中使用它。
5. Docker 启动设置的限制
一些训练工作负载需要能够配置特定的 docker run 设置。例如,如果你的工作负载将训练数据存储在共享内存中(例如,在 /dev/shm 中),并且你的数据样本特别大(例如,高分辨率的 3D 点云),你可能需要指定增加分配给 Docker 容器的共享内存量。虽然 Docker 通过 shm-size 标志来实现这一点,但你的托管服务可能会阻止你控制这一点。
6. 训练环境访问受限
托管训练的副作用之一是对训练环境的访问性降低。有时你需要直接连接到你的训练环境,例如,调试故障时。当运行托管训练任务时,你实际上将所有控制权交给了服务,包括你随意访问机器的能力。请注意,一些解决方案支持对托管环境的有限访问,前提是适当配置训练任务(例如,见这里)。
7. 缺乏 Spot 实例终止的通知
Spot 实例 是 CSPs 通常以折扣价格提供的未使用机器。权衡是这些机器可能会被突然撤回。当使用未管理的 Spot 实例时,你会收到 终止通知,这使你有一点时间优雅地停止应用程序。如果你正在运行培训工作负载,你可以利用这一提前通知捕获模型的最新状态,并将其复制到持久存储中,以便稍后恢复使用。
托管训练服务的一个引人注目的特性是它们 管理 Spot 生命周期 ,即你可以选择在成本较低的 Spot 实例上进行训练,并依赖托管服务在可能的情况下自动恢复中断的作业。然而,当通过 Amazon SageMaker 等托管服务使用 Spot 实例时,你不会收到终止通知。因此,当你恢复训练作业时,将从你捕获的最新模型状态开始,而不是预占时模型的状态。根据你捕获中间检查点的频率,这种差异可能会影响你的训练成本。
成本考虑
需要注意的另一个托管训练服务的属性是通常与其使用相关的额外成本。AI 开发可能是一项昂贵的任务,你可能会选择通过放弃托管训练服务的便利,换取一个更简单的自定义解决方案,从而减少成本。
Kubernetes 替代方案
容器编排系统有时被提出作为托管训练服务的替代方案。在撰写本文时,最受欢迎的选择是 Kubernetes。凭借其高度的自动扩展性以及对将应用拆分为微服务的支持,Kubernetes 已成为许多现代应用开发人员的首选平台。这对于包含复杂流程及多个相互依赖组件的应用尤为如此,有时称为有向无环图(DAG)工作流。一些端到端的 ML 开发管道可以视为 DAG 工作流(例如,从数据准备和处理开始,到模型部署结束)。实际上,基于 Kubernetes 的解决方案通常应用于 ML 管道。然而,就仅培训阶段而言(如本文所述),我们通常知道实例的确切数量和类型,可以认为 Kubernetes 提供的附加价值有限。Kubernetes 的主要缺点是它通常需要依赖专门的基础设施团队进行持续的支持和维护。
训练管理最小要求
在没有 CSP 训练管理服务的情况下,让我们定义一些我们希望的基本管理功能。
-
自动启动 — 我们希望训练脚本在实例启动后自动运行。
-
自动实例终止 — 我们希望机器实例在训练脚本完成后自动终止,以便我们不会为机器不使用时支付费用。
-
支持多实例训练 — 我们需要能够启动一个集群,以便进行多节点训练。
-
持久日志 — 我们希望训练日志输出写入持久存储,以便在训练机器终止后仍然可以随时访问。
-
检查点捕获 — 我们希望将训练成果,包括定期检查点,保存到持久存储中。
-
训练任务总结 — 我们希望有一种方法来审查(和比较)训练实验。
-
在抢占性终止时重启(高级功能) — 我们希望能够利用折扣 抢占实例 的容量,以降低训练成本而不影响开发的连续性。
在下一节中,我们将展示如何在 GCP 中实现这些功能。
穷人的 GCP 训练管理
在本节中,我们将展示一个基本的训练管理解决方案,使用 Google 的 gcloud 命令行工具(基于 Google Cloud SDK 版本 424.0.0)来 创建 VM 实例。我们将从一个简单的 VM 实例创建命令开始,并逐步补充额外的组件,以纳入我们所需的管理功能。请注意,gcloud compute-instances-create 命令包含一长串可选标志,这些标志控制实例创建的许多元素。为了演示的目的,我们将仅关注与我们的解决方案相关的控制。我们假设:1)您的环境已 设置 以使用 gcloud 连接到 GCP,2)默认网络已适当配置,3)存在具有访问权限的 托管服务账户。
1. 创建 VM 实例
以下命令将启动一个g2-standard-48虚拟机实例(包含四个NVIDIA L4 GPU),并使用公共M112虚拟机镜像。或者,您可以选择使用自定义镜像。
gcloud compute instances create test-vm \
--zone=us-east1-d \
--machine-type=g2-standard-48 \
--image-family=common-cu121-debian-11-py310 \
--image-project=deeplearning-platform-release \
--service-account=my-account@my-project.iam.gserviceaccount.com \
--maintenance-policy=TERMINATE
2. 自动启动训练
一旦机器实例启动并运行,我们希望自动开始训练工作负载。在下面的示例中,我们将通过将startup script传递给 gcloud compute-instances-create命令来演示这一点。我们的启动脚本将执行一些基本的环境设置步骤,然后运行训练作业。我们首先调整PATH环境变量以指向我们的 conda 环境,然后从 Google Storage 下载包含源代码的 tarball,解压它,安装项目依赖项,最后运行我们的训练脚本。演示假设 tarball 已经创建并上传到云中,并且它包含两个文件:一个要求文件(requirements.txt)和一个独立的训练脚本(train.py)。实际上,启动脚本的具体内容将取决于项目。
gcloud compute instances create test-vm \
--zone=us-east1-d \
--machine-type=g2-standard-48 \
--image-family=common-cu121-debian-11-py310 \
--image-project=deeplearning-platform-release \
--service-account=my-account@my-project.iam.gserviceaccount.com \
--maintenance-policy=TERMINATE \
--metadata=startup-script='#! /bin/bash \
export PATH="/opt/conda/bin:$PATH" \
gsutil cp gs://my-bucket/test-vm/my-code.tar . \
tar -xvf my-code.tar \
python3 -m pip install -r requirements.txt \
python3 train.py'
3. 完成后自毁
管理培训的一个引人注目的特点是您只需为所需的部分付费。更具体地说,一旦您的训练作业完成,训练实例将自动拆除。实现这一点的一种方法是在训练脚本的末尾附加一个自毁命令。请注意,为了启用自毁功能,实例需要使用适当的scopes设置进行创建。有关更多详细信息,请参阅API 文档。
gcloud compute instances create test-vm \
--zone=us-east1-d \
--machine-type=g2-standard-48 \
--image-family=common-cu121-debian-11-py310 \
--image-project=deeplearning-platform-release \
--service-account=my-account@my-project.iam.gserviceaccount.com \
--maintenance-policy=TERMINATE \
--scopes=https://www.googleapis.com/auth/cloud-platform \
--metadata=startup-script='#! /bin/bash \
export PATH="/opt/conda/bin:$PATH" \
gsutil cp gs://my-bucket/test-vm/my-code.tar . \
tar -xvf my-code.tar \
python3 -m pip install -r requirements.txt \
python3 train.py \
yes | gcloud compute instances delete $(hostname) --zone=zone=us-east1-d'
需要记住的是,尽管我们有意这样做,但有时实例可能不会被正确删除。这可能是由于特定错误导致启动脚本提前退出,或由于进程停滞而阻止其完成运行。我们强烈建议引入后端机制,以验证未使用的实例是否被识别并终止。实现这一点的一种方法是调度定期的Cloud Function。请参阅我们的近期文章,我们在其中提出了使用无服务器函数来解决在 Amazon SageMaker 上训练时出现的问题。
4. 将应用程序日志写入持久存储
鉴于我们训练时的实例在完成后会被终止,我们需要确保系统输出写入持久日志。这对监控作业进度、调查错误等非常重要。在 GCP 中,这是通过 Cloud Logging 提供的。默认情况下,会为每个 VM 实例收集输出日志。可以使用与 VM 关联的实例 ID通过 Logs Explorer 访问这些日志。以下是访问训练作业日志的示例查询:
resource.type="gce_instance"
resource.labels.instance_id="6019608135494011466"
为了能够查询日志,我们需要确保捕获并存储每个 VM 的实例 ID。这必须在实例终止之前完成。在下面的代码块中,我们使用 compute-instances-describe API 来检索我们的 VM 的实例 ID。我们将 ID 写入文件,并将其上传到 Google Storage 中与我们项目关联的路径下的专用元数据文件夹中以供将来参考:
gcloud compute instances describe test-vm \
--zone=us-east1-d --format="value(id)" > test-vm-instance-id
gsutil cp test-vm-instance-id gs://my-bucket/test-vm/metadata/
我们进一步将元数据文件夹填充了我们 compute-instances-create 命令的全部内容。这将在后续过程中派上用场:
gsutil cp create-instance.sh gs://my-bucket/test-vm/metadata/
5. 将工件保存到持久存储中
重要的是,我们必须确保所有训练作业的工件在实例终止之前上传到持久存储中。最简单的方法是将一个命令附加到我们的启动脚本中,该命令将整个输出文件夹与 Google Storage 同步:
gcloud compute instances create test-vm \
--zone=us-east1-d \
--machine-type=g2-standard-48 \
--image-family=common-cu121-debian-11-py310 \
--image-project=deeplearning-platform-release \
--service-account=my-account@my-project.iam.gserviceaccount.com \
--maintenance-policy=TERMINATE \
--scopes=https://www.googleapis.com/auth/cloud-platform \
--metadata=startup-script='#! /bin/bash \
export PATH="/opt/conda/bin:$PATH" \
gsutil cp gs://my-bucket/test-vm/my-code.tar . \
tar -xvf my-code.tar \
python3 -m pip install -r requirements.txt \
python3 train.py \
gsutil -m rsync -r output gs://my-bucket/test-vm/output \
yes | gcloud compute instances delete $(hostname) --zone=zone=us-east1-d'
这个解决方案的问题在于它只会在训练结束时同步输出。如果由于某种错误我们的机器崩溃,我们可能会丢失所有中间工件。一个更具容错性的解决方案是,在整个训练作业过程中,将工件上传到 Google Storage(例如,在固定的训练步骤间隔):
6. 支持多节点训练
要运行多节点训练作业,我们可以使用 compute-instances-bulk-create API 来 创建一组 GPU VM。下面的命令将创建两个 g2-standard-48 VM 实例。
gcloud compute instances bulk create \
--name-pattern="test-vm-#" \
--count=2 \
--region=us-east1 \
--target-distribution-shape=any_single_zone \
--image-family=common-cu121-debian-11-py310 \
--image-project=deeplearning-platform-release \
--service-account=my-account@my-project.iam.gserviceaccount.com \
--on-host-maintenance=TERMINATE \
--scopes=https://www.googleapis.com/auth/cloud-platform \
--metadata=startup-script='#! /bin/bash \
export MASTER_ADDR=test-vm-1 \
export MASTER_PORT=7777 \
export NODES="test-vm-1 test-vm-2" \
export PATH="/opt/conda/bin:$PATH" \
gsutil cp gs://my-bucket/test-vm/my-code.tar . \
tar -xvf my-code.tar \
python3 -m pip install -r requirements.txt \
python3 train.py \
gsutil -m rsync -r output gs://my-bucket/test-vm/output \
HN="$(hostname)" \
ZN="$(gcloud compute instances list --filter=name=${HN} --format="value(zone)")" \
yes | gcloud compute instances delete $HN --zone=${ZN}'
gcloud compute instances describe test-vm-1 \
--zone=us-east1-d --format="value(id)" > test-vm-1-instance-id
gsutil cp test-vm-1-instance-id gs://my-bucket/test-vm/metadata/
gcloud compute instances describe test-vm-2 \
--zone=us-east1-d --format="value(id)" > test-vm-2-instance-id
gsutil cp test-vm-2-instance-id gs://my-bucket/test-vm/metadata/
与单个 VM 创建命令相比,有一些重要的区别:
-
对于批量创建,我们指定一个区域而不是区域。我们选择通过将 target-distribution-shape 标志设置为 any_single_zone 来强制所有实例位于一个区域内。
-
我们在启动脚本中添加了多个环境变量定义。这些将用于正确配置训练脚本以在所有节点上运行。
-
要删除 VM 实例,我们需要知道它创建时的zone。由于在运行批量创建命令时我们并不知道这一点,我们需要在启动脚本中以编程方式提取它。
-
我们现在捕获并存储both创建的 VM 实例的 ID。
在下面的脚本中,我们演示如何使用环境设置在PyTorch中配置data parallel training。
import os, ast, socket
import torch
import torch.distributed as dist
import torch.multiprocessing as mp
def mp_fn(local_rank, *args):
# discover topology settings
gpus_per_node = torch.cuda.device_count()
nodes = os.environ['NODES'].split()
num_nodes = len(nodes)
world_size = num_nodes * gpus_per_node
node_rank = nodes.index(socket.gethostname())
global_rank = (node_rank * gpus_per_node) + local_rank
print(f'local rank {local_rank} '
f'global rank {global_rank} '
f'world size {world_size}')
dist.init_process_group(backend='nccl',
rank=global_rank,
world_size=world_size)
torch.cuda.set_device(local_rank)
# Add training logic
if __name__ == '__main__':
mp.spawn(mp_fn,
args=(),
nprocs=torch.cuda.device_count())
7. 实验总结报告
托管服务通常会暴露一个 API 和/或一个仪表盘,以查看训练作业的状态和结果。你可能会想在自定义管理解决方案中包含类似功能。实现此功能的方法有很多,包括使用Google Cloud Database来维护训练作业的元数据,或者使用第三方工具(如 neptune.ai, Comet, 或 Weights & Biases)来记录和比较训练结果。以下函数假设训练应用程序的返回代码已被上传(来自启动脚本)到我们的专用metadata文件夹,并简单地遍历 Google Storage 中的作业:
import os
import subprocess
from tabulate import tabulate
from google.cloud import storage
# get job list
p=subprocess.run(["gsutil","ls","gs://my-bucket/"], capture_output=True)
output = p.stdout.decode("utf-8").split("\n")
jobs = [i for i in output if i.endswith("/")]
storage_client = storage.Client()
bucket_name = "my-bucket"
bucket = storage_client.get_bucket(bucket_name)
entries = []
for job in jobs:
blob = bucket.blob(f'{job}/metadata/{job}-instance-id')
inst_id = blob.download_as_string().decode('utf-8').strip()
try:
blob = bucket.blob(f'{job}/metadata/status-code')
status = blob.download_as_string().decode('utf-8').strip()
status = 'SUCCESS' if status==0 else 'FAILED'
except:
status = 'RUNNING'
print(inst_id)
entries.append([job,status,inst_id,f'gs://my-bucket/{job}/my-code.tar'])
print(tabulate(entries,
headers=['Job', 'Status', 'Instance/Log ID', 'Code Location']))
上述代码将生成以下格式的表格:

训练实验表(按作者分类)
8. 支持 Spot 实例使用
我们可以通过将provisioning-model标志设置为SPOT,以及将instance-termination-action标志设置为DELETE,轻松修改我们的单节点创建脚本以使用Spot VM。不过,我们希望我们的训练管理解决方案能够管理 Spot VM 使用的完整生命周期,即识别 Spot 抢占并在可能时重新启动未完成的作业。支持此功能的方法有很多。在下面的示例中,我们定义了一个专用的 Google Storage 路径 gs://my-bucket/preempted-jobs/ ,该路径维护一个未完成训练作业名称的列表,并定义一个关闭脚本以识别 Spot 抢占并将当前作业名称添加到列表中。我们的关闭脚本是文档中推荐脚本的精简版本。实际上,你可能希望包含捕获和上传最新模型状态的逻辑。
#!/bin/bash
MY_PROGRAM="python"
# Find the newest copy of $MY_PROGRAM
PID="$(pgrep -n "$MY_PROGRAM")"
if [[ "$?" -ne 0 ]]; then
echo "${MY_PROGRAM} not running shutting down immediately."
exit 0
fi
echo "Termination in progress registering job..."
gsutil cp /dev/null gs://my-bucket/preempted-jobs/$(hostname)
echo "Registration complete shutting down."
我们将关闭脚本的内容复制到一个shutdown.sh文件中,并添加metadata-from-file标志,指明脚本的位置。
gcloud compute instances create test-vm \
--zone=us-east1-d \
--machine-type=g2-standard-48 \
--image-family=common-cu121-debian-11-py310 \
--image-project=deeplearning-platform-release \
--service-account=my-account@my-project.iam.gserviceaccount.com \
--maintenance-policy=TERMINATE \
--scopes=https://www.googleapis.com/auth/cloud-platform \
--provisioning-model=SPOT \
--instance-termination-action=DELETE \
--metadata-from-file=shutdown-script=shutdown.sh \
--metadata=startup-script='#! /bin/bash \
export PATH="/opt/conda/bin:$PATH" \
gsutil cp gs://my-bucket/test-vm/my-code.tar . \
tar -xvf my-code.tar \
python3 -m pip install -r requirements.txt \
python3 train.py \
gsutil -m rsync -r output gs://my-bucket/test-vm/output \
yes | gcloud compute instances delete $(hostname) --zone=zone=us-east1-d'
在 GCP 中使用关机脚本有一些注意事项,你应该确保了解。特别是,具体行为可能会根据机器镜像和环境中的其他组件有所不同。在某些情况下,你可能会选择将关机行为直接编程到机器镜像中(例如,参见这里)。
最终的解决方案组件需要一个云函数,它遍历gs://my-bucket/preempted-jobs/中的项目列表,并为每个项目检索相关的初始化命令(例如,gs://my-bucket/test-vm/metadata/create-instance.sh),并尝试重新运行。如果成功,它会从列表中移除作业名称。
import os
import subprocess
from google.cloud import storage
storage_client = storage.Client()
bucket = storage_client.get_bucket('my-bucket')
jobs=bucket.list_blobs(prefix='users/preempted-jobs/')
for job in jobs:
job_name=job.name.split('/')[-1]
job_name='test-vm'
script_path=f'{job_name}/metadata/create-instance.sh'
blob=bucket.blob(script_path)
blob.download_to_filename('script.sh')
os.chmod('script.sh', 0o0777)
p = subprocess.Popen('script.sh', stdout=subprocess.PIPE)
p.wait()
if(p.returncode==0):
job.delete()
云函数可以编程定期运行和/或我们可以设计一个机制来触发它从关机脚本。
多节点 Spot 使用:当将 Spot 使用应用于多节点作业时,我们需要解决可能只有一部分节点被终止的问题。处理这个问题最简单的方法是编程让应用程序识别当一些节点变得无响应时,停止训练循环,并继续关机。
受管理训练定制
构建自己的受管理训练解决方案的一个优势是可以根据特定需求进行定制。在上一节中,我们演示了如何设计一个自定义解决方案来管理 Spot VM 生命周期。我们还可以类似地使用云监控警报、云订阅/发布消息服务和无服务器云函数等工具,来量身定制其他挑战的解决方案,例如清理停滞作业、识别利用不足的 GPU 资源、限制 VM 的总体运行时间以及管理开发者使用模式。请参见我们的近期帖子,我们演示了如何将无服务器函数应用于受管理训练环境中的这些挑战。
请注意,我们定义的管理解决方案解决了我们上述列出的受管理训练的每个限制:
-
gcloud compute-instances-create API 公开了 GCP 提供的所有 VM 实例类型,并允许你指定你选择的机器镜像。
-
gcloud compute-instances-bulk-create API 允许我们以确保所有节点在同一区域中共同部署的方式启动多个节点。
-
我们的解决方案支持在非容器化环境中运行。如果你选择使用容器,你可以用任何设置和用户权限进行配置。
-
GCP VMs 支持 SSH 访问(例如通过 gcloud compute-ssh 命令)。
-
我们描述的 Spot VM 生命周期支持捕获和处理抢占通知。
说了这么多……
使用托管训练服务(例如Amazon SageMaker、Google Vertex AI和Microsoft Azure ML)的便利性毋庸置疑。它们不仅处理了我们上面列出的所有托管需求,还通常提供额外功能,如超参数优化、平台优化的分布式训练库、专用开发环境等。确实,有时开发自己的管理解决方案是有很好的理由的,但在走这条路之前,完全探索使用现有解决方案的所有机会可能是个好主意。
总结
尽管云计算可以为开发 AI 模型提供理想的领域,但合适的训练管理解决方案对于使其使用有效和高效至关重要。尽管 CSP 提供专门的托管训练服务,但它们并不总是与我们的项目要求对齐。在这篇文章中,我们展示了如何通过使用一些高级控制的低级、未管理的机器实例创建 API 设计一个简单的管理解决方案。自然,一刀切的方案不适用于所有情况;最理想的设计将高度依赖于你 AI 项目的具体细节。但我们希望我们的文章能为你提供一些开始的思路。
一个简单的 TensorFlow 和 TensorFlow 对象检测 API 的逐步安装指南
安装 TensorFlow 和 TensorFlow 对象检测 API 可能会很繁琐和耗时。这个指南将帮助你快速完成整个设置过程。
·发布于 Towards Data Science ·阅读时间 8 分钟·2023 年 2 月 15 日
--
尽管有一个官方指南来安装 TensorFlow 及其对象检测 API,但你仍然可能遇到许多问题(特别是当你希望安装 GPU 支持时)。这篇文章不会做任何假设,而是会详细指导你完成整个过程,以帮助你快速上手。
这些逐步说明分为几个主要部分,但在我们开始之前,我想指出一个重要的说明:虽然在普通的新电脑上进行计算机视觉任务是可行的,但我强烈建议使用安装了 NVIDIA GPU(显卡)的机器。附带说明,AMD 显卡通常不被 TensorFlow 或 PyTorch 支持。

一个相当复杂的 TensorFlow 安装 — 图片由 Dall-E 提供,提示由作者编写。
在现代深度学习框架的支持下,特别是针对 NVIDIA GPU 进行优化,你可以 1) 快速训练模型,并且 2) 迅速进行预测。如果你使用的是仅有 CPU 的配置,我建议你不要 (重新) 训练模型,而是仅安装一个模型并通过该预训练模型进行预测。
也就是说,这个指南将适应你的需求,无论你的硬件要求是什么。 以下的说明比较通用,但针对 Windows 用户指出了关键步骤。
让我们跳过以下步骤,准备好你的机器进行对象检测:
-
安装 VS Code(和 GIT)
-
获取 Anaconda 或继续使用你的通用环境
-
获取 CUDA 和 CUDNN 驱动程序以支持你的 GPU
-
获取 C++构建工具和 MS Visual Studio 安装
-
安装 Protoc
-
从 TensorFlow 下载预训练模型
-
运行演示笔记本以测试安装
1. 安装你选择的编辑器和 GIT
虽然你可以使用任何文本编辑器或 IDE,但我喜欢使用 VS Code(特别是因为 Atom 现在被认为是过时的),原因很简单:它易于使用,提供了各种扩展,具有很好的同步功能,最重要的是,与Jupyter 笔记本一起使用非常棒。
[## Visual Studio Code - 重新定义代码编辑
Visual Studio Code 是一个重新定义并优化了的代码编辑器,适用于构建和调试现代 Web 和云…
如前所述,你还应该安装 GIT(版本控制)。如果尚未安装,请从以下链接安装GIT bash:
Git 附带内置的 GUI 工具(git-gui, gitk),但也有一些第三方工具供用户使用…
2. 启动并运行你的 Anaconda 安装
虽然你可以使用“全局 Python 安装”,但在 Anaconda 环境中工作是一个好习惯。Anaconda 环境的巨大优势在于,它使管理当前项目的工作依赖关系变得容易。从这里下载并安装你的 Anaconda 安装程序:
[## 在 Windows 上安装 - Anaconda 文档
更喜欢视觉学习?观看下面链接中的视频以了解如何在 Windows 上安装 Anaconda!下载 Anaconda…
安装后,确保打开“anaconda 提示符”并使你的环境运行起来:
conda create -n MyEnvironment python=3.10
conda activate MyEnvironment
# find more here:
# https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html
3. 安装 NVIDIA 的 GPU 驱动程序
为了使你的模型能够正确使用 GPU,你需要直接从 NVIDIA 安装驱动程序。需要下载以下两个资源:
-
CUDA 工具包 (
developer.nvidia.com/cuda-downloads)
确保选择正确的操作系统(Windows、Linux 或 MacOS)——你需要注册一个帐户以触发下载。第一步,确保你已经正确安装了 CUDA Toolkit。这是必要的,因为 CUDNN 下载的是一个 zip 文件,而不是安装文件。
安装 CUDA 后,确保导航到以下安装路径(你的路径可能略有不同,但本质上,这是你需要去的地方):
>> C:/Program Files/NVIDIA GPU Computing Toolkit/CUDA/v1X.X/
在这个文件夹中,你会找到子文件夹bin、include和lib。这些文件夹在解压后的 CUDNN 文件夹中也存在。确保将每个 CUDNN 文件夹的内容复制到相应的 CUDA 文件夹中(路径如上所示)。
这只会添加文件,确保你不要替换任何文件夹!
4. 安装 C++ Build Tools 和微软的© Visual Studio©
为了使 TensorFlow 正常工作,必须在你的计算机上安装 C++ Build Tools。这个步骤不可绕过,即使这个安装文件较大(大约 2GB),也请继续进行。
## Visual Studio 2022 Community Edition - Download Latest Free Version
尝试我们的免费、功能齐全且可扩展的 IDE,创建适用于 Windows、Android 和 iOS 的现代开发者应用程序……
安装完上述 Visual Studio 后,你将被引导到可能的“工作负载”。

这个步骤的关键部分
完成这些步骤后,我们接近结束这个安装过程!在下一步中,我们需要安装另一个 TensorFlow 的依赖项,称为 Protoc。
5. 安装 Protoc
下载适用于你的设备和操作系统的最新 Protoc 版本——再次说明,可以是 Windows、Linux 或 Mac。同时确保选择适合你处理器架构的文件。
## Releases · protocolbuffers/protobuf
Protocol Buffers - Google 的数据交换格式 - Releases · protocolbuffers/protobuf
将解压后的文件夹内容存储到你能够再次找到的地方,并且这个地方应该适合长期保存(理想情况下,不是下载文件夹)。
在这个文件夹中,确保找到包含protoc.exe文件的“bin”文件夹——exe仅适用于 Windows 用户,你在其他操作系统上会看到不同的文件格式。在我的情况下,我有以下路径,请确保高亮并复制到剪贴板。
D:\Program Files\protoc-3.12.4-win64\bin
按下 Windows 按钮,开始输入“环境”,这应该会显示以下结果:

从这里选择环境变量并获取刚刚弹出窗口中的“路径”:

将上述链接添加到 Path 变量中,以确保你可以直接从命令行执行“protoc”。如果路径设置正确,你也可以通过 CMD 命令“where protoc”来测试结果。
6. 在你的计算机上安装 TensorFlow 及其预训练模型
为了将 TensorFlow 模型安装到你的计算机上,你需要从 TensorFlow GitHub 仓库中下载它们。为此,打开你选择的文件夹,右击并选择“Git Bash here”(再次假设你已经按照本指南第 1 步操作)。
输入以下语句:
# install TF as:
conda install -c conda-forge tensorflow
# download the detection models
git clone https://github.com/tensorflow/models
这又是一个大约 1GB 的下载,需要加载到你的计算机上,以便选择不同的检测器。

在模型正确下载后,使用命令行导航到研究文件夹,逐步完成对象检测的安装——更具体地说,是 TensorFlow、对象检测 API 以及像 open CV2 等辅助依赖项。
# https://tensorflow-object-detection-api-tutorial.readthedocs.io/en/latest/install.html#protobuf-installation-compilation
cd reasearch
protoc object_detection/protos/*.proto --python_out=.
# From within TensorFlow/models/research/
cp object_detection/packages/tf2/setup.py .
python -m pip install --use-feature=2020-resolver .
为了完整性,你现在还可以测试 CUDA/CUDNN 安装是否成功,使用以下 Python 代码:
import tensorflow as tf
print(tf.config.list_physical_devices())
## Results in:
[PhysicalDevice(name='/physical_device:CPU:0', device_type='CPU'), \
PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')] # yay
# to explicitly run on CPU or GPU, you can always do the following:
with tf.device('GPU') as gpu:
print('This is executed on the GPU')
7. 运行演示检测模型
成功!此时你已安装好使用预训练 TensorFlow 模型进行对象检测所需的一切。在下面的链接中,你可以下载一个 Jupyter 笔记本以测试安装:
[## models/eager_few_shot_od_training_tf2_colab.ipynb at master · tensorflow/models
你现在不能执行该操作。你在另一个标签页或窗口中登录了。你在另一个标签页或窗口中注销了…
链接中的 Colab 笔记本使用了几张橡皮鸭的图片,以提升模型检测对象的能力。只需将笔记本下载到你的计算机(不再需要使用 Colab),并逐步执行单元格。确保注释掉所有关于 Colab 的引用(加载库和函数调用)。
结果令人惊叹,通过迁移学习和仅仅 5 张橡皮鸭的新图像,ssd_resnet50 基础检测器能够识别视频中的对象,如下所示:

基于预训练的 ssd_resnet50 模型的目标检测器结果
结论
设置 TensorFlow 及其依赖项的“基础设施”可能会很麻烦。尤其是在漏掉一步或另一部时,找到源头或最终的错误信息往往会令人非常沮丧。通过本指南,用户可以按照最基本的步骤,仅需几分钟即可让 TensorFlow 及其检测 API 正常运行。
如果你觉得这篇文章有帮助,我会很感激你的“关注” 🫀,直到那时:
{照顾好自己,如果可能的话,也照顾好其他人}
— 借用自 Stephen Dubner
链接到官方安装说明(这不包括 NVIDIA 驱动程序的安装步骤):
[## 安装 - TensorFlow 2 目标检测 API 教程文档
虽然安装和使用 TensorFlow 并不需要 Anaconda,但我建议这样做,因为它的…
tensorflow-object-detection-api-tutorial.readthedocs.io](https://tensorflow-object-detection-api-tutorial.readthedocs.io/en/latest/install.html?source=post_page-----c1660d4ae533--------------------------------)
改善数据科学面试的简单方法
通过技术问题框架识别前 5%的候选人
·
关注 发表在 Towards Data Science · 13 分钟阅读 · 2023 年 7 月 13 日
--
在这篇文章中,我分享了一个关于我作为一个缺乏经验的数据科学招聘经理所犯的错误的故事,以及这个错误如何改变了我进行技术面试的方式。我还会讲解一个示例数据科学面试题,并展示更强的候选人与较弱的候选人在解决问题时的不同方法。虽然我将讨论重点放在数据科学上,但我大部分的见解和建议对任何技术角色,包括软件工程、数据工程等,都具有相关性。
不过,首先,简要介绍一下我自己。
我已经在软件工程和数据科学/机器学习领域工作了大约九年。我在各种规模的公司工作过——最大的公司是 Wayfair(13k 员工),最小的公司是我目前的雇主 Fi(约 100 员工),我在 Fi 担任数据副总裁。我现在正接近一个拐点,我的职业生涯一半时间是作为个人贡献者(IC),另一半时间则作为经理/总监/副总裁。在后半段时间里,我建立或继承了从两个人到 15 个人的团队。在这段时间里,我招聘了大约 20 人,进行了数百次面试,并设计了无数的面试流程。
在我担任招聘经理期间,我做了许多成功的招聘,但也犯过一些错误。例如,在我刚开始担任招聘经理并负责从零开始构建面试流程时,我犯了一个最大的招聘错误。我花了另一两年时间才完全理解我犯的错误。但一旦我能够明确表达出来,我知道这是可以避免的,并采取了措施确保不再发生。
这篇文章是关于那个错误的,以及我怎么做才能避免再次犯错。
我的招聘错误

由 Eastman Childs 提供的照片,来源于 Unsplash
在 2019 年,我从高级机器学习工程师晋升为首席数据科学家,这是一项管理职位。我的团队希望建立新的建模应用程序,这需要不同于我之前构建的模型和集成。由于我最近刚刚担任管理职位,所以没有时间自己构建所有必要的基础设施。因此,我开始招聘一名高级数据科学家来帮助构建和维护新的模型和集成。
面试过程
我设计了一个面试流程,包括一个招聘经理筛选、一个家庭作业项目和几个面试小组。除了跨职能面试外,所有面试都具有技术性质,涉及某种形式的机器学习、数据工程或软件工程挑战和设计问题。在几个月内,我们找到了理想的候选人。

我的 2019 年数据科学面试流程
新员工的前几周进展顺利。当他们掌握了技术栈、团队成员和工作流程后,我给他们分配了一个更大的项目。
症状出现
在他们被分配到项目几周后,我注意到他们的任务花费的时间比预期的要长。因此,我每周花额外的时间与他们一起工作,以确保进度保持正常。但不幸的是,情况没有改善。几乎每次我们开会讨论进展和下一步时,似乎都没有取得进展。相反,他们会提出新的技术障碍,从他们的角度来看,这些障碍需要解决才能继续前进。我记得我感到沮丧,因为我很难理解他们提出的所有技术障碍是如何如此相关的,因为它们似乎是凭空出现的。
我记得我们已经在这个项目上工作了两个月,而我们预计这个项目需要两周才能完成,但我们仍然没有一个可行的解决方案。更糟糕的是,我们甚至没有一个明确的完成时间表。
根本问题

图片由Alexander Hafemann拍摄,来源于Unsplash
现在,我已经管理和招聘了多年,并且经历了许多新员工的成功与失败,我能够准确地阐述潜在的问题是什么,以及我哪里做错了。
根本问题是,他们缺乏在其角色中取得成功所必需的技能集。表面上看,他们似乎缺乏技术能力,因为他们经常遇到无法迅速解决的技术障碍。但事实并非如此。实际上,他们的技术能力非常出色。
实际上,他们缺乏理解技术应用与业务需求之间联系的能力,这阻碍了他们知道何时以及如何进行权衡。这表现为无法克服的技术障碍,每一个都可以通过简化问题陈述来避免。
例如,他们不断遇到的一个挑战是由于他们处理的数据集的大小。但每次他们将此作为问题时,我都会建议将数据集缩减到仅包括我们感兴趣的前三或四个特征,然后过滤掉可能相关的记录。这样做会将整个数据集的总体量减少到原始大小的不到 0.5%,这将避免因数据量而产生的问题,并能提供整个数据集 80%的附加价值。但每次我提出这个建议时,很明显他们没有考虑过,尽管我一再提到这一点。
技术问题框架
总结一下——新员工在同时维持对业务背景和技术背景的强有力理解方面遇到困难,因此他们着手解决的技术任务通常比实际需要的复杂。换句话说,他们在技术问题框架方面存在问题,即将商业目标构建为技术目标的能力,以及理解一组需求如何代表一个潜在商业目标的能力。
对于那些不熟悉技术问题框架或数据团队典型工作流程的人来说——通常,需求由产品经理(PM)或经理/技术负责人提供。但即使在需求提供给个人贡献者(IC)的情况下,需求也从未完全详尽。因此,IC 必须能够理解导致这些需求的目标。如果他们不能自己做到这一点,那么他们将需要由经理或 PM 密切观察。这限制了团队的扩展性,并通常会造成 IC 与其经理/PM 之间的摩擦。
当我反思这种情况时,很明显我哪里出错了——我没有构建一个评估技术问题框架的面试,而这项技能对于他们在角色中成功是必需的。一旦意识到这一点,我开始尝试将其融入我的面试过程中。幸运的是,我发现最有效的方法只需进行小的调整。
调整面试
这是我不同之处。
在至少一个技术面试中,我将技术任务嵌入到一个实际的商业场景中,在这个场景中,需要充分理解额外的背景才能适当地解决问题。
除了评估技术能力之外,这个调整后的面试还评估候选人从需求中推断出项目实际意图的能力,并确保在设计技术解决方案时实现这一意图。
接下来,我将通过一个不评估技术问题框架的面试示例,并讨论什么是强有力的解决方案。然后,我将展示同一个面试,但加入技术问题框架调整,展示它如何改变对强有力解决方案的定义。
你可以在这里找到我用于这个面试的原始数据集。你还可以在这里找到配置为 Kaggle 笔记本的面试提示。
示例 1:没有技术问题框架评估的数据科学建模面试
这是一个没有技术问题框架评估的面试提示。
###############################################
# Interview WITHOUT technical problem framing #
# as part of the assessment. #
###############################################
# We have provided you with a dataset
# consisting of patient health information
# related to cardiac arrest (heart-attacks).
# Each record represents a patient that
# visited the Emergency Room (ER) because they
# were experiencing chest pains. Each column
# corresponds to a measurement that was taken
# at the time they arrived at the ER, including
# the type of chest pain they were
# experiencing. The dataset also contains a
# binary column that indicates whether or not
# the patient ended up having a heart-attack
# within 48 hours of their ER visit.
import pandas as pd
df = pd.read_csv(f"{filepath}/heart.csv")
display(df.head(5)
# Your task is to construct a model that
# predicts whether a candidate will have a
# heart-attack based on the provided inputs.
def predict_heart_attack(row):
"""
Accepts one row of heart-attack dataset.
Returns 0 or 1 as the prediction.
"""
# TODO
pass

心脏病数据集的前五行数据。
我曾经在现场进行此面试,那时方便使用一个小而干净的数据集。数据集很小(303 行和 13 个输入),且相对干净,因此任何有一定 ML 经验的候选人都可以在没有太大困难的情况下构建分类器。
评估
较弱的候选人很容易被识别,因为他们通常在分配的时间内甚至难以建立一个基本模型,更不用说一个好的模型了。面试官更微妙的任务是从“好”的候选人中识别出“优秀”的候选人。除了展示在短时间内建立一个有效分类器的能力外,较强的候选人通常通过(1)采取迭代方法——他们迅速使某些东西有效,然后加以改进,以及(2)做出深思熟虑的决策来区分自己。例如,当我询问他们为何选择特定的性能度量来评估模型表现时,他们会给出具体的答案。较弱或经验较少的候选人会给出答案,但没有任何真正的理由。
示例 2:数据科学建模面试,具有技术问题框架评估
这是相同的面试问题,但嵌入了业务场景,因此它将技术问题框架作为评估的一部分。
###############################################
# Interview WITH technical problem framing as #
# part of the assessment. #
###############################################
# An Emergency Room (ER) is receiving an
# overwhelming number of patients experiencing
# chest pains, which are a symptom of heart
# attacks. Patients who are showing other
# symptoms of heart attacks should be
# prioritized (fast-tracked) upon entering the
# ER waiting room in order to mitigate the
# effects of the heart-attack, or avoid it
# altogether.
#
# On average, the ER is equipped to fast-track
# 20% of patients who are experiencing chest
# pain, allowing them to skip the patient
# queue. Currently, the ER's policy is to
# fast-track any patients who are
# experiencing Type 2 chest pain (atypical
# angina). This corresponds to a value of
# `df['cp'] == 1` in the dataset. The ER staff
# thinks that their existing policy is
# suboptimal, and is requesting that you
# perform an analysis on this patient data in
# order to develop a policy that better
# prioritizes high-risk patients.
#
# We have provided you with a dataset
# consisting of patient health information
# related to heart-attacks. Each record
# represents a patient that visited the ER
# because they were experiencing chest pains.
# Each column corresponds to a measurement that
# was taken at the time they arrived at the ER,
# including the type of chest pain they were
# experiencing. The dataset also contains a
# binary column that indicates whether or not
# the patient ended up having a heart-attack
# within 48 hours of their ER visit.
import pandas as pd
df = pd.read_csv(f"{filepath}/heart.csv")
display(df.head(5)
# Your task is to use the dataset to construct
# a fast-track policy that is better than the
# ER's current policy.
def fast_track(row):
"""
Accepts one row of heart-attack dataset.
Returns 0 or 1 as the decision to
fast-track.
"""
# TODO
pass

心脏病数据集的前五行。
注意,问题的技术方面保持不变——使用完全相同的数据集,解决方案签名也相同。但现在有额外的信息改变了理想解决方案的特征。
新问题陈述
增加的业务背景引入了两个候选人在开始解决方案之前需要理解的新信息。第一个是只有 20%的患者可以被快速处理。这对应于 60.6 名患者,如果我们四舍五入,则为 61 名:
.20 * len(df) # Outputs 60.6
因此,我们可以通过快速处理“挽救”的最大患者数量是 61,因为 ER 不能快速处理更多。
ER 背景提供的第二个新信息是 ER 有一个基准策略,需要超越该策略才能考虑新政策。这个基准策略导致正确预测 41 次心脏病发作:
# The ER baseline policiy is to fast-track
# any patient with Type 2 chest pain,
# which corresponds to
# `df['cp'] == 1`. So the ER baseline
# strategy is to return 1 when
# df['cp'] == 1, and 0 otherwise.
(
df.groupby(['cp'])[['had_heart_attack']]
.agg(['mean', 'count'])
)

groupby的输出,显示了每种胸痛类型的心脏病发作率的细分。
.82 * 50 # outputs 41
将添加的约束(总共 61 次快速处理)与超越基准(41 次正确预测)的目标结合,我们可以将新目标制定为:找到一个召回率@k(k=61)大于 41 的分类器。
较弱的候选人
技术问题构建能力较弱的候选人通常会忽略这两条信息,直接进入解决方案阶段。这通常会导致两种次优解:一种是高精度但召回率等于或低于 41 的解,另一种是高召回率但精度极差,导致前 61 名快速处理的患者心脏病发作不会超过 41 例。作为面试官,如果我看到候选人走错了方向,我会给出提示来引导他们。一些候选人能够注意到我的提示并正确调整方向,而其他人则仍然难以确定要解决的正确问题。
强有力的候选人
擅长技术问题构建的候选人会以不同的方式处理问题。他们不会从一开始就跳入解决方案模式,而是花时间彻底阅读提示,通常会阅读多次,以确保理解上下文。
接下来,他们会做一些与成功密切相关的事情,这也是我特别关注的:
最好的候选人在开始之前会写出他们的方案,然后询问我(面试官)这是否合理。
当我观察到这一点时,这对我来说是如音乐般悦耳。为什么?因为这正是我希望他们做的,如果他们加入我的团队。我希望有人能够在开始之前提前阐明他们的计划,并且具有在开始之前与我讨论的意识。尽管这在前期需要更多时间,但可以减少面试中途反复调整方案的需求,从而确保剩余时间的有效利用。
能够明确表达正确问题的候选人通常也能解决挑战。这不应该让人惊讶,因为超越基准并不难。例如,以下简单的基于规则的解决方案就超越了基准:
def fast_track(row):
"""
A very simple solution that still beats the
baseline.
"""
# "cp" is the column for chest pain.
if row['cp'] == 2 and row['sex'] == 0:
return 1
elif row['cp'] == 1 and row['sex'] == 0:
return 1
else:
return 0
# Check the performance
df['pred'] = df.apply(
lambda row: fast_track(row),
axis=1)
top_k_preds = df.sort_values('pred').tail(61)
recall_at_k = len(
top_k_preds
.query('had_heart_attack == 1')
.query(pred == 1))
print(f"Recall@61 = {recall_at_k}")
# Outputs Recall@61 = 50
但是,如果候选人能够清楚地陈述问题并将其解决到最大程度(完美召回率为 k=61),那么绝对会获得额外的分数。
技术问题构建面试的好处
技术问题构建面试的主要好处是,能够通过面试并因此被录用的人能够更加独立地操作。因为他们能够内化他们被要求改进的目标,他们可以减少来自经理和产品负责人的额外工作量。这对于扩展技术团队的影响至关重要,尤其是在支持较少的小型组织中,经理还需承担 IC 的职责,因而有限的精力无法监督大量项目。
例如,我们能够保持 Fi 的数据团队非常小巧灵活,这很大程度上是因为我们只招聘具有强大技术问题框定能力的个人。我们目前的团队只有四个人(不久将增加到五个),但我们满足了一个拥有 100+员工的企业的所有数据相关需求,掌握了所有 ETL 流程、数据仓库设计和维护、Tableau 报告、深度分析和根本原因分析、机器学习和预测建模,以及最近的新特性研发。我们覆盖的领域几乎包括了业务的每个方面——财务、市场营销、客户体验、工程、硬件、固件、运营和产品。我们能够承担如此多的工作并覆盖如此多的领域,是因为团队中的每个人都擅长将一个模糊定义的问题映射到一个技术问题陈述上。
敬请期待
敬请关注未来的帖子,我将讨论如何提升自己框定技术问题的能力,以及如果你是经理的话,如何提升团队的能力。
加速你的 Python 代码的简单方法 — 保持更新
原文:
towardsdatascience.com/a-simple-way-to-speed-up-your-python-code-stay-up-to-date-e633b73619f9
优化
仅通过更新库即可提高最多 15 倍的速度
·发表于 Towards Data Science ·阅读时长 13 分钟·2023 年 1 月 10 日
--

图片由 Gerd Altmann 提供,来自 Pixabay
很多时间和金钱都花在尝试优化代码,以便其尽可能快速和高效,在数据科学领域,这一点变得更加重要,因为现在需要处理的数据集非常庞大。
一个简单且经常被忽视的代码优化方法是确保你选择的编程语言及相关库尽可能地保持最新。
你可能会惊讶于相对较少的时间和精力,可以带来一些显著的好处。
介绍
说保持更新可以帮助“优化”代码是一种相当显而易见的说法,但盲目更新软件或库而不了解其变化,可能会引发灾难。
在阅读完本文后,你应该对为什么应该保持更新有一个清晰的了解。你将会有一个切实可行的计划,确保在优化与不浪费宝贵时间之间找到平衡。此外,你还将了解可能出现的陷阱以及如何避免它们。
总结一下,使用最新版本的 NumPy(1.24.0)来展示保持软件和库最新的实际好处。
为什么我应该保持更新?

简单的答案是,你可以从以下项目中受益:
-
崭新的功能
-
优化
-
错误修复
所有这些都是由了解软件的人员为你实现的,记住……
你不是软件开发专家
如果你是数据科学专业人士(或爱好者),我假设你是,那么你主要关注的是处理、操控和分析数据,以获得洞察和预测。
尽管你在软件开发和一般编码方面可能有一定的能力,但公平地说,这并不是你的专长。
因此,你依赖于高级直观语言(Python、R、Matlab 等)和大量库,这些库提供了与你的工作领域相关的各种功能和优化是完全合理的。让你可以专注于你的专业。
依赖专家,他们更懂
如果你能将优化融入你的代码中,那很好!但这不应是占用你时间的事。
事实证明,还有一大批其他专业人士,他们非常了解软件开发。他们努力确保你使用的语言和库得到优化,并不断改进。为你提供所需的准确工具,应用于你的工作中。
然而,为了利用这些优化,你需要留意,否则你可能会错过。
阅读发布说明,这很重要

图片来源:Pixabay
语言及相关库的新版本以惊人的频率发布。然而,如果你不关注实际发生的变化,你可能会错过潜在的好处,或者引入错误/不合理的参数。
一个很好的例子是,NumPy 1.24.0 中对 NumPy np.in1d 函数的算法更新,这也被广泛使用的 np.isin 函数所利用。
np.in1d(由np.isin使用)在传递两个整数数组时现在可以切换到更快的算法(速度提高超过 10 倍)。这通常会自动使用,但你可以使用kind="sort"或kind="table"分别强制使用旧的方法或新方法。
错过收益
上述引文中需要注意的是,kind="table" “通常会自动使用”,这意味着并非总是如此。
你可能会错过 10 倍的速度提升
你可能会因为未满足开发者设定的自动使用新方法的要求,而错过 10 倍的速度提升:
如果为 None,将自动选择‘table’,如果所需的内存分配小于或等于ar1和ar2大小之和的 6 倍,否则将使用‘sort’。这样做是为了避免默认情况下使用大量内存,尽管‘table’在大多数情况下可能更快。
如果你阅读了发行说明,那么只需在方法参数中添加kind=”table”,就是确保执行速度显著提升的简单更改,无论内存分配如何。
注意: 这种自动选择可能是为了确保新方法不会在你的代码中引发错误。在某些情况下,它可能比以前的方法使用更多的内存,这在内存受限的环境中可能是一个问题。开发人员对边界情况的覆盖非常明智。
引入错误或混乱的代码
如果你没有设置新参数(因为你没有阅读发行说明),那么开发人员非常明智地设置了一个默认值。
如果为 None,将自动选择‘table’,如果所需的内存分配小于或等于ar1和ar2大小之和的 6 倍。
很好!这样你可能会自动获得 10 倍的加速。然而:
如果选择了‘table’,assume_unique 将没有效果。
在这种情况下,“assume_unique”没有效果是一个小问题,因为开发人员似乎已经考虑了边界情况,以确保如果你之前分配了“assume_unique”,它将被忽略。
然而,这很混乱,因为你的代码指定了一个不相关的参数,这可能会在未来导致其他人(甚至你自己)感到困惑。
还值得注意的是,可能存在开发人员不够彻底的情况,或无法避免的破坏性更改。如果发生这种情况,你将突然遇到无法运行的代码。
新方法
除了可能错过你已经在代码中使用的方法的改进之外,你还可能错过对你的项目或工作流程可能有用的全新方法。
在上一篇关于一热编码数据的文章中,我提到了最近发布的 Pandas 1.5.0 中实现的一种新方法,即[from_dummies()](https://pandas.pydata.org/docs/reference/api/pandas.from_dummies.html),它是 Pandas 常用的一热编码方法[get_dummies()](https://pandas.pydata.org/docs/reference/api/pandas.get_dummies.html#pandas.get_dummies)的反向操作:
OneHotEncoder vs get_dummies vs to_categorical
[towardsdatascience.com
在这种方法发布之前,反向一热编码将是一项手动操作。
在你使用的库中创建了大量的新方法,但了解它们何时出现的唯一方法是查看发布说明。
无论开发人员实施了什么方法,都将得到优化,并且减少错误的可能性
…并且记住,很可能开发人员实施的任何方法都会比你自己编写的代码经过优化,并且不容易出现错误。他们毕竟是专业的软件开发人员,编写无错误且经过良好测试的代码并非易事。
保持更新的策略

图片由 Patrick Perkins 拍摄,刊登在 Unsplash
时间就是金钱,正如那句名言所说。
现实情况是,典型项目可能包含大量的库,因此不切实际逐一阅读每个库每次小更新的发布说明,所以明智的做法是优先排序。
关于特定发布点的更新
常见的编号惯例包括由点分隔的三个数字。例如,目前的 NumPy 发布版本是 1.24.0。
了解这些数字(一般而言)表示的含义可以帮助规划何时需要关注:
-
主版本号 (1.24.0):软件或库的重大和显著变化。可能包括不向后兼容的更改。 这需要在升级前仔细审查。
-
次版本号 (1.24.0):通常是小功能变化/改动或更大范围的错误修复。
-
修补版本号 (1.24.0):通常是较小范围的错误修复。
如果主版本号发生变化,则绝对必要查看发布说明,因为可能会有破坏性更改(即在某些情况下你的代码可能会完全停止工作)。
次版本号的变化是你应该关注的内容,因为它在潜在收益、新方法或改进方法方面是最优的。
如果没有时间,通常可以忽略修补版本号,除非你在等待已知错误的修复。
选择最重要的库
如果时间紧迫,你应该选择使用最频繁或对项目最相关的库(即那些影响最大的库),并在更新到来时跟上任何相关变化。正如前一节所述,这主要涉及关注主版本号和次版本号的变化。
并不是每个补丁版本发布时都必须更新库,但跟踪发布的更改会给你提供在对项目有利时更新的机会。
实际上,更新越小,发生错误时确定错误的难度越小,减少缓解所需的时间,并降低风险。
更新策略 - 总结
-
关注所有库的主要版本号更改。
-
关注最常用或对项目最相关的库的次要版本号更改。
-
补丁版本号更改通常可以忽略,除非你在积极等待修复某个错误。
升级的潜在危险

照片由 Raúl Nájera 拍摄,来自 Unsplash
如前所述,保持更新可以带来实际的好处,但也可能导致不必要的问题和因调试新错误而浪费的时间。
再次提醒,通过关注发布说明,大多数情况下可以避免这些问题。
我将参考 NumPy 1.24.0 版本的发布说明作为预期内容的示例。它布局清晰,信息丰富(并非所有库/软件都是如此):
[## 发布说明 - NumPy v1.24 手册
编辑描述
你将面临的主要问题通常是由于以下原因之一。
弃用 / 已过期的弃用
这可能是最需要关注的事项。当方法被弃用时,它最终会被移除(时间尺度各不相同)。这非常好,因为假设你知道弃用的情况,它会给你时间实施解决方法,或切换到库中的更新方法。
然而,如果你查看发布说明并注意到你的某个方法被列为过期弃用,则在升级之前需要立即修复,否则你的代码将会停止工作。
兼容性说明 / 更改
这比弃用要轻微,但可能很棘手,因为它们可能会改变函数的行为和/或输出。如果你没有意识到这一点,可能不会立即注意到。
array.fill(scalar) 的行为可能略有不同
由于逻辑与项分配对齐,
numpy.ndarray.fill在某些情况下现在可能会略有不同。之前的类型转换在使用无法在目标
dtype中表示的值或目标具有objectdtype 时,可能会产生略微不同的结果。
“可能会稍有不同”这句话确实很模糊!如果这与你的代码相关,这可能会导致“有趣”的错误。如果你有非常敏感的实现使用了这个函数,值得关注一下上述情况。
如果我不能升级怎么办?

图片来自 Towfiqu barbhuiya 的 Pexels
在工业界,现实是你可能会面临超出你控制范围的升级过程限制。这可能是由于选择的基础操作系统/容器,或由于你正在处理的项目中的其他领域的限制,需要非常特定的版本。
记住,没有可靠的信息,无法做出决策。
然而,仍然值得跟进关键包和库的发展。如果没有其他,至少这是一种推动可能相当复杂和/或昂贵的升级过程实施的 ammunition,因为好处可能超过升级成本。记住,没有可靠的信息,无法做出真正的决策。
保持最新的好处的一个具体例子

图片来自 Rodolfo Quirós 的 Pexels
最近,NumPy 的小版本号从 1.23.5 更改为 1.24.0。正如本文早些时候解释的那样,小版本号的更改可能会导致“轻微功能变化”,而不仅仅是补丁更新所带来的简单错误修复。
几个这些“小功能变化”声称会使其原始函数的版本显著加快:
np.in1d
np.in1d(由np.isin使用)现在可以切换到更快的算法(速度提升超过 10 倍)
NumPy 比较函数
比较函数(
numpy.equal、numpy.not_equal、numpy.less、numpy.less_equal、numpy.greater和numpy.greater_equal)现在更快了,因为它们已经用通用内部函数进行了向量化。对于具有 SIMD 扩展 AVX512BW 的 CPU,整数、浮点和布尔数据类型的性能提升分别达到 2.57 倍、1.65 倍和 19.15 倍(以 N=50000 为例)。
测试声明
尽管我没有理由怀疑这些数据,但我认为实际测试一下可能会很有趣,因此接下来的部分将对旧版本和新版本进行基准测试,以查看实际世界中的速度提升。
笔记本
以下所有代码都可以在 jupyter 笔记本中找到。
本节详细说明了笔记本的位置,以及在线环境(如 Colab 和 Deepnote)的环境设置要求。
原始笔记本可以在这里找到,适用于本地环境:
[## notebooks/python-libraries-update at main · thetestspecimen/notebooks
你现在无法执行该操作。你在另一个标签或窗口中登录。你在另一个标签中注销了…
…或在 Deepnote 或 Colab 中快速启动。
Python 1.24.0:
Python 1.23.5:
环境设置 — 本地或 Deepnote
无论是使用本地环境还是 Deepnote,只需确保安装了适当版本的 NumPy。实现这一点的最简单方法是将其添加到你的“requirements.txt”文件中。
对于 Deepnote,你可以在右侧窗格的文件部分创建一个名为“requirements.txt”的文件,并添加以下行:
numpy==1.24.0
(根据需要更改版本号)。
环境设置 — Colab
由于 Colab 中无法访问类似“requirements.txt”文件的内容,你需要显式地安装正确版本的 NumPy。为此,请在空白单元格中运行以下代码以安装适当版本:
!pip install numpy==1.24.0
(根据需要更改版本号)。
然后在尝试运行任何代码之前刷新网页。
测试
进行以下速度测试,以概述在使用最新 NumPy 版本(1.24.0)与之前版本(1.23.5)时对 np.in1d 和 np.equal 方法的改进:
-
使用
np.equal方法创建两个长度为 50000 的整数数组(方法运行 100 万次) — 根据文档,numpy 1.24.0 速度应提升至 2.75 倍 -
使用
np.equal方法创建两个长度为 50000 的布尔数组(方法运行 100 万次) — 根据文档,numpy 1.24.0 速度应提升至 19.15 倍 -
使用
np.in1d和kind="sort"比较两个整数数组(方法运行了 1 万次)——该方法在 numpy 1.23.5 和 1.24.0 中均可用——在 numpy 1.23.5 和 1.24.0 中速度应该相同(这是一个很好的交叉检查,可以确保其他测试的结果可以直接比较) -
使用新
kind="table"方法比较两个整数数组(方法运行了 1 万次)——该方法仅在 numpy 1.24.0 中可用——根据文档,速度应该比“sort”方法快最多 10 倍
输入数据
时间测试设置
结果 —— NumPy 1.23.5
结果——NumPy 1.24.0
总结

结果比较 —— 作者提供的图片
从结果部分可以看出,所获得的结果大致符合预期,所有方法都实现了执行速度的提升:
-
使用方法
np.equal的两个整数数组 —— 使用 numpy 1.24.0 时快 4.67 倍 -
使用方法
np.equal的两个布尔数组 —— 使用 numpy 1.24.0 时快 15.56 倍 -
使用
np.in1d和kind="sort"方法比较两个整数数组 —— 使用 numpy 1.23.5 和 1.24.0 的执行时间基本相同(10,000 次迭代需要 18.5 秒) -
使用新
kind="table"方法比较两个整数数组 —— 使用 numpy 1.24.0 和新引入的kind="table"方法快 3.84 倍
结论
软件开发者不断改进我们日常使用的语言和库的努力不应被忽视。这是提高项目代码效率、速度和可靠性的最简单、最直接的方法之一。
从本文中列出的非常小的示例可以看出,收益可能非常显著。只需要一点组织,并愿意花时间审阅你最重要的库/软件的版本说明。
如果你觉得这篇文章有趣或有用,请记得关注我,或订阅我的新闻通讯以获取更多类似内容。
如果你还没有,考虑订阅 Medium。你的会员费不仅直接支持我,还支持你阅读的其他作者。你还将获得对 Medium 上所有故事的全面无限制访问。
使用我的推荐链接注册将会让我获得少量佣金,不会对你的会员资格产生任何影响,因此如果你选择这样做,我会很感激。
## 通过我的推荐链接加入 Medium - Mike Clayton
阅读 Mike Clayton 的每一篇故事(以及 Medium 上的其他成千上万位作者的故事)。你的会员费直接支持…
实施 dbt 模型单元测试的简单(但有效)方法
单元测试 dbt 模型一直是 dbt 生态系统中最关键的缺失部分之一。本文提出了一种新的单元测试方法,依赖于标准和 dbt 最佳实践。
·
关注 发表在 Towards Data Science · 9 分钟阅读 · 2023 年 8 月 18 日
--
照片由 Fabio Ballasina 提供,发布在 Unsplash
自从 dbt 将软件工程最佳实践引入数据工程领域以来,它的功能和周边生态系统不断扩展,覆盖了更多的数据转换领域。
然而,“数据工程与软件工程最佳实践”的一个关键部分仍然模糊不清且尚未解决:单元测试。
单元测试的重要性、它们为何对任何一行代码在称为“生产就绪”之前至关重要,以及它们为何不同于 dbt 测试或数据质量测试,已经被 精彩阐述和解释。但如果我们要用一分钟的电梯陈述总结它们的重要性,那就是:
在数据工程中,通常有两个不同的元素需要测试:数据和我们的代码——dbt 测试(以及其他数据质量系统/工具)允许我们测试数据,而单元测试允许我们测试代码。
考虑到上述情况,社区自然进行了多个倡议,以通过开源单元测试功能增强 dbt(如 Equal Experts 的 dbt 单元测试包 或 GoDataDriven 的 dbt 专用 Pytest 插件)。然而,这些包在功能上仍然有限,并且具有较高的学习曲线。
本文介绍了一种更简单却更优雅的方法,依靠标准和 dbt 最佳实践来实现可扩展且可靠的单元测试流程。
单元测试模型与 CTE
在深入探讨方法之前,让我们首先定义一下我们希望在何种级别运行单元测试。这里需要回答的问题与我们在 dbt 项目中的“可测试单元”的定义有关。
我们的可测试单元是什么?
如果我们坚持软件工程最佳实践,那么一个可测试单元就是一个具有清晰输入和输出的小段代码——并且不依赖于外部因素。根据 dbt 项目及其复杂性,这一定义可以适用于 dbt 模型及其构成的公共表表达式(CTEs)。
如果我们的 dbt 项目主要包含短小而相对简单的模型(每个模型少于 100 行代码),那么在模型级别定义单元测试是合适的。然而,如果我们的 dbt 项目包含长而复杂的模型,那么在 CTE 级别定义单元测试将是更高效的方法。
这是一种视角问题
dbt Labs(dbt Core 的开发公司)更倾向于第一种方法。因此,毫不奇怪,他们的单元测试提案专注于模型级别的测试,并将 CTE 级别的测试视为反目标。对于小型或新建的 dbt 项目,这可能是一个非常合理的设计决策。然而,如果你拥有一个成熟或大型的 dbt 项目,那么为了避免复杂模型而增加不必要的临时模型可能不是一个好主意。
对于大多数 dbt 项目,在一个 dbt 模型中对同一实体执行多个操作(变换、聚合等)是完全可以接受的:我们可以首先对给定的列进行变换,然后使用它来聚合数据,或者我们可以创建一个新的标志,然后依赖它来应用一些业务逻辑或筛选数据。这些操作可以在同一模型中的不同 CTE 中进行,使 CTE 成为一个自然的可测试单元。
本文中介绍的方法将重点关注 CTE 的单元测试,而不是整个模型,尽管相同的原则可以调整用于模型级别的单元测试。
秘密武器:缩小宇宙
实施 dbt 模型的单元测试系统的主要复杂性在于“dbt 模型”可以有很多种形式。从一个 1000 行的庞大select语句到一长串 CTE 和子查询,如果我们想构建一个可以用于任何 dbt 项目的通用单元测试系统,就需要考虑所有这些场景。
然而,我们可以通过限制问题的范围来完全避免这种复杂性。与其构建一个在任何 dbt 项目中都能工作的单元测试系统,不如只为我们的dbt 项目构建一个。正如 Don Draper 会告诉你的,我们可以随时“改变对话”:
与其为 dbt 模型(一个我们无法控制的宇宙)构建单元测试系统,不如为我们的 dbt 模型(一个我们可以控制的更小宇宙)构建系统。
改变我们目标宇宙的范围大大简化了问题,因为我们了解我们的 dbt 模型及其特性。更重要的是,我们可以为它们的结构和内容定义标准和指南。

如何通过“缩小”宇宙来简化单元测试方法的视觉表示(作者提供的图像)
上述原则也可以应用于我们希望构建与 dbt 相关的自定义和功能的其他领域。设定标准并限制我们对“dbt 模型”的期望,可以简化问题,并为直接解决方案打开大门。
导航熟悉的宇宙:标准化我们的 dbt 模型结构
现在我们已经将工作的范围限制在我们的 dbt 模型上,我们可以通过定义一个强制执行的模型结构来明确我们的输入,这个结构可以通过自动化(如 CI 操作)来强制执行。如果你已经在维护没有推荐模型结构的 dbt 项目,你可以在更新现有模型时逐步开始强制执行它。
理想的结构将取决于你的用例,但 dbt Labs 推荐的 import-intermediate-final 方法在大多数情况下是一个安全的选择。它基本上将 dbt 模型分为三种类型的 CTEs:
-
导入 CTEs:这些是模型的第一部分,仅仅是从其他模型中读取数据。我们可以进行列重命名和过滤,但在这个阶段不应对数据进行转换。理想情况下,它们的名称应以
import_前缀开头。 -
中间 CTEs:这是工作的大部分内容发生的地方。我们可以使用这些 CTE 来转换、连接和汇总数据,应用业务逻辑,以及进行所有其他操作以生成模型的期望输出——使得这些 CTE 成为我们希望单元测试的对象。理想情况下,它们的名称应以
intermediate_前缀开头。 -
最终 CTE:在这个 CTE 中,我们将不同的导入和中间 CTE 连接起来,以定义模型的输出——而不应用任何额外的转换。这个 CTE 应该命名为
final。
然后,作为模型的最后一部分,select * from final 语句将生成其输出。

使用 import-intermediate-final 结构的示例 dbt 模型(作者提供的图像)
既然我们有了可以围绕其设计的模型结构,定义单元测试系统变得更加容易。
但是,值得注意的是,理想情况下我们应该在开始使用 dbt 之前定义模型结构——以及其他 dbt 使用的基础。这是我和 Zendesk 团队在决定实施基于 dbt 的数据转换框架时关注的关键领域之一,我们在 Zendesk 工程博客上发布了这篇文章详细描述了我们的标准工作。
基于标准:一个简单的单元测试系统
基于我们讨论的结构,为了确保对 dbt 模型进行高效的单元测试代码覆盖,我们可以依赖测试中间 CTEs 和最终 CTE——因为导入 CTEs 不包含我们需要测试的代码。

将 dbt 模型分为需要测试的输入和代码(作者提供的图像)
现在我们可以根据上述标准定义单元测试系统的不同组件及其交互方式。
模拟输入:导入 CTEs 的输出
我们系统的第一个组件是模拟输入(基本上是我们希望用来测试不同执行场景的样本数据)。在结构上,这应该类似于我们的导入 CTE 的输出——因此,如果我们从名为 customers 的模型中读取两列数据,则模拟输入应包含相同的结构。

将导入 CTE 的输出转换为单元测试的模拟输入(图片由作者提供)
这些输入应该定义为 dbt 种子(因此我们提供包含模拟数据的 CSV 文件),理想情况下放在一个单独的 tests dbt 项目中(以避免使我们的主项目混杂单元测试种子)。
预期结果:中间 CTE 的期望输出
现在我们已经定义了输入,我们可以使用相同的过程(dbt 种子)来定义期望的输出。这些输出对应于每个中间 CTE 执行后的数据的预期状态。
对于每个中间 CTE,我们可以有多个输出,以对应我们想测试的场景,以最大化代码覆盖率并确保我们测试边界情况。(可以在同一个 CSV 中使用 test_id 列来分隔不同的测试,或者提供多个 CSV 文件。)
运行测试:选择的奢侈
系统的最后一个架构组件是执行断言的过程(将 CTE 的实际输出与预期输出进行比较)。
从技术上讲,我们只想比较数据仓库中的两个表:
-
从我们的预期 dbt 种子生成的表(我们在 CSV 文件中提供的预期输出)
-
通过运行我们正在测试的 CTE 生成的表(使用模拟数据作为其输入)
然后可以使用现有的包,如 dbt 审计助手 或数据质量系统如 Soda Core(使用 SodaCL 的内置比较检查)。
连接各部分
最后,现在所有组件都已定义,我们只需通过一个模块(可以用 Python 或其他任何语言编写)将这些组件连接起来,该模块执行以下操作:
-
编译我们要测试的 dbt 模型(以用实际值替换变量和宏)。
-
解析编译后的模型并检索中间 CTE 的列表(理想情况下,我们可以依赖
intermediate_前缀,但这可以通过多种方式实现)以及最终 CTE(使用其名称)。 -
对于每个 CTE,将导入 CTE 和其他中间 CTE 的引用替换为相应的模拟数据引用(因此,我们将引用 dbt 种子,而不是 CTE)。在这里,我们可以依赖前缀(
import_和intermediate_)来找到需要替换的 CTE 名称。 -
对于每个 CTE,将其新主体(在替换引用后)作为 dbt 模型写入我们的测试 dbt 项目中。
-
运行
dbt seed命令在我们的测试项目中(以重新创建模拟数据表和预期输出表),理想情况下使用一个仅刷新与我们当前测试的模型相关的表的标签。 -
使用我们选择的工具(一个 dbt 包或类似 Soda 的工具)执行断言,并根据输出打印结果或执行某些操作。
通过上述过程,我们可以在一次执行中测试每个中级 CTE(以及最终 CTE),而不必担心一个 CTE 的实际输出会干扰下一个 CTE。这意味着,对于给定的 CTE,dbt 仅会在运行时使用模拟输入(因为它引用了我们提供的 dbt 种子)——输入包括导入 CTE 的输出和我们引用的其他中级 CTE 的预期输出。

一个中级 CTE 的示例输入,其中我们引用了两个导入 CTE 和另一个中级 CTE(图像来源:作者)
结论
在本文中,我们介绍了一种可扩展且灵活的单元测试方法,用于 dbt 模型(在 CTE 级别),这需要相对较少的工程工作,并专注于执行标准。尽管该方法不能解决所有 dbt 项目的单元测试问题,但我相信许多团队可以在他们的项目中利用它(或其变体)来增加一个重要的安全层:测试代码。
这种方法可以根据 dbt 项目的特定情况进行调整和增强,并不适用于所有场景(例如,如果大部分代码位于宏中)——但标准化仍然是一个强大的盟友,能够简化在不同领域提升 dbt 的路径。
如需更多数据工程内容,您可以订阅我的新闻通讯《数据浓缩》,我将在其中讨论各种与数据工程和技术相关的主题:
## Data Espresso | Mahdi Karabiben | Substack
数百名订阅者。数据工程更新和评论,伴随您的下午浓缩咖啡。点击阅读…
一位站点搜索工程师的日志:在 Elasticsearch 查询构建中应对相关性挑战
与 Elasticsearch 的工作就像是在玩乐高
·
关注 发表在 Towards Data Science · 15 分钟阅读 · 2023 年 6 月 13 日
--
Mourizal Zativa 在 Unsplash 上的照片
介绍
在过去的 22 个月里,我一直担任站点搜索工程师,使用 Elasticsearch 来帮助提高我们餐饮平台的相关性。我总共部署了 83 个版本,包括 3 个主要版本。
每周大约发布一次,我可以说我们的搜索引擎不仅比两年前好得多,而且我也学到了很多。虽然仍然远未达到一个伟大的搜索引擎,但在我看来,有些事情值得分享。更重要的是,我非常希望得到反馈。
本博客文章旨在提供一种设计 Elasticsearch 查询模板的方法,以处理常见的网站搜索问题,包括跨不同字段的匹配、结果提升和测试。我们将共同识别默认方法的问题,然后逐步提出一种新的方法来解决这些问题。
这个 Github 仓库:github.com/dvquy13/elasticsearch-sharing 包含了本文讨论的示例和代码。
主要
我们现在扮演餐厅平台搜索工程师的角色,该平台允许 diners 发现并预订他们的下一餐。我们经验不多,但幸运的是,该应用程序一开始并不要求像 Google 那样的准确度。关键是逐步取得可见的进展!
好的,让我们深入探讨。首先,我们确保用户可以通过名称搜索餐厅。在这里,我们可以依赖简单的默认 query-match 来完成这个工作。
# Index our first two restaurants
POST _bulk
{ "index" : { "_index" : "restaurant", "_id" : "001sabichuong" } }
{ "restaurant_name": "Sa Bi Chuong", "cuisine": "Vietnamese", "rating": 5.0 }
{ "index" : { "_index" : "restaurant", "_id" : "002vietnamesephonoodle" } }
{ "restaurant_name": "Vietnamese Pho Noodle", "cuisine": "Vietnamese", "rating": 4.0 }
# Test searching for one
# Should return Vietnamese Pho Noodle
GET restaurant/_search
{
"query" : {
"match" : { "restaurant_name": "vietnamese" }
}
}
上面的代码片段可以在 Kibana 的 Dev Tools > Console 中运行,如果你按照仓库的说明,你会在 localhost:5601 找到它。
代码是自解释的。我们要求 Elasticsearch 返回名称中包含 vietnamese 的餐厅。我们收到了一个 Vietnamese Pho Noodle 的结果。没有问题。
但我们很快发现,名称并不是我们在用户提交查询时想要查看的唯一地方。鉴于关键字 vietnamese,我们还应该返回餐厅 Sa Bi Chuong,因为它在 cuisine 中标记为越南餐厅。一个 multi_match 查询可以准确地做到这一点。
# Matching multiple fields
# Should return all 2 Vietnamese restaurant with the Vietnamese Pho Noodle on top
GET restaurant/_search
{
"query" : {
"multi_match" : {
"query": "vietnamese",
"fields": [ "restaurant_name", "cuisine" ]
}
}
}
# Result
"hits": {
...
"hits": [
{
"_index": "restaurant",
"_id": "002vietnamesephonoodle",
"_score": 0.6931471,
"_source": {
"restaurant_name": "Vietnamese Pho Noodle",
"cuisine": "Vietnamese",
"rating": 4
}
},
{
"_index": "restaurant",
"_id": "001sabichuong",
"_score": 0.18232156,
"_source": {
"restaurant_name": "Sa Bi Chuong",
"cuisine": "Vietnamese",
"rating": 5
}
}
]
}
默认 TFIDF 的问题
注意上述分数。第一个分数比第二个高出大约 4 倍,这表明它与查询 vietnamese 的相关性要高得多。有人可能会假设,因为在多个字段中匹配会使分数更高。
每当我们有疑问时,可以使用 Elasticsearch 的 explain 来获取详细的评分组成部分说明。
# Let's use explain=true to see what happens under the hood
# Vietnamese Pho Noodle is on top because of the default implementation of TFIDF that penalizes the matching at cuisine field because there are multiple restaurants with cuisine=Vietnamese while there are only one restaurant with name=Vietnamese
# Question: But why having the name Vietnamese in its name makes it more Vietnamese than other restaurants?
GET restaurant/_search
{
"query" : {
"multi_match" : {
"query": "vietnamese",
"fields": [ "restaurant_name", "cuisine" ]
}
},
"explain": true
}
# Result
"hits": {
"hits": [
{
"_id": "002vietnamesephonoodle",
"_score": 0.6931471,
"_source": {
"restaurant_name": "Vietnamese Pho Noodle",
"cuisine": "Vietnamese",
"rating": 4
},
"_explanation": {
"value": 0.6931471,
"description": "max of:",
"details": [
# Matching in field `cuisine` yields score=0.18
# Note that by default the score is calculated by TFIDF
# More info about Elasticsearch TFIDF: https://www.elastic.co/guide/en/elasticsearch/reference/8.6/index-modules-similarity.html#bm25
{
"value": 0.18232156,
"description": "weight(cuisine:vietnamese in 1) [PerFieldSimilarity], result of:",
"details": [...]
},
# Matching in field `restaurant_name` yields score=0.69
{
"value": 0.6931471,
"description": "weight(restaurant_name:vietnamese in 1) [PerFieldSimilarity], result of:",
"details": [...]
}
# Because the final score is "max of:" those two above scores,
# it is equal to the matching score with `restaurant_name`
]
}
},
{
"_id": "001sabichuong",
"_score": 0.18232156,
"_source": {
"restaurant_name": "Sa Bi Chuong",
"cuisine": "Vietnamese",
"rating": 5
},
# Similarly since there's no matching with `restaurant_name`,
# here the final score is equal to the matching score of `cuisine`
"_explanation": {
"value": 0.18232156,
"description": "max of:",
"details": [
{
"value": 0.18232156,
"description": "weight(cuisine:vietnamese in 0) [PerFieldSimilarity], result of:",
"details": [...]
}
]
}
}
]
}
上面我们可以看到 Vietnamese Pho Noodle 排在最前面,是因为默认实现的 TFIDF 处罚了在菜系字段中的匹配,因为有多个餐厅的 cuisine=Vietnamese,而只有一个餐厅的 name=Vietnamese。
深入到 _explanation 块,我们意识到分数差异源于 restaurant_name 的 TFIDF 匹配输出。这是预期的,因为该算法假设如果关键词不常见且通常在许多文档中找到(类似于自动处理停用词的解决方案),那么它是更好的信号。在我们的例子中,两家餐厅都有越南菜,因此根据 TFIDF,这种匹配并不能说明文档的相关性。
我们是否应该鼓励这种行为是一个问题。真的有越南人名在名称中会使一个餐厅比另一个更“越南”吗?
另一个 TFIDF 的问题是它考虑了字段的长度。
# Let's add another restaurant
POST _bulk
{ "index" : { "_index" : "restaurant", "_id" : "003vietnamesepho" } }
{ "restaurant_name": "Vietnamese Pho", "cuisine": "Vietnamese", "rating": 3.0 }
# In the below example we see that the new Vietnamese Pho restaurant is ranked higher...
GET restaurant/_search
{
"query" : {
"multi_match" : {
"query": "vietnamese pho",
"fields": [ "restaurant_name", "cuisine" ]
}
},
"explain": true
}
你可以在帖子末尾的附录 #1 中找到详细而冗长的结果。简而言之,我们看到结果将越南河粉餐厅排名第一,然后是越南河粉面。分析组成分数表明,主要区别在于越南河粉的 length=2(词)而越南河粉面的 length=3。这感觉不直观,因为我们知道第二家餐厅的评分更高,考虑到实际情况,两者匹配用户关键字一样。
使用 function_score 进行重新排序(提升)
当我们谈论 rating 时,我们可以用 function_score 包裹我们的查询,以将该信息纳入修改匹配分数,从而更好地控制我们的排名。
GET restaurant/_search
{
"query": {
"function_score": {
# Our main query is wrapped in a function_score clause
"query": {
"multi_match" : {
"query": "vietnamese",
"fields": [ "restaurant_name", "cuisine" ]
}
},
# We define the functions that will be applied on top of the matching scores
# returned by our main query
"functions": [
{
"field_value_factor": {
"field": "rating",
"modifier": "none",
"missing": 1
}
}
],
# Retrieve the max boosting defined inside `functions`
# Above there is only one boosting so it's applied by default
"score_mode": "max",
# Multiply the matching score with the boosting calculated from functions
"boost_mode": "multiply"
}
}
}
# Result
{
"hits": {
"hits": [
{
"_index": "restaurant",
"_id": "002vietnamesephonoodle",
"_score": 1.7885544,
"_source": {
"restaurant_name": "Vietnamese Pho Noodle",
"cuisine": "Vietnamese",
"rating": 4
}
},
{
"_index": "restaurant",
"_id": "003vietnamesepho",
"_score": 1.5706451,
"_source": {
"restaurant_name": "Vietnamese Pho",
"cuisine": "Vietnamese",
"rating": 3
}
},
{
"_index": "restaurant",
"_id": "001sabichuong",
"_score": 0.66765696,
"_source": {
"restaurant_name": "Sa Bi Chuong",
"cuisine": "Vietnamese",
"rating": 5
}
}
]
}
}
高评分的餐厅现在排在前面了。但是餐厅 Sa Bi Chuong 的 rating=5 怎么样?它作为最后的结果似乎我们没有“足够”提升。
我们可能会开始稍微调整 function_score,以使其发生。这是一种实现方式,以非线性方式模拟提升,从而有效地在评分为 5 的文档上应用强烈提升。
GET restaurant/_search
{
"query": {
"function_score": {
"query": {
"multi_match" : {
"query": "vietnamese",
"fields": [ "restaurant_name", "cuisine" ]
}
},
"functions": [
# Apply a non-linear function to model that
# a rating of 5 has much more weight than rating of 4 (not just 25% more)
{
"filter": {
"range": {
"rating": {
"gte": 5,
"lte": 5
}
}
},
"weight": 10
},
{
"filter": {
"range": {
"rating": {
"gte": 4,
"lt": 5
}
}
},
"weight": 2
}
],
"score_mode": "max",
"boost_mode": "multiply"
}
}
}
# Result
{
"hits": {
"hits": [
{
"_index": "restaurant",
"_id": "001sabichuong",
"_score": 1.3353139,
"_source": {
"restaurant_name": "Sa Bi Chuong",
"cuisine": "Vietnamese",
"rating": 5
}
},
{
"_index": "restaurant",
"_id": "002vietnamesephonoodle",
"_score": 0.8942772,
"_source": {
"restaurant_name": "Vietnamese Pho Noodle",
"cuisine": "Vietnamese",
"rating": 4
}
},
{
"_index": "restaurant",
"_id": "003vietnamesepho",
"_score": 0.52354836,
"_source": {
"restaurant_name": "Vietnamese Pho",
"cuisine": "Vietnamese",
"rating": 3
}
}
]
}
}
我们可能会思考:“现在的函数提升看起来不是太武断了吗?它对其他情况有效吗?”确实,这是我们应该问自己的问题。随着越来越多的需求,我们的查询模板将变得越来越复杂,导致我们所做的修改之间的冲突。
让我们来看下一个例子,来说明我所说的“冲突”。
复杂性随着模糊匹配而来
虽然不是必需的,但处理用户的拼写错误总是一个不错的功能,尤其是当他们现在熟悉像 Google 这样的智能搜索引擎时。Elasticsearch 有一个名为 fuzzy matching 的内置机制,可以通过选项 fuzziness 进行配置。
# The use of `bool` query below is to implement the logic: At least one condition should match
PUT _scripts/01-default-fuzzy-search-template
{
"script": {
"lang": "mustache",
"source": {
"query": {
"function_score": {
"query": {
"bool": {
"must": [
{
"bool": {
"should": [
{
"multi_match" : {
"query": "{{query_string}}",
"fields": [ "restaurant_name", "cuisine" ]
}
},
{
"multi_match" : {
"query": "{{query_string}}",
"fields": [ "restaurant_name", "cuisine" ],
# For the purpose of this demo, default behavior works well enough
"fuzziness": "AUTO"
}
}
]
}
}
]
}
},
"functions": [
{
"filter": {
"range": {
"rating": {
"gte": 5,
"lte": 5
}
}
},
"weight": 10
},
{
"filter": {
"range": {
"rating": {
"gte": 4,
"lt": 5
}
}
},
"weight": 2
}
],
"score_mode": "max",
"boost_mode": "multiply"
}
}
},
"params": {
"query_string": "My query string"
}
}
}
注意,我们只是创建了一个查询模板而不是运行一个查询。现在我们可以使用参数调用查询,这是 Elasticsearch 引入的一个很好的功能,可以使我们的代码看起来不那么令人畏惧。就像这样:
GET /_search/template
{
"id": "01-default-fuzzy-search-template",
"params": {
"query_string": "vietnames"
}
}
上述查询返回了我们期望的越南餐馆,给定一个拼写错误的关键词vietnames。在后台,模糊匹配使用了Levenshtein 编辑距离,它通过修改一个字符串使其变成另一个字符串的次数来衡量字符串之间的相似度。在我们的例子中,我们只需要在末尾添加一个字母e,将vietnames变成vietnamese。对算法来说,这是一项非常简单的任务。也许有人会争辩说,这对我们的开发人员来说也很简单。2 行代码和一个新的漂亮特性。
好吧,真正有趣的地方在于别处。一天,我们的销售团队突然向我们投诉搜索结果错误。即使人们明确搜索kbbq(这是korean bbq的常见缩写),他们也会得到日本烧烤餐馆而不是韩国烧烤餐馆。
餐馆如下:
POST _bulk
{ "index" : { "_index" : "restaurant", "_id" : "004parkhangseokbbq" } }
{ "restaurant_name": "Park Hang-seo's KBBQ", "cuisine": "Korean", "rating": 2.0 }
{ "index" : { "_index" : "restaurant", "_id" : "005bestbbqintown" } }
{ "restaurant_name": "Best BBQ in town", "cuisine": "Japanese", "rating": 5.0 }
查询:
{
"id": "01-default-fuzzy-search-template",
"params": {
"query_string": "kbbq"
}
}
结果:
{
"hits": {
"hits": [
{
"_index": "restaurant",
"_id": "005bestbbqintown",
"_score": 8.384459,
"_source": {
"restaurant_name": "Best BBQ in town",
"cuisine": "Japanese",
"rating": 5
}
},
{
"_index": "restaurant",
"_id": "004parkhangseokbbq",
"_score": 2.5153382,
"_source": {
"restaurant_name": "Park Hang-seo's KBBQ",
"cuisine": "Korean",
"rating": 2
}
}
]
}
}
为了理解发生了什么,我们需要启用explain=true来查看是什么因素贡献了最终的分数。由于这次输出过于冗长,以下是发现:
-
Best BBQ in town餐馆的关键词匹配分数(在提升之前)为 0.8,低于Park Hang-seo's KBBQ的 1.2 -
如果没有应用提升,我们会看到
Park Hang-seo's KBBQ餐馆排在第一位 -
但随后,来自
rating的提升修改了分数,导致我们看到的排序结果
一种框架化问题的方法是我们目前的提升算法不完美。假设我们有一个更好的公式,能够取得正确的平衡,那么问题应该可以解决。但几乎无法保证新公式不会引发其他问题。我们不希望这些问题在没有任何警示的情况下悄然进入系统,然后在某一天被利益相关者标记出来。我们希望成为第一个发现这些问题的人,特别是每当我们进行任何更改时。因此,在讨论潜在解决方案之前,我希望我们都同意,接下来我们应该做的非常重要的事情是(是的,你可能和我有一样的想法)建立一个测试/评估机制。
我们应该如何为这个搜索应用创建测试用例?
在我看来,第一个挑战是数据迁移。查询和文档都可能随着时间的推移而增长,因此静态模拟数据集可能在一个月后不再很好地代表搜索相关性。接下来的问题与我们的思维方式有关。有时我们可能需要考虑是否需要 100% 通过测试用例才能解决这个新的紧急问题。例如,有些情况下,如果你修复了一些问题,其他测试用例的搜索结果排序可能会稍有改变。如果我们硬编码排名,那么我们可能会为了调整查询模板而苦苦挣扎。但实际上,我们往往不需要排名完全预定义,也不完全确定哪个排序实际上是最优的。我们应该考虑使用一种软机制,通过量化系统的相关性并使用阈值来代替。
在这里我们将讨论如何使用 Elasticsearch 排名评估 API 来实现这种评估方案:
GET restaurant/_rank_eval
{
# Query template comes in really handy when used in conjunction with _rank_eval
"templates": [
{
"id": "01-default-fuzzy-search-template",
"template": {
"id": "01-default-fuzzy-search-template"
}
}
],
"requests": [
{
"id": "kbbq_query",
# Here we manually define the true positives with rating >= 1.0
# The actual rating number helps when using metrics that takes into account
# the ranking of the search results
"ratings": [
{ "_index": "restaurant", "_id": "004parkhangseokbbq", "rating": 3 },
{ "_index": "restaurant", "_id": "005bestbbqintown", "rating": 1 }
],
"template_id": "01-default-fuzzy-search-template",
"params": {
"query_string": "kbbq"
}
},
{
"id": "vietnamese_query",
"ratings": [
{ "_index": "restaurant", "_id": "001sabichuong", "rating": 3 },
{ "_index": "restaurant", "_id": "002vietnamesephonoodle", "rating": 3 },
{ "_index": "restaurant", "_id": "003vietnamesepho", "rating": 3 }
],
"template_id": "01-default-fuzzy-search-template",
"params": {
"query_string": "vietnamese"
}
}
],
"metric": {
"dcg": {
"k": 5,
"normalize": true
}
}
}
结果:
{
"metric_score": 0.8549048706984328, # This is the overall metric score, best is 1.0, worst is 0.0
"details": {
"kbbq_query": {
# This kbbq_query has a imperfect score because it ranks the more relevant result lower
"metric_score": 0.7098097413968655,
"unrated_docs": [],
"hits": [
{
"hit": {
"_index": "restaurant",
"_id": "005bestbbqintown",
"_score": 8.384459
},
"rating": 1
},
{
"hit": {
"_index": "restaurant",
"_id": "004parkhangseokbbq",
"_score": 2.5153382
},
"rating": 3
}
],
"metric_details": {
...
}
},
"vietnamese_query": {
"metric_score": 1,
"unrated_docs": [],
"hits": [
...
],
"metric_details": {
...
}
}
},
"failures": {}
}
让我们尝试通过引入一些变更来使评估分数更接近完美的 1.0。
我们修订后的搜索模型
在开始设计新的查询模板之前,我们可以先退一步,真正思考一下我们应该如何建模搜索引擎。以下是要点:
-
精确匹配总是会排在模糊匹配等非精确匹配之上;
-
精确匹配不考虑字段长度或词汇/文档频率。如果两个文档在某个字段中的精确匹配相同,它们应该具有相同的关键词匹配分数;
-
在相同的匹配级别内(无论是精确匹配还是模糊匹配),虽然初始的关键词匹配分数应该是相同的,但可以通过距离、流行度等某些修正器进行重新排序。然而,修正后的分数不应使最终分数超过上层级别的基准分数,例如,修正后的模糊分数不应大于精确基准分数。这是为了确保本质上的 #1。
如果你看足球,这类似于英超等联赛如何排名他们的球队。不论球队 L 比球队 M 多进了多少球,或者两队之间的交锋结果如何,如果球队 M 的积分比球队 L 高,那么球队 M 排名会更高。其他措施仅用于决胜负。
这种理解可以转化为我们如何使用 Elasticsearch 来表达我们的模型。
一种方法是使用 dis_max 查询结合 constant_score 查询。其思路是将每种匹配类型分类到不同的分数级别,其中一个级别的分数是下一个级别的两倍。落入某一匹配级别的文档(平局)将由修正器重新排序,但最终新的分数不会超过上层基准分数。以下是新的查询模板:
PUT _scripts/02-constant-score-search-template
{
"script": {
"lang": "mustache",
"source": {
"query": {
"function_score": {
"query": {
"bool": {
"must": [
{
"bool": {
"should": [
{
# `dis_max` query gets the max score of an array of clauses
"dis_max": {
"queries": [
{
# `constant_score` says that if matches, return a constant score
"constant_score": {
"filter": {
"multi_match" : {
"query": "{{query_string}}",
"fields": [ "restaurant_name", "cuisine" ]
}
},
# This is the constant that is returned as score
# Note that the exact number is chosen intentionally
# Here the upper level will be twice the lower level
# and we will restrict the modifiers to be only
# able to boost by at most 100% the base score
# so that the lower level can not exceed the upper
"boost": 2
}
},
{
"constant_score": {
"filter": {
"multi_match" : {
"query": "{{query_string}}",
"fields": [ "restaurant_name", "cuisine" ],
"fuzziness": "AUTO"
}
},
"boost": 1
}
}
]
}
}
]
}
}
]
}
},
"functions": [
# Design the modifiers to be multiplier of maximum 1.9999 the base score
{
"weight": 1
},
{
"field_value_factor": {
"field": "rating",
"modifier": "ln",
"missing": 1
},
"weight": 0.1
}
],
"score_mode": "sum",
"boost_mode": "multiply"
}
}
},
"params": {
"query_string": "My query string"
}
}
}
当我们重新运行评估时,可以观察到标准化的 DCG 指标现在的分数等于 1.0,表示完美的准确性!
总结
本博客文章重点是让你置身于需要衍生适合站点搜索引擎需求的查询模板的 Elasticsearch 工程师角色。我们简要覆盖了以下主题:
-
多字段关键词匹配
-
理解默认 Elasticsearch 评分
-
默认 TFIDF 的问题
-
通过属性提升搜索结果
-
模糊匹配
-
Elasticsearch 查询模板评估与排名评估 API
-
使用
dis_max和constant_score构造查询
虽然绝对不是最佳的,我希望博客中的某些部分能帮助你更接近利用 Elasticsearch 解决自己的问题。
我也非常感激任何评论或反馈。如果你想深入讨论,请在此帖子下评论或在 Github 仓库中打开一个问题:github.com/dvquy13/elasticsearch-sharing。
谢谢大家!
附录
#1:详细解析默认 TFIDF 匹配,其中字段值的长度影响整体匹配分数
# Result
{
"hits": {
"hits": [
{
"_id": "003vietnamesepho",
"_score": 1.0470967,
"_source": {
"restaurant_name": "Vietnamese Pho",
"cuisine": "Vietnamese",
"rating": 3
},
"_explanation": {
"value": 1.0470967,
"description": "max of:",
"details": [
{
"value": 0.13353139,
"description": "sum of:",
"details": [
{
"value": 0.13353139,
"description": "weight(cuisine:vietnamese in 0) [PerFieldSimilarity], result of:",
"details": [...]
}
]
},
{
"value": 1.0470967,
"description": "sum of:",
"details": [
# Matching score with "vietnamese"
{
"value": 0.52354836,
"description": "weight(restaurant_name:vietnamese in 0) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.52354836,
"description": "score(freq=1.0), computed as boost * idf * tf from:",
"details": [
{
"value": 2.2,
"description": "boost",
"details": []
},
{
"value": 0.47000363,
"description": "idf, computed as log(1 + (N - n + 0.5) / (n + 0.5)) from:",
...
},
{
"value": 0.50632906,
"description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
"details": [
{
"value": 1,
"description": "freq, occurrences of term within document",
"details": []
},
{
"value": 1.2,
"description": "k1, term saturation parameter",
"details": []
},
{
"value": 0.75,
"description": "b, length normalization parameter",
"details": []
},
# Notice the length=2 here is in the denominator,
# which means that the higher the length the less
# the score
{
"value": 2,
"description": "dl, length of field",
"details": []
},
{
"value": 2.6666667,
"description": "avgdl, average length of field",
"details": []
}
]
}
]
}
]
},
# Matching score with "pho"
{
"value": 0.52354836,
"description": "weight(restaurant_name:pho in 0) [PerFieldSimilarity], result of:",
# Details are exactly like above
"details": [...]
}
]
}
]
}
},
{
"_id": "002vietnamesephonoodle",
"_score": 0.8942772,
"_source": {
"restaurant_name": "Vietnamese Pho Noodle",
"cuisine": "Vietnamese",
"rating": 4
},
"_explanation": {
"value": 0.8942772,
"description": "max of:",
"details": [
{
"value": 0.13353139,
"description": "sum of:",
"details": [...]
},
{
"value": 0.8942772,
"description": "sum of:",
"details": [
{
"value": 0.4471386,
"description": "weight(restaurant_name:vietnamese in 1) [PerFieldSimilarity], result of:",
"details": [
{
"value": 0.4471386,
"description": "score(freq=1.0), computed as boost * idf * tf from:",
"details": [
...,
{
"value": 0.4324324,
"description": "tf, computed as freq / (freq + k1 * (1 - b + b * dl / avgdl)) from:",
"details": [
...,
# Here the length=3 (greater than length=2 of the
# above restaurant)
{
"value": 3,
"description": "dl, length of field",
"details": []
},
...
]
}
]
}
]
},
{
"value": 0.4471386,
"description": "weight(restaurant_name:pho in 1) [PerFieldSimilarity], result of:",
"details": [...]
}
]
}
]
}
}
]
}
}
一个数据科学人士可以喜爱的利益相关者分析
原文:
towardsdatascience.com/a-stakeholder-analysis-data-science-folk-can-love-f5c3f2a0aae1
利益相关者分析在有效的数据科学项目中的力量
·发布在Towards Data Science ·8 分钟阅读·2023 年 6 月 29 日
--
TLDR:技术技能不够。你必须知道如何与关心并从你的工作中受益的人进行互动。本文将帮助你使用流行的“权力——兴趣”矩阵框架来思考这一互动过程。
介绍
如果你是一名数据科学家,你知道数据科学项目的成功不仅仅依赖于数据科学家的技术能力。对算法、统计和数据处理技术的深入理解是必不可少的。然而,还有另一个同样关键的方面经常被忽视:利益相关者分析。
回顾利益相关者分析,展望未来
承认,不论好坏——在我的职业生涯中,并非每一个项目都包括了明确而有力的利益相关者分析。回顾过去,这一职业“遗漏”却带来了重要的教训。
这就是利益相关者分析的内容。
利益相关者分析是一种在项目管理和商业管理中使用的方法,用于识别和理解项目问题中主要利益方的需求和期望。这涉及到找出谁拥有影响力和权力,谁对项目结果有切身利益,以及如何与这些关键个人或团体进行互动。
数据科学中的利益相关者
从许多角度来看,这相当简单。大多数数据科学项目,无论大小或领域,都涉及到广泛的利益相关者——从项目经理和高层到用户和客户。理解和管理这些利益相关者可以显著影响你如何启动项目、如何维持项目进展以及如何交付结果。简而言之,利益相关者分析如果做得好,可能会强烈影响项目的进程和最终成功。
深入探讨利益相关者分析与数据科学
在本文中,我们将深入探讨权力-兴趣网格,这是一个用于利益相关者分析的强大工具,并探讨其在数据科学领域的重要性。我们将讨论数据科学专业人士如何利用这一模型更好地应对项目复杂性、改善沟通并提升整体项目结果。
数据科学人士喜欢好的数据可视化,对吧?首先,这是网格的可视化。

图片来源:作者在 Canva 中的插图。
权力-兴趣网格
除了数据可视化,还有哪个数据科学家不喜欢好的 2×2 分析框架(例如,那个混淆矩阵是金子般的)。这里是另一个数据科学家喜欢的 2×2 框架,称为权力-兴趣网格。
该网格,也称为权力-影响力网格或影响力-冲击网格,是一个用于利益相关者分析的工具。它帮助我们根据利益相关者的:
-
权力——影响项目结果的能力——和他们
-
兴趣——对项目结果的关注程度。
该网格是一个方形,分为四个象限,代表以下利益相关者群体:
高权力,高兴趣
这些利益相关者对项目具有重要影响,并且对项目结果有强烈的兴趣。他们是关键角色,他们的需求和关注应该是首要任务。通常包括决策者,如项目赞助人、高层管理人员和主要客户——例如,可能是高管。
高权力,低兴趣
这个象限中的利益相关者有能力影响项目,但对结果并不是特别投入。保持这些利益相关者的满意和知情至关重要,因为他们的权力如果管理不当可能会阻碍你的工作。他们可能包括那些没有直接参与项目但拥有组织影响力的高层执行人员。
高权力象限中的所有人共同点是,他们可以成为你工作的强大盟友。关键在于保持或提升那些尚未强烈感兴趣的人的兴趣。
低权力,高兴趣
这些利益相关者非常关心项目,但没有很高的权力来影响项目的方向。他们需要得到充分的信息和参与,因为他们的兴趣可能转化为有价值的支持。这个群体通常包括受项目结果影响的最终用户或员工。
低权力,低兴趣
该小组成员对项目既没有显著的权力也没有高兴趣。虽然他们可能需要的管理努力最少,但监控他们并提供信息(根据要求或主动)是一种良好的做法,以确保没有担忧、问题或惊喜出现。没有人喜欢惊喜。这个小组的例子可能包括外部监管机构、公众、其他运营部门的成员,或与你的更大组织相关的几乎任何人。
不应忽视那些权力较小的人对你在数据科学工作中的潜在影响。尽管这些人权力较低,但他们的兴趣较高,会成为你的盟友,并且可能成为支持的来源。那些权力和兴趣都低的人,如果得到良好的参与,也可能成为重要的支持来源——这需要时间。
通过将利益相关者绘制到这个网格上,数据科学家和项目经理可以识别出在哪些方面应集中他们的沟通和参与工作,从而实现更具战略性和有效的利益相关者管理方法。
为什么数据科学家应该关注
乍一看,利益相关者分析可能与数据科学家的典型职责相去甚远。在算法和数据处理的快节奏世界中,这甚至可能感觉像是一个绕道。然而,理解利益相关者及其角色可以对项目的长期成功产生深远的影响,就像数据科学专业人士熟悉的长期投资的回报一样。
需要了解的一个重要方面是,利益相关者的动态从未是静态的——它们是不断演变的。今天的低兴趣或低权力的利益相关者可能会转变为明天的关键高权力和高兴趣的影响者。请记住:
-
一个初级员工可能会升职到高级职位。
-
一个被动的用户可能会变成一个积极的推广者 / 批评者。
-
一个小客户可能会变成一个大客户。
数据科学就像利益相关者分析一样,是一场持久战,有效地进行需要理解这个不断变化的环境。
在数据科学的背景下,利益相关者分析不仅有帮助,而且是必不可少的。数据科学项目通常涉及各种各样的利益相关者,从技术团队和业务部门到客户和最终用户。这些利益相关者对项目有着不同的期望、要求和理解程度。误解或不一致可能会破坏即使是最精心规划的项目。
考虑到这些因素,Power-Interest 网格在数据科学工作中可以或应该发挥作用。该网格使数据科学家能够理解各种利益相关者的影响力和兴趣,并帮助他们相应地制定沟通和协作策略。
通过投入时间进行利益相关者分析,数据科学家可以确保他们不仅开发出技术上合理的解决方案,而且开发出被相关人员充分理解、接受和重视的解决方案。
在数据科学中应用 Power-Interest 网格
这个框架在项目级别非常重要。但 Power-Interest 网格也是一个多功能的工具,能够应用于项目管理之外的领域。它对数据科学家特别有用,为战略决策、沟通和团队合作提供了宝贵的见解。
下面是你如何在数据科学中应用 Power-Interest 网格的方法:
步骤 1:识别你的利益相关者
首先确定直接或间接与你的数据科学工作互动的个人或团体。为了获得最佳结果,请将他们列在纸上或白板上。这些可能包括你的同事、经理、其他部门、客户,甚至是与你的数据产品互动的用户。
步骤 2:评估利益相关者的权力和兴趣
评估每个利益相关者与您的工作相关的权力和兴趣。权力可以通过利益相关者所持有的影响力来衡量,而兴趣则指他们的参与程度或他们受到你工作影响的程度。
步骤 3:在网格上绘制你的利益相关者
一旦你评估了每个利益相关者的权力和兴趣,就将他们绘制在网格上。这种可视化可以帮助你理解在沟通和合作时应该优先考虑谁(以及如何)。
步骤 4:制定管理策略
制定策略来管理你与每个象限(见上文)中利益相关者的关系及其附图。当你制定策略时,要考虑如何以及何时进行沟通。将特定的时间表安排在日历中可能会有帮助。
步骤 5:审查和调整
利益相关者的动态会随着时间的推移而变化。定期审查你的利益相关者分析,并根据需要调整策略。这将帮助你跟上变化并保持有效的关系。
将 Power-Interest 网格融入到你的数据科学实践中,你可以建立更好的关系,改善沟通,并做出更明智的决策。记住,成功的数据科学不仅仅是关于模型和算法——它还涉及到管理关系和理解人们。
结论
通过学习、遵循并应用这种利益相关者分析,任何数据科学专业人士都能提升他们的工作。对于未经训练的眼睛来说,数据科学可能看起来极其技术性和算法驱动。然而,正如经验丰富的从业者所知,成功的数据科学还涉及理解数据所处的背景、与之互动的人以及持有兴趣的利益相关者。
利益相关者分析,尤其是使用像 Power-Interest 网格这样的工具,提供了一种战略方法来管理这些重要的关系。通过了解利益相关者的权力和兴趣,你可以驾驭角色的复杂性,增强沟通效果,并提供真正符合利益相关者期望的成果。
将利益相关者分析纳入你的数据科学工具包,将使你的工作更有效,项目更成功,利益相关者更满意。所以,拥抱权力-兴趣网格,开始享受战略性、以利益相关者为中心的数据科学实践的好处。毕竟,成功的数据科学不仅仅是创造强大的算法,它还在于创造有意义的影响,这是我们所有人都能喜爱的。
感谢阅读
你准备好了解更多关于数据科学职业的信息了吗?我提供一对一的职业辅导,并拥有一个每周发送邮件的列表,帮助数据专业人士找工作。 联系我们了解更多。
感谢阅读。随时发送你的想法和意见。你可以仅仅说声你好。如果你真的需要告诉我哪里做得不对,我期待尽快与你交流。Twitter: @adamrossnelson LinkedIn: Adam Ross Nelson。

准确检测峰值和谷值的逐步指南。
原文:
towardsdatascience.com/a-step-by-step-guide-to-accurately-detect-peaks-and-valleys-9abc49a2eac3
峰值检测是许多应用中的一个挑战步骤。阅读并了解如何在 1D 向量和 2D 数组(图像)中准确检测峰值和谷值。
·发表于 Towards Data Science ·13 分钟阅读·2023 年 9 月 25 日
--

照片由 Willian Justen de Vasconcellos 提供,来源于 Unsplash
我们的人脑在峰值检测方面相对于其上下文非常出色。眼睛看似简单的任务,对于机器来说却可能是一项具有挑战性的任务。一般来说,峰值和谷值表示(重要的)事件,例如价格/交易量的突然增加或减少,或需求的急剧上升。挑战之一是峰值/谷值的定义,这在不同的应用和领域中可能有所不同。其他挑战可能更具技术性,例如噪声信号可能导致许多误报,或单一阈值可能无法准确检测局部事件。在本博客中,我将描述如何在不对峰值形状做假设的情况下准确检测一维向量或二维数组(图像)中的峰值和谷值。此外,我还将演示如何处理信号中的噪声。 分析使用findpeaks 库进行,并提供了动手示例以供实验。
峰值和谷值简要介绍。
检测信号中的(突发)变化是许多应用中的重要任务,需要进行报告或监控。大致上有两种类型的“突发变化”,即所谓的异常值和关注峰值,这两者在概念上是不同的。异常值是指显著偏离数据集中正常值的数据点,而关注峰值是信号中具有分析或领域相关性的特定数据点或区域。 如果您需要深入了解并有实际示例的异常值检测,可以参考这些博客 [1, 2]。
[outlier-detection-using-principal-component-analysis-and-hotellings-t2-and-spe-dmodx-methods-625b3c90897?source=post_page-----9abc49a2eac3--------------------------------] ## 使用主成分分析和 Hotelling’s T2 及 SPE/DmodX 方法检测异常值
由于 PCA 的灵敏度,它可以用于检测多变量数据集中的异常值。
[towardsdatascience.com [outlier-detection-using-distribution-fitting-in-univariate-data-sets-ac8b7a14d40e?source=post_page-----9abc49a2eac3--------------------------------] ## 使用分布拟合在单变量数据集中检测异常值
了解如何使用概率密度函数检测异常值,以便快速轻量化模型并提供可解释性……
[towardsdatascience.com
但什么是关注峰值?
峰值是指大于其周围点的点或区域。这可以是局部最大值或全局最大值,也可以是重复的数据点,这些点不一定需要偏离正常或预期值。
这种定义的弹性使得峰值检测成为一项具有挑战性的任务。换句话说,“峰值应该比其周围点高多少?” 和 “如何定义局部最大值?” 这更加复杂的是,信号通常不免受到噪声的干扰。峰值检测有多种技术,包括简单的阈值设定,但也包括使用导数、小波分析和/或卷积。峰值检测方法的吸引力在于处理噪声的能力,并且不会对信号-峰值做出(过于强烈的)假设。推理是你需要检测局部最大值,这些最大值可能会根据应用和背景发生变化。例如,ECG 信号在形状和幅度上与服务器负载测量不同。一个知名的具有峰值检测功能的 Python 库是SciPy 中的find_peaks [3]。然而,该函数不能对检测到的峰值进行排序或优先处理,也没有内置的噪声处理功能。在接下来的部分中,我将演示如何检测峰值和谷值,处理信号噪声,测量峰值强度,并对结果进行排序。我将使用 findpeaks 库为 1D 向量和 2D 数组演示这一过程。
Findpeaks 库包含四种专门用于峰值检测的方法。
Findpeaks 库旨在检测 1 维向量和 2 维数组(图像)中的峰值和谷值,而不对峰值形状或基线噪声做出假设。该库实现了四种不同的方法用于峰值和谷值的检测与排序。每种方法都有其独特的优势,使其适用于特定应用。在下一部分中,我将通过实际的 Python 示例介绍每种方法。如果你想自己试验这些示例,请首先 pip 安装Findpeaks package。
pip install findpeaks
拓扑方法(1)。
拓扑方法是基于持久同源性的思想的高效解决方案[4]。该方法在函数值内部排序后以线性时间运行——实际上是一个单一循环。持久同源性的思想是直观的。在接下来的代码部分,我们将导入 Findpeaks 库并加载一个示例数据集:
import matplotlib.pyplot as plt
# Import library
from findpeaks import findpeaks
# Initialize
fp = findpeaks(method='topology')
# Example 1d-vector
X = fp.import_example('1dpeaks')
# Plot
plt.figure(figsize=(15, 8), dpi=100)
plt.plot(X)
plt.grid(True)
plt.xlabel('Time')
plt.ylabel('Value')

图 1:示例信号,包含一些局部峰值和谷值。(图像来自作者)
我们可以清楚地看到图 1 中最强(或最高)的峰是点 1,其次是点 2,等等。为了检测这些峰,我们可以考虑一个持续下降到更低水平的水位。在顶部(局部最大值)有一个岛屿的诞生。当水位降低时,两个岛屿可以合并。当较低的岛屿再次与较高的岛屿合并时,称为死亡。通过这种方式,每个候选峰可以用诞生与死亡点进行标注。候选峰可以绘制在所谓的持久性图中,如图 2 所示。
# Fit topology method on the 1D vector
results = fp.fit(X)
# Plot the results
fp.plot_persistence(figsize=(25,8))

图 2:持久性图的示例,显示出生与死亡水平。(图片来源于作者)
右面板中的对角线(左下角到右上角)代表出生和死亡水平相同的点。换句话说,位于对角线上的点是平坦的信号或没有峰值或其他变化的水平线。感兴趣的峰值应该位于对角线的右侧。
让我们逐步分析图 2。对于第一个点(右下角),我们看到出生于score=1.5,死亡于score=0。对于第二个峰值,我们看到score=1.2,死亡水平为score=0.8(蓝色箭头)。这种方法将每个峰值相对量化,因此可以排除低持久性(即接近对角线)的峰值。感兴趣的峰值可以被排序/优先考虑并选择。此方法的优点在于它可以应用于 1D 向量和 2D 数组(图像)。另一个优点是它返回可解释的结果,错误的阳性结果可以通过在持久性评分上剪裁结果来移除。缺点是嘈杂的信号可能导致成千上万的结果,并可能变得计算密集。然而,我们可以通过排名轻松选择顶级结果。我将在本博客的后续章节中演示这一点。
Mask 方法(2)。
Mask 方法接受一个 2D 数组(图像),并使用滑动的8x8 窗口通过局部最大值滤波器检测峰值。它比简单的阈值处理稍微复杂,因为阈值处理是在滑动窗口内使用局部最大值滤波器进行的。尽管这是一个直接的方法,但在背景稳定的情况下效果很好。其优点在于直观、可解释且计算速度快。缺点是可能需要密集的预处理步骤。然而,许多预处理步骤都由Findpeaks 库处理。在接下来的代码部分中,我们将加载一个示例图像,执行预处理,并检测峰值:
# Import library
from findpeaks import findpeaks
# Initialize
fp = findpeaks(method='mask')
# Example 2d image
X = fp.import_example('2dpeaks')
# Plot RAW input image
plt.imshow(X)
# Fit using mask method
results = fp.fit(X)
# Plot the pre-processing steps
fp.plot_preprocessing()

图 3。Mask 方法应用于具有自动预处理的图像。从左到右显示了输入图像到缩放、颜色转换和去噪。(图片来源于作者)
输出包括Xraw、Xprocessed和Xdetect,它们的尺寸与输入图像相同NxM。最终检测到的峰值存储在Xdetect中,可以使用plot功能进行绘制:
# The output contains multiple variables
print(results.keys())
# dict_keys(['Xraw', 'Xproc', 'Xdetect'])
# Plot detected peaks
fp.plot()

图 4。使用 Findpeaks 库中的 Mask 方法检测到的峰值。数字表示峰值的强度(图片来源于作者)
Findpeaks 库 还包含将 2D 图像转换为 3D 网格图的 plot_mesh 功能。这些视觉效果有助于更好地理解峰值的强度。例如,当我们查看图 4 时,可能很难看到哪些峰值最强,而图 5 中的网格图提供了更好的见解。
# Create mesh plot from 2D image.
fp.plot_mesh()
# Rotate to make a top view
fp.plot_mesh(view=(90,0))

图 5. 由 Findpeaks 生成的网格图。(图片来源:作者)。
Peakdetect 方法 (3)。
Findpeaks 库中的第三种方法是 peakdetect 方法。该方法基于 Billauer 的研究 [2, 3],其优点在于能够在嘈杂信号中找到局部最大值和最小值。噪声非常常见,通常的解决方案是用某种低通滤波器来平滑曲线。然而,信号平滑的代价是原始信号中的峰值可能会丢失或被压制。此方法需要设置 lookahead 参数,该参数是从峰值候选点向前看的距离,以确定它是否为实际峰值。默认值设置为 200,但对于较小的数据集(即 <50 数据点)可调整。在接下来的代码部分,我们将创建一个包含 10,000 个数据点的一维向量,并检测峰值和谷值。我们可以使用绘图功能绘制检测到的峰值和谷值,如图 6 所示。首先,确切的峰值和谷值用交叉和点突出显示,而整个区域也被提取并用不同的颜色标记。
# Import libraries
import numpy as np
from findpeaks import findpeaks
# Create example data set
i = 10000
xs = np.linspace(0,3.7*np.pi,i)
X = (0.3*np.sin(xs) + np.sin(1.3 * xs) + 0.9 * np.sin(4.2 * xs) + 0.06 * np.random.randn(i))
# Initialize
fp = findpeaks(method='peakdetect', lookahead=200, interpolate=None)
# Fit peakdetect method
results = fp.fit(X)
# Plot
fp.plot()

图 6. 使用 Findpeaks 中的 peakdetect 方法检测峰值和谷值。(图片来源:作者)
Caerus 方法 (4)。
Caerus 方法旨在检测一维信号中的峰值和谷值。它确定局部最小值和对应的局部最大值,而不受时间框架、尺度或趋势的影响。该方法基于前向滚动窗口,迭代评分成千上万的窗口。对于每个窗口,计算从起始到结束位置的百分比。生成的矩阵是 windowxlength dataframe,其中最高评分的百分比,即高于最低百分比的百分比,被使用。然后汇总评分最好的百分比。最终区域基于 距离 和 百分比 的起始位置变化确定。该方法在发现波动数据(如股市数据)中的峰值和谷值方面具有强大的优势。
要查看此方法的实际效果,我们需要设置最小百分比 minperc 参数和窗口大小。minperc 声明了一个起始位置,窗口用于确定从起始点开始的信号是否有百分比变化。使用较小的窗口大小(例如 50)可以检测局部最小值,而较大的窗口大小(例如 1000)将关注全局最小值。在以下代码部分中,我们将用 caerus 方法初始化,并检测至少有 5%变化的峰值。
# Import library
from findpeaks import findpeaks
# Initialize findpeaks with cearus method.
# The default setting is that it only return peaks-vallyes with at least 5% difference.
fp = findpeaks(method='caerus', params={'minperc':5, 'window':50})
# Import example data
X = fp.import_example('facebook')
# Fit
results = fp.fit(X)
# Make the plot
fp.plot()

图 7. Cearus 方法的结果。底部面板是股票数据,红色垂直线表示峰值,绿色垂直线表示谷值。中间面板是处理后的数据,显示了窗口内的累计成功次数。顶部面板是窗口中的百分比差异。(图片来自作者)
除了检测确切的峰值和谷值位置外,我们还可以标记整个区域,如下所示。标记的区域也直观地合理,因为它们正确标记了随时间增长的区域。

图 8. Cearus 方法的结果。除了峰值和谷值(红色和绿色垂直线)外,还可以提取和标记区域。(图片来自作者)
预处理是一个重要的任务。
之前描述的每种峰值检测方法都有其自身的优缺点。一个重要的任务是对输入信号进行预处理,以防止检测到假阳性结果。现实世界的数据通常是不完整的、嘈杂的,并且需要归一化或缩放。Findpeaks 库包含各种预处理功能,以帮助完成这些步骤,如去噪、插值、缩放、归一化和缩放。在下一节中,我们将深入了解可用的预处理功能,特别是针对 2D 数组的功能。
图像预处理步骤。
findpeaks 库执行 4 个预处理步骤,这些步骤按照下面所示的特定顺序执行。每个步骤都可以通过设置输入参数来控制。最后一步后,应用峰值检测方法。
-
图像缩放 可以帮助提高峰值检测的效果,并显著减少计算时间。
-
缩放像素值 在[0-255]之间是使图像可比和峰值检测更稳健的重要步骤。
-
转换为灰度 减少了计算负担,并使图像具有可比性。当图像是 RGB 颜色时,它将被转换为 2D 数组。
-
噪声过滤 是一个关键步骤。有关更多细节,请参见下一节。
峰值检测前的噪声去除。
噪声是不受欢迎的附加到输入信号上的成分,通常是测量信号的干扰。噪声遵循特定分布,并且通常依赖于应用(图 9)。换句话说,为某些应用有效去除/过滤噪声可能需要不同的技术。下图展示了不同类型的噪声及其分布。例如,去除盐和胡椒噪声的方法与去除高斯噪声的方法不同。

图 9. 三种不同类型的噪声及其分布。(图像来自作者)
一个明显的例子是处理来自 Sentinel-1 卫星的 SAR 图像。这些图像已知会受到斑点噪声的影响,从而降低图像质量。它是由多个分布目标的回波波动造成的,局部强度高,并增加了局部区域的平均灰度级。Findpeaks 库包含各种噪声去除滤波器:
-
fastnl
-
双边
-
李
-
李增强
-
李 Sigma
-
宽
-
霜冻
-
中值滤波
-
均值滤波器
所有滤波器在特定条件下都可以去除或过滤噪声,目的是保留原始信号不变。例如,双边滤波器使用高斯滤波器来保持边缘。Lee Sigma和fastnl滤波器非常适合去除 SAR 图像中的斑点噪声。我们将加载一个带有斑点噪声的示例图像,并直观检查噪声过滤和峰值检测的性能。为了演示,我们将首先在不进行噪声过滤预处理的情况下执行峰值检测,如下一个代码段、图 10 和图 11 所示。
# Import library
from findpeaks import findpeaks
# Initializatie
fp = findpeaks(scale=None, denoise=None, togray=True, imsize=(300, 300))
# Import image example
img = fp.import_example('2dpeaks_image')
# Fit
fp.fit(img)
# Tens of thousands of peaks are detected at this point.
fp.plot()
fp.plot_mesh()

图 10. 左侧面板展示了原始输入图像。中间面板展示了灰度化后的图像。右侧面板展示了检测到的峰值。检测到了成千上万的假阳性结果。(图像来自作者)

图 11. 网格图可以帮助直观地查看噪声水平与感兴趣信号的比较。我们可以看到峰值(左中),但很难将其与噪声区分开来。(图像来自作者)
从此时起,我们将通过缩放像素值、执行灰度化转换和fastnl噪声过滤来预处理图像。下图展示了预处理的步骤。最后一步是用于峰值检测的拓扑方法。
# Import library
from findpeaks import findpeaks
# Initializatie
fp = findpeaks(method='topology',
togray=True,
imsize=(300, 300),
scale=True,
denoise='fastnl',
params={'window': 31})
# Import image example
img = fp.import_example('2dpeaks_image')
# Fit
fp.fit(img)
# Plot
fp.plot_preprocessing()
[findpeaks] >Import [.\findpeaks\data\2dpeaks_image.png]
[findpeaks] >Finding peaks in 2d-array using topology method..
[findpeaks] >Resizing image to (300, 300).
[findpeaks] >Scaling image between [0-255] and to uint8
[findpeaks] >Conversion to gray image.
[findpeaks] >Denoising with [fastnl], window: [31].
[findpeaks] >Detect peaks using topology method with limit at None.
[findpeaks] >Fin.

图 12. 从原始输入图像(左侧面板)到预处理和去噪图像(右侧面板)。(图像来自作者)
如图 12 所示,最终的去噪图像清晰地去除了斑点噪声。但它是否足够好以检测正确的峰值?在下一步中,我们可以检查检测到的峰值(见下文)。现在我们可以绘制图像,并叠加上检测到的峰值,如图 13 所示。我们可以清楚地看到正确的区域被检测到。通过网格图,我们可以更好地直观感受感兴趣的峰值(图 14)。
# Plot the top 15 peaks that are detected and examine the scores
fp.results['persistence'][1:5]
# x y birth_level death_level score peak valley
# 2 131 52 166.0 103.0 63.0 True False
# 3 132 61 223.0 167.0 56.0 True False
# 4 129 60 217.0 194.0 23.0 True False
# 5 40 288 113.0 92.0 21.0 True False
# 6 45 200 104.0 87.0 17.0 True False
# 7 87 293 112.0 97.0 15.0 True False
# 8 165 110 93.0 78.0 15.0 True False
# 9 140 45 121.0 107.0 14.0 True False
# Take the minimum score for the top peaks off the diagonal.
limit = fp.results['persistence'][0:5]['score'].min()
# Plot
fp.plot(limit=limit)
# Mesh plot
fp.plot_mesh()

图 13. 顶部的 5 个峰值被突出显示,并正确地检测为感兴趣的峰值。 (图像来自作者)

图 14. 预处理步骤以及去噪结果清晰地减少了噪声,并保持了感兴趣的信号完整。 (图像来自作者)
结语。
我探讨了 1D 向量和 2D 数组(图像)的峰值检测概念。使用 Findpeaks 库,可以轻松地使用四种不同的方法检测峰值和谷值。它将插值、归一化、缩放、噪声过滤以及峰值和谷值的检测过程进行了流水线处理。输出是一个包含候选峰值和谷值及其位置和类别标签的数据框。结果可以通过各种绘图功能进行探索。
保持安全,保持警觉。
干杯,E。
如果你觉得这篇文章对你有帮助,欢迎 关注我。如果你考虑购买 Medium 会员,可以通过使用我的 推荐链接来支持我的工作。这与购买一杯咖啡的价格相同,但允许你每月阅读无限的文章。
软件
让我们联系吧!
参考文献
-
E. Taskesen, 使用主成分分析和 Hotelling 的 T2 以及 SPE/DmodX 方法进行异常值检测, Medium
-
E. Taskesen, 使用分布拟合进行单变量数据集的异常值检测**, Medium
-
根据峰值属性在信号中查找峰值, Scipy.
-
H. Edelsbrunner 和 J. Harer,《计算拓扑学:入门》,2010 年,ISBN 0–8218–4925–5。
成为成功的数据分析顾问的逐步指南
在数据领域中不断学习总是一个好主意
·发表于 Towards Data Science ·阅读时间 5 分钟·2023 年 6 月 5 日
--

图片由 Jukan Tateisi 提供,来源于 Unsplash
数据是这一年代的关键词,组织积极将数据转化为业务,并投资于“数据驱动的决策”。凭借数据所提供的洞察力,组织不断寻求利用能够推动业务增长的因素。
在每一步,企业希望信任那些能够理解大数据和小数据、评估当前流程并为企业推荐未来技术、制定连贯数据战略的专业人士。
数据分析顾问是谁?
大多数人不了解数据科学。
大多数人理解故事。
数据顾问的职责是提供方向并改进数据分析和报告流程。作为数据顾问,你参与定义组织的数据获取和分析策略,优化当前的数据流程,促进数据讲故事,并为团队和组织的成功奠定基础。
随着时间的推移,组织希望找到能够在业务和数据交汇点上工作的专业人士,旨在发现有用的信息、提供结论并支持决策,即 数据分析顾问。
数据分析顾问的职责
作为数据分析顾问,你的日常工作可能涉及以下一个或多个方面,这让这个工作更加令人兴奋!
-
评估当前的数据流程以识别机会和问题
-
推荐潜在的数据使用和存储改进
-
创建数据模型并改进数据流程
-
指导数据和业务团队通过数据管理流程
-
设计和管理数据库、数据流和数据自动化图
-
进行跨职能对话并为差距开发解决方案
-
协助团队进行数据集成、迁移和仓储
-
为新员工和现有员工进行培训,以实现有效和高效的数据分析
-
生成数据故事(分析、报告、仪表盘)并进行沟通
-
为团队提供数据治理、数据战略和数据可扩展性方面的建议
如何成为数据分析顾问?
1. 理解数据生命周期并加以处理
任何数据问题都有开始、中间和结束。
数据分析围绕数据生命周期展开 — 从业务理解到将洞察传达回业务,这一过程对数据分析顾问来说是不可避免的。
随着你在数据生命周期中的工作,工作的整体意义会日渐清晰 —
-
商业理解: 定义目标并提出相关问题
-
数据挖掘: 爬取、收集并准备项目所需的数据
-
数据清理: 修复数据中的不一致之处,并为分析做好准备
-
数据探索: 对定义的问题提出假设
-
特征工程: 选择你分析中重要的特征
-
数据建模: 创建数据模型并用它们进行预测
-
数据可视化: 与关键利益相关者沟通洞察
2. 获取商业洞察
尽管商业洞察可能对不同的人意味着不同的东西,但从数据的角度来看,商业洞察可以定义为理解商业问题并通过数据解决这些问题的能力。本质上,业务应该能够将解决方案与效果联系起来。
当你理解业务时,你知道要向利益相关者和数据提出什么问题,并且知道在分析中期望什么。商业洞察(或商业敏锐)专业人士可以更好地评估挑战并成功应对。
在我作为数据分析顾问的经验中,将业务与数据连接起来是我最受赞赏的技能,并且对于升职至关重要。
3. 创建你的数据顾问工具包
你具备了工作所需的硬技能 — SQL、Python、Tableau 等……但数据分析顾问真正需要的是自信、良好的讲故事(沟通)技巧和良好的幽默感。
作为数据分析顾问,你不仅需要理解业务、KPI 等;你还处于创建数据模型和数据策略的前沿。因此,当你谈论财务时,你正在开发一个预测模型,这在财年结束时为你的业务节省了几百万美元。
此外,新技术每天都在出现。因此,在每个时点,你作为数据分析顾问的工具包必须具备你的团队、角色所需的硬技能和软技能,以及那些使你的工作更加轻松的技能。
4. 专注于用数据讲故事并满足你的受众
高管和 C-suite 要求一个故事——一个贯穿整个过程的连贯叙事,将数据和信息整合起来,展示更广泛的意义。越早学习用数据讲故事,你就越容易理解业务需求和要求,以及如何与高管对接(让他们成为你的前排拉拉队员)。
通过讲故事,你是你故事的导演,可以根据需要为受众量身定制——从极其技术性的(引用数据模型的准确性或为什么预测模型不适用)到使用商业语言(为什么,什么,如何,哪里和何时)。将你的内容传达给受众是数据分析顾问高度重视的技能。
5. 在工作中学习
这就是我所做的,并且到目前为止,这是一条非常值得的职业道路!
我在数据领域的职业生涯从数据分析师开始,刚从研究生院毕业时,掌握了职位所需的硬技能,并在工作中磨练了作为数据专业人员所需的软技能。
不论你现在身处何地,如果数据咨询是你的使命,我建议你培养从日常工作中学习的能力,向他人学习,发展你在角色中所需的技能,并加以实践。
他们说,实践出真知,在这里,实践可以让你对新角色充满信心,并影响你工作的整体质量。
这篇长博客就到这里。感谢你的阅读!希望你觉得这篇文章有趣。请在评论中告诉我你在讲故事方面的经历,你在数据领域的旅程,以及你在 2023 年期待什么!
如果你喜欢阅读这样的故事,可以考虑通过这个链接注册成为 Medium 会员。
祝数据分析愉快!
Rashi 是来自芝加哥的数据高手,喜欢分析数据并创造数据故事以传达洞察。她是全职的医疗数据分析顾问,周末喜欢一边喝咖啡一边写数据博客……
从零开始构建有效的数据质量策略的逐步指南
如何根据用户期望建立一个可解释的数据质量框架
·
关注 发布于 Towards Data Science ·8 分钟阅读·2023 年 8 月 3 日
--
由 Rémi Müller 拍摄,刊登于 Unsplash
作为数据工程师,我们有责任(或应该有责任)确保我们提供的数据质量。这不是新鲜事,但每次参与数据项目时,我都会问自己相同的问题:
-
我应该什么时候开始关注数据质量?
-
我应该多担心数据质量呢?
-
我应该关注数据质量的哪些方面?
-
我从哪里开始?
-
我的数据什么时候足够好可以使用?
-
我如何向利益相关者突出我的数据质量?
完美是不存在的,你不希望在展示数据带来的全部价值时失去动力。你需要在质量和投入的时间之间找到平衡。回答这些问题是找到平衡的关键。
本文的目标是分享一个逐步指南,帮助你获得构建有效数据质量战略所需的所有答案,以满足业务需求。这个过程涉及利益相关者、产品负责人、开发人员之间的协作,以及与潜在用户分享数据质量指标。
此外,我将展示为数据产品开发的实际成果,这些数据将用于营销活动报告工具,演示战略如何最终转化为业务价值。
最后,我将介绍在数据网格实施中,数据产品如何帮助我们在访问数据之前与用户共享数据的质量水平。
让我们从第一个问题开始
我应该什么时候开始关注数据质量?
我认为我们每个人都有对第一个问题的内心回答:从零天开始。 从一开始就关注和理解数据质量期望是确保信任和早期用户采用的关键。这会带来早期反馈,帮助我们在开发过程中进行改进。作为数据生产者,我们不希望因为任何初期质量问题而损害数据的可信度。
我们应该多担心数据质量?
这个问题是特定于用例的。要回答这个问题,你的团队必须理解我们数据要解决的需求的本质。起点是了解我们的数据将如何被使用。
我们可以与利益相关者和业务负责人召开会议,以获取他们打算如何使用数据的见解。通过这种合作,我们将制定与用户实际需求和期望一致的数据质量标准。
这将是我们实际例子的成果,即用于营销活动报告工具的数据整合。

数据使用模式结果示例(图像由作者提供)
从这个例子中我们得到:
-
多频繁和多少人将访问我们的数据,以便了解我们需要提供何种性能。
-
我们的数据需要多么完整和准确,以便了解需要在数据管道中添加何种控制措施。提供高水平的准确性和完整性涉及唯一性、完整性和不一致性管理检查。
-
我们的数据需要多么新鲜,以便了解我们需要多频繁地运行转换来刷新数据。
-
当我们的数据需要可访问时,以便了解我们需要提供的可用性。
服务水平目标
这个实践的最终结果是为我们的服务水平目标(SLO)绘制基准。在数据质量中,服务水平目标(SLO)是定义特定数据服务或过程的数据质量期望水平的具体且可测量的目标。SLO 设定了可量化的指标和阈值,以确保数据符合预定义的质量标准,并与用户和利益相关者的需求和期望保持一致。
在我们的场景中,我们可以定义一个 SLO,即我们的数据应每 6 小时重新计算一次。如果数据超过了这个阈值,就不符合这个特定的 SLO。
我应该关注数据质量的哪些方面?
现在,我们在团队内可以降低到数据质量维度的抽象层次。数据质量维度代表了数据质量的特定方面,具有一些特定的特征。每个数据质量维度关注数据的特定方面,并帮助识别可能需要改进的领域。
这些维度包括:
-
准确性:数据值反映现实并且没有错误的程度。
-
完整性:所有必要数据元素的存在程度,没有缺失值。
-
一致性:不同来源或相同数据集内数据的和谐与一致程度。
-
及时性:数据的最新程度。
-
唯一性:每条记录在数据集中是唯一且未重复的程度。
通过了解我们的数据和 SLO 的使用模式,我们确定了需要改进的维度,并将它们与我们场景中带来的实际价值关联。这有助于我们识别需要关注的数据质量最相关方面,并开始思考具体的行动。

数据质量维度直接与业务实际价值相关联的示例(图像由作者提供)
根据我们的示例,我们将使用模式会议中识别的数据质量维度与它直接提供的业务价值关联起来。
我从哪里开始?
一旦确定了数据质量维度及其所提供的业务价值,我们将进行团队内的协作会议,以为有效实现每个维度设定具体、可测量和可实现的目标。这些目标将作为定义可操作任务的基础,如在转换阶段添加数据质量测试、进行差距分析或引入稳健的数据清理流程。通过将我们的数据质量努力与这些明确的目标对齐,我们确保我们的行动直接满足业务需求,并提高整体数据质量。
过程中发现的所有行动将被添加到我们的待办事项列表中,并由团队进行优先级排序。最终结果是一个针对业务需求定制的数据质量框架,使我们能够跟踪进展。

我们的目标和行动的数据质量框架,以确保我们数据的业务价值(图片由作者提供)
拥有一个对利益相关者可见且易于解读的数据质量框架有一些好处:
-
它提供了清晰度,说明数据质量如何在组织内部得到管理、监控和改进。
-
促进信任和透明度的数据管理实践
-
减少对数据质量标准误解的可能性
-
展示了团队和组织对数据质量及其在推动业务成功中的重要性的承诺。
我的数据何时足够好以供消费?
你的框架将回答这个问题。一旦你实现了所有目标,准备好你的数据以满足业务期望,你就可以放心地将其提供给用户,并寻求他们的反馈以进一步改进。
记住,你工作的输入是使用模式会议中识别的服务水平目标。由于你的数据与这些目标对齐,没有理由因为担心它尚未满足要求而停留在此。
一旦你的数据发布,应该做什么?
监控
数据质量策略中定义的所有行动和目标需要主动监控。利用可以建立警报并通过各种渠道进行沟通的监控工具对于早期检测至关重要。
记录事件并根据其影响的维度进行分类也是至关重要的。这种做法使你能够集中精力关注特定领域,并识别策略中的潜在差距。更好的是,如果你维护一个事件报告,它可以让你反思你在特定领域的工作如何有助于随着时间的推移减少事件数量。

按月和数据质量维度的事件日志。在贴纸上会有事件的简要描述(图片由作者提供)
定期修订框架
你的团队必须定期审查事件日志,并相应地更新数据质量框架,以填补识别出的差距。这确保了你的行动和目标反映现实并保持最新。
服务水平指标和透明度
测量你服务水平目标的实现情况是必要的。对于每个 SLO,你应该有一个服务水平指标(SLI),显示 SLO 的实现情况。例如,在我们的例子中,你可以有一个 SLI,显示在过去 X 天内生产中没有超过 6 小时的旧数据的成功百分比(时效性维度)。这帮助用户了解数据的表现,并建立对其质量的信任。

我们的数据质量维度的服务水平指标(图片由作者提供)
实践中的透明度是提高用户采纳的关键,而服务水平指标负责提供这种透明度。
共享我们的数据质量指标
对于共享我们的数据质量指标(SLI),我非常喜欢在数据网实施中采用数据产品概念。
我们的数据质量策略具有以下特点:
-
它是业务特定的,因为目标来源于业务需求
-
透明,因为我们可以与用户共享并希望与用户共享
-
易于理解,因为我们的数据质量框架易于解读
这与数据网对数据产品的定义完全一致。我强烈推荐使用数据网方法,将数据及其质量指标封装到数据产品中,以增强透明度。
为什么要共享我们的数据质量指标数据产品
根据定义,数据网中的数据产品是自包含的、特定于业务的数据能力单元。它们封装数据、处理逻辑和数据质量检查,促进去中心化的数据所有权,并与更广泛的数据生态系统无缝集成。它们旨在满足特定的业务需求,易于查找和透明。作为我们数据质量框架的组成部分,数据产品确保我们的策略与独特的业务需求精准对齐,为业务特定的数据质量提供清晰和透明。
数据产品在数据质量背景下的一个关键优势是它们能够持有自己的 SLI。通过将数据质量指标直接集成到数据产品中,并通过用户友好的目录使其可见,我们赋予用户完全了解数据质量的能力,进行搜索、请求访问和探索。这种透明性和清晰性增强了用户信心,鼓励更大的采纳。
结论
在本逐步指南中,我们已经学习了如何设定覆盖业务需求的可衡量服务水平目标(SLO),识别数据质量维度,并使我们的行动与目标一致,以实现 SLO 定义的期望。通过拥抱数据产品所提供的透明性和可理解性,我们可以有效地共享我们的数据质量指标,建立信任并增加用户采纳。请记住,完美并不存在。持续监控、事件记录和定期修订帮助我们保持数据质量框架的最新状态。
按照这些步骤,你将能够创建一个强大的数据质量框架,并建立一套作为可共享知识库的数据质量文档,这些文档对利益相关者和团队成员都易于解读。更好的是,你的数据质量框架保持了完美的努力-需求平衡,将使你的团队能够在数据准备好以满足业务需求时迅速发布数据。
祝数据工程愉快!
参考资料
开发基于地图的应用程序的逐步指南(第一部分)
原文:
towardsdatascience.com/a-step-by-step-guide-to-develop-a-map-based-application-part-i-757766b04f77
概念验证 — 构建一个极简的演示
·发表于 Towards Data Science ·19 分钟阅读·2023 年 1 月 24 日
--

图片来源于 GeoJango Maps 在 Unsplash
地图是可视化和理解地理数据的强大工具,但需要特定的技能来有效设计。
在本步骤指南中,我们将深入探讨如何构建一个基于地图的应用程序,展示客户周围加油站的价格。我们将涵盖从原始概念验证(POC)到最小可行产品(MVP)的不同关键步骤。
系列文章:
第一部分:概念验证 — 构建一个极简的演示
第二部分:如何使用 React 构建网页应用(静态布局)
第三部分:使用 React 为你的网页应用添加互动性
第四部分:使用 PostgreSQL、FastAPI 和 Docker 构建后端
本文涵盖内容
在本系列的第一篇文章中,我们正在准备我们的应用程序的概念验证。
我们刚刚有了一个想法,希望将其转化为有价值的东西,但我们还不知道这是否可行。我们需要寻找数据,评估其相关性,并构建一些基本内容,以展示原始想法是否可行。
此外,更重要的是,我们需要向决策者(可能不是技术人员)展示项目的价值。

从概念验证到最小可行产品,作者插图
特别地,由于该项目的特殊性,我们将在本文中讨论以下主题:
-
如何将 .xml 文件解析为表格数据
-
根据点之间的距离筛选和组合 DataFrames
-
使用 Plotly 构建基础地图
-
使用 ipywidgets 为地图添加交互功能
在文章结束时,我们将拥有一个互动地图,可以在决定进一步产品开发之前用于 POC 演示。

POC 阶段结束时的产品示例,作者插图
本次 POC 的完整代码可以在我的 GitHub上找到。
关于应用的一些背景
在当前经济形势下,我们观察到加油站的油价波动剧烈。我们还看到——至少在我来自法国的地方——因罢工和一些恶意的加油站老板趁机抬高价格而导致的短缺。考虑到这一点,加油站的价格对许多需要加油的人来说可能成为一个重要的标准。
好消息是:在法国,通过实施的开放数据政策,可以实时访问加油站价格。原始格式的数据对许多人来说没有直接的价值,因为难以立即理解。相反,需要通过适当的用户界面将其展示,以便信息能够迅速过滤和理解。
我们将构建这个用户界面:一个网页应用程序,允许用户检索周围加油站的油价。我这里专注于法国市场(因为数据公开且易于访问),但我们的工作可以轻松适用于全球其他地方的数据。
关于使用的数据集的简要说明
在其开放数据政策中,法国维护了许多免费访问的数据集。特别地,我们将使用法国燃油价格——即时流数据集,其使用遵循开放许可证 V2,并可以用于商业目的。
我们还将使用市镇行政区划 API来获取与城市相关的信息,该 API 也遵循开放许可证 V2。
访问数据
我们的冒险从这里开始:在开放数据的丛林中,我们发现了这个有趣的燃油价格数据集。我们还不知道它的具体内容和包含的数据。
如下所示,有多个文件可以下载:

包含数据集的页面的翻译版本,作者插图
这里需要避免的第一个错误是让我们的项目充满过多的数据:并不是因为数据是可用的,我们就需要它。
在这里,我们通常只想展示“即时”价格的可能性给用户,并不打算进行历史价格分析。因此,探索历史数据至少在现在看来是浪费时间,我们将专注于即时数据集。
时间是宝贵的资源,许多想法永远不会变成实际的产品。重要的是要高效,并确保我们只关注我们想展示的概念。
根据文档,数据集包含诸如地址、邮政编码、坐标和价格等信息,还有其他对 POC 不太有用的信息(如提供的其他服务)。
数据提取
可以通过下载链接获取即时数据源,该链接会下载一个包含本地计算机文件的 zip 文件。
donnees.roulez-eco.fr/opendata/instantane
这很好,但即使我们处于 POC 阶段,我们也需要预见项目中可能出现的一些“致命”约束。特别是这里,假设我们每天至少更新一次数据,我们不希望手动更新,并且需要评估一种自动访问数据的方法。
希望我们可以直接在 Python 中加载 zip,结合 urllib 和 zipfile 库直接从 URL 对 .zip 文件进行操作。
def load_from_url_in_memory(url):
'''This function load the xml file from the url in memory after unziping it
It assume only one file with a .xml extention is available in the zip
'''
with urllib.request.urlopen(url) as url:
s = io.BytesIO(url.read())
z = zipfile.ZipFile(s)
return z
探索 .xml 文件
XML 是一种常用于数据存储和数据交换的标记语言。即使像 JSON 这样的新格式越来越受欢迎,XML 在许多应用和行业中仍然被广泛使用,了解它的工作原理仍然是有意义的。
.xml 文件形成树状结构,从“根”开始,分支到子元素。每个元素都有自己的属性和可能的子元素。
让我们通过在一个简单的文本编辑器中打开文件来查看它。
文件的第一行提供了一些元数据,例如使用的编码:
<?xml version="1.0" encoding="ISO-8859-1" standalone="yes"?>
这里提到,要读取文件,我们需要使用 ISO-8859–1 编码,这在我们解析文件时会很有用。
.xml 文件由多个具有不同深度的元素组成:
<pdv_liste>
<pdv id="40140005" latitude="4374653.865" longitude="-132117.12497344" cp="40140" pop="R">
<adresse>9 AVENUE DE CRAMAT</adresse>
<ville>SOUSTONS</ville>
<horaires automate-24-24="1">
<jour id="1" nom="Lundi" ferme=""/>
<jour id="2" nom="Mardi" ferme=""/>
<jour id="3" nom="Mercredi" ferme=""/>
<jour id="4" nom="Jeudi" ferme=""/>
<jour id="5" nom="Vendredi" ferme=""/>
<jour id="6" nom="Samedi" ferme=""/>
<jour id="7" nom="Dimanche" ferme=""/>
</horaires>
<services>
<service>Laverie</service>
<service>Station de gonflage</service>
<service>Automate CB 24/24</service>
</services>
<prix nom="Gazole" id="1" maj="2023-01-18 07:27:47" valeur="1.891"/>
<prix nom="E85" id="3" maj="2023-01-14 08:15:44" valeur="1.059"/>
<prix nom="E10" id="5" maj="2023-01-18 07:27:47" valeur="1.839"/>
</pdv>
<pdv>
...
</pdv>
</pdv_liste>
这里的 pdv 元素代表我们的加油站。在标签内部,有多个参数,这些是元素的属性(id、纬度、经度、邮政编码……)
<pdv id="40140005" latitude="4374653.865" longitude="-132117.12497344" cp="40140" pop="R">
每个 pdv 也有多个子元素:
-
adresse: 车站的地址
-
ville: 车站所在的城市
-
horaires: 车站的开放时间
-
services: 代表提供的额外服务,如洗车、自动取款机等……
-
prix: 每种燃料类型的价格(作为属性提供)
这个数据结构可以通过下面的树形图表示:

我们的 .xml 文件的数据结构,作者插图
从.xml 到表格数据
这种树状数据结构不适合进行简单操作,我们希望将其解析为表格版本,这将更方便以后进行过滤和聚合操作。
为了高效存储数据,我们将创建两个表:
-
用于表示加油站及其元数据(地址、城市等)的表格。
-
用于表示燃料、其站点及其价格的表格
你可以使用如dbdiagram.io/这样的工具来设计你的数据结构。

数据结构的示例视图,作者插图。
在这个 POC 阶段,我们不会设置 SQL 服务器,而是将数据存储在 pandas DataFrame 中以加快速度。我们也可以将其作为起点存储在.csv 中,但文件很轻,所以目前没有必要这样做。
当我们探索应用程序的 MVP 时,我们将以更清晰的方式设置数据库。
解析.xml 文件
到这一步我们知道我们想要的 数据结构,我们只需正确解析文件。
在继续之前,我想提醒你注意使用自动解析器时要特别小心,因为它们可能会忽略文件中可用的部分数据。
例如,使用内置的pandas.read_xml()方法,未进行任何特定调整,将返回一个不完整的 DataFrame。
url = f'https://donnees.roulez-eco.fr/opendata/instantane'
xml_file = load_from_url_in_memory(url)
pd.read_xml(z.open(xml_file), encoding="ISO-8859-1")
上面的代码会产生下面的 DataFrame,其中大部分信息丢失:

使用pd.read_xml和默认参数快速解析不会产生相关结果。
相反,我们将编写自己的数据提取器,这将为我们提供回顾递归函数的机会,并且基于 python xml库。
注意:即使我们处于 POC 阶段,保持代码结构清晰也是重要的。我们将为数据加载模块构建一组适当的函数,这些函数可以在后续过程中少量调整后重复使用。这不会额外消耗时间,但将为 MVP 节省大量工作。
为了正确解析文件,我们使用xml库。让我们从访问根元素开始,pdv_list。
import xml.etree.ElementTree as ET
#Link of instant data feed
url = f'https://donnees.roulez-eco.fr/opendata/instantane'
#from the function defined earlier
xml_file = load_from_url_in_memory(url)
with z.open(xml_file, "r") as file:
#Create the tree using xml parser
tree = ET.parse(file)
#Access the root node
root = tree.getroot()
根变量包含元素pdv_list。
<Element 'pdv_liste' at 0x0000019EAEF3B400>
子元素可以通过迭代简单访问(在我们的例子中,每个子元素将是一个“pdv”元素)。
for child in root:
#Do something
为了处理无论顺序如何的数据,我们将创建一个站点字典,包含我们想要保留的所有“站点”表的元数据,以及一个用于“油品类型”数据集的oil_type字典。
为了保持整洁,根据每个节点的复杂程度,我们将设计自定义函数。下面的代码展示了我们的自定义解析器如何工作。
def parse_pdv(pdv, stations, oil_types):
'''This function retrieve all the data available in a pdv element'''
station = {}
#Get attributes of the pdv
station.update(pdv.attrib)
#It is precised in the doc that lats and longs have to be divided by 100 000
station['latitude'] = float(station['latitude'])/100000
station['longitude'] = float(station['longitude'])/100000
#Rename the id key
station["station_id"] = station["id"]
del station["id"]
#Explore children, by type of children
for element in pdv:
if element.tag == "adresse":
station["adress"] = element.text
if element.tag == "ville":
station["city"] = element.text
if element.tag == "horaires":
#Some children, more complicated to parse, have their own subfunctions
station = parse_schedules(element, station)
if element.tag == 'prix':
#Some children, more complicated to parse, have their own subfunctions
oil_types = parse_oil(element, station, oil_types)
stations.append(station)
return stations, oil_types
def parse_xml(z):
'''Parse the whole xml'''
xml_file = [file for file in z.namelist() if file.endswith('.xml')][0]
with z.open(xml_file, "r") as file:
#Create the tree using xml parser
tree = ET.parse(file)
#Access the root node
root = tree.getroot()
#Initiate stations and oil_types
stations = []
oil_types = []
for pdv in root:
stations, oil_types = parse_pdv(pdv, stations, oil_types)
stations_df = pd.DataFrame(stations)
oil_types_df = pd.DataFrame(oil_types)
return stations_df, gas_types_df
这一组函数将返回这两个 DataFrame,并且如果我们想要使用 SQL 表,将来应该相对容易修改。

关于 oil_type 的示例 DataFrame,作者插图

关于车站的示例 DataFrame,作者插图
过滤函数
到目前为止,我们拥有所有加油站的纬度、经度和价格。我们不希望向用户展示所有信息,只展示其周围提供相关气体类型的加油站。
在接下来的部分,我们将开发所有功能,以将原始数据集转换为过滤后的数据集。
计算两点之间的纬度/经度坐标距离
纬度和经度以度为单位表示,且由于距离是计算在地球表面上的,简单的欧几里得几何不能应用。
为了计算两点之间的距离,我们将使用哈弗辛公式:
def haversine_distance(lat1, lon1, lat2, lon2):
'''Calculate the distance between two points (lat1,lon1) and (lat2, lon2) in km'''
R = 6371
lat1, lon1, lat2, lon2 = map(np.radians, [lat1, lon1, lat2, lon2])
dlat = lat2 - lat1
dlon = lon2 - lon1
a = np.sin(dlat / 2)**2 + np.cos(lat1) * np.cos(lat2) * np.sin(dlon / 2)**2
c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))
distance = R * c
return distance
无论函数来自你还是其他开发人员,都不要忘记测试它以验证结果输出是否正确。
在我的情况下,我正在使用 Google Earth 距离功能来检查两个我知道纬度和经度的城市之间的距离,并确认该函数返回预期的输出。
根据标识为纬度/经度的点过滤数据集
我们现在可以创建一个基于源纬度/经度的哈弗辛距离过滤加油站数据集的函数。
def filter_dataset_from_source_point(stations_df, lat_point, lon_point, distance = 20):
return stations_df.loc[haversine_distance(stations_df.latitude, stations_df.longitude, lat_point, lon_point)<=distance]
我在我周围的地方测试了这个函数,以检查我是否能正确检索到周围的加油站,结果确实如此。
filter_dataset_from_source_point(stations_df, lat_point = 49.131, lon_point = 2.521, distance = 5)

过滤我所在村庄周围的示例数据集,作者插图
按气体类型过滤
用户只对销售其汽车所用气体类型的加油站感兴趣。我们现在可以在 station_id 和我们的气体类型之间进行连接,以根据气体类型检索和过滤行。
def filter_dataset_by_gas_type(sub_station_df, gas_types_df, gas_type):
joined_df = sub_station_df.set_index("station_id") \
.join(
gas_types_df.set_index("station_id"), how = 'left'
)
return joined_df.loc[joined_df.nom==gas_type]
filter_dataset_by_gas_type(sub_station_df, gas_types_df, "SP98").dropna(axis=1)

应用 filter_dataset_by_gas_type 后的结果示意图,仅显示一些相关列。作者插图
获取法国城市的坐标和邮政编码
目前我们拥有的情况还不错,但还不够好:我们不希望用户自己输入其位置的纬度和经度,因为这些信息不容易获取。
相反,我们希望提供一个尽可能简单获取纬度/经度的功能。实现这一点的方法有很多,许多 API(如 Google Map API)已经提供了以不同复杂度(和不同价格)可访问的信息。为了进行概念验证,我们希望保持简单,所以我们将寻找一个基于邮政编码的城市中心坐标数据集。
注意:我们有一个包含坐标和邮政编码信息的车站数据集,但仅限于有加油站的城市。我们的目标是使数据集尽可能全面。
经过一些研究,我找到了另一个开放的数据集,可以通过 API 快速检索所需的数据。如介绍中所提,这些数据也在开放许可证下,可以自由使用。
url = f"https://geo.api.gouv.fr/communes?fields=codesPostaux,nom,centre"
req = requests.get(url).json()
#We need to unpack the json via a loop as we want duplicated lines for each postal code
city_data = []
for elmt in req:
for code_postal in elmt['codesPostaux']:
city = {}
city["postal_code"] = code_postal
city["name"] = elmt['nom']
city["lat"] = elmt['centre']['coordinates'][1]
city["lon"] = elmt['centre']['coordinates'][0]
city_data.append(city)
cities_df = pd.DataFrame(city_data)

我们的 cities_df 的示例 DataFrame,作者插图
我们现在可以开发最终的模块:一个函数,它将接受 3 个参数(邮政编码、距离和气体类型),并返回周围加油站的价格(我暂时假设 stations_df、cities_df 和 gas_type_df 为全局变量,但我将在文章末尾将其打包到一个类中)。
from builtins import ValueError
def get_close_gas_prices(postal_code, distance_km, gas_type):
global cities_df
global stations_df
global gas_types_df
sub_cities = cities_df.loc[cities_df.postal_code==postal_code]
#If the postal code is not value, raise a value error
if len(sub_cities)==0:
raise BadInputError("Postal code is not a valid code")
lat_point, lon_point = sub_cities.iloc[0][["lat","lon"]]
sub_station_df = filter_dataset_from_source_point(stations_df, lat_point, lon_point, distance_km)
return filter_dataset_by_gas_type(sub_station_df, gas_types_df, gas_type)[["latitude" ,"longitude" ,"cp" ,"city","nom", "valeur"]]
在我的城市再次测试(注意这里有一个额外的站点,可能由于邮政编码 -> 纬度/经度的微小差异):
get_close_gas_prices(postal_code='60560', distance_km=5, gas_type='SP98')

CP 数据集示例,作者插图
这里还有一个重要的注意事项:虽然气体类型将通过有限的下拉列表进行选择,但用户有责任填写正确的邮政编码。如果邮政编码不正确,sub_cities DataFrame 将为空,访问其第一个元素将引发错误。
我们更倾向于通过捕获错误来处理这种情况,使用我们自定义的异常,这将更容易在 MVP 中集成。
from builtins import Exception
class BadInputError(Exception):
def __init__(self, message):
self.message = message
使用格式错误或不在我们数据库中的邮政编码将生成此错误消息:

我们自定义异常的插图,作者插图
构建一个简单的演示器
到目前为止,一切顺利,我们展示了通过简单地使用邮政编码、距离和燃料类型快速访问数据库中的相关信息的可能性。
我们仍然在使用一些 Python 函数,这些函数展示给管理层或投资者可能会显得不够美观。为了向他们证明推动项目进入下一个开发阶段的价值,我们希望将其集成到一个小型演示器中。
我们可以创建一个在本地运行的小型仪表板,使用plotly和dash,但最快的方法是简单地 在笔记本中创建一个小组件。更高级的仪表板将在原型阶段演示。
在笔记本中显示地图
在 Jupyter Notebook 中有多种显示地图的方法,比如优秀的folium库(如果你想了解更多关于这个库的信息,可以查看这篇文章)。
在我的情况下,我更愿意使用 plotly,以便为下一阶段的开发做好准备,我们将在用户界面中直接嵌入 plotly 地图。
这一部分在我们的开发中非常重要:如果你想在后续开发阶段获得机会,它必须给你展示的人留下“哇”的感觉。你可能有世界上最好的想法,但如果包装难看,人们是不会买的。
为了让内容清晰,这是我们想在地图上展示的信息:
-
使用的城市中心坐标
-
显示研究范围的透明圆圈
-
提供所请求燃料类型的站点坐标
-
为了更清晰,我们还将映射站点的颜色与价格偏离平均值的情况
使用 Scattermapbox 的简单地图
Plotly 集成了 Mapbox GL JS,允许我们构建一个 瓦片地图,该地图可以处理如地图导航和缩放等操作,同时不会占用过多的 CPU 资源。
构建地图需要两个要素:
-
包含要可视化数据的轨迹,并与地图兼容(此处为 go.Scattermapbox)
-
一个包含地图图层的布局。这通过指定一个 Mapbox 字典来完成。注意:你需要一个免费的 Mapbox 令牌来访问用于地图背景的 Mapbox 瓦片。
下面的示例显示了一个简单的地图(我们使用 FigureWidget 而不是 Figure 来将该地图直接集成到我们的部件中)
import plotly.graph_objects as go
#I hardcode my lat/lon for now, we integrate this in a proper class later
lat,lon = cities_df.loc[cities_df.postal_code=='60560'][["lat","lon"]].iloc[0]
stations = go.Scattermapbox(
lat= station_df.latitude,
lon= station_df.longitude,
mode='markers',
marker = dict(
size = 14,
symbol = 'fuel',
),
showlegend = False
)
#create the layout
layout = go.Layout(
height = 700,
width = 700,
mapbox = dict(
accesstoken = token,
style ='streets',
center = dict(
lat = lat,
lon = lon
),
zoom = 10,
),
)
fig = go.FigureWidget(data=[stations], layout=layout)
fig.show()
这段代码应用于我们预筛选的数据集后,将得到:

来自 plotly 地图的输出,作者插图
目前还不算特别花哨,但这是一个很好的开始。
添加价格指示器
我们可以简单地向地图添加其他轨迹,以在基础地图上添加更多信息。
由于无法直接更改图标的颜色,我们将添加一个额外的 ScatterMapBox,其中包含我们可以修改颜色的圆形点。
prices = go.Scattermapbox(
lat = station_df.latitude,
lon = station_df.longitude,
mode ='markers',
marker = dict(
size = 30,
color = station_df.valeur.astype(float),
colorscale = 'PiYG_r'
),
opacity = 0.5,
text = station_df.valeur+' €/L',
showlegend = False
)
#This is a bit hacky, but it allows us to add a solid border around the circle
solid_price_border = go.Scattermapbox(
lat= station_df.latitude,
lon= station_df.longitude,
mode='markers',
marker=dict(
size=45,
color='black',
),
opacity = 0.8,
text=station_df.valeur+' €/L',
showlegend = False,
)

带价格映射的 plotly 地图输出,作者插图
不要低估你选择的颜色映射的重要性。你可以用多种方式来表示信息,传达的消息可能不尽相同。我们这里使用了一个 分歧色彩图,它适合展示价格相对于样本平均值的相对差异。我还利用了经典的心理偏差,将红色调与负面事物(这里是高价格)相关联,而绿色与正面事物(低价格)相关联。
作为示例,这里是使用默认颜色映射获得的结果:

关于将颜色映射到价格的一个不应跟随的示例,作者插图
添加用户位置和研究范围
添加用户位置是最简单的部分,我们可以简单地添加一个红点,象征研究的中心位置t。
研究范围稍微复杂一些。我们将使用另一个 Scattermapbox 通过纬度和经度在圆的边缘表示圆形。为此,我们首先需要一个函数来计算从坐标和半径得到的圆周坐标。
def points_on_circle(lat, lon, radius, num_points):
'''This function return a list of coordinates as lat/lon of size num_points forming a circle of a certain radius centered on a
a point of coordinate lat,lon. Thanks internet :)
'''
points = []
R = 6371
for i in range(num_points):
bearing = 360/num_points*i
lat2 = np.arcsin(np.sin(np.radians(lat))*np.cos(radius/R) +
np.cos(np.radians(lat))*np.sin(radius/R)*np.cos(np.radians(bearing)))
lon2 = np.radians(lon) + np.arctan2(np.sin(np.radians(bearing))*np.sin(radius/R)*np.cos(np.radians(lat)),
np.cos(radius/R)-np.sin(np.radians(lat))*np.sin(lat2))
points.append((np.degrees(lat2), np.degrees(lon2)))
points = np.array(points)
points = np.vstack([points, points[0]])
return points[:,0], points[:,1]
现在我们可以生成一个要点列表,并将它们作为新的轨迹添加到我们的图表中。
circle_lats, circle_longs = points_on_circle(lat,lon, 5, 50)
research_zone = go.Scattermapbox(
lat= circle_lats,
lon= circle_longs,
mode='lines',
fill='toself',
fillcolor = "rgba(1,1,1,0.2)",
marker=dict(
size=45,
color='black',
),
opacity = 0.8,
showlegend = False,
)

添加研究参数区域和用户位置,作者插图
在小部件中的最终打包
我们的演示几乎完成了,只需要添加一点互动性。为此,我将把我们到目前为止做的所有工作打包,并包括 3 个组件来控制燃料类型、研究距离和邮政编码。
我将跳过完整的打包过程,只将我们迄今为止看到的内容放入一个通用类中,以处理不同的状态。如果你不熟悉使用 ipywidgets 的互动笔记本,可以查看 这份文章列表。
URL = f'https://donnees.roulez-eco.fr/opendata/instantane'
CITY_PATH = 'cities_coordinates.csv'
INIT_POST_CODE = '75001'
INIT_FUEL = 'SP98'
INIT_DIST = 3
import plotly.graph_objects as go
class FuelWidget:
def __init__(self):
'''We initiate the instance of our class with the functions we saw so far
1\. We download the latest data from the stations price and gen our 2 dataframe
2\. We load the postal code -> lat/lon .csv
3\. We filter the dataset based on initial values
4\. We initiate the layout with the figure we saw before and some UI components
'''
self._distance = INIT_DIST
self._fuel = INIT_FUEL
self._post_code = INIT_POST_CODE
#Load the stations data
z = load_from_url_in_memory(URL)
self._stations_df , self._gas_types_df = parse_xml(z)
#Load the cities data
self._cities_df = pd.read_csv(CITY_PATH, index_col=0, dtype={"postal_code":str})
#Filter the dataset with the initial constants
self._filter_by_postal_code(self._post_code, self._distance, self._fuel)
#Initialise the layout
self._init_layout()
def _init_layout(self):
'''This function initiate the widget layout and the different callbacks'''
#Select fuel dropdown
fuel_type_dropdown = Dropdown(options = self._gas_types_df.nom.unique(),
value = INIT_FUEL,
description = "Fuel type")
fuel_type_dropdown.observe(self._on_fuel_change, names='value')
#Select max distance dropdown
distance_dropdrown = Dropdown(options = [*range(1,30)],
value = INIT_DIST,
description = "distance (km)")
distance_dropdrown.observe(self._on_distance, names='value')
#Select postal code from text
postal_code_text= Text(placeholder="Postal Code")
postal_code_text.observe(self._on_change_text, names='value')
#Create the figure based on initial value
self._fig = init_chart(self._sub_station_with_prices,
self._lat_pos,
self._lon_pos,
self._distance)
#Create the widget
self._widget = VBox([postal_code_text,
HBox([fuel_type_dropdown,
distance_dropdrown]),
self._fig])
回调函数将执行相同的操作:观察变化、更新过滤后的数据集,并更新图形。例如,这里是 _on_distance 回调:
def _on_distance(self, change):
'''This callback is called when the distance dropdown change, refilter the dataset,
and update the figure
'''
self._distance = change["new"]
done = self._filter_by_postal_code(self._post_code, self._distance, self._fuel)
if done:
self._update_fig()
最后,让我们看看更新图形的方法。一旦数据集被过滤,我们将生成一个新图形,并用生成的新轨迹更新当前图形的关键元素。
def _update_fig(self):
#Initialize a new figure
new_fig = init_chart(self._sub_station_with_prices, self._lat_pos, self._lon_pos, self._distance)
with self._fig.batch_update():
#Update all the lat longs for our 5 traces
for k in range(5):
self._fig.data[k].lat = new_fig.data[k].lat
self._fig.data[k].lon = new_fig.data[k].lon
#Update the color markers of the prices
self._fig.data[2].marker = new_fig.data[2].marker
#Update the overlayed text
for k in [1,2,3]:
self._fig.data[k].text = new_fig.data[k].text
#Update the new center of the map
center = {"lat":self._lat_pos, "lon":self._lon_pos}
self._fig.layout.mapbox.center = center
就这样!我们只需添加一个“public”方法 display,该方法将在笔记本中调用以显示小部件,我们的演示就准备好了。
def display(self):
display(self._widget)

我们的 POC 演示正在运行,作者插图
结论
本章结束了我们产品开发的第一部分,也就是概念验证。
我们一步一步地构建了第一个演示,用以展示我们想法的潜力。这个演示在短时间内完成,仍然是简约的,我们将在下一个开发阶段——原型制作中进一步发展这个概念。
在下一个开发阶段,我们将修改这个简约的小部件,使其成为一个真正的 Web 应用程序,具有更多功能,并可以在你的计算机上本地使用。
如何使用 React 构建 Web 应用
原文:
towardsdatascience.com/a-step-by-step-guide-to-develop-a-map-based-application-part-ii-6d3fa7dbd8b9
开发基于地图的应用程序的逐步指南(第二部分)
·发表于 Towards Data Science ·阅读时间 20 分钟·2023 年 2 月 9 日
--

图片来源于 Lautaro Andreani 在 Unsplash
地图是可视化和理解地理数据的强大工具,但需要特定的技能才能高效设计。
在这份逐步指南中,我们将深入探讨构建一个基于地图的应用程序,以展示周围加油站的客户价格。我们将涵盖产品的不同关键步骤,从最初的概念验证(POC)到最小可行产品(MVP)。
系列文章:
第二部分:如何使用 React 构建 Web 应用(静态布局)
第三部分:使用 React 为你的 Web 应用添加互动性
第四部分:使用 PostgreSQL、FastAPI 和 Docker 构建后端
这篇文章的背景
在这篇文章中,我们将继续之前开始的加油站定位器应用的工作。这一次,我们将专注于准备可以使用静态数据集在本地运行的 Web 应用程序的第一个版本。该 Web 应用程序将使用React 框架构建,本文将尽可能详细地介绍给那些对该框架不太熟悉的读者。
我决定为这个应用程序使用 React,因为它是一个令人信服的框架,目前只有少数数据科学家/数据分析师在使用。如果你曾对其他框架的僵化感到沮丧,我希望你会在本文中找到一种设计 web 应用程序的替代方法。你会发现,学习曲线一开始可能看起来有点陡峭(尤其是如果你对 HTML/CSS/JavaScript 不熟悉),但一旦掌握,将这个框架加入你的工具箱将为构建动态、用户友好且可扩展的 web 应用程序打开许多机会。
注意:本文不会包括所有代码,仅提供片段以说明关键原则,但完整代码可以在 我的 GitHub 页面 上找到。
到文章结束时,我们将拥有我们的 React 应用程序的布局,在下一部分,我们将专注于为其添加互动性。

文章结束时,应用程序将“静态”渲染,作者插图
为什么使用 React?
有许多工具和框架可以用来构建漂亮而强大的 web 应用程序。在数据科学生态系统中,一些最常用的框架包括 Dash 或 Streamlit。这些工具的一个大优势是它们的简单性和易用性,对于一些使用场景来说非常高效且足够,特别是如果你主要使用 Python。
然而,这也带来了灵活性和自定义的权衡,如果你的应用程序变得更加复杂,开发这些框架中的功能所需的时间将呈指数级增长。
另一方面,React 是一个性能更强且允许更多灵活性的框架。它还鼓励使用模块化和基于组件的架构,这在添加新功能和维护项目时非常有用。最后,它是 JavaScript 生态系统的一部分,而 JavaScript 生态系统是世界上最大和最受欢迎的生态系统之一。
尽管有这些优势,你可能会对学习该框架感到犹豫,尤其是如果你来自 Python 世界。你不仅需要学习框架本身,还可能需要学习一些 JavaScript、HTML 和 CSS,这可能会非常耗时。
关于 HTML 和 CSS,即使你打算深入应用程序(即使是在 Dash 或 Streamlit 中),也需要对这些语言有基本了解。JavaScript 可能会稍微有些挑战,特别是如果你不太熟悉面向对象和异步编程,但一旦学会,它将可能让你更轻松高效地开发 web 应用程序(同时提高你的编程技能)。
设置项目
要启动一个由 React 提供支持的项目,你首先需要在计算机上安装 Node.js,它是用于运行 JavaScript 的运行时环境。
Node.js 可以在这里下载。
附注:事情变化很快。我在 2023 年 3 月写了这篇文章,我正在使用 React 18.2.0. 如果你在文章发布多年后阅读它,可能会有些过时。因此,请注意以下内容。
创建一个新项目
一旦安装完毕,我们将使用npx/npm(大致相当于 Python 中的 pip)通过命令“create-react-app”来设置 React 项目。
npx create-react-app fuel-station-front
在 CLI 中运行此命令将自动下载所有所需的包,并在以下文件夹中设置你的项目:
fuel-station-front/
|-- node_modules/
|-- public/
|-- index.html
|-- favicon.ico
|-- ...
|-- src/
|-- App.js
|-- index.js
|-- ...
|-- package.json
|-- package-lock.json
|-- README.md
这看起来可能很复杂,让我们逐文件详细了解:
node_modules/:此文件夹包含所有使用 npm 添加到项目中的包和依赖项。通常,你应该保持这个文件夹不变。
public/:此文件夹包含项目使用的静态文件。默认情况下,它包含基本文件,以运行在启动新项目时生成的默认页面。
src/:此文件夹是应用程序的核心,包含其源代码。我们将在这里编写应用程序的代码。最初,它包含运行默认页面的文件,当你启动一个新项目时会生成这个页面。
package-lock.js:此文件相当于 requirement.txt。它包含所有依赖项和通过 npm 安装的包的确切版本。
package.json:此文件相当于 setup.py,指定了项目的元数据。
运行项目
在项目的这一生命周期阶段(基本上是在本地构建和测试),我们将使用create-react-app工具的内置开发服务器。这对于快速开发很有用,但不够稳定,不能用于生产目的,我们将在另一篇文章中讨论这一点。
要运行测试服务器,只需进入项目文件夹并运行以下命令:
npm start

这将自动在你的默认浏览器中打开一个新网页,显示任何新 React 项目中模仿的默认页面。
“localhost” 意味着我们在自己的计算机上运行服务器,而 “:3000" 表示应用程序的端口号。

默认的 React 应用,作者插图
安装所需的包
React 拥有一个庞大的生态系统,许多包是开源的,会简化你的工作。
特别地,对于这个项目,我们将使用react-plotly.js,这是一个为 React 应用程序制作的 Plotly 封装器。
npm install react-plotly.js plotly.js
一些非常有趣的包,我还可以提到 react-router-dom,这是一个强大的 URL 映射器,用于管理多页面应用程序,以及 uuid,这是一个根据最新标准生成自动 ID 的包。我今天不会使用它们,但它们是必须了解的包。
提升编码体验
我将使用 VSCode 编写项目。许多扩展可用,以简化开发 React 网页应用程序时的工作。
简要谈谈其中的一些。
ES7 React/Redux/GraphQL/React-Native snippets: 这个扩展提供了许多快捷方式,用于快速设置一些模板代码。

ES7 动作,作者插图
Emmet: 这个插件通过提供许多快捷方式和缩写来自动生成代码,从而实现快速编码。

Emmet 动作,作者插图
Prettier: 这个扩展在你保存文件时会自动格式化你的代码。
构建布局
现在我们的环境已经正确设置,我们可以开始构建我们的应用程序。正如前面部分所提到的,这主要是在修改 src/ 文件夹中的文件后完成的。
让我们稍微看一下里面的内容:
|-- src/
|-- App.js
|-- index.js
|-- App.test.js
|-- reportWebVitals.js
|-- setupTests.js
|-- index.css
|-- App.css
|-- logo.svg
对我们来说唯一感兴趣的文件是 App.js,我们稍后会详细讨论。关于其他文件,简要说明:
-
index.js 负责应用程序的渲染。我们将不会修改这个文件
-
reportWebVitals.js 用于将网页的性能指标报告给分析服务,在我们使用 React 的层级中,将不会使用。
-
setupTests.js 是一个用于设置测试环境的文件。这也超出了本文的范围,我们将不做修改
-
logo.svg 仅仅是创建项目时在默认页面中使用的一张图片,可以删除
-
App.css 和 index.css 包含一些基本的 .css 文件,用于你的网页应用,如果需要,可以修改以实现不同的样式。
应用程序架构
在 React 中,我们通过组合自定义组件来构建应用程序。作为最佳实践,每个组件将由两个文件组成:
-
一个 .js 文件用于编写组件的逻辑:它的功能、如何管理其内部状态以及如何将状态传递给父组件和子组件
-
一个 .css 文件用于组件的美观和格式设置
让我们以我们的应用程序为例,看看我们的逻辑如何应用。我们希望我们的应用程序基本上有三个主要组件:
-
一个搜索区域表单将用于查找地点和燃料类型
-
一个地图,将显示车站,就像我们在上一篇文章中构建的那样
-
一个表格用于快速总结周围车站的主要信息
这将看起来像这样:
|-- src/
|-- App.js
|-- App.css
|-- AppComponents/
|-- StationsFilter/
|-- StationsFilter.js
|-- StationsFilter.css
|-- CustomTextBoxes/
|-- ...
|-- StationsMap/
|-- StationsMap.js
|-- StationsMap.css
|-- StationsTable/
|-- ...
|-- index.js
|-- ...
这可能看起来有很多文件和文件夹,但这将有助于保持项目的清晰和整洁,如果你将来需要返回修改或改进应用程序,这将使你的生活变得更加轻松。
应用基础布局
在这一点上,你可能想要构建一个应用程序的粗略布局。如果你没有网页设计师帮助,最好的办法是查看现代应用程序的示例,从中获取灵感。互联网充满了构建漂亮组件的模板,通过基本的 .css 知识和这些模板的帮助,你已经能够做出漂亮且现代的元素。
在我们开始项目时,我们想要构建一个基本的初始视图,以大致了解如何组织我们的组件。基本布局完成一半,实际上,网页应用程序基本上是由盒子中的盒子构成的……
对于我们的网络应用程序,我们保持简单:

基本应用布局,作者插图
在进一步讨论之前,我们还需要谈谈设计范式。在 HTML/CSS 中,你有很多组织组件的方式。我最喜欢的范式是Flexbox 范式,它提供了一种简单的方法来组织容器内的盒子。
使用这种范式,你可以设置“容器盒子”的组织样式:
-
子盒子的对齐方式是按行还是按列?
-
在这个结构中,子盒子是如何在主轴上组织的?
-
在这个结构中,子盒子在正交轴上是如何组织的?
上述布局遵循 Flexbox 范式。
灰色区域是主要容器。我们希望它包含两个“div”(即 HTML 语言中的盒子),按列组织:一个包含头部(一个标题,有时还有一些菜单选项),另一个包含主要组件。
这个 div 将以行的方式组织,从而将空间分成两部分:左侧部分,我们将有一个 div(橙色)用于过滤组件和价格表格,而右侧部分,将有地图组件,占据剩余空间。
export default function App() {
return (
// the gray box, we want its "organisation" to be in column
<div className="App">
//the header, being the first blue box
<header className="header">
<h1 className="h1-searchbar">Gas Station Finder</h1>
</header>
//the second blue box, containing the main components, which will be organised in row
<div className="main-components">
//Inside that blue box, we have another box "left-section" (the orange box) containing the Filter and the Table
<div className="left-section">
<StationsFilter />
<StationsTable />
</div>
//Finally we also have the other component, our map, inside the second blue box
<StationsMap />
</div>
</div>
);
}
App.css 将包含我们容器盒子的 .css,来看看吧!
.main-components{
height:90vh;
width:100%;
display: flex;
flex-direction: row;
align-items: center;
}
.left-section{
height:100%;
width: 39%;
background-color: #f4f4f4;
display: flex;
flex-direction: column;
align-items: center;
}
display: flex 指示容器是“弹性盒子”,子盒子将按照这个范式进行组织
flex-direction 指示盒子是按行还是按列组织的
align-items 用于在次要方向上组织对齐
justify-content(这里未使用)用于在主方向上组织对齐
初始化组件
现在所有上述内容都已设置,让我们用基本版本初始化所有组件,我们将随着时间的推移进行改进。
在 StationsFilter.js 中,我们将使用快捷方式“rfc”(如前一部分中的 .gif 所示),这将直接生成我们的组件模板,并添加相应 .css 文件的导入语句。
import "./StationsFilter.css";
export default function StationsFilter() {
return (
<div>StationsFilter</div>
);
}
这段小代码通过“export default”指示函数 StationsFilter 以后可以直接在另一个文件中导入,如:
import StationsFilter from 'pathtofile/StationsFilter';
并在其自身的 HTML 标签中使用:
<StationsFilter />
让我们为 StationsMap.js 和 StationsTable.js 制作相同的模板,我将在这里传递代码快照,它与我们之前做的完全相同。
处理过滤器布局
我们希望过滤器由两个组件组成:
-
一个用于按邮政编码查找地点
-
一个用于选择燃料类型
import "./StationsFilter.css";
export default function StationsFilter() {
const fuelTypes = ["E10","E85","Gazole","GPLc","SP95","SP98"]
return (
<form className="search-form">
<input type="text" placeholder="Postal Code" className="general-input" />
<select value="SP98" className="general-input">
{fuelTypes.map(e => <option value={e}>{e}</option>)}
</select>
<button className="send-request-button">Find Stations</button>
</form>
);
}
让我们详细查看一下:
-
我们首先定义一个类型为“form”的框,在其中放置我们的表单字段和按钮
-
我们创建一个“text”类型的输入框,用于接收文本。我们设置了一个占位符名称“邮政编码”,以给客户提供指示,并为 .css 设置了一个 className
-
我们创建一个带有 标签中的值被硬编码为“SP95”。我们将使用 useState 替换它:
const [gasType, setGasType] = useState("SP95"); <select value={selectedValue} className="general-input">...</select>我们现在需要在“onChange”事件在下拉菜单中触发时修改值:
const handleChangeDropdown = (event) => { setGasType(event.target.value); }; <select value={gasType} className="general-input" onChange={handleChangeDropdown}>onChange 回调有一个输入“event”,其中包含与更改相关的信息。在我们的例子中,我们对“event.target.value”中访问的新下拉值感兴趣。
作为第一个交互式组件之一,让我们详细说明一下这里发生了什么:
-
组件初始化时,gasType 变量由初始化为“SP95”的 useState 定义,因此这将首先出现在下拉菜单框中。
-
当用户更改值时,它触发“onChange”回调,这将调用我们的 handleChange 函数。
-
在 handleChange 中,我们用通过
event.target.value访问的新变量值来触发 gasType 的 setter,这将修改 gasType 及应用的渲染。
![]()
下拉菜单现在显示最新的点击值,作者插图
邮政编码文本区域
对于邮政编码的文本区域,逻辑非常类似,只是有一个不同之处:我们会强制“onChange”函数,只接受最多 5 位的数字。为此,我们只需通过正则表达式(仅数字,最多 5 位)测试下一个值,并仅在匹配正则表达式时更新 postalCode 值。
const [postalCode, setPostalCode] = useState(""); const handleChangeTextArea = (event) => { const newValue = event.target.value; if (/^\d{0,5}$/.test(newValue)) { setPostalCode(newValue); } }; <input type="text" placeholder="Postal Code" className="general-input" onChange={handleChangeTextArea} value={postalCode} />![]()
带有控制器的文本区域插图,作者插图
按钮“查找车站”
当按钮被点击时,我们想将下拉菜单和文本区域的信息传递给一个 API,该 API 将返回感兴趣城市 30 公里半径内的车站、价格和元数据。
点击按钮可以通过名为 onClick 的回调访问,当左键点击时触发。
现在我们准备函数,只需记录 postalCode 和 gasType。
const handleButtonClick = () => { console.log(`Postal Code: ${postalCode}, Gas Type: ${gasType}`) } <button className="send-request-button" onClick={handleButtonClick}> Find Stations </button>![]()
当我们点击“查找车站”时,我们从两个字段中检索最新信息,作者插图
关于 API 的一句话
在接下来的文章中,我们将更详细地讨论如何使用 Python + FastAPI 设置服务器端 API。现在假设服务已经可用,并且提供一个 GET 端点,提供从邮政编码和燃料类型获取的所有数据。
例如,使用下面的 URL:
将发送所有车站的元数据,30 公里半径内的经纬度,以及用于生成图表和表格的圆圈坐标。
{ "lat": 49.1414, "lon": 2.5087, "city": "Orry-la-Ville", "circle_lat": [49.411,49.410,...], "circle_lon": [2.5087,2.5347,...], "station_infos": [ { "address": "Centre Commercial Villevert 60300 Senlis", "price_per_L": 1.88, "price_tank": 113, "delta_average": 7, "better_average": 1, "google_map_link": "https://www.google.com/maps/search/?api=1&query=Centre+Commercial+Villevert+60300+Senlis", "distance": 10, "latitude": 49.221, "longitude": 2.591 }, { "address": "Rue de Pontoise 95540 Méry-Sur-Oise", "price_per_L": 1.9, "price_tank": 114, "delta_average": 6, "better_average": 1, "google_map_link": "https://www.google.com/maps/search/?api=1&query=Rue+de+Pontoise+95540+Méry-Sur-Oise", "distance": 26, "latitude": 49.061, "longitude": 2.17 }, ... ] }传递信息从一个组件到另一个组件
另一个在继续之前需要讨论的事情是如何将信息从我们的 StationsFilter 组件传递到我们的 App 组件。
在 React 中,信息可以通过“props”从父组件传递到子组件。这个“props”是一个对象(键/值),其中包含作为标签属性传递的不同元素。
在下面的示例中,我们通过 props.childVariable 访问子组件中的一个变量。
// A parent component, made of a state variable and a Child component export default function Parent() { [variable, setVariable] = useState(0) return ( <Child childVariable={variable} /> ) } // We pass "props" as parameter of our children component // This props is an Object (key/value) which contains all the attributes passed // in the parent component export default function Child(props) { //We access the variable via props.childVariable //It will return the current value stored in childVariable, which is in this case 0 console.log(props.childVariable) }通过这种方式,我们可以从父组件向子组件传播值。但反过来怎么样?如果你在子组件中修改了一个值,并希望将其传递给父组件,你可以像上面一样做,但不是传递变量,而是传递设置器。这样,当子组件中触发回调时,它可以触发来自父组件的设置器,从而修改父组件中定义的变量。
export default function Parent() { [variable, setVariable] = useState(0) return ( <Child childVariable={variable} setVariable={(newVal) => setVariable(newVal)}/> ) } export default function Child(props) { <button onClick={() => props.setVariable(props.childVariable+1)} /> }在上面的代码片段中,每次点击时,我们会触发一个函数,该函数获取当前变量(在 props.childVariable 中),加 1,然后触发设置器,以便将“变量”在父组件中设置为变量+1。
完成我们的 API 调用
记住这点理论后,让我们完成我们的组件。我们首先在 App.js 中创建多个状态变量,用于存储来自 API 的信息。
const [stationsData, setStationsData] = useState([]); const [latCity, setLatCity] = useState(0); const [lonCity, setLonCity] = useState(0); const [latCircle, setLatCircle] = useState([]); const [lonCircle, setLonCircle] = useState([]);然后我们将设置器传递给我们的 StationsFilter 组件,在那里我们将从 API 加载数据。
<StationsFilter setStationsData={setStationsData} setLatCity={setLatCity} setLonCity={setLonCity} setLatCircle={setLatCircle} setLonCircle={setLonCircle} /> //And don't forget to modify the definition of StationsFilter in StationsFilter.js //to include the props object ! //In StationsFilter.js: export default function StationsFilter(props) {...}现在我们需要准备 API 调用。在 React 中,这可以通过使用“fetch”方法完成,该方法接受一个 URL(用于请求)以及额外的参数,如头信息或其他请求参数。让我们直接在之前使用的“HandleClick”函数中实现它。
const handleButtonClick = () => { fetch(`${APIURL}/stations?oil_type=${gasType}&postal_code=${postalCode}`) .then((res) => { return res.json(); }) .then((data) => { props.setStationsData([...data["station_infos"]]); props.setLonCity(data["lon"]); props.setLatCity(data["lat"]); props.setLonCircle([...data["circle_lon"]]); props.setLatCircle([...data["circle_lat"]]); }); };一行一行地解释:
-
我们首先使用“fetch”,默认情况下,它对传递的 URL 进行 GET 调用。在我们的例子中,我们形成 URL 以包含状态变量 gasType 和 postalCode。
-
fetch 是一个异步函数,我们需要等待它完成后再处理 API 发送的数据。为此,我们使用回调 .then(),将其链式连接到我们的 API 调用。当 fetch 调用完成时,它将自动运行 .then() 中的代码。在这种情况下,我们接收到一个响应(来自 API),我们使用 res.json() 将其转换为 JSON。这也是一个异步函数,因此处理其结果必须通过链式调用另一个 .then() 回调来完成。
-
在最后的 .then() 中,我们检索数据并将其解析为 JSON,然后简单地解包为 lat、lon 和 stations_infos。然后我们触发在 App.js 中定义的不同设置器,以更新不同的状态变量。请注意,在 stationsData 的情况下,我们使用扩展操作 […someArray],这将创建 someArray 的副本。这是为了确保我们创建一个新的数组传递给 stationsData,而不是仅仅引用 someArray,这可能会导致一些意外的行为。
点击按钮现在将触发 API 调用,最终更新 App.js 中的状态变量。
连接 StationsTable 组件
如果你还记得之前的文章,我们通过使用一个预加载的 JSON 构建了 StationsTable 的预览,这个 JSON 的格式与当前存储在 stationsData 变量中的完全一致。
从 API 下载数据并将其存储在 stationsData 中是困难的部分。现在我们只需稍微修改我们的 StationsTable 组件,以便使用 stationsData 替代预加载的 JSON。
首先,我们在 StationsTable 中添加了一个名为 stationsData 的状态变量。
<StationsTable stationsData={stationsData} />然后我们向 StationsTable.js 中的函数声明添加 props。
export default function StationsTable(props) {...}最后,我们用包含在对象 props 中的 stationsData 替换预加载的 JSON。
<tbody> {props.stationsData.map((row) => {...})} </tbody>就这样,每次我们点击按钮时,表格中的数据将会被我们感兴趣的城市周边的站点价格替代。
![]()
表格更新中的实际操作,作者插图。
使用状态变量生成地图。
你应该现在已经习惯了,我们将按照之前的步骤进行:通过标签的属性将数据从 App 传递到 StationsMap,向 StationsMap 添加 props,并修改我们在上一篇文章中预生成的每个轨迹的相关部分。
<StationsMap stationsData={stationsData} latCircle={latCircle} lonCircle={lonCircle} lonCity={lonCity} latCity={latCity} />现在在 StationsMap.js 中,我们将逐一创建不同的轨迹,按照 Python figure 提供的模板(本系列的第一部分),并在需要时替换相关数据。
我在这里展示两个示例,然后变得有些冗余。
研究周边的圆圈轨迹:
const circleTrace = { fill:"toself", fillcolor:"rgba(1,1,1,0.2)", lat:props.latCircle, lon:props.lonCircle, marker:{"color":"black","size":45}, mode:"lines", opacity:0.8, showlegend:false, type:"scattermapbox", uid: uuidv4() }石油站的黑色边框:
const stationsBorder = { lat: props.stationsData.map((e) => e["latitude"]), lon: props.stationsData.map((e) => e["longitude"]), marker: { color: "black", size: 45 }, mode: "markers", opacity: 0.8, showlegend: false, text: props.stationsData.map((e) => e["price_per_L"].toString() + "€/L"), type: "scattermapbox", uid: uuidv4() };注意:Mapbox 轨迹需要每个轨迹的唯一标识符。为此,我们可以使用一个非常方便的工具包 uuid,它可以为我们生成随机 id。
npm install uuidimport { v4 as uuidv4 } from "uuid";当我们定义好所有轨迹后,我们可以将它们放入一个列表中,该列表将作为我们 plotly 组件的属性。
const data = [circleTrace,stationsBorder,stationsPriceColor, stationIconsTrace,pointLocation];现在我们可以处理图表布局,使用相同的方法:我们获取由 Python figure 暴露的字典,并用我们的状态变量替换“硬编码”的属性,使布局具有互动性。特别是,我们需要修改布局中心的“lat”和“lon”,以确保每次用户发起请求时,地图会自动重新聚焦到该请求的城市。
const { REACT_APP_API_KEY } = process.env; const layout = { mapbox: { accesstoken: REACT_APP_API_KEY, center: { lat: props.latCity, lon: props.lonCity }, style: "streets", zoom: 11, }, margin: { b: 0, l: 0, r: 0, t: 0 }, autosize: true, };此外,注意这里一个非常重要的事项:我们在布局中传递了我们的 Mapbox 访问令牌。这是你不想公开暴露的内容,所以你绝对不要将其硬编码在任何地方,否则其他人可能会将其用于其他目的。
要在项目中添加任何秘密变量,你应该在项目根目录中添加一个 .env 文件:
fuel-station-front/ |-- node_modules/ |-- public/ |-- src/ |-- package.json |-- package-lock.json |-- README.md |-- .env.env 应该被添加到你的 .gitignore 文件中,以防你意外将其推送到公共地方。
在 React 中,你需要遵守前缀 REACT_APP_,这将是框架读取的唯一变量。
在我们的例子中,.env 看起来很简单,如下所示:
REACT_APP_API_KEY=pk.auek...使用我们直接连接到 states 变量的“data”和“layout”,我们可以像在上一篇文章中一样简单地生成我们的 plotly 图表。
return ( <div className="component-embbeder"> <Plot data={data} layout={layout} style={{ height: "100%", width: "100%" }} useResizeHandler={true} /> </div> );让我们来看看网页应用的实际效果!
![]()
API 调用现在可以交互地修改地图和表格,作者插图
改进细节
我们的应用现在满足了主要的“用户故事”:通过一种油的类型和一个邮政编码,我们能够检索并显示周围加油站的价格信息。这已经是一个很大的进步。我们现在希望通过一些改进来进一步提升用户体验,这些改进将在本节中详细介绍。
通过 API 调用启动应用程序
我们当前版本的应用程序在一个毫无标记的地方启动,即在海洋中央。这对于首次到达的用户可能会有点困扰。为了解决这个问题,我们将通过一个 API 调用在巴黎附近为 SP98 启动应用程序。
为此,我们将使用在本文第一部分中描述的 useEffect “hook”。
为此,我们首先在 App.js 中传递 API 调用逻辑:
const ApiCallAndUpdateState = (gasType, postalCode) => { fetch(`${APIURL}/stations?oil_type=${gasType}&postal_code=${postalCode}`) .then((res) => { return res.json(); }) .then((data) => { setStationsData([...data["station_infos"]]); setLonCity(data["lon"]); setLatCity(data["lat"]); setLonCircle([...data["circle_lon"]]); setLatCircle([...data["circle_lat"]]); }); }然后我们将这个函数包装在一个 useEffect hook 中,使用 gasType=”SP98" 和 postalCode = “75001”,这对应于巴黎市中心。
useEffect(() => { ApiCallAndUpdateState("SP98","75001") }, [])由于我在 useEffect 的第二个参数中传递了一个空列表,这段代码将只在组件初始化时执行,这正是我们所期望的。
最后,现在我们将所有更新调用逻辑嵌入到一个函数中,我们可以对 StationsFilter 组件进行一些重构,将这个函数传递而不是所有状态变量的设置函数。
<StationsFilter ApiCallAndUpdateState={ApiCallAndUpdateState} />在 StationsFilter.js 中,我们只需在“HandleButtonClick”回调中触发这个新函数:
const handleButtonClick = () => { props.ApiCallAndUpdateState(gasType, postalCode) };添加一个距离滑块
在应用程序的当前版本中,我们在屏幕上显示了 30 公里范围内的所有加油站,这在低密度区域可能有意义,但在高密度区域可能会非常繁重。
![]()
30 公里范围的搜索……其中显示了太多信息,作者插图
为了解决这个问题,我们将使用一个滑块来控制搜索范围,从 1 公里到 30 公里。借助 React,我们可以访问大量的库和工具,在一些研究之后,我发现一个适合 Gas Finder 应用的滑块组件 这里。我不会在这里详细描述样式,只关注交互部分。
首先安装 react 库:
npm i react-slider我们可以简单地将组件导入到我们的项目中:
import ReactSlider from "react-slider";滑块将控制另一个状态变量(过滤距离)。让我们将其添加到我们的 App.py 中。我将其初始值设置为 5 公里作为起点。
const [distanceFilter, setDistanceFilter] = useState(5);我们将把滑块集成到 StationsFilter 和 StationsTable 之间。
<StationsFilter ApiCallAndUpdateState={ApiCallAndUpdateState} /> <ReactSlider className="horizontal-slider" markClassName="example-mark" thumbClassName="example-thumb" trackClassName="example-track" min={1} max={30} value={distanceFilter} renderThumb={(props, state) => ( <div {...props}>{state.valueNow}</div> )} onAfterChange={(e) => setDistanceFilter(e)} /> <StationsTable stationsData={stationsData} />我不会在这里详细说明 .css,组件接受许多参数,例如带有滑块和轨道的类,最小值和最大值。我根据 文档 的示例修改了这些值。
两点值得注意:
-
至于我们的下拉菜单,我们可以通过状态变量控制滑块的值。
-
我们将通过使用另一个回调函数 onAfterChange 来修改距离过滤器,它将在用户停止移动滑块后触发。
此时,我们的滑块控制距离过滤器,我们现在需要使用它来过滤数据点。为此,我们需要创建另一个数组,它将是 stationsData(从 API 获取的数据)的过滤版本。
const filteredData = stationsData.filter(stationPrice => stationPrice.distance <= distanceFilter);我们现在可以将这个 filteredData 传递给我们的组件,而不是 stationsData,它将根据距离自动更新可见的站点。
还有最后一件事要做:控制显示研究范围周边的圆圈的轨迹。目前,圆圈是基于 API 发送的 30km 距离。
有两种方法可以进行修改:我们可以创建一个新的 API 端点,它会根据纬度/经度/半径向我们发送点数组,并将创建圆形的逻辑保留在后端,特别是如果你更熟悉 Python。这种解决方案有一个巨大的缺点,就是每次用户稍微调整滑块时都会增加 API 调用的次数,我们希望避免这种情况以提高性能。
另一种解决方案是将我们的 Python 函数转置为 JavaScript 中计算圆形的函数,这将在客户端直接计算。出于性能原因,我们将选择第二种方案。
让我们首先创建一个新的 utils 文件夹,位于 StationsMap 中,并包含一个 drawCircle.js 文件:
|-- StationsMap/ |-- StationsMap.js |-- StationsMap.css |-- utils/ |-- drawCircle.js我将精确地转置我们在第一部分中使用的 Python 函数,但这次是在 JavaScript 中:
function calcPointsOnCircle(lat, lon, radius, numPoints) { const points = []; const R = 6371; for (let i = 0; i < numPoints; i++) { const bearing = (360 / numPoints) * i; const lat2 = Math.asin( Math.sin(toRadians(lat)) * Math.cos(radius / R) + Math.cos(toRadians(lat)) * Math.sin(radius / R) * Math.cos(toRadians(bearing)) ); const lon2 = toRadians(lon) + Math.atan2( Math.sin(toRadians(bearing)) * Math.sin(radius / R) * Math.cos(toRadians(lat)), Math.cos(radius / R) - Math.sin(toRadians(lat)) * Math.sin(lat2) ); points.push([toDegrees(lat2), toDegrees(lon2)]); } points.push(points[0]); return points; } function toRadians(degrees) { return (degrees * Math.PI) / 180; } function toDegrees(radians) { return (radians * 180) / Math.PI; }我不会详细说明那个公式,它是一个基本的数学操作,与本文无关。主要函数 calcPointsOnCircle 接受表示圆心的纬度/经度、半径和所需的点数。它返回一个由 (lat, lon) 元组组成的数组。
尽管如此,如果你是 JavaScript 新手,一个重要的点是,为了使你的函数可供其他文件使用,你需要在文件末尾导出它。
export { calcPointsOnCircle };现在这个函数可以直接在 StationsMap 中使用,以根据当前状态变量(纬度/经度和选择的距离)互动计算圆形。让我们看看代码的修改。
import { calcPointsOnCircle } from "./utils/drawCircle"; ... export default function StationsMap(props) { // We use our function to create an array of points based on the state variables const pointsOnCircle = calcPointsOnCircle( props.latCity, props.lonCity, props.distanceFilter, 100 ); // We use map() to extract all the lat and all the lon and pass them to the // circleTrace const circleTrace = { fill: "toself", fillcolor: "rgba(1,1,1,0.2)", lat: pointsOnCircle.map((e) => e[0]), lon: pointsOnCircle.map((e) => e[1]), marker: { color: "black", size: 45 }, mode: "lines", opacity: 0.8, showlegend: false, type: "scattermapbox", uid: uuidv4(), }; ...本节到此为止。到目前为止,我们有一个交互式滑块,它根据距离的过滤器控制屏幕上显示的数据。
![]()
滑块的操作,作者插图
保持当前的缩放级别
此时,每次用户操作滑块时,它都会重置 plotly 视图并重新生成布局。问题在于这也重置了缩放,这是我们想要避免的行为,见下图。
![]()
缩放问题,作者插图
解决这个问题的一种方法是通过滑块值来控制缩放,以便它自动适应圆的大小。在我们的例子中,这是一项非常简单的修改,因为我们是逐步增加距离过滤器。因此,最简单的方法是创建一个映射 {当前距离值 -> 所需缩放} 并用它来自动控制缩放级别。在 StationsMap 中:
const mapDistZoom = { 1: 14.0, 2: 13.45, 3: 12.9, ... }; ... const layout = { mapbox: { accesstoken: REACT_APP_API_KEY, center: { lat: props.latCity, lon: props.lonCity }, style: "streets", zoom: mapDistZoom[props.distanceFilter], }, margin: { b: 0, l: 0, r: 0, t: 0 }, autosize: true, };注意:为了进行映射,我简单地迭代寻找了不同值(30,25,20…)下的最佳缩放级别,并对中间值进行了线性插值。
在表格上方添加标题
这是对应用程序的一个小修改,但它为最终用户带来了更多的清晰度。当发出请求时,我们希望标题能够明确显示研究的位置和燃料类型。
API 调用已经提供了城市信息,当点击按钮发起新请求时,燃料类型可以被更新。
我们将很快处理需要修改的内容,因为这主要是重新使用我们到目前为止所做的工作,通过从一个组件传递/更新状态变量到另一个组件。
在 App.js 中,我们创建了两个新的状态变量
const [citySearch, setCitySearch] = useState(""); const [gasTypeSearch, setGasTypeSearch] = useState("");然后我们可以更新 ApiCallAndUpdateState 函数,这个函数在用户点击按钮时被调用。
const ApiCallAndUpdateState = (gasType, postalCode) => { fetch(`${APIURL}/stations?oil_type=${gasType}&postal_code=${postalCode}`) .then((res) => { return res.json(); }) .then((data) => { ... setGasTypeSearch(gasType); setCitySearch(data["city"]); }); };我们现在可以在 StationsFilter 和 StationsTable 之间简单地添加一个标题:
<h2 style={{ padding: "1px", margin: "5px", textAlign: "center" }}> {citySearch} - {gasTypeSearch} </h2>这现在导致表格上方出现了一个标题,显示了研究的当前位置,符合要求。
![]()
在表格上方添加标题带来了更多的清晰度,用户插图
地图上的一致颜色
在 plotly 中,当定义热图时,未准确设置上下限将默认将比例拉伸到人口的最小值和最大值。
这有一个主要的缺点:如果某个站点的价格远低于或远高于平均值,则颜色比例将不再具有相关意义。
另一个可能遇到的问题是,站点的颜色会根据用于过滤的距离而变化,因为站点的数量会改变,最大/最小价格也会改变,因此最终,比例也会变化。
为了避免这个问题,我们需要决定一个最小值和最大值。鉴于使用的发散颜色图,我们还希望这些范围是对称的。这个问题有很多可能的解决方案,我个人决定使用“30 公里半径内的价格平均值 +/- 10%”,假设高于或低于这个平均值 10%的价格无论如何都太高或太低。
要计算平均值,我们需要取未过滤的数据并计算总和。此操作在 Javascript 中使用 reduce 方法完成:
const sumPrice = stationsData.reduce( (total, value) => total + value["price_per_L"], 0 ); const avgPrice = sumPrice / stationsData.length;请注意,sumPrice 和 avgPrice 不需要作为状态变量:它们将始终在更新新的 stationsData 时计算。
我们现在可以像往常一样通过 props 将 avgPrice 传递给 StationsMap:
<StationsMap ... avgPrice={avgPrice} />并通过对价格进行阈值处理来更新图表,以使其高于或低于定义的边界。
const COLOR_PRICE_THRESHOLD = 0.1 const stationsPriceColor = { marker: { color: props.stationsData.map((e) => { var price = e["price_per_L"]; if (price > props.avgPrice * (1+COLOR_PRICE_THRESHOLD)) { price = props.avgPrice * (1+COLOR_PRICE_THRESHOLD); } if (price < props.avgPrice * (1-COLOR_PRICE_THRESHOLD)) { price = props.avgPrice * (1-COLOR_PRICE_THRESHOLD); } return price; }), ... }, ..., };下一张图展示了标准边界和我们自定义边界之间的区别,这带来了更多对比度:
![]()
设置上下边界会增加颜色对比度,作者插图
清理图表中的悬停信息
需要修复的小细节,plotly 的默认悬停需要一点自定义。目前有两个主要问题:
-
当用户检查一个站点时触发的悬停事件默认也显示 lat/lon,这不是我们想看到的(我们只想看到价格)
-
当鼠标经过黑色轨迹或红点时,也会显示坐标,我们希望移除这种交互
对于第一点,我们可以简单地传递一个hovertemplate 参数,该参数控制鼠标悬停时显示的内容。下面的公式仅在“text”属性中包含该参数,
移除默认的框。 hovertemplate: '%{text}<extra></extra>'对于第二点,更简单,我们只需传递一个参数
hoverinfo: "skip"这将完全移除轨迹的悬停交互。
处理 API 错误
在本文中,我们还将探索最后一件事:处理 API 错误。如果用户尝试一个不在数据库中的邮政编码,API 将返回自定义错误 400。当发生这种情况时,我们希望修改文本字段以使用户理解其输入存在问题,并用红色突出显示输入字段。
让我们开始在 App.js 中添加一个新的状态变量
const [apiError, setApiError] = useState(false);现在我们可以修改我们的 fetch 方法,以便在发生异常时执行某些操作。
第一步是如果我们没有收到 API 的正确响应,则抛出一个错误。
fetch(`${APIURL}/stations?oil_type=${gasType}&postal_code=${postalCode}`) .then((res) => { if (!res.ok) { throw new Error("Problem with the API..."); } return res.json(); }) .then((data) => {...})之后,我们需要在处理管道的末尾捕捉此错误,并将 setApiError 设置为“true”。我们还需要确保在请求成功后 apiError 被设置回“false”:
fetch(`${APIURL}/stations?oil_type=${gasType}&postal_code=${postalCode}`) .then((res) => {...}) .then((data) => { ... setApiError(false); }) .catch((error) => { setApiError(true); });我们可以最终将 apiError 传递给我们的 StationsForm 组件,并修改邮政编码文本字段的样式。实现这一点的一种方法是,当 apiError 为“true”时向文本字段添加额外的类……
<input className={"general-input" + (props.apiError ? " input-error" : "")} type="text" placeholder="Postal Code" onChange={handleChangeTextArea} value={postalCode} />… 并为该 input-error 类添加额外的 CSS
.input-error { border: solid 2px red; }![]()
当使用错误的邮政编码时,文本字段会更改为红色边框,作者插图
结论
这个最后的实现完全结束了专门用于开发 React 网络应用程序 UI 的章节。
React 在数据科学家/数据分析师社区中越来越受欢迎,尽管学习这个框架需要投入大量时间,但通过这个例子我们看到,一旦掌握,它可以让我们构建出稳健、强大且响应迅速的应用程序。
UI 完成后,我们现在已经完成了原型阶段的一半。在下一篇文章中,我们将讨论服务器端,并准备我们的 API 和数据库。
逐步指南:稳健的机器学习分类
原文:
towardsdatascience.com/a-step-by-step-guide-to-robust-ml-classification-5ce83592eb1d如何避免常见的陷阱并深入探讨我们的模型
·发表于 Towards Data Science ·阅读时间 17 分钟·2023 年 3 月 3 日
--
![]()
图片由 Luca Bravo 提供,来源于 Unsplash
在之前的文章中,我主要介绍了我认为有趣的单一算法。在这里,我将介绍一个完整的机器学习分类项目。目标是讨论一些机器学习项目中的常见陷阱,并向读者描述如何避免这些陷阱。我还将演示如何通过分析模型错误进一步挖掘,以获得通常未被发现的重要见解。
如果你想查看完整的笔记本,请点击这里 → 这里 ←
库
下面是我今天分析中使用的库的列表。它们包括标准的数据科学工具包以及所需的 sklearn 库。
import sys import os import pandas as pd import numpy as np import matplotlib.pyplot as plt import seaborn as sns from IPython.display import display %matplotlib inline import plotly.offline as py import plotly.graph_objs as go import plotly.tools as tls py.init_notebook_mode(connected=True) import warnings warnings.filterwarnings('ignore') from pandas import set_option from pandas.plotting import scatter_matrix from sklearn.preprocessing import StandardScaler, MinMaxScaler, QuantileTransformer, RobustScaler from sklearn.model_selection import train_test_split, KFold, StratifiedKFold, cross_val_score, GridSearchCV from sklearn.feature_selection import RFECV, SelectFromModel, SelectKBest, f_classif from sklearn.metrics import classification_report, confusion_matrix, balanced_accuracy_score, ConfusionMatrixDisplay, f1_score from sklearn.pipeline import Pipeline from sklearn.neighbors import KNeighborsClassifier from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier, VotingClassifier from scipy.stats import uniform from imblearn.over_sampling import ADASYN import swifter # Always good to set a seed for reproducibility SEED = 8 np.random.seed(SEED)数据
今天的数据集包括可以直接用在 sklearn 中的森林覆盖数据。以下是来自 sklearn 网站的描述。
数据集特点:
该数据集中的样本对应于美国 30×30 米的森林区域,收集用于预测每个区域的覆盖类型,即主要树种。共有七种覆盖类型,这使得这是一个多类别分类问题。每个样本有 54 个特征,特征在数据集主页上有描述。部分特征为布尔指标,其他则为离散或连续测量值。
实例数量:581 012
特征信息(名称 / 数据类型 / 测量 / 描述**)
-
高度 / 定量 / 米 / 以米为单位的高度
-
方面 / 定量 / 方位角 / 以度为单位的方位角
-
坡度 / 定量 / 度 / 以度为单位的坡度
-
Horizontal_Distance_To_Hydrology / 定量 / 米 / 到最近水体的水平距离
-
Vertical_Distance_To_Hydrology / 定量 / 米 / 到最近水体的垂直距离
-
Horizontal_Distance_To_Roadways / 定量 / 米 / 到最近道路的水平距离
-
Hillshade_9am / 定量 / 0 到 255 指数 / 早上 9 点的阴影指数,夏至
-
Hillshade_Noon / 定量 / 0 到 255 指数 / 中午的阴影指数,夏至
-
Hillshade_3pm / 定量 / 0 到 255 指数 / 下午 3 点的阴影指数,夏至
-
Horizontal_Distance_To_Fire_Points / 定量 / 米 / 到最近火灾点的水平距离
-
Wilderness_Area (4 个二元列) / 定性 / 0(缺失)或 1(存在) / 荒野区域指定
-
Soil_Type (40 个二元列) / 定性 / 0(缺失)或 1(存在) / 土壤类型指定
类别数:
- Cover_Type (7 种类型) / 整数 / 1 到 7 / 森林覆盖类型指定
加载数据集
这里有一个简单的函数可以将这些数据加载到你的笔记本中作为数据框。
columns = ['Elevation', 'Aspect', 'Slope', 'Horizontal_Distance_To_Hydrology', 'Vertical_Distance_To_Hydrology', 'Horizontal_Distance_To_Roadways', 'Hillshade_9am', 'Hillshade_Noon', 'Hillshade_3pm', 'Horizontal_Distance_To_Fire_Points', 'Wilderness_Area_0', 'Wilderness_Area_1', 'Wilderness_Area_2', 'Wilderness_Area_3', 'Soil_Type_0', 'Soil_Type_1', 'Soil_Type_2', 'Soil_Type_3', 'Soil_Type_4', 'Soil_Type_5', 'Soil_Type_6', 'Soil_Type_7', 'Soil_Type_8', 'Soil_Type_9', 'Soil_Type_10', 'Soil_Type_11', 'Soil_Type_12', 'Soil_Type_13', 'Soil_Type_14', 'Soil_Type_15', 'Soil_Type_16', 'Soil_Type_17', 'Soil_Type_18', 'Soil_Type_19', 'Soil_Type_20', 'Soil_Type_21', 'Soil_Type_22', 'Soil_Type_23', 'Soil_Type_24', 'Soil_Type_25', 'Soil_Type_26', 'Soil_Type_27', 'Soil_Type_28', 'Soil_Type_29', 'Soil_Type_30', 'Soil_Type_31', 'Soil_Type_32', 'Soil_Type_33', 'Soil_Type_34', 'Soil_Type_35', 'Soil_Type_36', 'Soil_Type_37', 'Soil_Type_38', 'Soil_Type_39'] from sklearn import datasets def sklearn_to_df(sklearn_dataset): df = pd.DataFrame(sklearn_dataset.data, columns=columns) df['target'] = pd.Series(sklearn_dataset.target) return df df = sklearn_to_df(datasets.fetch_covtype()) df_name=df.columns df.head(3)使用 df.info() 和 df.describe() 来更好地了解我们的数据,我们发现没有缺失数据,并且它由定量变量组成。数据集也相当大(> 580,000 行)。我最初尝试在整个数据集上运行这个,但花费了很长时间,所以我建议使用数据的一个部分。
关于目标变量,即森林覆盖类别,使用 df.target.value_counts(),我们看到以下分布(按降序排列):
**类别 2 = 283,301
类别 1 = 211,840
类别 3 = 35,754
类别 7 = 20,510
类别 6 = 17,367
类别 5 = 9,493
类别 4 = 2,747
重要的是要注意我们的类别是不平衡的,我们在选择评估模型的指标时需要牢记这一点。
准备你的数据
运行机器学习模型时最常见的误解之一是对数据进行处理而不是拆分。为什么这是一个问题?
假设我们计划使用整个数据集来缩放我们的数据。以下方程来自它们各自的链接。
Ex1 StandardScaler()
z = (x — u) / s
Ex2 MinMaxScaler()
X_std = (X - X.min()) / (X.max() - X.min())
X_scaled = X_std * (max - min) + min
我们应该注意到的最重要的一点是,它们包括了均值、标准差、最小值和最大值等信息。如果我们在拆分之前执行这些函数,训练集中的特征将基于测试集中的信息进行计算。这是一个数据泄露的例子。
数据泄漏是指使用来自训练数据集外的信息来创建模型。这些额外的信息可能使模型学习或知道一些它本来不会知道的东西,从而使正在构建的模型的估计性能无效。
因此,在了解数据集后,第一步是将其拆分,并保持测试集 未见 直到最后。在下面的代码中,我们将数据拆分为 80%(训练集)和 20%(测试集)。你还会注意到,我只保留了 50,000 个样本,以减少训练和评估模型所需的时间。相信我,你会感谢我的!
还值得注意的是,我们在目标变量上进行了分层。这对不平衡的数据集是一种良好的实践,因为它保持了训练集和测试集中类别的分布。如果我们不这样做,可能会有一些不足代表的类别在训练集或测试集中根本不存在。
# here we are first separating our df into features (X) and target (y) X = df[df_name[0:54]] Y = df[df_name[54]] # now we are separating into training (80%) and test (20%) sets. The test set won't be seen until we want to test our top model! X_train, X_test, y_train, y_test =train_test_split(X,Y, train_size = 40_000, test_size=10_000, random_state=SEED, stratify=df['target']) # we stratify to ensure similar distribution in train/test特征工程
当我们的训练集和测试集准备好之后,我们可以开始处理有趣的内容。这个项目的第一步是生成一些可能为训练我们的模型提供有用信息的特征。
这一步可能有些棘手。在现实世界中,这需要对你所处理的特定领域有专业的知识。完全透明地告诉你,尽管我热爱自然和户外活动,但我对某些树木为什么在特定地区生长并不精通。
出于这个原因,我咨询了 [1] [2] [3],他们对这个领域的理解比我更深入。我结合了这些参考资料中的知识,创建了下面的特征。
# engineering new columns from our df def FeatureEngineering(X): X['Aspect'] = X['Aspect'] % 360 X['Aspect_120'] = (X['Aspect'] + 120) % 360 X['Hydro_Elevation_sum'] = X['Elevation'] + X['Vertical_Distance_To_Hydrology'] X['Hydro_Elevation_diff'] = abs(X['Elevation'] - X['Vertical_Distance_To_Hydrology']) X['Hydro_Euclidean'] = np.sqrt(X['Horizontal_Distance_To_Hydrology']**2 + X['Vertical_Distance_To_Hydrology']**2) X['Hydro_Manhattan'] = abs(X['Horizontal_Distance_To_Hydrology'] + X['Vertical_Distance_To_Hydrology']) X['Hydro_Distance_sum'] = X['Horizontal_Distance_To_Hydrology'] + X['Vertical_Distance_To_Hydrology'] X['Hydro_Distance_diff'] = abs(X['Horizontal_Distance_To_Hydrology'] - X['Vertical_Distance_To_Hydrology']) X['Hydro_Fire_sum'] = X['Horizontal_Distance_To_Hydrology'] + X['Horizontal_Distance_To_Fire_Points'] X['Hydro_Fire_diff'] = abs(X['Horizontal_Distance_To_Hydrology'] + X['Horizontal_Distance_To_Fire_Points']) X['Hydro_Fire_mean'] = (X['Horizontal_Distance_To_Hydrology'] + X['Horizontal_Distance_To_Fire_Points'])/2 X['Hydro_Road_sum'] = X['Horizontal_Distance_To_Hydrology'] + X['Horizontal_Distance_To_Roadways'] X['Hydro_Road_diff'] = abs(X['Horizontal_Distance_To_Hydrology'] + X['Horizontal_Distance_To_Roadways']) X['Hydro_Road_mean'] = (X['Horizontal_Distance_To_Hydrology'] + X['Horizontal_Distance_To_Roadways'])/2 X['Road_Fire_sum'] = X['Horizontal_Distance_To_Roadways'] + X['Horizontal_Distance_To_Fire_Points'] X['Road_Fire_diff'] = abs(X['Horizontal_Distance_To_Roadways'] - X['Horizontal_Distance_To_Fire_Points']) X['Road_Fire_mean'] = (X['Horizontal_Distance_To_Roadways'] + X['Horizontal_Distance_To_Fire_Points'])/2 X['Hydro_Road_Fire_mean'] = (X['Horizontal_Distance_To_Hydrology'] + X['Horizontal_Distance_To_Roadways'] + X['Horizontal_Distance_To_Fire_Points'])/3 return X X_train = X_train.swifter.apply(FeatureEngineering, axis = 1) X_test = X_test.swifter.apply(FeatureEngineering, axis = 1)另外,当你处理大数据集时,pandas 可能会有些慢。使用 swifter,正如上面最后两行所示,你可以显著加快对数据框应用函数的时间。文章 → 这里 比较了几种加速此过程的方法。
特征选择
目前我们有超过 70 个特征。如果目标是获得表现最佳的模型,那么你可以尝试使用所有这些特征作为输入。话虽如此,商业中常常需要考虑性能和复杂性之间的权衡。
举个例子,假设我们使用所有这些特征时模型的准确率为 94%。然后,假设只有 4 个特征时准确率为 89%。我们愿意为一个更具可解释性的模型支付什么样的代价。始终权衡性能和复杂性。
记住这一点,我将立即执行特征选择,以尽量减少复杂性。Sklearn提供了许多值得考虑的选项。在这个例子中,我将使用SelectKBest,它将选择预先指定数量的特征,这些特征提供最佳性能。下面,我请求(并列出了)表现最佳的 15 个特征。这些特征将用于在下一部分中训练模型。
selector = SelectKBest(f_classif, k=15) selector.fit(X_train, y_train) mask = selector.get_support() X_train_reduced_cols = X_train.columns[mask] X_train_reduced_cols >>> Index(['Elevation', 'Wilderness_Area_3', 'Soil_Type_2', 'Soil_Type_3', 'Soil_Type_9', 'Soil_Type_37', 'Soil_Type_38', 'Hydro_Elevation_sum', 'Hydro_Elevation_diff', 'Hydro_Road_sum', 'Hydro_Road_diff', 'Hydro_Road_mean', 'Road_Fire_sum', 'Road_Fire_mean', 'Hydro_Road_Fire_mean'], dtype='object')基线模型
在这一部分,我将比较三种不同的分类器:
我提供了链接以便那些希望进一步研究每个模型的人。这些链接在超参数调优部分也会很有帮助,你可以在其中找到所有可以调整的参数,以改善你的模型。下面你会发现两个函数,用于定义和评估基线模型。
# baseline models def GetBaseModels(): baseModels = [] baseModels.append(('KNN' , KNeighborsClassifier())) baseModels.append(('RF' , RandomForestClassifier())) baseModels.append(('ET' , ExtraTreesClassifier())) return baseModelsdef ModelEvaluation(X_train, y_train,models): # define number of folds and evaluation metric num_folds = 10 scoring = "f1_weighted" #This is suitable for imbalanced classes results = [] names = [] for name, model in models: kfold = StratifiedKFold(n_splits=num_folds, random_state=SEED, shuffle = True) cv_results = cross_val_score(model, X_train, y_train, cv=kfold, scoring=scoring, n_jobs = -1) results.append(cv_results) names.append(name) msg = "%s: %f (%f)" % (name, cv_results.mean(), cv_results.std()) print(msg) return names, results第二个函数中有一些关键元素值得进一步讨论。其中第一个是StratifiedKFold。请回忆一下,我们将原始数据集拆分为 80%的训练集和 20%的测试集。测试集将保留用于最终评估我们表现最好的模型。
使用交叉验证将为我们提供更好的模型评估。具体来说,我设置了一个 10 折交叉验证。对于那些不熟悉的人,该模型在每一步中都在 k — 1 个折上进行训练,并在剩下的折上进行验证。最终,你将能够访问到 k 个模型的平均值和变异性,这比简单的训练-测试评估提供了更好的见解。正如我之前提到的,分层 K 折用于确保每个折中目标类的表示大致相等。
第二点值得讨论的是评分指标。有许多指标可用于评估模型的性能,通常有几个适合你的项目。重要的是要记住你试图通过结果展示什么。如果你在商业环境中工作,通常选择最容易向没有数据背景的人解释的指标。
另一方面,有些指标不适合你的分析。对于这个项目,我们有不平衡的类别。如果你访问上面提供的链接,你会发现适用于这种情况的选项。我选择使用加权 F1 分数。让我们简要讨论一下为什么我选择了这个指标。
一个非常常见的分类指标是准确率,它是正确分类的百分比。虽然这可能看起来是一个很好的选项,但假设我们有一个目标类不均衡的二分类问题(即组 1 = 90,组 2 = 10)。我们可能会得到 90% 的准确率,这听起来很棒,但如果我们进一步探讨,我们发现正确分类了所有组 1 的样本,却未能分类组 2 的任何样本。在这种情况下,我们的模型信息量并不大。
如果我们使用加权 F1 分数,我们会得到 42.6% 的结果。如果你有兴趣了解更多关于 F1 分数的信息 → 这里 有一篇文章解释了它是如何计算的。
在训练基准模型后,我已绘制了每个模型的结果。所有基准模型的表现都相对良好。请记住,此时我对数据没有做任何处理(即未进行转换或去除异常值)。额外树分类器的加权 F1 分数最高,为 86.9%。
![]()
10 倍交叉验证的结果如下。KNN 的表现最低,为 78.8%,RF 排在第二位,为 85.9%,而 ET 的加权 F1 分数最高,为 86.9%。图片由作者提供
数据转换
这个项目的下一步将研究数据转换对模型性能的影响。虽然许多基于决策树的算法对数据的量纲不敏感,但可以合理预期,测量样本之间距离的模型,例如 KNN,当数据缩放时性能会有所不同 [4] [5]。在本节中,我们将使用上述描述的 StandardScaler 和 MinMaxScaler 对数据进行缩放。下面是一个函数,它描述了一个管道,该管道将应用缩放器,然后使用缩放后的数据训练模型。
def GetScaledModel(nameOfScaler): if nameOfScaler == 'standard': scaler = StandardScaler() elif nameOfScaler =='minmax': scaler = MinMaxScaler() pipelines = [] pipelines.append((nameOfScaler+'KNN' , Pipeline([('Scaler', scaler),('KNN' , KNeighborsClassifier())]))) pipelines.append((nameOfScaler+'RF' , Pipeline([('Scaler', scaler),('RF' , RandomForestClassifier())]))) pipelines.append((nameOfScaler+'ET' , Pipeline([('Scaler', scaler),('ET' , ExtraTreesClassifier())]))) return pipelines使用 StandardScaler 的结果如下。我们看到有关数据缩放的假设似乎成立。随机森林和额外树分类器的表现几乎相同,而 KNN 的性能提高了大约 4%。尽管有所提升,但这两个基于树的分类器仍然优于缩放后的 KNN。
![]()
使用 StandardScaler 对数据进行 10 折交叉验证的结果。尽管表现有所提升至 83.8%,KNN 的表现仍然最低。RF 排名第二,为 85.8%,ET 再次以 86.8%的加权 F1 分数名列最高。图像由作者提供
使用 MinMaxScaler 时可以看到类似的结果。所有模型的结果几乎与使用 StandardScaler 时所呈现的结果完全相同。
![]()
使用 MinMaxScaler 对数据进行 10 折交叉验证的结果。每个模型的表现几乎与使用 StandardScaler 时相同。KNN 的表现仍然最低,为 83.9%。RF 排名第二,为 86.0%,ET 再次以 87.0%的加权 F1 分数名列最高。图像由作者提供
值得注意的是,我还检查了去除异常值的效果。为此,我移除了每个特征值超出±3 个标准差的值。我在这里没有展示结果,因为没有值超出这个范围。如果你有兴趣了解如何执行此操作,请随时查看本文开头提供的笔记本链接。
使用 GridSearchCV 进行超参数调整
下一步是通过调整超参数来尝试改进我们的模型。我们将在缩放后的数据上进行,因为它在考虑我们的三种模型时表现最佳。Sklearn 对此进行了更详细的讨论 → 这里。
我选择使用GridSearchCV(CV 表示交叉验证)。下面你将找到一个对我们一直使用的模型进行 10 折交叉验证的函数。这里唯一的额外细节是我们需要提供希望评估的超参数列表。
到目前为止,我们甚至没有查看我们的测试集。在开始网格搜索之前,我们将使用 StandardScaler 对训练数据和测试数据进行缩放。我们在这里这样做是因为我们要找到每个模型的最佳超参数,并将这些超参数作为输入到 VotingClassifier 中(我们将在下一节讨论)。
为了正确缩放我们的完整数据集,我们必须按照以下程序进行。你将看到缩放器仅在训练数据上进行拟合。训练集和测试集都基于训练集找到的缩放参数进行转换,从而消除任何数据泄漏的可能性。
scaler = StandardScaler() X_train_scaled = pd.DataFrame(scaler.fit_transform(X_train_reduced), columns=X_train_reduced.columns) X_test_scaled = pd.DataFrame(scaler.transform(X_test_reduced), columns=X_test_reduced.columns)class GridSearch(object): def __init__(self,X_train,y_train,model,hyperparameters): self.X_train = X_train self.y_train = y_train self.model = model self.hyperparameters = hyperparameters def GridSearch(self): cv = 10 clf = GridSearchCV(self.model, self.hyperparameters, cv=cv, verbose=0, n_jobs=-1, ) # fit grid search best_model = clf.fit(self.X_train, self.y_train) message = (best_model.best_score_, best_model.best_params_) print("Best: %f using %s" % (message)) return best_model,best_model.best_params_ def BestModelPredict(self,X_train): best_model,_ = self.GridSearch() pred = best_model.predict(X_train) return pred接下来,我提供了对每个模型测试的网格搜索参数。
# 1) KNN model_KNN = KNeighborsClassifier() neighbors = [1,3,5,7,9,11,13,15,17,19] # Number of neighbors to use by default for k_neighbors queries param_grid = dict(n_neighbors=neighbors) # 2) RF model_RF = RandomForestClassifier() n_estimators_value = [50,100,150,200,250,300] # The number of trees criterion = ['gini', 'entropy', 'log_loss'] # The function to measure the quality of a split param_grid = dict(n_estimators=n_estimators_value, criterion=criterion) # 3) ET model_ET = ExtraTreesClassifier() n_estimators_value = [50,100,150,200,250,300] # The number of trees criterion = ['gini', 'entropy', 'log_loss'] # The function to measure the quality of a split param_grid = dict(n_estimators=n_estimators_value, criterion=criterion)投票集成分类器
我们已经确定了优化我们模型的最佳参数组合。这些参数将作为输入用于一个投票分类器,这是一个集成估计器,它训练几个模型,然后汇总结果以获得更稳健的预测。我找到了一篇→ 文章,提供了关于投票分类器及其使用方式的详细概述。
每个模型的最佳参数列在下面。投票分类器的输出显示,我们在训练集上达到了 87.5% 的加权 F1 分数,在测试集上达到了 88.4%。还不错!
param = {'n_neighbors': 1} model1 = KNeighborsClassifier(**param) param = {'criterion': 'entropy', 'n_estimators': 300} model2 = RandomForestClassifier(**param) param = {'criterion': 'gini', 'n_estimators': 300} model3 = ExtraTreesClassifier(**param) # create the models based on above parameters estimators = [('KNN',model1), ('RF',model2), ('ET',model3)] # create the ensemble model kfold = StratifiedKFold(n_splits=10, random_state=SEED, shuffle = True) ensemble = VotingClassifier(estimators) results = cross_val_score(ensemble, X_train_scaled, y_train, cv=kfold) print('F1 weighted score on train: ',results.mean()) ensemble_model = ensemble.fit(X_train_scaled,y_train) pred = ensemble_model.predict(X_test_scaled) print('F1 weighted score on test:' , (y_test == pred).mean()) >>> F1 weighted score on train: 0.8747 >>> F1 weighted score on test: 0.8836错误分析
我们模型的表现相当不错。尽管如此,调查模型失败的地方可能非常有见地。下面,你将找到生成混淆矩阵的代码。让我们看看是否能学到一些东西。
from sklearn.metrics import plot_confusion_matrix cfm_raw = plot_confusion_matrix(ensemble_model, X_test_scaled, y_test, values_format = '') # add normalize = 'true' for precision matrix or 'pred' for recall matrix plt.savefig("cfm_raw.png")![]()
测试集上的混淆矩阵。图片由作者提供
立刻可以看出,代表性不足的类别学习得不是很好。这非常重要,因为尽管使用了适合评估不平衡类别的指标,你还是无法让模型学习那些不存在的东西。
为了分析我们的错误,我们可以创建可视化;然而,考虑到有 15 个特征和 7 个类别,这可能会开始感觉像那种你盯着看直到图像形成的迷幻立体图像。另一种方法如下。
机器学习错误分类
在本节中,我将比较预测值与测试集中的实际值,并创建一个新变量,‘error’。下面,我正在设置一个数据集,用于二分类分析,目标是错误与非错误,使用与上述相同的特征。
既然我们已经知道代表性不足的类别没有很好地学习,那么这里的目标是查看哪些特征与错误的关联最为明显,而不考虑类别。
# add predicted values test_df to compare with ground truth test_df['predicted'] = pred # create class 0 = no error , 1 = error test_df['error'] = (test_df['target']!=test_df['predicted']).astype(int) # create our error classification set X_error = test_df[['Elevation', 'Wilderness_Area_3', 'Soil_Type_2', 'Soil_Type_3', 'Soil_Type_9', 'Soil_Type_37', 'Soil_Type_38', 'Hydro_Elevation_sum', 'Hydro_Elevation_diff', 'Hydro_Road_sum', 'Hydro_Road_diff', 'Hydro_Road_mean', 'Road_Fire_sum', 'Road_Fire_mean', 'Hydro_Road_Fire_mean']] X_error_names = X_error.columns y_error = test_df['error']使用我们的新数据集,下一步是构建一个分类模型。这一次我们将添加一个步骤,使用SHAP。这将使我们能够理解每个特征如何影响模型,而在我们的案例中,这个模型是错误。
在下文中,我们使用了随机森林来拟合数据。我们再次使用 K 折交叉验证来更好地估计每个特征的贡献。最后,我生成了一个数据框,其中包含平均值、标准差和最大 SHAP 值。
import shap kfold = StratifiedKFold(n_splits=10, random_state=SEED, shuffle = True) list_shap_values = list() list_test_sets = list() for train_index, test_index in kfold.split(X_error, y_error): X_error_train, X_error_test = X_error.iloc[train_index], X_error.iloc[test_index] y_error_train, y_error_test = y_error.iloc[train_index], y_error.iloc[test_index] X_error_train = pd.DataFrame(X_error_train,columns=X_error_names) X_error_test = pd.DataFrame(X_error_test,columns=X_error_names) #training model clf = RandomForestClassifier(criterion = 'entropy', n_estimators = 300, random_state=SEED) clf.fit(X_error_train, y_error_train) #explaining model explainer = shap.TreeExplainer(clf) shap_values = explainer.shap_values(X_error_test) #for each iteration we save the test_set index and the shap_values list_shap_values.append(shap_values) # flatten list of lists, pick the sv for 1 class, stack the result (you only need to look at 1 class for binary classification since values will be opposite to one another) shap_values_av = np.vstack([sv[1] for sv in list_shap_values]) sv = np.abs(shap_values_av).mean(0) sv_std = np.abs(shap_values_av).std(0) sv_max = np.abs(shap_values_av).max(0) importance_df = pd.DataFrame({ "column_name": X_error_names, "shap_values_av": sv, "shap_values_std": sv_std, "shap_values_max": sv_max })为了更好的视觉体验,下面是一个 SHAP 总结图。左侧是特征名称。该图展示了每个特征对模型的影响,针对不同特征值的影响。虽然分散度(向右或向左的距离)描述了特征对模型的整体影响,着色则为我们提供了额外的信息。
![]()
错误分类模型的 SHAP 总结图。图像由作者提供
我们首先注意到,对模型影响最大的特征更多地与距离特征(即水源、道路或火源点)相关,而不是森林类型(荒野区域)或土壤类型。
接下来,当我们查看颜色分布时,我们可以看到第一个特征 Hydro_Road_Fire_mean 在高值与低值之间的差异比其他特征更明显。对于 Road_Fire_mean 也是如此,尽管程度较轻。
为了解释这意味着什么,我们可以提出这样的说法:当到水源、火源点和道路的平均距离较低时,更容易出现错误。
再次强调,我的林业‘专业知识’仅限于几周。我确实做了一些研究来帮助我解读这可能意味着什么,并发现了一些文章 [6] [7],这些文章表明,道路的距离是森林火灾风险的一个重要因素。
这让我推测森林火灾可能是影响我们数据集上的错误的重要因素。对我来说,被火灾影响的区域与未受影响的区域在森林多样性上的表现差异很大是合乎逻辑的。我相信有更多经验的人可以告诉我这是否有道理 😃
总结
今天,我们详细讲解了一个逐步的机器学习多分类问题。我们讨论了进行这些分析时的一些重要考虑因素,即在开始处理数据集之前分割数据集的重要性。这是机器学习项目中最常见的陷阱之一,可能导致严重问题,限制了我们推广发现的能力。
我们还讨论了选择适当的评估指标以评估我们模型的重要性。在这里,我们使用了加权 F1 分数,这适用于不平衡的类别。尽管如此,我们仍然看到被低估的类别没有得到很好的学习。
在我的笔记本中,我还包含了一个关于过采样的部分,使用了ADASYN来创建平衡的类别,它是SMOTE的一种变体。为了让你们不再悬念,过采样显著改善了训练集的结果(但没有改善测试集的结果)。
这引出了错误分析,这是任何机器学习项目中的重要部分。进行了二分类错误分析,结果可能表明森林火灾在许多模型错误中起到了作用。这也在一定程度上解释了为什么过采样没有改善我们的最终模型。
最后,感谢大家抽时间阅读这篇文章!我希望你们中的一些人觉得它有帮助 😃
BigQuery 中的行和列访问策略逐步指南
Google 的数据仓库提供了令人惊叹的方式来限制对表格和视图中信息的访问,无论是在垂直还是水平维度上,让我们来探索如何设置这些限制。
·发表于 Towards Data Science ·7 分钟阅读·2023 年 1 月 19 日
--
![]()
图片来自 Martin Olsen 在 Unsplash
管理数据仓库意味着多个用户,具有不同的角色和权限,应该能够查询他们所需的数据。
你可能会决定为每个角色构建一个特定的表格,但这将会消耗时间和资源,更不用说随着时间推移维护这个系统的困难了。
在本教程中,我们将创建一个“EMPLOYEES”表,包含各种级别的信息,从员工所在的“国家”... 到他/她的工资... 并考虑 3 个角色:
![]()
3 个角色的示意图 — 作者通过 www.freepik.com 创建的图像
-
人力资源:他们应该能够访问所有数据,以进行日常工作,没有任何限制。
-
数据科学家:他们可能只会访问一些为建模目的而选择的和匿名化的列。
-
数据公民:大多数列应该对他们隐藏,只允许创建高层次的查询和分析。
在 Google Cloud Platform 中创建一个新项目
我将假设你已经有了一个 GCP 帐户。如果没有,你可以轻松注册一个,并获得 30 天的 $300 额度。如果你已经有了,这个示例几乎不会花费你任何费用。
你应该从创建一个新项目开始,以隔离你的工作。
点击左上角的项目选择器,然后选择“新建项目”。
确保你可以在组织(如你的公司)下创建项目,否则你将无法执行一些策略。
![]()
![]()
在 GCP 中创建一个与组织关联的新项目 — 图片来源于作者
注意:这里创建的项目是“row-columns-policies”。
创建一个虚假的员工信息表
![]()
“salaries.csv” 内容显示在 BigQuery 中 — 图片来源于作者
我已经创建并上传了一个虚假的表格,您可以在我的 GoogleDrive 上查看,其中包含每 8600 名员工的信息:
-
名
-
姓
-
部门
-
国家
-
电子邮件
-
薪资
-
货币
-
薪资定位
-
最后绩效评估
-
资历
-
通勤时间
让我们跳转到“BigQuery”,点击“添加数据”和“本地存储”,并牢记两个重要点:
-
表必须存储在数据集中:我们将创建一个名为“EMPLOYEES”的数据集(见第二张截图)。您应注意选择的区域:定义策略时必须使用相同的区域。
-
由于 CSV 文件的结构简单,我们可以选择“自动检测”方案选项,这样就无需逐一声明每列的类型。
![]()
在 BigQuery 中创建新表 — 图片来源于作者
![]()
在 BigQuery 中创建新数据集 — 图片来源于作者
我们的“薪资”表现在已正确构建(见第一个截图),简单的 SQL 查询(见第二个截图)将揭示相应的数据:
![]()
BigQuery 中的 EMPLOYEES 表结构 — 图片来源于作者
SELECT * FROM `row-columns-policies.EMPLOYEES.SALARIES` ORDER BY Country, Last_Name, First_Name LIMIT 30![]()
SQL 探索结果在 BigQuery 中 — 图片来源于作者
创建一个“标签政策”分类以限制访问
从 BigQuery 菜单中,我们切换到“策略标签”(如果被要求,启用“Google Cloud Data Catalog”和/或“BigQuery 数据政策”API):
![]()
点击“创建分类”后,我们定义了三个级别的员工信息:
-
高(关键数据如薪资)
-
中等(如“最后绩效”或“薪资定位”)对于数据科学家在建模任务中可能会有帮助。
-
识别(与员工直接相关的信息)
- 两个子级:
-
识别 > 姓名
-
识别 > 电子邮件
注意:下面的两个子标签(姓名和电子邮件)将允许我们以后使用不同的掩码策略。
![]()
在 BigQuery/Dataplex 中创建新分类 — 图片来源于作者
注意:如前所述,请确保使用与数据集相同的区域。
一旦分类法创建完成,最关键的部分是启用它,如果你的项目不与组织相关,你可能会遇到问题(见 Google 相关文档)。
![]()
在 BigQuery/Dataplex 中强制执行分类法 — 图片来自作者
将标签策略应用于表列
标签策略可以通过 GCP 的“Dataplex”部分应用。搜索引擎将帮助你快速识别“SALARIES”表:
![]()
在 Dataplex 中搜索表 — 图片来自作者
通过“SCHEMA AND COLUMN TAGS”,我们可以轻松地将策略标签分配给一些敏感列:
![]()
在 Dataplex 中应用策略标签 — 图片来自作者
向项目中添加一个“查看者”主体
![]()
3 个角色的示意图 — 作者通过 www.freepik.com 创建的图片
假设一个数据公民想要访问这个项目,并进行关于员工在各个国家分布的高层次分析。
我们返回到项目的 IAM 部分,添加一个“查看者”角色的主体:
![]()
在 Google IAM 中分配“查看者”角色 — 图片来自作者
一旦连接,该主体将立即收到来自 BigQuery 的警告,告知他/她某些列的访问将受到限制:
![]()
使用 BigQuery 中的“查看者”角色查看 SALARIES 表 — 图片来自作者
实际上,由于我们拥有的权限最少,预览部分仅显示 3 列:
![]()
使用 BigQuery 中的“查看者”角色预览 SALARIES 表 — 图片来自作者
但它仍允许我们进行我们想要的分析,并按国家和部门获取员工数量的详细信息:
![]()
使用 BigQuery 中的“查看者”角色对 SALARIES 表进行 SQL 查询 — 图片来自作者
定义“掩码阅读者”角色及其对应的掩码策略
![]()
3 个角色的示意图 — 作者通过 www.freepik.com 创建的图片
现在假设人力资源部门要求数据科学家对员工进行一些统计分析。
她需要访问一些列,但不一定以其原始形式访问。
我们首先将“掩码阅读者”角色分配给这个新主体:
![]()
在 Google IAM 中分配“掩码阅读者”角色 — 图片来自作者
然后定义不同的掩码策略,根据我们的需要转换每一列。例如:
-
名字和姓氏应转换为NULL
-
电子邮件 应该被 “哈希化”,以保持唯一标识符,但无法与原始员工关联
-
薪资 应该转换为 “0” (整数的默认掩码策略)
![]()
在 Google BigQuery 中定义掩码规则的步骤 — 图片来自作者
![]()
在 Google BigQuery 中的一组掩码规则 — 图片来自作者
最后,我们需要授予对 “Medium” 标签信息的访问权限,并将这个新主体添加为该类别的授权查看者。
![]()
在 Google BigQuery 中将一个主体添加为掩码策略的查看者 — 图片来自作者
正如预期的那样,下面的 SQL 查询结果 — 当以“掩码读取器”角色连接时 — 将正确实现定义的规则:
SELECT * EXCEPT(First_Name, Last_Name) FROM `row-columns-policies.EMPLOYEES.SALARIES`![]()
在 Google BigQuery 中,掩码策略对掩码读取器角色的影响 — 图片来自作者
将行级策略添加到表访问管理中
现在假设,作为全球人力资源经理,我们需要向本地加拿大人力资源授予对该表的访问权限……同时不披露其他国家的数字。
我们可以为该用户设置行级安全性:
|CREATE ROW ACCESS POLICY Canadian_filter ON `row-columns-policies.EMPLOYEES.SALARIES` GRANT TO ('user:user@domain.com') FILTER USING (Country = 'Canada');这就像自动将任何由这个主体执行的查询扩展为 “ WHERE Country = ‘Canada’ ” 语句:
![]()
行级安全性在 Google BigQuery 中的结果 — 图片来自作者
这是减少一个或多个维度上可用信息的非常好方法!
这个最后的例子结束了我想在本文中探索的不同用例。
作为提醒,“列掩码”和“行级”策略提供了一种直接在数据平台中过滤数据的好方法。数据仓库中仅管理一个表,允许不同的解决方案和/或用户无缝查询,而不影响保密规则。
🍒 画龙点睛的是,这些功能是免费的,所以完全没有理由不利用它们!
像往常一样,我尝试确定所有必需的步骤,但如果我的教程中有任何遗漏的指令,请随时联系我!
(感谢我们的首席数据工程师 Ilyes Touzene 的校对!)
并且不要犹豫,浏览我在 Medium 上的其他贡献:
[## Pierre-Louis Bescond 在 Medium 上的文章
数据科学、机器学习和创新
选择和运行自己生成模型的逐步指南
如何在每天发布的模型海洋中进行导航?
·发表于 Towards Data Science ·阅读时间 5 分钟·2023 年 10 月 7 日
--
![]()
过去几个月,各种生成模型的参数规模大幅减少,比如刚刚发布的新 Mistral AI 模型。规模的减少为获取自己的个人助理 AI 提供了可能性,可以通过你的本地计算机与之绑定。这种本地推理非常诱人,以确保对你的数据进行机密计算。随着这些新发展,部署和管理 AI 工作负载的方式与 6 个月前相比有所不同,并且不断演变。如何使用这些模型来试验,甚至在你公司的基础设施上托管它们?
我认为,在使用任何由其他人托管的 API 模型之前,尝试不同类型的模型是一个好主意,以了解这些不同模型家族的表现。因此,让我们假设你不会立即使用 API 模型。你如何下载并使用一个模型呢?
对于此,你有两种类型的模型:专有模型和开放访问模型。专有模型如 OpenAI、Cohere 等都有各自的 API。开放访问模型可以是完全开放的,也可以是由于其许可证(如商业、非商业、仅研究目的等)而受限的。
找到这些模型的最佳地方是HuggingFace。在模型页面上,你可以看到他们提供了超过 350,000 个模型,覆盖了非常多样的任务。因此,你确实有很多选择!
![]()
HuggingFace 上的模型主页
需要记住的是,并非所有这些模型都在被使用或将来会被使用。有些模型可能只是某人在一个下午尝试的结果,然后再也没有更新。找到最有用模型的一个关键指标是查看有多少人下载了模型并喜欢它。例如,你可以根据你所寻找的任务类型进行筛选,如文本分类,然后你可以看到哪些模型是下载量最多和最流行的,并按照许可证等进行筛选。这将给你一个关于你所考虑任务的模型的整体概述,并帮助你找到它们。
转到文本生成任务,这是目前生成模型讨论的主要话题,你会发现流行的模型是新的 Mistral 模型 (
mistral.ai/news/announcing-mistral-7b),其参数量为 70 亿。现在,在 HuggingFace 上有这么多模型可用,如何知道哪个模型适合你的任务呢?首先需要注意的是,如下图所示,当你点击一个模型时,你会进入模型卡片页面,大多数模型已经有一个托管的交互界面,你可以从中获得模型输出的提示。除了这个托管的交互界面外,你还可以在下方看到所谓的 Spaces。Spaces 是托管在 HuggingFace 上的应用程序,人们在其中集成了你所查看的模型。所有这些界面都非常方便,让你了解这些模型的功能。
![]()
HuggingFace 上的 Mistral 7B 模型卡片
选择和运行模型
当然,模型选择时的约束将取决于你可用的基础设施和硬件。一个好的经验法则是,对于变换器模型,参数量超过 70 亿会使其在标准消费级 GPU 上难以良好运行。值得注意的是,你可能会发现一些模型优化方案,这些方案可以使模型在消费级硬件上运行。例如,可能会调整模型大小甚至计算精度,这可能适合你的具体任务,并允许你在自己的硬件上运行模型。
无论如何,一个好的建议是从较小的问题开始,然后逐步构建到解决你的任务的方案。一旦你弄清楚了需要什么模型和定制化,你可以查看这将对你的硬件需求意味着什么,以及是否可以实现。例如,假设对于我考虑的文本生成任务,Mistral 7B 模型在硬件需求方面符合我的需求。然后,HuggingFace 上的模型卡会给出如何下载和运行的说明。我通常使用 Google Collab (
colab.research.google.com/?utm_source=scs-index) 来了解运行时间和资源使用情况,但你也可以使用其他平台,如 Kaggle。![]()
在标准 Google Collab 笔记本上推理 Mistral 7B
对于 Mistral 7B 模型,它在基本的 Google Collab 引擎上运行,内存为 12GB。因此,这可以让你了解简单推理模型所需的资源,以及你是否需要优化模型以便在更少资源上运行,或者你是否已经具备所需的资源。
在完成选择模型和了解所需资源的初步工作后,你可能会受到当前硬件的限制,不得不尝试优化你的模型。幸运的是,有一个 GitHub 仓库 (
github.com/intel-analytics/BigDL) 详细介绍了大规模深度学习所需的不同优化方法。例如,它允许在标准计算机上运行 llama 语言模型。在那里,你可以找到所寻求的模型是否针对 CPU 计算(或至少单个 GPU)进行了优化,以及是否符合你的要求。在这篇博客文章中,我描述了选择和运行模型的第一步。如果你有兴趣了解更多关于特定任务的模型修改和模型部署的内容,HuggingFace 提供了一个很棒的课程 (
huggingface.co/learn/nlp-course/chapter1/1),可以免费获取。如果你喜欢这个故事,别犹豫,通过鼓掌或留言来表达你的欣赏!关注我在 Medium 上的更多数据科学内容!你还可以通过 LinkedIn 与我联系。
治疗与反应的故事
《预测营销活动受众治疗效果简介》
·
关注 发表在 Towards Data Science · 9 min read · 2023 年 3 月 3 日
--
图片由 geralt @ pixabay.com 提供
想象一下,你负责挑选应该收到下一个印刷广告材料的客户。
当然,只有对那些影响最大的人发放广告材料才是合理的。但这比说起来容易做。本文将探讨机器学习如何用来预测刺激(如促销)的治疗效果。
这种方法在营销活动中进行了说明,但也可以应用于其他问题。此外,本文介绍的方法有一个概念验证。您可以在这里找到它。
请注意,尽管我提到了来源,但本文基于我在该领域的经验。如果您发现任何改进的地方,请给我留言。
什么是反应及其测量方法?
反应的定义
任何接受处理的人可以以两种不同的方式响应。这个人可以忽视处理或对其做出反应。这种反应代表了处理效果。
首先,我们需要确定如何测量反应。在促销的背景下,购买本身在文献中通常被用作反应的二元术语。
如果反应的强度相当均匀,这可能就足够了。然而,对于大多数用例,反应的强度也应该被考虑。在我们的例子中,这将是订单的价值,这取决于反应的强度而变化。
因此,我建议将反应定义为通过促销活动带来的收入。
反应的测量
基于这个定义,我们现在可以继续测量处理效果。
这也是第一个挑战所在。购买可能是处理的因果效应,但也可能以“自然”的方式发生。例如,假设客户在收到广告后的两个月下订单。您会认为这个订单是广告的因果结果吗?根据我的经验,解决这个问题有两种方法:
有处理参考的反应
一种方法是将反应直接归因于促销处理。这可以通过跟踪基础处理的优惠券代码或广告专用商品编号来实现。如果这样的因果分配是可能的,可以称之为有处理参考的反应。
如果广告材料包含一个没有或很低最低订单金额的优惠券,根据我的经验,几乎所有客户都会使用它。然而,对于促销商品编号而言,进行因果分配则更加困难,因为许多客户也会直接在网络上搜索这些商品。这可能会根据具体业务有所不同。
![]()
有处理参考的反应示意图(作者提供的图片)
没有处理参考的反应
另一种方法是将销售归因于特定时间段的促销活动,例如广告活动后的 30 天。这可以描述为没有处理参考的反应。
确定最佳时间段需要仔细考虑——评估期应对所有广告材料保持一致,并且不应与后续处理重叠。它还应涵盖大多数可能的反应,同时保持尽可能简短。
![]()
无处理参考的响应示意图(图片由作者提供)
这两种方法都不是完美的。一方面,有些客户有意识或无意识地隐瞒了处理参考(例如,不兑换优惠券或不使用提供的项目编号)。另一方面,并不是每次购买都是由处理引发的,即使它在指定的时间段内。
在我看来,每当大比例的反应可以归因于某种广告媒介时,应使用带处理参考的收入。这使得模型创建更容易,并且需要更少的数据来获得良好的结果。
选择你的方法
一旦你决定了标签(因变量)的定义并创建了一些自变量,你就可以开始建模。根据标签的不同情况,预测客户反应可能需要不同的模型。
如引言中所述,我更喜欢将订单价值作为标签。然而,也可以仅预测购买概率。因此,这也应考虑在内。
![]()
ITE = 个体处理效果 / CATE = 条件平均处理效果(表由作者提供)
上表展示了多种建模处理效果的方法。如你所见,不同的标签定义需要不同的建模方法。
在这篇文章中,我将重点介绍可以建立因果关系的处理相关反应预测方法(见上表:“带处理参考”)。提升建模是一个独立的话题,需要单独的文章。我会在发布后将其链接到这里。在此之前,我很高兴推荐给你 Shelby Temple:
快速提升建模介绍
了解提升建模如何改进经典数据科学应用。
towardsdatascience.com
因此,本文仅处理直接归因于广告活动的销售预测。这种方法对于大多数用例是足够的,特别是因为提升建模需要没有接受任何广告材料的对照组。
数据集
多期数据集
为了创建一个有效的模型,利用多个过去促销的数据非常有帮助。如果这些活动覆盖整年,模型可以理解不同类型的广告材料和季节性变化如何影响结果。
历史数据可能并不总是需要用于那些由于促销而未发生变化的客户属性,如年龄和性别。然而,对于其他属性,例如客户多久前进行了购买,重要的是确保处理没有影响独立变量。(例如,包括因广告而进行的购买)
不幸的是,如果一个独立变量由于客户的反应发生了变化,就足以极大地影响整个模型。在这种情况下,模型在生产环境中的结果显著差于测试中的表现。为了避免这种情况,建议在创建任何变量之前,将促销前的数据与促销后的响应分开。
零膨胀数据集
建模客户响应的下一个挑战是数据集中存在大量零值。这是因为许多客户不会回应促销材料,因此不会进行购买。(相比之下,印刷广告材料的回应率达到 10%被认为是非常好的。)
为了更好地说明这一点,我使用了Kevin Hillstrom 在 Scikit-Uplift 包中包含的数据集作为本文的例子,并在随附的笔记本中展示。
数据集包含多个独立变量以及对电子邮件营销活动的响应。电子邮件活动的响应分布可以通过直方图显示,如下所示:
![]()
Hillstroem 数据集中的响应直方图。(图像由作者提供)
这类数据集通常被称为零膨胀数据集。处理这类数据集有两种不同的方法:过采样或建模。
对于过采样,我推荐SMOTER(用于回归的合成少数过采样技术)或SMOGN(带高斯噪声的合成少数过采样技术)。这些算法可以帮助建模零膨胀数据集。
建模
然而,我最喜欢的方法不是过采样,而是根据数据调整模型。
一些回归器可以默认处理零膨胀数据集,如基于树的回归模型,例如随机森林回归器。然而,经验表明,这并不是建立此类模型的最佳方式。
一方面,这归因于较低的可解释性,另一方面则是由于较差的性能,尤其是当数据集中包含大量独立变量时。
因此,我通常使用两步模型。在这里,数据集被分解为分类问题和回归问题。
![]()
使用两步模型预测零膨胀数据集的示例(图像由作者提供)
这种方法的优势在于回归不再需要学习包含零值的数据集,因为零/非零分类由其自身的估计器处理。
分类:
分类组件预测数据点为零的概率。分类器在完整的数据集上进行训练。(过采样仍然有用)
回归:
回归仅在大于零的数据点上进行训练。这样,回归不必处理零膨胀,并且可以选择大量回归器。
使用类似方法的模型通常被称为响应-支出元学习器(保险行业中的风险/严重性元学习器)。它们通常表现更好,并在选择模型时提供更多灵活性。
这两种模型的结果可以通过从购买概率和购买时的销售值形成期望购买值来相对容易地结合起来。
![]()
形成期望值的函数示例(图像由作者提供)
作为期望值主题的复习,我推荐阅读这篇文章。
通过使用游戏的简单示例来直观地解释期望值
towardsdatascience.com
基于这里介绍的程序,我还创建了一个简短的笔记本作为概念验证。在这里,我比较了随机森林回归器与元学习器的表现,后者适配了线性回归和逻辑回归。
你可以在这里找到它:相关笔记本
评估
元学习器可以作为回归进行评估,或者可以单独评估其组件(回归和分类)。
实际上,如果模型的表现没有传达给数据科学家,通常最好避免使用如 RSME 或 MAE 这样的值,而是回答问题:‘模型能多好地区分好客户与坏客户?’
为了演示这一点,我按预测质量的降序对客户进行排序(从预测最高的客户到预测最低的客户),并绘制在活动中实现的实际或理论销售额。(注意:用于评估的活动必须不包括在训练数据集中)。
![]()
优秀的接收者选择示例(合成数据,作者提供的图像)
使用好的选择模型,预测非常好的客户也应该产生非常高的收入,这些收入应迅速下降并趋近于零。
结论
在本文中,我分享了使用机器学习进行客户选择的经验。然而,需要注意的是,当客户响应无法充分衡量时,需要采取不同的方法。
为了解决这个问题,我的下一篇文章将扩展这里介绍的程序,以预测提升(个体处理效应/条件平均处理效应)。
否则,我很高兴你能坚持到现在,并感谢你的关注。如果你有任何问题或建议,请随时留言。
数据集:
数据集来自 Kevin Hillstrom 的博客 “MineThatData”,并在许多 Python 包和科学出版物中使用。在我的文章中,我提到数据集在 Python 包“Scikit-Uplift” 中的实现,该包在 MIT 许可证 下发布。
资源:
McCrary, M. 使用多阶段模型增强客户定位:预测零售业的客户销售和利润。 J Target Meas Anal Mark 17, 273–295 (2009).
doi.org/10.1057/jt.2009.22Torgo, L., Ribeiro, R.P., Pfahringer, B., Branco, P. (2013). SMOTE 回归。载于:Correia, L., Reis, L.P., Cascalho, J. (编辑) 《人工智能进展》。EPIA 2013. 计算机科学讲义集(),第 8154 卷. 施普林格,柏林,海德堡。
doi.org/10.1007/978-3-642-40669-0_33Torgo, L., Ribeiro, R.P., Branco P. (2017) SMOGN: 不平衡回归的预处理方法。载于:2017 年机器学习研究会议论文集
可能影响你的决策树和随机森林的微妙偏差
这可以很容易地被消除
·
关注 发表在 Towards Data Science ·10 分钟阅读·2023 年 12 月 28 日
--
Dall-E 生成的艺术决策树
你的决策树和随机森林可能会受到一些小的偏差影响,这种偏差可以轻松消除,几乎不需要成本。这就是我们在本文中探讨的内容。
免责声明:这篇文章讨论了作者最近进行的研究。
介绍
决策树和随机森林是机器学习中广泛应用的分类和回归技术。决策树因其可解释性而受到青睐,而随机森林则作为高度竞争且通用的最先进技术脱颖而出。常用的 CART 实现,如 Python 包中的 sklearn 和 R 包中的 tree 及 caret,假设所有特征都是连续的。尽管存在这种隐含的连续性假设,这两种技术仍然被广泛应用于具有各种特征类型的数据集。
在一篇最近的论文中,我们研究了违反连续性假设的实际影响,并发现它会导致偏差。重要的是,这些假设在实际中几乎总是被违反。在这篇文章中,我们展示并讨论了我们的发现,说明和解释了背景,并提出了一些简单的技术来减轻这种偏差。
一个激励性示例
让我们通过 UCI 资源库中的 CPU 性能 数据集来进行示例。我们将通过 common-datasets 包导入数据,以简化预处理过程,避免特征编码和缺失数据插补的需求。
import numpy as np from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import RepeatedKFold, cross_validate from scipy.stats import wilcoxon from common_datasets.regression import load_cpu_performance dataset = load_cpu_performance() X = dataset['data'] y = dataset['target'] # a cross-validation wrapper to simplify the code def cv_rf(X, y, regressor=RandomForestRegressor): return cross_validate( estimator=regressor(max_depth=11), X=X, y=y, cv=RepeatedKFold(n_splits=5, n_repeats=400, random_state=5), scoring='r2' )['test_score'] r2_original = cv_rf(X, y) r2_mirrored = cv_rf(-X, y)在实验中,我们评估了随机森林回归器在原始数据及其镜像版本(每个特征乘以 -1)上的表现。回归器的超参数(
max_depth=11)是在专门的模型选择步骤中选择的,通过在合理的深度范围内最大化 r2 得分来确定。用于评估的交叉验证比机器学习中通常使用的交叉验证要全面得多,总共进行了 2000 次折叠。print(f'original r2: {np.mean(r2_original):.4f}') print(f'mirrored r2: {np.mean(r2_mirrored):.4f}') print(f'p-value: {wilcoxon(r2_original, r2_mirrored, zero_method="zsplit").pvalue:.4e}') # original r2: 0.8611 # mirrored r2: 0.8595 # p-value: 6.2667e-04在 r2 得分方面,当属性被镜像时,我们观察到降低了 0.2 个百分比点。此外,这一差异在传统水平下统计上显著(p << 0.01)。
结果有些令人惊讶且不符合直觉。机器学习技术通常对某些类型的变换具有不变性。例如,k 最近邻对任何正交变换(如旋转)不变,线性技术通常对属性的缩放不变。由于决策树中的空间划分是轴对齐的,因此不能期望它对旋转不变。然而,它对缩放是不变的:对任何特征应用任何正的乘数都会导致完全相同的树。因此,轴的镜像处理一定存在一些问题。
一个有趣的问题出现了:如果镜像坐标轴可以带来更好的结果怎么办?我们是否应该在模型选择中考虑另一个自由度(乘以 -1),除了确定最优深度之外?好吧,在接下来的文章中我们将搞清楚这里发生了什么!
二叉决策树生成与推理
现在,让我们简要回顾一下使用二叉分类与回归树(CART)的构建和推理的一些重要特征,这些特征被大多数实现所使用。与其他树生成技术如 ID3 和 C4.5 相比,CART 树的一个显著不同之处在于它们不会以任何特殊方式处理类别特征。CART 树假设所有特征都是连续的。
给定一个训练集(分类或回归),决策树通过递归地使用诸如 feature < threshold 或 features <= threshold 的条件来划分训练集和特征空间来生成。条件选择通常是实现的一个固有属性。例如,Python 包 sklearn 使用形式为 feature <= threshold 的条件,而 R 包 tree 使用 feature < threshold。请注意,这些条件与所有特征都是连续的假设相一致。
然而,假设特征是连续的并不是一个限制。整数特征、通过某些编码的类别特征或二元特征仍然可以输入到这些树中。让我们以一个假设的贷款审批场景中的示例树为例(一个二元分类问题),基于三个属性:
-
graduated (binary): 如果申请人未毕业则为 0,如果申请人毕业则为 1;
-
income (float): 申请人的年收入;
-
dependents (int): 依赖人数;
目标变量是二元的:申请人是否违约(1)或偿还(0)。
![]()
为假设的贷款审批场景构建的决策树
树的结构以及节点中的条件(即哪个特征的阈值)是从训练数据中推断出来的。有关树生成的更多细节,请参考 维基百科上的决策树学习。
给定这样一棵树,新记录的推理是通过从叶子节点开始,递归应用条件,并将记录路由到条件输出对应的分支。当遇到叶子节点时,记录在叶子节点中的标签(或最终分布)将被返回作为预测。
条件设置与阈值
一个有限的训练记录集不能暗示特征空间的唯一分割。例如,上图中的树可能是从数据中归纳出的,其中没有毕业=0 且收入在]60k,80k 范围内的记录。树的归纳方法确定应在收入值 60k 和 80k 之间进行分割。在没有更多信息的情况下,中点(70k)被用作阈值。一般来说,也可以是 65k 或 85k。使用未标记区间的中点是一种常见的做法和合理的选择:与连续特征的假设一致,将未标记区间的 50%分配到左分支,50%分配到右分支。
由于使用中点作为阈值,树的归纳完全独立于条件操作符的选择:使用<=和<都会导致相同的树结构,阈值也相同,只是在条件操作符上有所不同。
然而,推断确实依赖于条件操作符。在示例中,如果要推断一个收入为 70k 的申请人记录,则在所示的设置中,它将被路由到左分支。然而,使用<操作符时,它将被路由到右分支。对于真正的连续特征,收入恰好为 70k 的记录被推断的可能性微不足道。然而,实际上,收入可能以 1k、5k 或最终 10k 为单位,这使得条件操作符的选择对预测有显著影响。
条件操作符和镜像的关系
为什么我们谈论条件操作符,而我们观察到的问题是特征的镜像? 这两者基本上是相同的。条件“feature < threshold”与条件“-feature <= -threshold”在本质上是等价的,因为它们导致相同但镜像的实数轴分割。也就是说,在这两种情况下,如果特征值等于阈值,该值位于特征值大于阈值的相同分区。例如,比较下面的两棵树,一棵是我们之前用于说明的树,除了所有条件操作符都改为<,另一棵是保持操作符不变,但树是镜像的:可以很容易看到,对于任何记录,它们都会导致相同的预测。
![上一个使用条件操作符<的树
![]()
基于镜像数据构建的树(仍然使用条件操作符≤)
由于树生成与条件选择无关,在镜像数据上构建树,然后预测镜像向量,相当于使用非默认的条件运算符(<)对非镜像记录进行推断。当森林的树拟合到镜像数据时,即使sklearn使用‘<=’运算符进行条件处理,它也会像使用‘<’运算符一样工作。因此,我们发现镜像处理的性能下降是由于阈值与特征值重合,导致在测试集评估过程中预测结果不同。
为了完整性,我们注意到树生成某些步骤中的随机化可能会导致在拟合镜像数据时出现略微不同的树。然而,这些差异在随机森林中平滑,特别是在 2k 折的评估中。观察到的性能下降是阈值与特征值重合的系统性效应的结果。
这种情况何时会发生?
主要有两种情况会增加这种现象的可能性:
-
当特征域包含高概率的等距值:这为阈值(作为两个观察值的中点)与特征值重合提供了条件。
-
建立了相对较深的树:通常情况下,树的深度增加时,训练数据在节点处变得更加稀疏。当某些观察值在更深的层次上缺失时,阈值可能会落在这些值上。
有趣的是,许多领域中具有少量等距值的特征非常普遍。例如:
-
医疗数据集中的年龄特征。
-
四舍五入的十进制数字(观察到第 2 位数字的,将形成一个网格)。
-
财务数据集中以百万或十亿为单位引用的货币金额。
此外,在
sklearn.datasets中的玩具回归和分类数据集中,几乎 97%的特征都是这种情况。因此,毫不夸张地说,高概率的等距特征几乎无处不在。因此,作为经验法则,建立更深的树或森林,阈值干扰特征值的可能性就越大。这是一种偏差,模型选择无济于事!
我们已经看到,这两个条件运算符(由数据镜像模仿的非默认运算符)可能导致具有统计显著性的不同预测结果。这两个预测不能同时无偏。因此,我们认为,无论使用哪种形式的条件,当阈值与特征值重合时,都引入了偏差。
另外,考虑到某种形式的条件可能与数据巧妙对齐并提升性能,这是一种诱人的想法。因此,可以使用模型选择来选择最合适的条件形式(或是否应该镜像数据)。然而,在特定的模型选择场景中,使用一些 k 折交叉验证方案,我们只能测试在例如,20% 的数据被移除(5 折)用于训练和评估时,哪种操作符通常是更有利的。当模型在所有数据上训练时,其他阈值可能会干扰特征值,我们没有信息来判断哪种条件会改善性能。
减轻随机森林中的偏差
消除偏差的自然方法是去除条件操作符选择的影响。这涉及使用两种操作符进行预测并平均结果。
在实践中,通过随机森林,利用数据镜像与改变条件操作符的等效性,这可以基本上以零成本近似完成。与其使用 N_e 个估计器的森林,不如构建两个规模为一半的森林,一个拟合原始数据,另一个拟合镜像数据,并取结果的平均值。请注意,这种方法适用于任何随机森林实现,仅需边际附加成本(如将数据乘以 -1 并平均结果)。
例如,我们在下面的 Python 代码中实现了这一策略,旨在从sklearn随机森林中去除偏差。
from sklearn.base import RegressorMixin class UnbiasedRandomForestRegressor(RegressorMixin): def __init__(self, **kwargs): # determining the number of estimators used in the # two subforests (with the same overall number of trees) self.n_estimators = kwargs.get('n_estimators', 100) n_leq = int(self.n_estimators / 2) n_l = self.n_estimators - n_estimators_leq # instantiating the subforests self.rf_leq = RandomForestRegressor(**(kwargs | {'n_estimators': n_leq})) self.rf_l = RandomForestRegressor(**(kwargs | {'n_estimators': n_l})) def fit(self, X, y, sample_weight=None): # fitting both subforests self.rf_leq.fit(X, y, sample_weight) self.rf_l.fit(-X, y, sample_weight) return self def predict(self, X): # taking the average of the predictions return np.mean([self.rf_leq.predict(X), self.rf_l.predict(-X)], axis=0) def get_params(self, deep=False): # returning the parameters return self.rf_leq.get_params(deep) | {'n_estimators': self.n_estimators}接下来,我们可以执行与之前相同的实验,使用完全相同的折叠:
r2_unbiased = cv_rf(X, y, UnbiasedRandomForestRegressor)让我们比较一下结果!
print(f'original r2: {np.mean(r2_original):.4f}') print(f'mirrored r2: {np.mean(r2_mirrored):.4f}') print(f'unbiased r2: {np.mean(r2_unbiased):.4f}') # original r2: 0.8611 # mirrored r2: 0.8595 # unbiased r2: 0.8608根据预期,无偏森林的 r2 分数介于原始森林的镜像数据有无之间的分数之间。消除偏差可能对性能有害;然而,我们再次强调,一旦森林用所有数据拟合,关系可能会颠倒,原始模型可能会比镜像模型产生更差的预测。通过去除对条件操作符的依赖来消除偏差,从而消除了由于依赖默认条件操作符而导致性能恶化的风险。
结论
已经确认并展示了与条件选择和特征取等距值的交互相关的偏差的存在。考虑到这类特征的普遍存在,这种偏差很可能出现在足够深的决策树和随机森林中。通过对两个条件操作符进行预测平均,可以消除潜在的有害影响。有趣的是,对于随机森林而言,这几乎没有成本。在我们使用的例子中,改进达到了
0.1–0.2个百分比点的 r2 分数水平。最后,我们强调这些结果同样适用于分类问题和单一决策树(见 预印本)。进一步阅读
对于更多细节,我推荐:
-
我们详细讨论此主题的预印本,包含更多测试、插图和替代偏差缓解技术:预印本。
-
包含可重复分析的 GitHub 仓库:
github.com/gykovacs/conditioning_bias -
本文背后的笔记本:
github.com/gykovacs/conditioning_bias/blob/main/blogpost/001-analysis.ipynb
如果你对类似内容感兴趣,不要忘记订阅!你还可以在 LinkedIn上找到我!
带有地理空间计算的可持续供应链模板(Python)
供应链网络足迹应包括运输和配送中心的温室气体计算,而不仅仅是其中之一
·发表于Towards Data Science ·阅读时间 11 分钟·2023 年 4 月 25 日
--
![]()
照片由 Edelbert Macapagal提供,来源于Unsplash
供应链数据科学的可持续性
供应链数据科学家通常需要对各种领域有深入了解,以支持各部门。这种广度有助于优化供应链网络全球而非优化局部段落;局部优化的组合很少能导致最佳整体解决方案。
可持续性是供应链分析中相对较新的领域。类似于对全球优化而非局部优化的期望,我们也希望通过批判性思维来实现可持续性指标下最有价值的结果。
美国环保局(US EPA)计算运输是美国五大经济部门中贡献最大的一项[1]。
交通是温室气体的重要来源,但供应链计算还需考虑其他来源。
为美国大多数居民提供商品和服务的供应链网络还依赖于配送中心来存储、重新包装和准备这些产品以备运输。以下结构分享了一种将交通和设施贡献结合起来的方法,示例数据和计算已发布在GitHub上:
-
交通软件计算温室气体排放
-
使用 Python 生成数据
-
供应链 GHG 排放场景:一个配送中心(DC)
-
供应链 GHG 排放场景:三个配送中心(DCs)
-
结论
-
下一篇文章:多目标优化
运输软件计算温室气体排放
许多运输管理系统(TMS)根据里程提供温室气体(GHG)排放的计算。
-
在美国,旅行的距离(以英里计算)可以乘以一个因子,例如每英里 0.36 克的 CO2e,用于小于卡车负载(LTL)卡车。CO2e 指的是二氧化碳当量单位,因为 CO2 只是运输的温室气体副产品之一。
-
温室气体排放可能更具体,比如考虑车辆的具体类型而不是行业平均值、卡车拖车中的重量以及其他因素。
供应链网络设计软件也可以在距离和成本旁边包括 CO2e 量。
不幸的是,仅考虑运输可能会导致某人错误地认为较短的运输距离意味着较低的 CO2e 排放。
较短的运输时间通常需要额外的设施,如配送中心或交叉码头。一个运输排放较低的供应链,其总排放量可能更大。
本文附带一个可持续供应链模板,发布在GitHub上,以展示在考虑运输和设施碳足迹时的供应链排放。
使用 python 生成数据
以下是考虑供应链可持续性时的关键数据点。我的近似值是在 python 中开发的,但其他工具也可以类似使用。
配送中心的地理编码位置将作为运输路线上的起点和终点。
-
我根据维基百科[2]选择了美国 50 个最大的大都市地区作为数据样本。每个大都市将作为一个目的地,而给定场景的选定起点将位于一个或多个大都市中。
-
下一步是检索纬度和经度用于距离计算和地图创建。这一过程称为地理编码。虽然我喜欢在 python 中自动化这个工作流程,但也有其他方式使用软件、电子表格、互联网资源等来收集地理信息。
-
我首选的免费开源 python 方法,geopy,由于一个依赖暂时无法使用,但在我十天后再次检查时该服务已恢复。对于以下脚本,我使用了 Google Maps API 帐户,幸运的是免除了我为本文下载的$0.32 的地理数据的费用。geopy 方法被注释掉,但在脚本中也有显示。
在以下的 python 脚本中,我提供了城市和州,并返回了每个位置的纬度和经度。我本可以提供邮政编码或完整地址,以获得更准确的地理坐标。
我逐次运行这个脚本,以确保数据验证容易,然后我用最新的城市数据更新了 CSV 文件。有一次,我在 Google Maps API 输入中将“California”写成了“加利福尼亚”,省略了城市名称,但这个过程帮助我立即发现了错误。
列举位置之间的路线列表
-
在这个简化模型中,每条路线将有一个起点位置和一个终点位置。我通常将这些路线称为起点-终点对(或 OD 对)。源点-目的点对或长途路线是其他常见的描述词。
-
为了拥有我可能在下一步建模场景中使用的所有路线,我想在所有可能的起点和所有可能的终点之间创建路线。为了创建场景,我稍后将根据这些路线的子集过滤数据框。
-
使用 pandas 的交叉合并,50 个起点和 50 个终点的笛卡尔积结果是 2500 个 OD 对。
计算位置之间的距离
-
下一步是将刚创建的origin_destinations_pairs数据框中的每条路线的距离添加进去。
-
当旅行长距离时,地球的曲率在距离计算中非常重要。haversine 公式辅助函数计算这些大圆距离(GCD)[3]。我在下一步中使用的温室气体计算器也利用了大圆距离。虽然可以获得实际的卡车行驶距离,但使用 haversine 弧线距离通常更简单,并且在这种情况下将确保我在模型中合并的数据格式一致。
-
我对这些距离结果做了一处修改。一个位置到自身(例如,从休斯顿到休斯顿)的距离为 0 英里。我将这 50 个“自我”路线的距离改为 10 英里,假设同一城市中可能有到其他点的路线。
计算运输和配送中心的温室气体排放量
免责声明:以下因素是基于有限研究的假设。使用这些假设的目的是展示场景结果的变化。我尊重为组织调整这些因素所需的工作量,但这超出了本文的范围。
-
对于运输温室气体排放,经过评估几个在线计算器,我决定使用 每英里 0.36 克 CO2e 的近似值/假设,这来自 DHL[3] 的特定路线的少于整车装载(LTL)卡车。简而言之,我输入了起点和终点,确认了 CO2e 克数是基于大圆距离(GCD)的,然后除以总英里数以获得每英里的克数。
-
对于一个普通配送中心的温室气体排放,我结合了每平方英尺的配送中心(DC)能源使用的公共数据 [4] 和该能源的排放影响 [5],得出了每天 443 公斤 CO2e的例子/假设 DC。
-
模型将假设所有发货都发生在一天内。这样,日常配送中心(DC)排放量和日常发货排放量可以加总为每日总量。
供应链温室气体排放场景:一个配送中心(DC)
以下场景演示了在已提到的条件和假设下的总温室气体排放量。利用排放因子,我使用了以下计算来处理利用一个配送中心的场景。
为了简化,这些计算做了(不正确的)假设:
-
所有发货都可以在一天内完成。
-
每辆卡车在返回配送中心的途中也可以找到发货。
第一个函数 footprint_for_one_dc() 需要筛选相关场景的路线数据。例如,如果我想了解在田纳西州孟菲斯的一个配送中心及 50 个运输目的地的距离和温室气体排放,那么我筛选的数据框将有 50 行。
第二个函数 map_supply_chain_network() 使用 folium 创建配送中心、目的地以及配送中心和目的地之间的“鸟瞰”线路图。这些类型的 OD 配对地图有时被称为蜘蛛图或网络分析图。
一个位于美国中部的配送中心,位于田纳西州孟菲斯
对于第一个场景,我筛选了数据框 distance_between_od_pairs(如上所述),以孟菲斯为起点,并运行了两个函数以进行计算和地图创建。
孟菲斯配送中心的位置每天的数值为:
-
DC 和运输排放的总量为 467 公斤 CO2e
-
24 公斤 CO2e 的运输排放
![]()
使用 Leaflet 和 OpenStreetMap 通过 folium 在 python 中的孟菲斯配送中心(图片来源:作者)
一个位于美国中部的配送中心,位于密苏里州圣路易斯
对于第二个场景,我没有筛选孟菲斯的起点,而是筛选了圣路易斯的起点。然后我运行了两个函数以进行计算和地图创建。
圣路易斯的配送中心位置每天的数值为:
-
DC 和运输排放总计 466 kg CO2e
-
运输排放 23 kg CO2e
由于假设每个 DC 的足迹相同,我们可以推断在这个数据情景下,圣路易斯作为单一位置的运输足迹比孟菲斯好 1 kg CO2e。
![]()
圣路易斯 DC 使用 Leaflet 和 OpenStreetMap 通过 Python 的 folium 制作(图片由作者提供)
华盛顿州西雅图的一个配送中心
在第三种情景中,我选择了西雅图作为 DC 位置,以展示当单个 DC 远离美国中心时的结果。
西雅图的配送中心位置产生了每日值:
-
DC 和运输排放总计 494 kg CO2e
-
运输排放 51 kg CO2e
在我们评估这些情景数据的指标时,选择单一配送中心位置的西雅图可能是最差的选择。
然而,总计 494 kg CO2e 仅比前两个情景高出约 6%。在这些情景的数据中,如果 DC 使用低碳能源,这可能会使单个西雅图配送中心在这些假设下的环境指标上变得更具吸引力。即便有良好的环境指标,运输和总设施成本仍可能是导致选择将 DC 设在其他地点的因素。
![]()
西雅图 DC 图片由作者使用 Leaflet 和 OpenStreetMap 通过 Python 的 folium 制作
供应链温室气体排放情景:三个 DC
最后,最后一个情景选择了在美国相对均匀分布的三个 DC:加州河滨、德州休斯顿和俄亥俄州哥伦布。
如何选择最优的 DC 集合,请滚动至本文底部,查找关于多目标优化的下一篇文章链接(如有)。
为了计算有更多 DC 的情景的温室气体排放,数据整理过程需要更多步骤:
-
我希望每个配送中心只服务一个目的地;每个目的地仅由最近的 DC 服务。这是一种常见的做法,用于主要运输通道,但次要和三级通道通常是选项。
-
配送中心可以更小,以满足相同的目的地需求,因此每个设施的温室气体排放将被更新。我将假设每个 DC 的规模相似,以简化计算和模型。
为了选择最近的 DC,首先我筛选了三个源 DC,类似于早期的情景。接下来,我按最小距离筛选目的地,以选择每个目的地的最近 DC。
由于这三个配送中心将服务于与一个中央配送中心相同的一部分目的地,每个配送中心需要携带更多的库存,以确保在需要时产品可供销售。虽然可以从较远的配送中心发货以弥补较近地点的缺货,但最低库存水平可以通过供应链平方根法则来估算。否则,将需要更复杂的模拟来考虑这种可能性。
供应链平方根法则估算了我们需要多少更多的库存以维持相同的服务水平。例如,对于三个配送中心与一个配送中心:
square_root(3 / 1) = 1.73
相比于一个配送中心,三个配送中心需要的库存量是 1.73 倍,或者增加 73%。由于有三个配送中心来覆盖这部分数量,如果配送中心的足迹按线性比例扩展,并且每个配送中心都相等,那么每日 433 kg CO2e 的配送中心排放将增加到 766 kg CO2e,或每个较小的配送中心大约 256 kg CO2e。
加州河畔城、德克萨斯州休斯顿和俄亥俄州哥伦布的三个配送中心
在第四种情景中,我选择了三个看起来视觉上分开的配送中心。这些配送中心不一定是最优选择,但如何做出最优选择的过程将在下一篇文章中介绍。
河畔城、休斯顿和哥伦布的配送中心总共为:
-
779 kg CO2e 配送中心和运输排放
-
11 kg CO2e 运输排放
与一个中央配送中心相比,这三个配送中心将运输排放减少了 50%以上。但由于额外的配送中心,总排放增加了 40%。
![]()
三个配送中心的图像由作者使用 Leaflet 和 OpenStreetMap 通过 python 中的 folium 创建
结论
在某些条件下,更多的配送中心和更短的运输距离可能会导致较少的温室气体排放。但这个有限的例子表明,更短的最终运输目的地可能并不总是表示碳足迹较低的供应链网络。
为了设计一个可持续的供应链,需要考虑运输和设施的影响以及其他因素。
要使用模板或贡献改进模板,请访问GitHub。
下一篇文章:多目标优化
尽管本文关注于可持续性,但成本或时间通常是供应链指标的优先考虑因素。
下一篇文章将总结运筹学在平衡复杂数学优化问题中的多种方法。由于最小成本通常不会同时带来最佳服务水平或可持续性指标,因此需要平衡。
如果你希望在下一篇文章发布时收到通知,可以在 Medium 上关注我,并点击我个人资料上的订阅选项。这里还会包括一个(更新:) 和链接:
sabolch-horvat.medium.com/theres-only-three-ways-to-do-multi-objective-optimization-411a72f35d3c。更新 2023 年 10 月 6 日:分享多目标优化的想法比我预期的要复杂。这是我第一次尝试提供一些我积累的见解。
我见过的所有方法都使用这三种方法中的一种或多种作为基础框架。
如果你喜欢这篇文章并想阅读(和撰写)更多类似的文章,可以考虑点击我个人资料图片旁的关注按钮和/或使用我的推荐链接订阅 Medium 会员:
sabolch-horvat.medium.com/membership[1] “温室气体排放源。” 美国环保局,2011 年,
www.epa.gov/ghgemissions/sources-greenhouse-gas-emissions。2023 年 4 月 18 日。[2] “大都市统计区。” 维基百科,维基媒体基金会,2023 年 3 月 15 日,
en.wikipedia.org/wiki/Metropolitan_statistical_area。[3] “哈弗森公式。” 维基百科,维基媒体基金会,202,3 年 4 月 7 日,
en.wikipedia.org/wiki/Haversine_formula。[4] “碳排放计算器。” DHL 网站,访问于 2023 年 4 月 8 日,
www.dhl-carboncalculator.com/。[5] “建筑活动子类别的电力消费总量和条件强度,2012 年。” 美国能源信息署,2016 年发布,
www.eia.gov/consumption/commercial/data/2012/c&e/cfm/pba4.php。[6] Ries, J.M., Grosse, E.H. 和 Fichtinger, J. (2016)。仓储的环境影响:针对美国的情景分析。《国际生产研究杂志》,doi: 10.1080/00207543.2016.1211342
-





















































































浙公网安备 33010602011771号