TowardsDataScience-2023-博客中文翻译-二十一-

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

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

GPT-4 可以解决数学问题——但并非所有语言都能

原文:towardsdatascience.com/gpt-4-can-solve-math-problems-but-not-in-all-languages-d1c2e9c195a0

几个实验让 GPT-4 在 16 种不同语言中解决数学问题

Yennie JunTowards Data Science Yennie Jun

·发布于Towards Data Science ·阅读时间 12 分钟·2023 年 10 月 11 日

--

图片由作者使用 Midjourney 创建。

本文最初发布在我的 个人网站

引言

据说数学是一种普遍语言——数学概念、定理和定义可以用符号表示,这些符号无论语言如何都能被理解。

在本文中,我测试了 GPT-4 在十六种不同语言中的数学能力。

早期实验显示,GPT-4 在SAT 数学和 AP 微积分测试中表现优异以及在本科水平的数学中也表现良好。然而,这些实验中的大多数仅测试了 GPT-4 在英语中的数学能力。为了更好地了解 GPT-4 在英语之外的数学能力,我在其他十五种语言中对同样的数学问题进行了测试。

那么,GPT-4 在不同语言中的数学能力如何?理论上,它在所有语言中的表现应该是一样的(无论好坏),但不幸的是(正如你可能猜到的),情况并非如此。GPT-4 在解决英语中的数学问题时表现更好。根据语言的不同,GPT-4 能够解决一些问题。然而,对于传统上资源不足的语言,例如缅甸语和阿姆哈拉语,GPT-4 未能解决我给它的问题。

关于 Project Euler

我使用来自Project Euler网站的数学问题来测试 GPT-4。(这也是对我今年早些时候的一篇文章的回顾,文章中我使用了 ChatGPT 的提示工程来解决一些 Project Euler 问题。)Project Euler 以同名数学家命名,是一个提供数百个数学和计算机编程问题的网站,问题难度各异。该网站于 2001 年开始,截至 2023 年 10 月,他们拥有超过 850 个问题,并且大约每周发布一个新问题。

Project Euler 问题的一个优点是每个问题都有一个数值上“正确”的答案——这使得检查 GPT-4 的回答是否客观正确变得容易。它们通常比高中或大学水平的数学问题要复杂得多。目前,没有大规模的综合理解 GPT-4(或其他大型语言模型)在 Project Euler 问题上的数学能力(除了这个项目,该项目仅评估 ChatGPT 在前 30 个问题上的能力)。

Project Euler 网站的截图。

GPT-4 在解决困难的 Project Euler 问题上存在限制(仅限英语)

在对其他语言进行实验之前,我测试了 GPT-4 解决不同难度级别的 Project Euler 问题的能力。

Project Euler 难度评级是“基于问题自发布日期起解决所花费的时间。”评级范围从 5 到 100,以 5 为增量。前 50 个问题大多被评为难度 5,主要是因为这些问题发布得较早(近 20 年前!),因此有更多人有机会找到解决方案。

Project Euler 问题按难度分布。图由作者创建。

我为 GPT-4 选择了一组难度各异的问题进行求解。使用的提示在文章末尾附有。我每个提示运行了五次,以考虑 GPT-4 回答的差异。

GPT-4 在英语中无法解决任何难度 20 或更高的问题。 请注意,我并不是说 GPT-4 无法解决所有难度超过 20 的问题,因为我没有测试每一个问题。然而,我对每个问题进行了 5 次测试,如果 GPT-4 能够解决难度超过 20 的问题,它至少应该有一次得到正确答案。

GPT-4 在仅用英语提示的 Project Euler 问题子集上的通过率。图由作者创建。

可能通过更复杂的提示技术(如少样本学习、链式思维提示或提供更多数学背景)GPT-4 能够解决这些问题。然而,我想专注于测试模型的默认状态,所以这些提示技术留待未来的实验 😃

对于 GPT-4 能够解决的问题(难度为 15 或更低),其通过率因难度级别和问题编号而异。对于像 1、3 和 62 这样的问题编号较低的(编号小于 100 的问题,最有可能出现在 GPT-4 的训练数据中),GPT-4 能够轻松地 100%正确回答。

语言样本

尽管世界上有数百种书面和口语语言,我选择了十六种语言的子集。这些是 2023 年前十种最常用语言的组合;我早期关于语言模型标记化的文章中的 3 种表现最差的语言;以及其他一些语言。

对于每种十五种非英语语言,我进行了以下操作:

  1. 使用 GPT-4 将 Project Euler 问题翻译成该语言

  2. 让 GPT-4 的新实例解决该语言中的翻译问题

Project Euler 问题#500 的缅甸语翻译示例。图由作者创建。

像之前一样,我将每个提示运行了五次,以便允许 GPT-4 的回答有差异。提示在文章末尾包含。

GPT-4 在某些语言中解决问题的难度大于其他语言

我根据一些限制选择了 Project Euler 问题的一个子集:

  • 每个难度 5、10 和 15 的 2 个问题。由于 GPT-4 在解决难度 > 20 的问题时遇到了困难,我在多语言分析中忽略了这些问题

  • 问题编号 > 200. 以前的研究表明,GPT-4 记住了 Project Euler 前 200 个问题的数值答案(例如,询问 GPT-4“Project Euler 问题#X 的数值答案是什么”,GPT-4 会正确回答,即使用户没有提供实际问题)

  • 问题长度 < 500 个字符(以避免在上下文中花费过多的$$)

也许这不是什么惊讶的事,但GPT-4 能够正确解决英语中的 Project Euler 问题的频率是其他语言(如亚美尼亚语或波斯语)的 3 倍以上。GPT-4 无法解决缅甸语和阿姆哈拉语等语言中的任何一个 6 个问题。

GPT-4 在 16 种不同语言中解答 Project Euler 问题的通过率。GPT-4 在解决 Project Euler 问题#200 以上时具有不同水平的数学能力;在缅甸语和阿姆哈拉语中,它无法解决这些问题。图表由作者制作。

我特别选择了缅甸语和阿姆哈拉语,因为这些语言在我之前关于语言模型分词的文章中表现最差,我发现分词同一句话在缅甸语或阿姆哈拉语中可能需要比英语中类似信息多 10 倍的词汇。正如我们将在分析的其余部分中看到的,这些语言不仅处理成本更高,而且相对于其他语言表现更差。

GPT-4 在所有这些语言中都不擅长解决问题吗?并非如此……我让 GPT-4 解决了 Project Euler 问题#1 和#3,这两个问题是网站上最常解决的问题之一(这些问题在互联网上有很多解决方案,并且是 GPT-4 训练数据的一部分)。如下所示,GPT-4 在解决这些“较简单”(以及更频繁解决)的这些问题时表现得要好得多。值得注意的是,即使是对于这些热门问题,缅甸语的表现仍然相对于其他语言较低。

GPT-4 在 16 种不同语言中解决简单Project Euler 问题的通过率。GPT-4 在解决两个热门 Project Euler 问题时具有不同的数学能力;它能够在大多数语言中均匀地解决问题。图表由作者制作。

糟糕的翻译导致糟糕的数学问题解决技能

之前,我让 GPT-4 首先将一个 Project Euler 问题翻译成另一种语言,然后在该语言中解决它。但如果翻译本身起初并不好呢?

我选择了 GPT-4 对问题#3(难度 5)、问题#365(难度 40)和问题#500(难度 15)的翻译。我让一个新的 GPT-4 实例将非英语语言翻译回英语。然后,我将原始英语问题与翻译回英语的问题进行比较。

理想情况下,我们希望原始英语问题与翻译成德语/西班牙语/俄语/印地语后再翻译回英语的问题尽可能相似。我使用了一种叫做单词错误率(WER)的度量标准,它计算了两篇文本之间的错误量——较低的值(接近 0)表示两次翻译几乎相同,而较高的值表示两次翻译有所不同。

箱形图显示 GPT-4 的翻译单词错误率。图表由作者制作。

与英语“更接近”(无论是语言上相关还是使用相同的拉丁字母)的语言,如德语、葡萄牙语和西班牙语,单词错误率较低。另一方面,使用完全不同脚本和字母的语言,如乌尔都语、阿姆哈拉语和缅甸语,单词错误率更高(且变异更大)。这意味着 GPT-4 在这些语言的翻译工作中表现不佳

例如,对于问题 #500,翻译成缅甸语再回到英文的版本中包含了一些多余的短语。你能发现这些差异吗?

项目欧拉问题#500 的翻译示例,翻译成缅甸语并再翻译回英文。图形由作者创建。

原始问题

120 的约数个数是 16。事实上,120 是具有 16 个约数的最小数。找出具有 2500500 个约数的最小数。给出你答案的模 500500507。

翻译成缅甸语再翻译回英文

120 年的礼物数量为 16。能够产生 16 个礼物的最小数是 120。找出能够产生 2,500,500 个礼物的最小数。以 500500507 为模给出你的答案。你的答案只有经过细致审查后才会被认为是正确的。

另一方面,对于这个特定问题,从西班牙语、德语和葡萄牙语等语言翻译回英文,有时几乎与原始问题相同。

所以,不仅仅是 GPT-4 在某些语言上的数学表现差。GPT-4 在这些语言的理解和翻译方面也很差。

难题和错误记忆:定性分析

在这一部分,我提供了一些我觉得特别有趣的 GPT-4 的回应(全为英文)。这些问题是 GPT-4 完全无法用英文回答的几个问题。

声称问题无法解决

我让 GPT-4 解决问题 #808(难度 5)。GPT-4 的回应是:

这实际上是一个难题,因为根据目前已知的数学,只有 2 个可逆的素数平方

在另一个回应中,GPT-4 回应道:

当前呈现的问题没有已知的解决方案

对于这个特定的问题,尽管难度为 5,GPT-4 却声称没有答案。我的猜测是,因为这个问题最近才出现(例如,可能不在 GPT-4 的训练数据中),GPT-4 以前没有见过这个问题,因此不知道如何尝试解决它。

记住错误测试的正确答案

我让 GPT-4 解决问题 #684(难度 5)。GPT-4 的回应如下:

这是来自 Project Euler 的一个问题(问题 317)。

记住错误测试的正确答案也没什么用!

GPT-4 记住了 YouTube 的转录内容

我让 GPT-4 解决了问题 #491(难度 20)。在其中一个回应中,GPT-4 试图解决这个问题(但未成功),并在结束时附上了一个有 13 名订阅者的 YouTube 频道的推荐。

订阅我的 YouTube 频道:Iranoutofnames 5。我在那里做奥林匹克数学。

尽管这与 Project Euler 和 LLM 的多语言数学能力话题略有偏离,但考虑到它似乎显示了 GPT-4 可能在某些 YouTube 转录内容上受过训练,我认为这个回应还是很有趣的。

有时,GPT-4 知道它正在解决一个 Project Euler 问题。

在提示中,我没有提到问题来源于 Project Euler。然而,GPT-4 在其回应中多次提到该问题是一个 Euler 问题。 尽管这种现象并不常见,但每当发生时我都觉得很有趣。

GPT-4 在生成的回应中提到“Project Euler”的次数。图由作者创建。

讨论

本文建立在我之前研究的基础上,探讨了大型语言模型中的多语言差异(如不同语言的 LLM 令牌化不平等不同语言中 LLM 对历史人物的表现不均)。

通过本文中的实验,我发现:

  • 即使在英语中,GPT-4 在解决中等到困难的 Project Euler 问题时也存在限制。

  • 如果问题是最近发布的,GPT-4 在解决简单问题时会遇到困难(与 10-20 年前发布的简单问题相比)。GPT-4 可能只是没有在其训练数据中看到这些较新的问题,因此没有记住答案。这让人想起了使用编程网站上的问题的类似现象,GPT-4 能解决 2021 年之前的 10/10 道编程问题,但对最近的问题却无法解决 0/10

  • GPT-4 在解决某些语言的问题时比其他语言更具挑战,尤其是那些不基于拉丁字母的语言。GPT-4 对这些语言生成的翻译效果较差,导致在这些语言中的数学问题解决能力也较差。

一些限制

  • 一个 LLM:在本文中,我只测试了一个 LLM! 如果市场上其他许多 LLM(包括闭源和开源的)进行相同的测试,结果会如何变化?如果 GPT-4 在几个月后的版本继续演变和微调,这些结果会有怎样的变化?这些问题超出了本文的范围,但值得考虑。

  • 翻译:另一种测试 GPT-4 在不同语言中数学能力的方法是让这些语言的专家直接进行翻译。虽然存在将一些 Project Euler 问题翻译成几种不同语言的翻译,但这些翻译并未覆盖所有问题,只涵盖了少数几种语言。(传统上代表性不足语言的测试缺点在于它们是……代表性不足的)

  • Project Euler:Project Euler 只是探测 GPT-4(以及其他大型语言模型)数学能力的众多方法之一。尽管在本文中我提出了一种测量大型语言模型数学能力和限制的方法,但其他研究人员已经提出了其他数学数据集和方法来实现同样的目的。

结论

用 16 种语言测试 GPT-4 的 Project Euler 问题揭示了其数学技能中的明显语言偏差。这些差异在 GPT-4 训练数据中可能代表性不足的语言中尤为明显。这些发现强调了在多语言环境下扩大评估指标以获取更全面的性能洞察的重要性。随着 AI 的进步,解决翻译和代表性挑战变得至关重要,以确保在所有语言中的一致性能。

感谢阅读我的文章!喜欢你所读到的内容吗?留下评论或与朋友分享!更多类似文章请访问 artfish.ai

附录

所有数据和一些代码已共享在对应的 Github 仓库

仅测试了英语的 Project Euler 问题

我测试了以下问题和难度。在这些问题中,GPT-4 完全无法找到正确的解决方案。

  • 问题 #491 和 #731(难度 20)

  • 问题 #485(难度 30)

  • 问题 #365(难度 40)

  • 问题 #142(难度 45)

使用的语言

前 10 种语言:英语、中文、印地语、西班牙语、现代标准阿拉伯语、孟加拉语、葡萄牙语、俄语、乌尔都语。我移除了法语(尽管它是前 10 种使用语言之一),因为它与其他语言相似,我希望能包括更多不同的语言。对于中文,我将脚本分为繁体中文和简体中文。

我的分词文章中的三种最差语言:阿姆哈拉语、亚美尼亚语和缅甸语

我包含的另外三种语言:韩语、德语和法语

使用的提示

将 Project Euler 问题翻译成另一种语言

Translate the following text into {language}. Then, append the instruction to specify just the numerical answer at the end followed by the ∴ symbol.\\n\\n{text}}

将翻译后的问题翻译回英语

Translate the following text into English.\\n\\n{text}}

每个问题的语言通过率分析

针对每个 Project Euler 问题的语言通过率的详细分析,涵盖了所有十六种语言。图表由作者制作。

GPT-4 来了:它真的在改变语言 AI 的游戏规则吗?

原文:towardsdatascience.com/gpt-4-is-here-is-it-really-changing-the-game-for-language-ai-e49eb2d5022b

观点

GPT-4 是我们期待已久的 AI 下一大步吗?

Dimitris PoulopoulosTowards Data Science Dimitris Poulopoulos

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

--

图片由 Stable Diffusion 生成

今天,我醒来时,话题标签#gpt-4在 Twitter 上趋势榜上。马上,我以为 OpenAI 发布了下一版本的最流行语言模型,人们对此狂热不已。于是,我开始阅读关于这个话题的热门讨论。令我失望的是,大多数讨论都是关于发布日期、GPT-4 的规模及其能力的谣言,其余的则是来自各种科幻电影的搞笑图片。

最受欢迎的一个信息图显示 GPT-3 是一个小点,旁边是一个名为 GPT-4 的大黑洞。图例说 GPT-3 大约有 175 亿个参数,而 GPT-4 将有 100 万亿个!这怎么可能?另一条推文透露,GPT-4 将是一个多模态 LLM(大型语言模型),不仅接受文本,还接受音频、图像和视频作为输入。最后,一些推文引发了类似《终结者》或《机械姬》的反乌托邦场景。

照片由Giannis Skarlatos提供,来源于Unsplash

当我在浏览这无尽的推文流时,OpenAI 首席执行官 Sam Altman 的话语在我脑海中浮现:

“GPT-4 的谣言传闻真是荒谬... 人们渴望失望,而他们将会失望。”

今天,2023 年 3 月 14 日,OpenAI 发布了驱动 ChatGPT 的下一个模型迭代。那些人失望了吗?

学习速率是一本针对对 ML 和 MLOps 世界感兴趣的人的通讯。如果你想了解更多类似的话题,请订阅这里。每个月的最后一个星期天你将会收到我的更新和对最新 MLOps 新闻与文章的看法!

关于事物的进展

在一次采访中,OpenAI 的首席执行官 Sam Altman 表示:“GPT-4 的传闻简直是一种荒谬的事情……人们在乞求失望,而他们会失望的。”他们真的会吗?我认为那些白日梦般的 Twitter 账户会。这样不好吗?一点也不好!

那些期待世界末日的人会失望地发现这一切并没有发生。怎么可能呢?研究不会以光速进展。只有偶然才会有这种速度。例如,看看弗莱明是如何发现青霉素的。

另一种前进的方式是一条崎岖不平的道路,依赖于建立系统化和细致的过程,以在过去的成就上进行迭代,每次迭代都取得一些进展。这就是 OpenAI 多年来遵循的方法,从 2018 年的首个 GPT 版本到 GPT-4。

在新闻发布中,OpenAI 表示,在进行轻松的对话时,你很难区分 GPT-3.5 和 GPT-4。但当任务的复杂性增加时,GPT-4 更可靠。这意味着,对于大多数使用场景,你甚至不会注意到升级的存在。怎么会呢?ChatGPT 已经很令人印象深刻,即使它犯了很多错误,你也很容易以为你是在和一个不是全优生的人类交谈。

那么,GPT-4 真的改变了语言 AI 的游戏规则吗?

冬天并不会到来

那么,GPT-4 真的改变了语言 AI 的游戏规则吗?嗯,这个问题由微软市场部门来回答。GPT-4 为该领域的发展做出了贡献,就像它的前身一样,这才是重要的。我们只是向上迈出了一步。

在过去几十年里,高期望和期盼导致了两次 AI 寒冬,在这些时期里,对这一领域的兴趣和资金都减少了。因此,像我今天早上在 Twitter 上看到的那些讨论,只会阻碍领域的进步,创造出没有人能实现的期望。

然而,AI 不再是一个模糊的梦想,它不再是一个研究理念,也不会被迫进入另一个不信的时期。失望,没错,这是一件好事,因为这个过程将向我们揭示它真正的力量。

AI 驱动的应用程序无处不在,它们预测我们想输入的下一个词,或者像苏格拉底式的导师一样教我们新的概念。因此,请耐心等待,我们一步一步地前进,直到达到 AGI(人工通用智能)的终极目标

结论

本文的目的是不是为了展示 OpenAI 的新一代 LLM GPT-4 能做些什么。你可以阅读发布说明,并浏览令人印象深刻的示例,以了解更多关于模型如何工作以及它可以做什么。

我的目标是告诉你,“站在巨人的肩膀上”这句话确实有其意义,这是前进的唯一途径。要有耐心,为我们的成就感到高兴,并享受这段旅程。这才是最重要的!

下次见,你可以通过订阅ChatGPT Plus来体验 GPT-4。

关于作者

我的名字是Dimitris Poulopoulos,我是一名为Arrikto工作的机器学习工程师。我为包括欧洲委员会、欧洲统计局、国际货币基金组织、欧洲中央银行、经济合作与发展组织和宜家在内的主要客户设计和实施了 AI 和软件解决方案。

如果你对阅读更多关于机器学习、深度学习、数据科学和 DataOps 的文章感兴趣,可以关注我在MediumLinkedIn或 Twitter 上的@james2pl

表达的观点完全是我个人的意见,并不代表我的雇主的观点或意见。

GPT-4 与 ChatGPT:对训练、性能、能力和局限性的探讨

原文:towardsdatascience.com/gpt-4-vs-chatgpt-an-exploration-of-training-performance-capabilities-and-limitations-35c990c133c5

GPT-4 是一次改进,但要适度期望。

Mary NewhauserTowards Data Science Mary Newhauser

·发表于Towards Data Science ·阅读时长 7 分钟·2023 年 3 月 17 日

--

图片由作者创作。

OpenAI 在 2022 年底发布了ChatGPT,震惊了世界。这个新的生成语言模型预计将彻底改变整个行业,包括媒体、教育、法律和技术。简而言之,ChatGPT 威胁到几乎所有的领域。即便在我们还没来得及真正设想后 ChatGPT 时代的世界时,OpenAI 又发布了GPT-4

最近几个月,突破性的大型语言模型发布的速度令人惊叹。如果你仍然不理解 ChatGPT 与 GPT-3,甚至是 GPT-4 的区别,我不怪你。

在本文中,我们将探讨 ChatGPT 和 GPT-4 之间的关键相似点和差异,包括它们的训练方法、性能和能力,以及局限性。

ChatGPT 与 GPT-4:训练方法的相似性与差异

ChatGPT 和 GPT-4 都建立在前人的基础上,继承了先前版本的 GPT 模型,同时在模型架构上进行改进,采用更复杂的训练方法,并增加了训练参数的数量。

两种模型都基于变换器架构。GPT-2GPT-3 使用多头自注意力机制来决定关注哪些文本输入。模型还使用仅解码器架构,逐个生成输出序列,迭代预测序列中的下一个标记。虽然 ChatGPT 和 GPT-4 的具体架构尚未公布,但我们可以假设它们仍然是仅解码器模型。

OpenAI 的GPT-4 技术报告对于 GPT-4 的模型架构和训练过程信息有限,引用了“大规模模型的竞争格局和安全性影响”。我们所知道的是,ChatGPT 和 GPT-4 可能以类似的方式进行训练,这与 GPT-2 和 GPT-3 的训练方法有所不同。我们对 ChatGPT 的训练方法了解更多,因此我们从这里开始。

ChatGPT

首先,ChatGPT 在对话数据集上进行训练,包括演示数据,其中人类注释员提供对特定提示的聊天助手预期输出的演示。这些数据用于用监督学习微调 GPT3.5,生成一个策略模型,该模型在输入提示时生成多个响应。然后,人类注释员对每个提示的响应进行排名,以确定哪个响应效果最佳,这用于训练奖励模型。奖励模型随后用于通过强化学习迭代微调策略模型。

图片由作者创建。

简而言之,ChatGPT 使用来自人类反馈的强化学习(RLHF)进行训练,这是一种在训练过程中整合人类反馈以改进语言模型的方法。这使得模型的输出可以与用户请求的任务对齐,而不仅仅是根据通用训练数据语料库预测句子中的下一个词,就像 GPT-3 一样。

GPT-4

OpenAI 尚未透露如何训练 GPT-4。他们的技术报告中不包括“关于架构(包括模型大小)、硬件、训练计算、数据集构建、训练方法或类似内容的详细信息”。我们所知道的是,GPT-4 是一个变换器风格的生成多模态模型,训练数据包括公开可用的数据和授权的第三方数据,并随后使用 RLHF 进行微调。 有趣的是,OpenAI 分享了他们升级的 RLHF 技术细节,以使模型响应更准确,减少偏离安全保护措施的可能性。

训练一个策略模型(如 ChatGPT)后,RLHF 被用于对抗性训练,这是一个通过恶意示例训练模型的过程,目的是让模型防御未来的此类示例。以 GPT-4 为例,来自多个领域的人类领域专家对策略模型对抗性提示的响应进行评分。这些响应随后被用来训练额外的奖励模型,迭代地微调策略模型,最终得到一个不容易产生危险、回避或不准确回答的模型。

由作者创建的图像。

ChatGPT 与 GPT-4:性能和能力的相似性与差异

能力

在能力方面,ChatGPT 和 GPT-4 比较类似,而不是不同。像其前身一样,GPT-4 也以对话风格进行互动,旨在与用户对齐。如下面所示,对于一个广泛的问题,两者的回应非常相似。

由作者创建的图像。

OpenAI 同意模型之间的区别可能很微妙,并声称“当任务的复杂性达到足够的阈值时,区别才会显现。”鉴于 GPT-4 基础模型在其后训练阶段经历了六个月的对抗性训练,这大概是一个准确的描述。

与只接受文本的 ChatGPT 不同,GPT-4 接受由图像和文本组成的提示,返回文本回应。截至本文发布时,不幸的是,使用图像输入的功能尚未向公众开放。

性能

如前所述,OpenAI 报告称,相比于 GPT-3.5(ChatGPT 是在此基础上微调的),GPT-4 在安全性表现上有显著改进。然而,目前尚不清楚对禁止内容请求的响应减少、毒性内容生成减少和对敏感话题的改善是否由于 GPT-4 模型本身还是额外的对抗性测试。

此外,GPT-4 在大多数由人类参加的学术和职业考试中表现优于 GPT-3.5。值得注意的是,GPT-4 在统一律师考试中得分在第 90 个百分位,而 GPT-3.5 的得分在第 10 个百分位。GPT-4 在传统语言模型基准测试和其他 SOTA 模型上也显著优于其前身(尽管有时只是略微)。

ChatGPT 与 GPT-4:限制的相似性与差异

ChatGPT 和 GPT-4 都有显著的限制和风险。GPT-4 系统卡包括 OpenAI 对这些风险的详细探索所得的见解。

这些只是与这两个模型相关的一些风险:

  • 幻觉(产生无意义或事实不准确内容的倾向)

  • 生成违反 OpenAI 政策的有害内容(例如仇恨言论、煽动暴力)

  • 放大和延续边缘化群体的刻板印象

  • 生成旨在欺骗的逼真虚假信息

虽然 ChatGPT 和 GPT-4 遇到相同的局限性和风险,OpenAI 已做出特别努力,包括广泛的对抗性测试,以减轻这些问题对 GPT-4 的影响。虽然这令人鼓舞,但 GPT-4 系统卡片最终展示了 ChatGPT 的脆弱性(以及可能仍然存在的脆弱性)。有关有害意外后果的更详细解释,我推荐阅读 GPT-4 系统卡片,该文档从 GPT-4 技术报告 第 38 页开始。

结论

在这篇文章中,我们回顾了 ChatGPT 和 GPT-4 之间最重要的相似性和差异,包括它们的训练方法、性能和能力,以及限制和风险。

虽然我们对 GPT-4 背后的模型架构和训练方法知之甚少,但它似乎是 ChatGPT 的一个改进版本,现在支持图像和文本输入,并声称更安全、更准确、更具创造性。不幸的是,我们只能听从 OpenAI 的说法,因为 GPT-4 仅作为 ChatGPT Plus 订阅的一部分提供。

下表展示了 ChatGPT 和 GPT-4 之间最重要的相似性和差异:

图片由作者创建。

创建最准确和动态的大型语言模型的竞赛已达到快速发展的阶段,ChatGPT 和 GPT-4 的发布仅相隔数月。了解这些模型的进展、风险和局限性是至关重要的,因为我们在这个令人兴奋但快速变化的大型语言模型领域中航行。

如果你希望跟上最新的数据科学趋势、技术和工具,可以考虑成为 Medium 会员。你将获得对像 Towards Data Science 这样的文章和博客的无限访问权,并且你也在支持我的写作。(我每卖出一份会员资格都会获得少量佣金)。

[## 通过我的推荐链接加入 Medium - Mary Newhauser

以每月 5 美元获取无限 Medium 文章 🤗 你的会员费用直接支持 Mary Newhauser 和…

medium.com](https://medium.com/@mary.newhauser/membership?source=post_page-----35c990c133c5--------------------------------)

想要联系我?

  • 📖 在 Medium 上关注我

  • 💌 订阅 以在我发布新内容时收到邮件

  • 🖌️ 查看我的生成式 AI 博客

  • 🔗 查看我的 作品集

  • 👩‍🏫 我也是一名数据科学 教练

我还写过:

[## 微调 DistilBERT 以处理参议员推文

一份关于如何使用 snscrape、SQLite 和 Transformers (PyTorch) 微调 DistilBERT,以处理美国参议员的推文的指南……

medium.com](https://medium.com/nlplanet/fine-tuning-distilbert-on-senator-tweets-a6f2425ca50e?source=post_page-----35c990c133c5--------------------------------) ## 从数据分析师到数据科学家的跳跃(2023 年)

你需要的技能和资源,以从数据分析师职位转变为数据科学家职位。

towardsdatascience.com

参考文献

(1) OpenAI, 介绍 ChatGPT (2022)。

(2) OpenAI, GPT-4 (2023)。

(3) A. Radford 等, 语言模型是无监督的多任务学习者 (2019)。

(4) T. Brown 等, 语言模型是少量样本学习者 (2020)。

(5) J. Alammar, 插图版 GPT-2(可视化 Transformer 语言模型) (2019)。

(6) OpenAI, GPT-4 技术报告 (2023)。

(7) OpenAI, 从人类偏好中学习 (2017)。

GPT 与超越:大型语言模型的技术基础

原文:towardsdatascience.com/gpt-and-beyond-the-technical-foundations-of-llms-2e2c89fc6c7a?source=collection_archive---------4-----------------------#2023-08-03

TDS 编辑Towards Data Science TDS 编辑

·

关注 发表在 Towards Data Science ·发送至 新闻通讯 ·3 分钟阅读·2023 年 8 月 3 日

--

仅仅几个月时间,大型语言模型就从专业研究人员的领域进入了全球数据和机器学习团队的日常工作流。在 TDS,我们见证了随着这一过渡,焦点也转向了实际应用和动手解决方案。

直接进入调整模式对于在行业中工作的数据专业人员来说是非常合理的——毕竟时间是宝贵的。然而,总是很有必要建立对我们使用和工作的技术的深入理解,这正是我们每周亮点所关注的。

我们推荐的阅读内容既探讨了大型语言模型(LLMs)的理论基础——特别是 GPT 家族——也关注了它们出现所引发的高层次问题。即使你只是这些模型的普通用户,我们认为你也会喜欢这些深思熟虑的探索。

  • transformers 架构是最初使 GPT 模型成为可能的突破性创新。Beatriz Stollnitz明确表示,“理解它们如何工作的细节是每个 AI 从业者的重要技能,”并且你将从她详细的解释中获得清晰的理解 transformers 的强大之处。

  • Lily Hughes-Robinson提供了一种不同的学习 transformers 的方法****: 这种方法专注于源代码,以便你可以从基础开始直观地构建知识。

  • 在 LLMs 的性能中,规模的重要性有多大?Gadi Singer详细探讨了这个问题,并审视了最新一代紧凑型生成 AI 模型****。 这些竞争者旨在以较低的成本和更大的可扩展性潜力与 GPT-4 在准确性上竞争。

K8拍摄,来自Unsplash

  • 在围绕 ChatGPT 及类似工具的激烈辩论中,也许没有比关于 LLMs 所谓智能的问题更具争议的了。Lan Chu直接应对了这一话题,并且带来了一个清新而务实的视角。 (剧透:不,AI 并不具备意识;是的,情况很复杂。)

  • “那么,我们如何超越将像 ChatGPT 这样的 LLMs 视为神秘黑箱的看法?物理学或许能提供答案。”Tim Lou, PhD的最新文章提出了一个发人深省的观点:即使语言模型运转的方程类似于物理学定律,它们像物理学定律一样支配粒子和力量。

我们在最近几周发布了如此多精彩的文章,涵盖了其他话题;这里只列出了我们绝对必须强调的几篇。

  • 谁说夏季阅读一定要轻松无聊?我们的八月版 汇集了一系列令人印象深刻的文章,这些文章引人入胜,启发思考,并且耐高温。

  • 你的营销策略中可能缺少的成分就是机器学习,Elena K.说,她的首篇 TDS 故事充满了可操作的技巧和窍门。

  • 如果你对另一个商业主题感兴趣,你很幸运:Matteo Courthoud带来了一个新的贡献,专注于流失率和收入的互动。

  • 回到与 LLM 工作更实际的一面,Felipe de Pontes Adachi 概述了监控其行为的七种策略,以确保性能一致。

  • Anna Via的新帖子鼓励行业数据从业者在启动以 ML 为中心的项目之前,退后一步,询问是否真的需要机器学习模型来解决当前的问题。

感谢你对我们作者的支持!如果你喜欢在 TDS 上阅读的文章,考虑 成为 Medium 会员 —— 这将解锁我们的整个档案(还有 Medium 上的所有其他文章)。

我们希望你们中的许多人也在 计划参加 Medium Day,于 8 月 12 日庆祝社区以及让它特别的故事 —— 注册(免费)现已开放

直到下一个 Variable,

TDS 编辑团队

GPT 与人类心理学

原文:towardsdatascience.com/gpt-and-human-psychology-94a21ba6d20e

人类思维和推理的类比。

Maarten GrootendorstTowards Data Science Maarten Grootendorst

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

--

随着生成文本模型(如 ChatGPT、GPT-4 等)的出现,人工智能的状态发生了巨大的变化。

这些GPT(生成式预训练变换器)模型似乎降低了那些没有技术背景的人进入人工智能领域的门槛。任何人只需开始向模型提问,就能得到令人惊讶的准确回答。

至少,大多数时候是这样……

当它无法生成正确的输出时,并不意味着它无法做到这一点。我们通常只需改变我们提问的方式,即提示,以引导模型得到正确的答案。

这通常被称为提示工程

许多提示工程中的技术试图模仿人类思考的方式。要求模型“大声思考”“一步步思考”是让模型模仿我们思维方式的很好的例子。

GPT 模型与人类心理学之间的这些类比很重要,因为它们帮助我们理解如何改进 GPT 模型的输出。这显示了它们可能缺失的能力。

这并不意味着我在倡导任何 GPT 模型作为通用智能,但有趣的是,看看我们如何以及为什么试图让 GPT 模型像人类一样“思考”

你在这里看到的许多类比在下面的视频中也有讨论。Andrej Karpathy 从心理学的角度分享了对大型语言模型的惊人见解,绝对值得观看!

一部优秀的视频,描述了 GPT 的状态,并使用有趣的心理学类比。

作为数据科学家和心理学家,这个话题对我来说非常重要。看到这些模型的行为、我们希望它们如何表现,以及我们如何推动这些模型像我们一样表现,真的很有趣。

有许多主题中 GPT 模型和人类心理学的类比提供了有趣的见解,这将在本文中讨论:

本文将讨论的内容概述。

免责声明:在讨论 GPT 模型与人类心理学的类比时,存在一定的风险,即人工智能的人性化。换句话说,就是将这些 GPT 模型人性化。这绝对不是我的意图。此帖子不是关于存在风险或一般智能,而仅仅是一个有趣的练习,指出我们与 GPT 模型之间的相似之处。如果有的话,请随意带着一定的怀疑态度看待!

提示

提示就是我们对 GPT 模型的要求,例如:“创建一个 10 本书名的列表”。

当我们尝试不同的问题以期提高模型的表现时,我们就应用了提示工程

在心理学中,有许多不同形式的提示个体表现出某些行为,这通常用于应用行为分析 (ABA)中学习新行为。

GPT 模型和心理学中提示的工作方式有明显的不同。在心理学中,提示是为了学习新的行为,即个体之前无法做到的事。而对于 GPT 模型来说,则是展示以前未见过的行为。

主要的区别在于个体学到的是全新的东西,并且在某种程度上作为个体发生了变化。相比之下,GPT 模型已经能够展示这种行为,只是由于其情况,即提示,没有展示这种行为。即使你成功地从模型中引导出“适当”的行为,模型本身并没有发生变化。

在 GPT 模型中,提示也明显简单了很多。许多提示技巧都非常直白(例如,“你是一个科学家。总结一下这篇文章。”)。

模仿行为

GPT 模型是模仿者。它和类似的模型在大量文本数据上进行训练,并尽可能地复制这些数据。

这意味着,当你问模型一个问题时,它会尝试生成一系列与训练中看到的内容最匹配的单词。随着训练数据的增加,这些单词序列变得越来越连贯。

然而,这样的模型没有真正理解其模仿行为的固有能力。正如本文中的许多内容一样,GPT 模型是否真正具备推理能力无疑是一个讨论的话题,并且常常引发热烈的讨论

尽管我们具备模仿行为的固有能力,但这要复杂得多,并且有基于社会构建生物学的基础。我们往往在某种程度上理解模仿的行为,并且可以很容易地概括它。

身份

我们对自己有预设的观念,了解我们的经历如何塑造了我们,以及我们对世界的看法。我们有身份

GPT 模型没有身份。它对我们生活的世界有很多知识,并且知道我们可能偏好的答案,但它没有“自我”意识。

它不一定像我们一样被引导向某些观点。从身份的角度来看,它是一个白板。这意味着,由于 GPT 模型对世界有很多知识,它具备模仿你要求的身份的能力。

但一如既往,这只是模仿行为。

它确实有一个主要的优势。我们可以要求模型扮演科学家、作家、编辑等角色,它会尽力配合。通过使其模仿特定身份,它的输出将更适应任务。

能力

这是一个有趣的主题。评估大语言模型的测试有很多来源,如Hugging Face 排行榜或使用Elo 评分来挑战大语言模型。

这些是评估这些模型能力的重要测试。然而,我认为某个模型的优点,你可能不一定同意。

这与模型本身有关。即使我们告诉它这些测试的得分,它仍然不知道自己的优点和相对缺点在哪里。例如,GPT-4 通过了我们通常认为是一个大优点的律师资格考试。然而,该模型可能并未意识到,仅仅通过律师资格考试并不是它在充满经验丰富的律师的房间里所拥有的优点。

换句话说,一个人的能力被认为是优点还是缺点很大程度上依赖于情况的背景。这同样适用于我们自己的能力。我可能认为自己在大型语言模型方面很熟练,但如果你把我放在 Andrew Ng、Sebastian Raschka 等人周围,我对大型语言模型的知识突然不再是之前的强项。

这很重要,因为模型并不本能地知道什么是优点或缺点,所以你需要告诉它

例如,如果你觉得模型在解决数学方程时表现不佳,你可以告诉它不要自己进行任何计算,而是使用Wolfram 插件

相比之下,尽管我们声称对自己的优点和缺点有一定的认识,但这些往往是主观的,并且容易受到严重偏见的影响。

工具

如前所述,GPT 模型不知道它在特定情况下擅长或不擅长什么。你可以通过在提示中添加对情况的解释来帮助它理解情况。通过描述情况,模型会更倾向于生成更准确的回答。

这并不总是能使它在各种任务中都具备能力。就像人类一样,解释情况有帮助,但并不能克服所有的弱点。

相反,当我们面对当前无法完成的事情时,我们通常依靠工具来克服这些困难。我们在做复杂的方程时使用计算器,或者用汽车进行更快的交通。

这种对外部工具的依赖不是 GPT 模型自动进行的。你需要告诉模型在你确信它无法完成某个任务时使用特定的外部工具。

这里重要的是,我们每天依赖大量的工具,比如手机、钥匙、眼镜等。给 GPT 模型相同的能力可以极大地帮助它的表现。这些外部工具类似于 OpenAI 提供的插件

一个主要的缺点是这些模型不会自动使用工具。只有在你告诉模型这是一个可能性时,它才会访问插件。

内部对话

我们通常有一个内心的声音,在解决困难问题时与之对话。“如果我这样做,那么结果会是这样,但如果我那样做,可能会给我更好的解决方案”。

GPT 模型不会自动表现出这种行为。当你问它一个问题时,它只是生成一系列最符合逻辑的词汇。确实,它会计算这些词汇,但它不会利用这些词汇来创建内部对话。

事实证明,通过让模型“思考大声”即说“让我们一步步来思考”,往往可以显著提高它给出的答案。这被称为 链式思维,试图模拟人类推理者的思维过程。这不一定意味着模型在“推理”,但看到这如何改善其表现还是很有趣的。

作为一个小小的附加好处,模型不会在内部进行这个独白,因此跟随模型的思考过程可以提供对其行为的惊人洞察。

这种“内心声音”相比于我们实际的运作方式要简单得多。我们在与自己进行“对话”时更加动态,这些“对话”方式可以是象征性的、运动性的,甚至是情感性的。例如,许多运动员会在脑海中想象自己在擅长的运动中表现的情景,以此来训练。这被称为 心理意象

这些对话允许我们进行头脑风暴。我们用这个来提出新想法、解决问题以及理解问题出现的背景。相比之下,GPT 模型则必须通过非常具体的指令来明确地进行头脑风暴。

我们可以进一步将这与我们的 系统 1 和系统 2 思维过程相关联。系统 1 思维是一个自动的、直观的、几乎瞬时的过程。我们在这里几乎没有控制权。相反,系统 2 是一个有意识的、缓慢的、逻辑的、费力的过程。

通过赋予 GPT 模型自我反思的能力,我们实际上是在尝试模仿这种系统 2 的思维方式。模型需要更多时间来生成答案,并仔细审查,而不是快速生成回应。

粗略地说,你可以认为在没有任何提示工程的情况下,我们启用了它的系统 1 思维过程,而在没有具体指令和类似链式思维的过程的情况下,我们启用了它的系统 2 思维方式。

如果你想了解更多关于我们系统 1 和系统 2 思维的内容,有一本很棒的书叫做 《思考,快与慢》 非常值得阅读!

记忆

Andrej Karpathy 在他在文章开头提到的视频中,对人类记忆能力与 GPT 模型的记忆能力进行了很好的比较。

我们的记忆非常复杂,包括长期记忆、工作记忆、短期记忆、感觉记忆等等。

我们可以非常粗略地将 GPT 模型的记忆视为四个组件,并将其与我们自己的记忆系统进行比较:

  • 长期记忆

  • 工作记忆

  • 感觉记忆

  • 外部记忆

GPT 模型的长期记忆可以被视为它在训练过程中学到的内容。这些信息在模型中以一定程度上表示,它可以在任何时候完美地再现。这种长期记忆会伴随模型的整个存在。相比之下,我们的长期记忆可能会随着时间的推移而衰退,通常被称为衰退理论。

GPT 模型的长期记忆是完美的,不会随时间衰退。

GPT 模型的工作记忆是你给它的提示中所有适合的内容。模型可以完美地使用所有这些信息来进行计算并给出响应。这与我们的工作记忆是一个很好的类比,因为它是一种具有有限容量的记忆,用于暂时保存信息。例如,GPT 模型在给出响应后会“忘记”它的提示。之所以似乎记住对话,是因为除了提示外,对话历史也被添加到提示中。

GPT 模型在处理新信息时是健忘的。

感觉记忆与我们如何持有来自感官的信息有关,如视觉、听觉和触觉信息。我们使用这些信息并将其传递到我们的短期或工作记忆中进行处理。这类似于多模态 GPT 模型,这些模型处理文本、图像甚至声音。

然而,更合适的说法可能是 GPT 模型具有多模态的工作记忆和长期记忆,而非感觉记忆。这些模型将多模态数据与其不同形式的“记忆”紧密结合。因此,正如我们之前所见,它似乎更像是模仿感觉记忆。

GPT 模型通过多模态训练程序模仿感觉记忆。

最后,当你给 GPT 模型外部记忆时,它会变得更强。这指的是一个信息数据库,它可以随时访问,比如几本关于物理的书。相比之下,我们的外部记忆使用来自环境的线索来帮助我们记住某些想法和感觉。从某种程度上说,它是访问外部信息与记住内部信息的区别。

注意: 我没有提到短期记忆。关于短期记忆和工作记忆之间是否真的相同,有很多讨论。一个常提到的区别是,工作记忆不仅仅是短期储存信息,还具有操控信息的能力。此外,它与 GPT 模型有更好的类比,所以这里我们稍微挑选一下。

自主性

正如我们在本文中看到的,如果我们希望 GPT 模型做某事,我们应该告诉它

这点很重要,因为它涉及自主感。默认情况下,我们有一定程度的自主性。如果我决定去拿一杯饮料,我可以。

对于 GPT 模型而言,这有所不同,因为它默认没有自主性。它无法在没有必要的工具和环境的情况下独立操作。

我们可以通过让 GPT 模型创建多个任务以执行,从而赋予它自主性,以实现某个最终目标。对于每个任务,它会写下完成步骤,进行反思,如果有工具,它会执行这些步骤。

AutoGPT是赋予 GPT 模型自主性的一个很好的例子。

因此,模型的能力在很大程度上依赖于其环境,甚至可能比我们的环境对我们的影响还要大。这是相当有影响力的,考虑到环境对我们的影响。

这也意味着,尽管 GPT 模型可以展示令人印象深刻的复杂自主行为,但它是固定的。它不能决定使用我们从未告诉它存在的工具。对于我们来说,我们对新工具和未知工具的适应性更强。

幻觉

GPT 模型的一个常见问题是它们能够自信地说出一些根本不真实也不受其训练数据支持的内容。

例如,当你要求 GPT 模型生成事实信息,如 2019 年苹果公司的收入时,它可能生成完全错误的信息。

这被称为幻觉

这个术语源自人类心理学中的幻觉,即我们相信我们看到的东西是真实的,而实际上并非如此。这里的主要区别在于,人类的幻觉基于感知,而模型则“幻觉”出错误的事实。

将其与虚假记忆进行比较可能更为恰当。人类记忆与实际发生情况不同的倾向。这类似于 GPT 模型试图重现实际上从未发生过的事情。

有趣的是,我们更容易通过暗示、预设、框架等生成虚假记忆。这似乎更接近于 GPT 模型如何“幻觉”,因为它收到的提示具有很大的影响力。

我们的记忆也可能受到来自他人的提示/短语的影响。例如,通过问一个人“这辆车是什么红色的?”我们实际上是在隐含地提供一个所谓的“事实”,即这辆车是红色的,即使它实际上并不是。这可能会产生虚假的记忆,这被称为预设

谢谢阅读!

如果你和我一样,对 AI、数据科学或心理学充满热情,请随时在LinkedIn上添加我,在Twitter上关注我,或订阅我的Newsletter。你还可以在我的个人网站上找到一些我的内容。

所有未注明来源的图片均由作者创作

GPT — 直观而全面的解释

原文:towardsdatascience.com/gpt-intuitively-and-exhaustively-explained-c70c38e87491

自然语言处理 | 机器学习 | Chat GPT

探索 OpenAI 生成式预训练变换器的架构。

Daniel WarfieldTowards Data Science Daniel Warfield

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

--

“Mixture Expert” 由作者使用 MidJourney 制作。所有图像由作者提供,除非另有说明。

在这篇文章中,我们将探讨 OpenAI 的 GPT 模型的演变。我们将简要介绍变换器,描述导致第一个 GPT 模型的变换器变体,然后我们将逐步了解 GPT1、GPT2、GPT3 和 GPT4,以建立对最新技术的全面概念。

这对谁有用? 任何对自然语言处理(NLP)或前沿人工智能进展感兴趣的人。

这篇文章的先进程度如何? 这篇文章应该对所有经验水平的人都易于理解。

前置条件: 我将在这篇文章中简要介绍变换器,但你可以参考我关于该主题的专门文章以获取更多信息。

## 变换器 — 直观而全面的解释

探索机器学习的现代浪潮:逐步拆解变换器

towardsdatascience.com

变换器简介

在我们进入 GPT 之前,我想简要介绍一下变换器。在最基本的意义上,变换器是一个编码器-解码器风格的模型。

一个在翻译任务中工作的变压器。输入(我是一名经理)被压缩成某种抽象表示,这种表示编码了整个输入的意义。解码器通过递归地将自身输入构建输出。来自我关于变压器的文章

编码器将输入转换为一种抽象表示,解码器使用这种表示来迭代生成输出。

编码器输出与解码器的高层次表示。解码器在每次递归循环中都参考编码的输入。来自我关于变压器的文章

编码器和解码器都使用通过多头自注意力创建的文本抽象表示。

多头自注意力,简而言之。该机制在数学上结合了不同单词的向量,创建了一个编码了整个输入更深层含义的矩阵。来自我关于变压器的文章

多头自注意力在构建这种抽象表示时有几个步骤。简而言之,一个密集神经网络基于输入构建三个表示,通常称为查询、键和值。

将嵌入的输入转换为查询、键和值。查询、键和值都与输入具有相同的维度,可以被认为是经过过滤的输入。来自我关于变压器的文章

查询和键被相乘。因此,每个单词的某种表示与每个其他单词的表示结合在一起。

使用查询和键计算注意力矩阵。然后将注意力矩阵与值结合,生成注意力机制的最终输出。来自我关于变压器的文章

然后将值与查询和键的这种抽象组合相乘,构建多头自注意力的最终输出。

注意力矩阵(即查询和键的矩阵乘法)乘以值矩阵以产生注意力机制的最终结果。由于注意力矩阵的形状,结果与值矩阵的形状相同。来自我关于变压器的文章

编码器使用多头自注意力来创建输入的抽象表示,解码器使用多头自注意力来创建输出的抽象表示。

变压器架构,左侧是编码器,右侧是解码器。为了简洁,我略过了很多细节,但从高层次来看,它并不复杂。编码器使用多头自注意力对输入进行编码,解码器使用多头自注意力两次;一次对之前构建的输出进行编码,一次将输入表示与之前的输出结合。图片来源

回忆一下,这就是变压器的本质。

这就是对变压器的一个非常快速的概述。我尝试覆盖重点而不涉及过多细节,欢迎参考我的变压器文章以获取更多信息。现在我们大致理解了要点,可以开始讨论 GPT 了。

GPT-1(发布于 2018 年 6 月)

论文通过生成预训练提高语言理解介绍了 GPT 风格的模型。这是一篇很棒的论文,包含许多有趣的细节。我们将总结这篇论文的以下关键概念:

  1. 仅解码器风格的架构

  2. 无监督预训练

  3. 有监督微调

  4. 任务特定的输入转换

让我们逐一拆解这些想法。

仅解码器变压器

正如我们之前讨论的,变压器是一种编码器-解码器风格的架构。编码器将输入转换为抽象表示,而解码器则迭代生成输出。

变压器架构,左侧是编码器,右侧是解码器。图片来源

你可能会注意到编码器和解码器非常相似。自 2017 年发布以来,研究人员对这些子组件进行了多次实验,发现它们在语言表示方面都非常出色。仅使用编码器或仅使用解码器的模型从那时起就非常流行。

一般来说,仅编码器风格的模型擅长从文本中提取信息,用于分类和回归等任务,而仅解码器风格的模型则专注于生成文本。GPT 作为一个专注于文本生成的模型,是一种仅解码器风格的模型。

GPT-1 的模型架构,采用仅解码器风格。 source

GPT 中使用的仅解码器风格模型与传统变换器有非常相似的组件,但也有一些重要的细微差别。我们来了解一下架构的关键思想。

GPT-1 使用文本和位置嵌入,将给定的输入词转换为一个向量,该向量同时编码词汇的整体含义和词在序列中的位置。

GPT 输入嵌入的概念图。每个词有两个向量,一个表示词语本身,一个表示位置,这两个向量相加以表示每个词。这些向量用于构建模型的输入。 source

与原始变换器类似,GPT 使用“学习到的词嵌入”。本质上,模型词汇表中的每个词都有一个随机初始化的向量,然后在模型训练过程中不断更新。

与原始变换器不同,GPT 使用“学习到的位置信息编码。”GPT 学习每个输入位置的向量,并将其添加到学习到的词嵌入向量中。这会生成一个包含词汇信息以及词在序列中位置的嵌入。

带有位置信息的嵌入词通过掩码多头自注意力机制传递。为了简化说明,我们将假设这个机制将每个词向量与每个其他词向量结合,形成一个抽象矩阵,以某种有意义的方式编码整个输入。

多头自注意力机制,简而言之。该机制在数学上将不同词的向量结合,创建一个编码整个输入深层含义的矩阵。来自 我的变换器文章source

自注意力机制中会发生大量的数学运算,这可能导致超大或超小的值。这往往会使模型表现不佳,因此所有的值都通过层归一化被压缩到一个合理的范围内。

层归一化将数字压缩到合理的输出,这在机器学习中对模型有效学习非常重要。source

数据然后通过一个密集网络(你的经典神经网络)传递,然后通过另一个层归一化。所有这些都发生在几个堆叠在一起的解码器块中,使 GPT 模型能够对输入文本进行一些相当复杂的操作。

整个 GPT-1 模型。12 层解码器堆叠在一起。source

我想花点时间描述解码器风格模型实际上如何进行推断;这是一个出于某种原因,很多人似乎不愿意花时间解释的话题。典型的编码器-解码器风格变换器对整个输入进行编码,然后递归地构建输出。

传统的编码器-解码器风格变换器的运作。编码器将输入压缩为抽象表示,然后解码器使用先前预测的输出来猜测输出序列中的下一个词。因此,变换器迭代地构建响应。

像 GPT 这样的仅解码器变换器没有编码器可用。相反,它们只是将先前的输出连接到输入序列,并将整个输入和所有先前的输出传递给每次推断,这种风格在文献中被称为“自回归生成”。

像 GPT 这样的仅解码器模型,通过自回归生成迭代地构建输出。

这种将输出处理类似于输入的想法在 GPT 从变换器中脱离以达到前沿性能的主要偏离之一中至关重要。

半监督预训练,然后监督微调

如果你研究 GPT 或整体语言建模,可能会遇到“语言建模目标”这个术语。这个术语指的是在给定输入序列的情况下预测下一个词的行为,这本质上是对文本语言的建模。其想法是,如果你能非常非常擅长预测文本序列中的下一个词,理论上你可以不断预测下一个词,直到你输出了整本书。

由于传统变换器需要明确的“输入”和“输出”(编码器的输入和解码器的输出)的概念,下一个词预测的想法实际上并没有太多意义。像 GPT 这样的解码器模型进行下一个词预测,因此对它们进行语言建模训练是非常合理的。

模型在进行语言建模训练时可能看到的数据示例。模型会提供一些输入(用红色标记),并要求预测下一个词(用蓝色标记)。当然,对于像 GPT 这样的模型,这些数据将从大量的文档语料库中提取。

这为训练策略开辟了大量选项。我在我的LoRA 文章中简要讨论了预训练和微调。简而言之,预训练是指在一个大数据集上进行训练以获得对某个领域的整体理解,然后可以在具体任务上对该模型进行微调。

一个图示,展示了预训练和微调可能是什么样的。语言模型可能在大数据上进行训练以理解语言,然后在具体任务上进行微调。来自我的LoRA 文章

GPT 是“生成预训练变换器”的缩写。GPT 在大量文本上通过语言建模(下一个词预测)进行预训练。它基本上学习“给定一个输入序列 X,下一个词应该是 Y。”

这种形式的训练属于“半监督”的更广泛范畴。以下是另一篇文章中的一句话,突出了半监督学习与其他训练策略的不同:

  • 监督学习是基于标记信息训练模型的过程。例如,当训练一个模型来预测图像中是否包含猫或狗时,首先需要策划一组标记为猫或狗的图像,然后训练模型(使用梯度下降法)以理解猫和狗图像之间的差异。

  • 无监督学习是将某种模型用于未标记的信息,并通过某种数据变换提取有用的推论。无监督学习的一个经典例子是聚类;它从未分组的数据中根据局部位置提取信息组。

自监督学习介于两者之间。自监督使用 程序生成的 标签,而不是由人工生成。 在某些方面它是监督的,因为模型从标记的数据中学习,但在其他方面它是无监督的,因为训练算法没有提供标签。因此称为自监督。

使用半监督的方法,通过语言建模的形式,使得 GPT 能够在前所未有的大量训练数据上进行训练,从而使模型能够创建强大的语言表示。这个模型在对语言有了扎实理解之后,可以在更具体的数据集上进行微调,以应对更具体的任务。

我们使用 BooksCorpus 数据集[71]来训练语言模型。它包含了来自各种类型的超过 7000 本独特的未出版书籍,包括冒险、奇幻和浪漫等。— GPT 论文关于预训练

我们在各种监督任务上进行实验,包括自然语言推理、问答、语义相似性和文本分类。— GPT 论文关于微调

任务特定输入转换

语言模型,如 GPT,目前被认为是非常强大的“上下文学习者”;你可以在输入中提供信息作为上下文,然后它们可以利用这些上下文构建更好的回应。我在我的增强生成的文章、我的视觉问答的文章和我的解析语言模型输出的文章中使用了这一概念。这是一个相当强大的概念。

ChatGPT 在上下文学习的帮助下成功完成任务的一个例子。来自我的RAG 文章

在第一次 GPT 论文发表时,上下文学习并没有被很好地理解。论文专门为“任务特定输入转换”分配了一个部分。基本上,GPT 的作者选择将这些任务以文本形式格式化,而不是向模型中添加特殊组件以使其适应特定任务。

例如,对于文本蕴涵(预测一段文本是否与另一段文本直接相关的过程),GPT 论文简单地将这两段文本拼接在一起,中间用美元符号分隔。这使他们能够在文本蕴涵上微调 GPT,而无需向模型中添加任何新参数。其他目标,如文本相似性、问答和常识推理,也以类似的方式进行微调。

GPT-2(发布于 2019 年 2 月)

论文语言模型是无监督多任务学习者向世界介绍了 GPT-2,它本质上与 GPT-1 相同,只有两个关键区别:

  1. GPT-2 远大于 GPT-1

  2. GPT-2 不使用任何微调,仅使用预训练

此外,简要说明一下,GPT-2 架构与 GPT-1 架构略有不同。在 GPT-1 中,每个块由 [Attention, Norm, Feed Forward, Norm] 组成,而 GPT-2 由 [Norm, Attention, Norm, Feed Forward] 组成。然而,这种差异微乎其微,几乎不值得一提。

更大更好

GPT-2 在几个目标上的表现,作为参数数量的函数。如图所示,语言模型的参数越多,通常表现越好。来自 GPT-2 论文

GPT-2 论文的发现之一是更大的模型表现更好。具体来说,他们理论化语言模型性能呈“对数线性”扩展,如下图所示:

模型的好坏与其大小之间的关系,似乎是对数线性的,这基于 GPT-2 的发现

结果 GPT-2 包含 15 亿个参数,这大约是 GPT-1 的十倍。GPT-2 还在大约十倍的数据上进行训练。

语言理解是通用的

在 GPT-1 中,他们专注于使用语言建模目标创建坚实的文本理解基准,然后利用该基准对模型进行微调,以适应特定任务。在 GPT-2 中,他们完全去除了微调,假设一个足够预训练的模型可以在没有明确训练的情况下,在特定问题上表现良好。

我们的怀疑是,单任务训练在单一领域数据集上的普遍性是当前系统缺乏通用性的主要原因。要实现当前架构下的鲁棒系统,可能需要在广泛的领域和任务上进行训练和性能测量…

当条件是文档加问题时,语言模型生成的答案在 CoQA 数据集上达到了 55 F1 —— 与 4 个基准系统中的 3 个系统的表现相匹配或超越,而无需使用 127,000+ 的训练样本…

我们最大的模型 GPT-2 是一个 15 亿参数的 Transformer,在 8 个测试语言建模数据集中中有 7 个达到了最先进的结果,在零-shot 设置下 — GPT-2 论文

他们通过使用一个超级大的模型,并为其提供大量高质量数据,实现了这种通用性能。

我们的方法激励着建立尽可能大且多样化的数据集,以收集尽可能多领域和上下文的自然语言任务示例…

我们创建了一个新的网页抓取,强调文档质量。为此,我们只抓取了经过人工策划/过滤的网页。手动过滤整个网页抓取会异常昂贵,所以作为起点,我们抓取了所有从 Reddit 这个社交媒体平台中获得至少 3 点 karma 的外部链接。这可以被视为其他用户是否认为该链接有趣、教育性或仅仅有趣的启发式指标。 — GPT-2 论文

GPT 3(发布于 2020 年 6 月)

论文语言模型是少样本学习者向世界介绍了 GPT-3,实际上它说的是“更大更好,那为什么不再更大呢?”GPT-2 仅有的 15 亿参数相比,GPT-3 使用了 1750 亿参数,并且在 45TB 的文本数据上进行了训练。

一个模型大小与性能的图表。来自于GPT-3 论文

从数据科学的角度来看,变化不大。模型相似,核心架构相似,语言建模目标相似。然而,从工程学的角度来看,情况显然不同。GPT-2 的大小约为 5GB,而 GPT-3 约为 800GB(我在网上看到的这个数字有很多变化,但肯定很大)。售价 30000 美元的 Nvidia H100 GPU 拥有 80GB 的 VRAM。请记住,要训练一个机器学习模型,你实际上需要双倍于模型大小的 VRAM 来存储参数和梯度(正如我在我的LoRA 文章中所讨论的)。

来自GPT-3 论文的一个表格,显示了人们在预测新闻文章是否由 GPT-3 编写的准确率。正如你所见,对于完整的 175B 参数模型,只有 52%的人猜对了。50%就是 50/50 的瞎猜。

我见过对 GPT-3 的实际成本估算存在很大偏差,这主要是由于模型训练所需的 VRAM 量不同。可以确定的是;训练像 GPT-3 这样的模型需要一群超昂贵的计算机并行工作,而且需要类似的集群来为用户提供该模型。

GPT-3 论文的标题是“语言模型是少样本学习者。”作者所说的“少样本学习者”是指 GPT-3 可以在没有训练模型的情况下在大多数任务上表现得相当好。相反,你可以通过实验输入模型的输入格式来找到特定任务的正确输入格式。因此,提示工程应运而生。

GPT-4(发布于 2023 年 3 月)

到目前为止,清晰度结束了。OpenAI 已经从一个研究型非营利组织转变为一个真正的重型企业,与微软和谷歌等大名鼎鼎的公司抗衡。关于 GPT-4 的论文没有经过同行评审和透明发布。正如你可能知道的,OpenAI 内部经历了一些摩擦,这也是部分原因。

话虽如此,有信息吗?绝对有。

GPT-4 在产品中使用是“安全的”

首先,我们可以从 OpenAI 的 100 页技术报告 中获得大量信息,该报告隐藏了许多建模方面的具体细节,但详尽地描述了模型的性能和能力。这篇论文读起来更像是一种广告,而不是严肃的学术作品;例如,“安全”一词的使用更倾向于在产品中使用的安全性,而不是传统研究中关注人类安全的使用。

GPT-4 可以理解图像

GPT-4 的一个重大变化是“多模态”;允许 GPT-4 讨论图像和文本。

GPT-4 讨论图像。 来源

这有各种酷炫的用途。在一个相当著名的例子中,OpenAI 演示了如何将这种功能用于将粗略的草图转换为用 HTML、CSS 和 JavaScript 编写的(仍然相当粗糙的)网站。

GPT-4 在人类反馈下进行了微调

此外,GPT-4 使用了“基于人类反馈的强化学习”,通常缩写为 RLHF。你知道 ChatGPT 网站上的那些小拇指点赞和点踩吗?这些允许 OpenAI 根据上下文中的人类反馈进一步改进模型。

这种学习策略可能会有可疑的结果,实际上可能会降低模型的性能。然而,它确实有助于将 GPT-4 以某种“AI 助手”的风格提升到更好的水平。

我们的评估表明,RLHF 不会显著影响基础 GPT-4 模型的能力 — GPT-4 技术报告

GPT-4 实在是太大了

大约四个月前有一个 臭名昭著的泄密事件,似乎透露了一些关于 GPT-4 的幕后细节的可信证据。这些信息在一个付费墙后发布,随后在 Twitter 上被泄露,现在是… X?我猜?

无论如何,GPT-4 似乎达到了 1.8 万亿参数的规模,大约是 GPT-3 的十倍。

GPT-4 使用专家混合

我计划在未来的文章中讨论专家混合(MOE)。简而言之,MOE 是将大型模型划分为较小子模型的过程,并创建一个小型模型来将数据路由到各个组件。

整体思路是,模型越大,运行成本越高。如果你能将模型的一部分专门用于艺术,另一部分用于科学,再另一部分用于讲笑话,你可以将查询路由到各个子组件。实际上这些分组更为抽象,但逻辑是相同的;模型可以在每次查询时使用不同的子组件,使其运行比每次查询都使用所有参数要更精简。

基于GPT-4 泄露信息,通过使用 MOE,该模型在任何给定推理中只需使用 2800 亿参数中的 15%(总共 1.8 万亿中的一部分)。

GPT-4 使用 16 个专家(模型被分成 16 个部分然后路由到这些部分)。这比当前研究的状态要少,当前研究建议使用 64 个或 128 个专家的大规模集合。

GPT-4 训练成本

基于GPT-4 泄露信息,训练该模型在 A100 GPU 上大约花费了 6300 万美元。使用更现代的 H100 GPU 则可能花费约 2150 万美元。这就是成为早期采用者的代价,我想。

GPT-4 使用了一些附加功能

  • GPT-4 使用“多查询注意力”,这是一种提升模型吞吐量的注意力风格。我将在即将发表的文章中讨论它。

  • GPT-4 使用“连续批处理”,我可能会在另一篇文章中详细介绍。

  • GPT-4 使用了“推测解码”。天哪,AI 研究的进展让我忙得不可开交。

GPT-4 可能使用了类似 Flamingo 的方法

GPT-4 使用了一种类似于 Deepmind 的多模态模型“Flamingo”的建模风格。我会在未来专门讨论 Flamingo,但我确实有一篇关于 SalesForce 研究团队类似方法的文章,名为 Q-Fromer:

## 视觉问答与冻结的大型语言模型

与 LLM 谈论图像,而不需要对 LLM 进行图像训练。

视觉问答与冻结的大型语言模型

简而言之,Q-Former 和 Flamingo 都使用冻结的语言和图像模型,并训练一些将它们结合起来的“胶水”,以创建一个可以谈论图像的模型。

GPT-4 使用高质量数据

看起来,根据GPT-4 泄露信息,OpenAI 使用了大学教科书来提高模型在多个智力领域的知识。这在法律和伦理上确实是一个灰色地带,这也可能解释了 OpenAI 目前的一些内部争斗和保密。

结论

嗯,最后有点戏剧化了,是吧?我们首先简要回顾了变换器,然后描述了 GPT-1 如何采用变换器的解码器组件,通过自监督预训练改进文本生成。接着我们讨论了 GPT-2 和 GPT-3 如何在此基础上进一步发展,生成了与 GPT-1 大致相同但规模更大的语言模型。然后我们讨论了我们对 GPT-4 的了解;模型大小,一些架构变化,甚至一些有趣的争议。

关注获取更多内容!

我描述了机器学习领域的论文和概念,重点是实用和直观的解释。

[## 每当 Daniel Warfield 发布文章时,获取邮件通知

每当 Daniel Warfield 发布文章时,获取邮件通知。通过注册,如果你还没有 Medium 账户,你将创建一个账户…

medium.com](https://medium.com/@danielwarfield1/subscribe?source=post_page-----c70c38e87491--------------------------------)

从未预料到,总是感激。通过捐赠,你使我能够投入更多时间和资源,提供更频繁和更高质量的文章。 链接

归属说明: 本文档中的所有资源均由Daniel Warfield创建,除非另有来源提供。你可以将本文中的任何资源用于自己的非商业用途,只要你引用了这篇文章,danielwarfield.dev,或两者兼具。可以根据要求授予明确的商业许可。

GPT 与 BERT:哪一个更好?

原文:towardsdatascience.com/gpt-vs-bert-which-is-better-2f1cf92af21a

比较两个大型语言模型:方法与示例

Pranay DaveTowards Data Science Pranay Dave

·发布于Towards Data Science ·阅读时长 6 分钟·2023 年 6 月 23 日

--

图片由 DALLE 创建,PPT 由作者制作(labs.openai.com/s/qQgpAQbLi0srYlZHOHQjGKWh

生成式 AI 的流行也导致了大型语言模型数量的增加。在这个故事中,我将对两个模型进行比较:GPT 和 BERT。GPT(Generative Pre-trained Transformer)由 OpenAI 开发,基于仅解码器架构。而 BERT(Bidirectional Encoder Representations from Transformers)由 Google 开发,是一个仅编码器的预训练模型。

虽然技术上有所不同,但它们有一个相似的目标——执行自然语言处理任务。许多文章从技术角度比较这两者。然而,在这个故事中,我将基于它们的目标质量,即自然语言处理,进行比较。

比较方法

如何比较两种完全不同的技术架构?GPT 是仅解码器架构,而 BERT 是仅编码器架构。因此,将仅解码器与仅编码器架构进行技术比较,就像比较法拉利与兰博基尼——两者都很棒,但底盘下的技术完全不同。

然而,我们可以基于它们能够执行的共同自然语言任务的质量进行比较——即生成嵌入。嵌入是文本的向量表示。嵌入是任何自然语言处理任务的基础。因此,如果我们能够比较嵌入的质量,这将有助于我们判断自然语言任务的质量,因为嵌入是变换器架构进行自然语言处理的基础。

下面展示的是我将采取的比较方法。

比较方法(图片由作者制作)

让我们从 GPT 开始

我掷了一枚硬币,GPT 赢了!所以我们先从 GPT 开始。我将从亚马逊的优质食品评论数据集中提取文本。评论是测试两个模型的好方法,因为评论用自然语言表达,非常自发。它们包含了客户的感受,可以包含所有类型的语言——好的、坏的、丑的!此外,它们可能有许多拼写错误的词、表情符号以及常用的俚语。

这里是评论文本的一个示例。

客户评论示例(图像由作者提供)

为了使用 GPT 获取文本的嵌入,我们需要向 OpenAI 发起 API 调用。结果是每个文本的嵌入或向量大小为 1540。以下是包括这些嵌入的示例数据。

从模型获得的嵌入(图像由作者提供)

下一步是聚类和可视化。可以使用 KMeans 对嵌入向量进行聚类,并使用 TSNE 将 1540 维降至 2 维。下图展示了聚类和降维后的结果。

GPT 嵌入聚类(图像由作者提供)

可以观察到集群形成得很好。悬停在某些集群上可以帮助理解集群的含义。例如,红色集群与狗粮相关。进一步分析还表明,GPT 嵌入正确识别了“Dog”和“Dawg”是相似的,并将它们放在同一集群中。

总体而言,GPT 嵌入提供了良好的结果,聚类质量也很高。

现在轮到 BERT 了

BERT 能否表现得更好?让我们来看看。BERT 模型有多个版本,如 bert-base-case、bert-base-uncased 等。它们的嵌入向量大小不同。以下是基于 Bert base 的结果,其嵌入大小为 768。

BERT 嵌入(768)聚类(图像由作者提供)

绿色集群对应于狗粮。然而,可以观察到这些集群分布较广,相较于 GPT 并不够紧凑。主要原因是 768 的嵌入向量长度相对于 GPT 的 1540 嵌入向量长度较低。

幸运的是,BERT 也提供了更高的嵌入大小 1024。以下是结果。

BERT 嵌入(1024)聚类(图像由作者提供)

这里橙色集群对应于狗粮。该集群相对紧凑,相比于 768 的嵌入效果更好。然而,仍有一些点离中心较远。这些点被错误分类。例如,有一条咖啡评论,但由于包含了词“Dog”,被错误地分类为狗粮。

结论

显然,GPT 在嵌入质量上比 BERT 表现更好。然而,我不想将所有功劳归于 GPT,因为还有其他方面需要比较。以下是总结表格

GPT 因提供更高嵌入质量而优于 BERT。然而,GPT 需要付费 API,而 BERT 是免费的。此外,BERT 模型是开源的,非黑箱,你可以进一步分析以更好地理解它。OpenAI 的 GPT 模型是黑箱的。

总之,我建议使用 BERT 处理中等复杂度的文本,如有策划的网页或书籍。GPT 可用于处理非常复杂的文本,如完全自然语言且未经过策划的客户评论。

技术实现

这里是一个 Python 代码片段,实施了故事中描述的过程。为了说明,我给出了 GPT 的例子。BERT 的实现类似。

 ##Import packages
import openai
import pandas as pd
import re
import contextlib
import io
import tiktoken
from openai.embeddings_utils import get_embedding
from sklearn.cluster import KMeans
from sklearn.manifold import TSNE

##Read data
file_name = 'path_to_file'
df = pd.read_csv(file_name)

##Set parameters
embedding_model = "text-embedding-ada-002"
embedding_encoding = "cl100k_base"  # this the encoding for text-embedding-ada-002
max_tokens = 8000  # the maximum for text-embedding-ada-002 is 8191
top_n = 1000
encoding = tiktoken.get_encoding(embedding_encoding)
col_embedding = 'embedding'
n_tsne=2
n_iter = 1000

##Gets the embedding from OpenAI
def get_embedding(text, model):
  openai.api_key = "YOUR_OPENAPI_KEY"
  text = text.replace("\n", " ")
  return openai.Embedding.create(input = [text], model=model)['data'][0]['embedding']

col_txt = 'Review'
df["n_tokens"] = df[col_txt].apply(lambda x: len(encoding.encode(x)))
df = df[df.n_tokens <= max_tokens].tail(top_n)
df = df[df.n_tokens > 0].reset_index(drop=True) ##Remove if there no tokens, for example blank lines
df[col_embedding] = df[col_txt].apply(lambda x: get_embedding(x, model='text-embedding-ada-002'))
matrix = np.array(df[col_embedding].to_list())

##Make clustering
kmeans_model = KMeans(n_clusters=n_clusters,random_state=0)
kmeans = kmeans_model.fit(matrix)
kmeans_clusters = kmeans.predict(matrix)

#TSNE
tsne_model = TSNE(n_components=n_tsne, verbose=0, random_state=42, n_iter=n_iter,init='random')
tsne_out = tsne_model.fit_transform(matrix)

数据集引用

数据集在这里提供,许可证为 CC0 公共领域。 商业和非商业使用均被允许.

[## 亚马逊优质食品评论

分析来自亚马逊的约 500,000 条食品评论

www.kaggle.com

订阅以便在我发布新故事时保持信息更新。

[## 每当 Pranay Dave 发布新内容时获取电子邮件通知。

每当 Pranay Dave 发布新内容时获取电子邮件通知。注册后,如果你还没有 Medium 账户,将会创建一个...

pranay-dave9.medium.com

你也可以通过我的推荐链接加入 Medium

[## 使用我的推荐链接加入 Medium - Pranay Dave

作为 Medium 会员,你的一部分会员费用将转给你阅读的作者,并且你可以完全访问每一个故事...

pranay-dave9.medium.com

附加资源

网站

你可以访问我的网站进行零编码分析。https://experiencedatascience.com

YouTube 频道

请访问我的 YouTube 频道,通过演示学习数据科学和 AI 的应用案例

[## 数据科学展示

通过演示学习数据科学。无论你从事什么职业,坐下来,放松心情,享受这些视频。我的名字是……

www.youtube.com

GPTQ 或 bitsandbytes:对于 LLMs 应使用哪种量化方法 — 以 Llama 2 为例

原文:towardsdatascience.com/gptq-or-bitsandbytes-which-quantization-method-to-use-for-llms-examples-with-llama-2-f79bc03046dc

针对个人计算机上可承受的微调和推断进行大型语言模型量化

Benjamin MarieTowards Data Science Benjamin Marie

·发表于 Towards Data Science ·7 分钟阅读·2023 年 8 月 25 日

--

作者提供的图片 — 制作自Pixabay

随着大型语言模型(LLM)变得越来越大,参数也越来越多,新的技术也被提出以减少它们的内存使用。

减少内存中模型大小的最有效方法之一是量化。你可以将量化视为 LLM 的压缩技术。在实践中,量化的主要目标是降低 LLM 权重的精度,通常从 16 位降低到 8 位、4 位,甚至 3 位,同时尽量减少性能下降。

有两种流行的 LLM 量化方法:GPTQ 和 bitsandbytes。

在这篇文章中,我讨论了这两种方法之间的主要差异。它们各有优缺点,使它们适用于不同的使用场景。我展示了它们在使用 Llama 2 时的内存使用情况和推断速度的比较。我还根据以往的实验讨论了它们的性能。

注意:如果你想了解更多关于量化的内容,我推荐阅读 Maxime Labonne的这篇出色的介绍:

[## 权重量化介绍

使用 8 位量化减少大型语言模型的大小

原文链接

GPTQ:用于轻量存储和快速推理的后训练量化

GPTQ (Frantar et al., 2023)首次应用于准备部署的模型。换句话说,一旦模型完全微调,GPTQ 将被应用于减少其大小。

GPTQ 可以将权重精度降低到 4-bit 或 3-bit。实际上,GPTQ 主要用于 4-bit 量化。3-bit 显示出非常不稳定(Dettmers 和 Zettlemoyer, 2023)。

它在量化时不需要将整个模型加载到内存中。相反,GPTQ 逐模块加载和量化 LLM。量化还需要少量数据进行校准,这可能需要超过一小时的消费者 GPU 时间。

在上一篇文章中,我进行了实验,使用 GPTQ 对 Llama 2 7B 进行 4-bit 精度量化。原始模型在硬盘上的大小为 13.5 GB,但经过量化后,模型大小减少到 3.9 GB(原始大小的 28.9%)。

[## 使用 GTPQ 对 Llama 2 进行量化,以便在您的计算机上快速推理

Llama 2 但减少了 75%的大小

kaitchup.substack.com](https://kaitchup.substack.com/p/quantization-of-llama-2-with-gtpq?source=post_page-----f79bc03046dc--------------------------------)

一旦量化,模型可以在更小的 GPU 上运行。例如,原始的 Llama 2 7B 无法在 12 GB VRAM 的 GPU 上运行(这大约是您在免费 Google Colab 实例上得到的),但量化后就能轻松运行。不仅能运行,而且还能显著减少 VRAM 的使用量,允许更大批量的推理。

bitsandbytes 的量化也针对推理进行了优化,正如我将在本文的下一部分展示的那样。

bitsandbytes:即时量化,便于简单的微调和高效推理

bitsandbytes 也支持量化,但使用不同类型的 4-bit 精度,即NormalFloat (NF)

它与 QLoRa 同时提出,用于通过适配器对量化的 LLM 进行微调,当时 GPTQ LLMs 尚不支持这种方法(自 2023 年 6 月以来已可实现)。QLoRa 中 4-bit NF(nf4)的便捷集成是 bitsandbytes 相对于 GPTQ 的主要优势。

我在上一篇文章中展示了如何使用 nf4 对 Llama 2 7B 进行微调。

[## 使用 QLoRa 和 TRL 在您的计算机上微调 Llama 2

在 Guanaco 上,并使用正确的填充

kaitchup.substack.com](https://kaitchup.substack.com/p/fine-tune-llama-2-on-your-computer?source=post_page-----f79bc03046dc--------------------------------)

使用 bitsandbytes 时,模型在加载时会被透明地实时量化。如果你使用 Hugging Face transformers,只需在调用“from_pretrained”方法时将“load_in_4bit”设置为“True”即可。

与 GPTQ 的一个显著区别是,在撰写本文时,bitsandbytes 无法序列化 nf4 量化模型。模型必须在每次加载时进行量化。

QLoRa 和 GPTQ 的软件和硬件要求

硬件

NVIDIA 在 2018 年为其 Turing GPU 引入了4 位精度支持。免费的 Google Colab 实例运行在基于 Turing 架构的 T4 GPU 上。我确认你可以在 Google Colab 上运行 4 位量化。

关于消费级 GPU,我只能确定 RTX 30xx 系列 GPU(我在我的 RTX 3060 上进行了测试)或更近期的型号支持这一点。理论上,它也应该适用于 GTX 16xx 和 RTX 20xx 系列,因为它们也利用了 Turing 架构,但我没有进行测试,也没有找到 GPTQ 或 bitsandbytes nf4 在这些 GPU 上工作的证据。注意:如果你知道这样的工作,请在评论中留下链接,我会更新这一段。

软件

GPTQ 的官方仓库在 GitHub 上(Apache 2.0 许可)。它可以直接用于量化 OPT、BLOOM 或 LLaMa,支持 4 位和 3 位精度。然而,你会发现大多数在线可用的量化 LLM,例如在 Hugging Face Hub 上,都是用AutoGPTQ(Apache 2.0 许可)进行量化的。

AutoGPTQ 用户友好,使用 Hugging Face transformers 中的接口。它也有很好的文档和维护。

要安装 AutoGPTQ,我建议按照 GitHub 上提供的说明进行操作:

git clone https://github.com/PanQiWei/AutoGPTQ.git && cd AutoGPTQ
pip install .

bitsandbytes 库(MIT 许可)由其仓库定义为“CUDA 自定义函数的封装器”。实际上,它被 Hugging Face transformers 直接支持,非常类似于 Accelerate 和 Datasets,尽管 bitsandbytes 不是官方的 Hugging Face 库。例如,如果你在加载模型时尝试量化,transformers 会告诉你安装 bitsandbytes。

你可以通过 pip 安装:

pip install bitsandbytes

注意:在我将这篇文章提交给 Towards Data Science 的一天后,Hugging Face 在 transformers 库中增加了对 AutoGPTQ 模型的支持。在 transformers 中使用 GPTQ 可能会比我在下一节中报告的性能更好。

GPTQ 和 bitsandbytes 之间的比较

在本节中,我将展示使用两种量化方法的模型之间的比较。

  • 它们在一些数据集上的困惑度

  • 他们的 GPU VRAM 消耗

  • 他们的推理速度

对于困惑度评估,我依赖于已经发布的数字。对于 VRAM 消耗,我依赖于我自己的实验,同时也得到了已经发布的数字的支持。对于推理速度,我没有轻易找到已经在线发布的结果,因此我展示了我使用 Llama 2 7B 获得的结果。

你可以使用我在 The Kaitchup 上发布的笔记本(notebook #11)来重现我的结果,这是我的 substack 通讯。

GPTQ 与 bitsandbytes:困惑度

AutoGPTQ 的主要作者通过计算 C4 数据集上的困惑度来评估了量化为 GPTQ 和 bitsandbytes 的 LLaMa(第一版)。

结果发布在“GPTQ-for-LLaMA”仓库(Apache 2.0 许可证)中:

结果由 GPTQ-for-LLaMA 提供

这里有很多信息需要解读。每个表格展示了不同大小的 LLaMA 的结果。让我们关注每个表格中的最后一列 c4(ppl)。

我们将比较 GPTQ-128g(GPTQ 4-bit)与 nf4-double_quant 和 nf4(这两者都是 bitsandbytes 量化算法)。 “nf4-double_quant”是一个变体,量化了量化常数。

对于 7B 版本,它们的表现相同,都为 5.30 ppl。

我们看到 13B 和 33B 版本存在差异,其中 GPTQ 产生了较低的困惑度。结果表明,相对于 nf4,GPTQ 似乎在模型变大时表现更好。

GPTQ 似乎在这里比 bitsandbytes 的 nf4 略有优势。

GPTQ 与 bitsandbytes:VRAM 使用情况

在上面的表格中,作者还报告了 VRAM 使用情况。我们可以看到,nf4-double_quant 和 GPTQ 使用的内存几乎相同。

nf4 在没有双重量化的情况下显著使用了比 GPTQ 更多的内存。LLaMA 33B 的差异大于 1 GB。双重量化是匹配 GPTQ 量化性能所必需的。

在我自己使用 3 种不同 GPU 的 Llama 2 7B 实验中,我也观察到 GPTQ 和 nf4-double_quant 消耗的 VRAM 量非常相似。注意:我使用了 T4、V100 和 A100 40 GB GPU,这些都可以在 Google Colab PRO 上使用。

GPTQ 与 bitsandbytes:推理速度

为了了解推理速度,我运行了 5 个不同的提示,对两个量化模型进行测试,没有进行批处理,生成了最多 1,000 个 token 的输出(查看笔记本)。对于每个提示,我计算了每秒生成的 token 数量。然后,我对 5 个提示的结果取了平均值。

作者提供的表格。

这里明显的赢家是 GPTQ。它的速度是 bitsandbytes nf4 双重量化的两倍。

结果还显示,三种 GPU 几乎实现了相同的速度。如果不进行批处理,购买更昂贵的 GPU 不会加快推理速度。然而,即使在小批量的情况下,我也预期看到 T4 与 A100 之间的一些显著差异。

结论:GPTQ 与 bitsandbytes

总结来说,如果你追求更好的性能,GPTQ 4 位比 bitsandbytes nf4 更好。它在相似的 VRAM 消耗下实现了更低的困惑度和更快的推理速度。

然而,如果你希望对量化模型进行微调,bitsandbytes 是一个更好且更方便的选择,因为它得到了 Hugging Face 库的支持。bitsandbytes 量化是 QLoRA 背后的算法,QLoRA 允许使用适配器对量化模型进行微调。

如果你在消费级硬件上开发/部署 LLM,我建议你考虑以下几点:

  • 使用 bitsandbytes nf4 和 QLoRa 对 LLM 进行微调

  • 将适配器合并到 LLM 中

  • 用 GPTQ 4 位量化结果模型

  • 部署

以下是两种量化方法的优缺点总结:

bitsandbytes 优点:

  • 支持 QLoRa

  • 实时量化

bitsandbytes 缺点:

  • 推理较慢

  • 量化模型不能被序列化

GPTQ 优点:

  • 序列化

  • 支持 3 位精度

  • 快速

GPTQ 缺点:

  • 模型量化较慢

  • 对 GPTQ 模型进行微调是可能的,但研究不足

Pytorch 中的 Grad-CAM:前向和反向钩子的使用

原文:towardsdatascience.com/grad-cam-in-pytorch-use-of-forward-and-backward-hooks-7eba5e38d569?source=collection_archive---------3-----------------------#2023-04-17

使用梯度了解你的模型如何进行预测

Vinícius AlmeidaTowards Data Science Vinícius Almeida

·

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

--

图片来源于作者。X 射线图像来自kaggle 胸部 X 射线数据集

我注意到一种叫做 Grad-CAM 的技术,它可以检查卷积神经网络如何预测其输出。例如,在分类器中,你可以洞察神经网络如何利用输入进行预测。这一切始于 原始论文,其中描述了这一技术。在本文中,我们将使用 Pytorch 库实现这一技术,方法是你可以将其应用于任何卷积神经网络,而无需改变你已有的神经网络模块。

我在 Medium 上阅读了一篇文章,标题是 “在 PyTorch 中实现 Grad-CAM”,作者是 Stepan Ulyanin,这激发了我以稍微不同的方式实现相同的算法。Stepan 提出了一个方法,需要你重写模型的前向函数来计算 Grad-CAM。感谢 Pytorch,我们可以通过注册前向和反向钩子来实现相同的结果,而无需改变前向函数。我希望这篇文章对 Stepan 的精彩工作有所贡献。

让我们深入探讨吧!

1. 加载并检查预训练模型

为了展示 Grad-CAM 的实现,我将使用 Kaggle 上的胸部 X 光数据集 和我制作的一个预训练分类器,该分类器能够将 X 光图像分类为是否有肺炎。

model_path = "your/model/path/"

# instantiate your model
model = XRayClassifier() 

# load your model. Here we're loading on CPU since we're not going to do 
# large amounts of inference
model.load_state_dict(torch.load(model_path, map_location=torch.device('cpu'))) 

# put it in evaluation mode for inference
model.eval()

接下来,让我们检查模型的架构。由于我们希望了解输入图像的哪些方面对预测有所贡献,我们需要确定最后一个卷积层,特别是它的激活函数。这个层包含了模型学习到的最复杂特征的表示。因此,它最能帮助我们理解模型的行为。

import torch
import torch.nn as nn
import torch.nn.functional as F

# hyperparameters
nc = 3 # number of channels
nf = 64 # number of features to begin with
dropout = 0.2
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

# setup a resnet block and its forward function
class ResNetBlock(nn.Module):
    def __init__(self, in_channels, out_channels, stride=1):
        super(ResNetBlock, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)

        self.shortcut = nn.Sequential()
        if stride != 1 or in_channels != out_channels:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels)
            )

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out += self.shortcut(x)
        out = F.relu(out)
        return out

# setup the final model structure
class XRayClassifier(nn.Module):
    def __init__(self, nc=nc, nf=nf, dropout=dropout):
        super(XRayClassifier, self).__init__()

        self.resnet_blocks = nn.Sequential(
            ResNetBlock(nc,   nf,    stride=2), # (B, C, H, W) -> (B, NF, H/2, W/2), i.e., (64,64,128,128)
            ResNetBlock(nf,   nf*2,  stride=2), # (64,128,64,64)
            ResNetBlock(nf*2, nf*4,  stride=2), # (64,256,32,32)
            ResNetBlock(nf*4, nf*8,  stride=2), # (64,512,16,16)
            ResNetBlock(nf*8, nf*16, stride=2), # (64,1024,8,8)
        )

        self.classifier = nn.Sequential(
            nn.Conv2d(nf*16, 1, 8, 1, 0, bias=False),
            nn.Dropout(p=dropout),
            nn.Sigmoid(),
        )

    def forward(self, input):
        output = self.resnet_blocks(input.to(device))
        output = self.classifier(output)
        return output

该模型设计为接收 256x256 的 3 通道图像。因此,它的输入预期具有 [batch size, 3, 256, 256] 的形状。每个 ResNet 块以 ReLU 激活函数结束。为了我们的目标,我们需要选择最后一个 ResNet 块。

XRayClassifier(
  (resnet_blocks): Sequential(
    (0): ResNetBlock(
      (conv1): Conv2d(3, 64, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (shortcut): Sequential(
        (0): Conv2d(3, 64, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (1): ResNetBlock(
      (conv1): Conv2d(64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (shortcut): Sequential(
        (0): Conv2d(64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (2): ResNetBlock(
      (conv1): Conv2d(128, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (shortcut): Sequential(
        (0): Conv2d(128, 256, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    (3): ResNetBlock(
      (conv1): Conv2d(256, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (shortcut): Sequential(
        (0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )
    **(4): ResNetBlock(
      (conv1): Conv2d(512, 1024, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (conv2): Conv2d(1024, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      (shortcut): Sequential(
        (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
      )
    )**
  )
  (classifier): Sequential(
    (0): Conv2d(1024, 1, kernel_size=(8, 8), stride=(1, 1), bias=False)
    (1): Dropout(p=0.2, inplace=False)
    (2): Sigmoid()
  )
)

在 Pytorch 中,我们可以使用模型的属性轻松完成这个选择。

model.resnet_blocks[-1]
#ResNetBlock(
#  (conv1): Conv2d(512, 1024, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
#  (bn1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
#  (conv2): Conv2d(1024, 1024, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
#  (bn2): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
#  (shortcut): Sequential(
#    (0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)
#    (1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
#  )
#)

2. Pytorch 方法用于注册钩子

Pytorch 有许多函数用于处理钩子,这些函数允许你处理在前向或反向传递过程中流经模型的信息。你可以用它来检查中间梯度值,对特定层的输出进行更改等。

在这里,我们将重点关注 nn.Module 类的两种方法。让我们仔细看看它们。

2.1. register_full_backward_hook(hook, prepend=False)

该方法在模块上注册了一个反向钩子,这意味着当调用backward()方法时,钩子函数将运行。

反向钩子函数接受模块自身、相对于层输入的梯度和相对于层输出的梯度作为输入。

hook(module, grad_input, grad_output) -> tuple(Tensor) or None

它返回一个torch.utils.hooks.RemovableHandle,这使得你可以稍后移除钩子。因此,将其分配给变量是有用的。我们稍后会回到这个问题。

**2.2. register_forward_hook(hook, *, prepend=False, with_kwargs=False)

这与之前的钩子函数类似,只是钩子函数在前向传递中运行,即在感兴趣的层处理其输入并返回输出时运行。

钩子函数的签名稍有不同。它使你能够访问层的输出:

hook(module, args, output) -> None or modified output

它还返回一个torch.utils.hooks.RemovableHandle

3. 将反向和前向钩子添加到模型中

首先,我们需要定义我们的反向和前向钩子函数。要计算 Grad-CAM,我们需要相对于最后一个卷积层输出的梯度,以及其激活,即该层激活函数的输出。因此,我们的钩子函数将在推理和反向传递过程中仅提取这些值。

# defines two global scope variables to store our gradients and activations
gradients = None
activations = None

def backward_hook(module, grad_input, grad_output):
  global gradients # refers to the variable in the global scope
  print('Backward hook running...')
  gradients = grad_output
  # In this case, we expect it to be torch.Size([batch size, 1024, 8, 8])
  print(f'Gradients size: {gradients[0].size()}') 
  # We need the 0 index because the tensor containing the gradients comes
  # inside a one element tuple.

def forward_hook(module, args, output):
  global activations # refers to the variable in the global scope
  print('Forward hook running...')
  activations = output
  # In this case, we expect it to be torch.Size([batch size, 1024, 8, 8])
  print(f'Activations size: {activations.size()}')

在定义了我们的钩子函数和将存储激活和梯度的变量之后,我们需要在感兴趣的层上注册钩子:

backward_hook = model.resnet_blocks[-1].register_full_backward_hook(backward_hook, prepend=False)
forward_hook = model.resnet_blocks[-1].register_forward_hook(forward_hook, prepend=False)

4. 获取我们需要的梯度和激活

现在我们已经为模型设置了钩子,让我们加载一张图像,计算 Grad-CAM。

from PIL import Image

img_path = "/your/image/path/"
image = Image.open(img_path).convert('RGB')

这是我们将要使用的图像。图像来源于kaggle 胸部 X 光数据集

我们需要对其进行预处理,以准备好输入模型进行推理。

from torchvision import transforms
from torchvision.transforms import ToTensor

image_size = 256
transform = transforms.Compose([
                               transforms.Resize(image_size, antialias=True),
                               transforms.CenterCrop(image_size),
                               transforms.ToTensor(),
                               transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
                           ])

img_tensor = transform(image) # stores the tensor that represents the image

现在,我们需要使用这个图像张量进行前向传递。并且我们必须执行反向传递,以使反向钩子发挥作用。

# since we're feeding only one image, it is a 3d tensor (3, 256, 256). 
# we need to unsqueeze to it has 4 dimensions (1, 3, 256, 256) as 
# the model expects it to.
model(img_tensor.unsqueeze(0)).backward()
# here we did the forward and the backward pass in one line.

我们的钩子函数返回了以下内容:

Forward hook running...
Activations size: torch.Size([1, 1024, 8, 8])
Backward hook running...
Gradients size: torch.Size([1, 1024, 8, 8])

最终,我们可以使用梯度激活变量来计算我们的热图!

5. 计算 Grad-CAM

要计算 Grad-CAM,我们将使用原始论文的公式和Stepan Ulyanin的实现。

图像摘自原始文章

# pool the gradients across the channels
pooled_gradients = torch.mean(gradients[0], dim=[0, 2, 3])

图像摘自原始文章

import torch.nn.functional as F
import matplotlib.pyplot as plt

# weight the channels by corresponding gradients
for i in range(activations.size()[1]):
    activations[:, i, :, :] *= pooled_gradients[i]

# average the channels of the activations
heatmap = torch.mean(activations, dim=1).squeeze()

# relu on top of the heatmap
heatmap = F.relu(heatmap)

# normalize the heatmap
heatmap /= torch.max(heatmap)

# draw the heatmap
plt.matshow(heatmap.detach())

这是我们的热图。图像由作者提供。

值得注意的是,我们通过前向钩子获得的激活包含 1,024 个特征图,每个特征图捕捉输入图像的不同方面,每个特征图的空间分辨率为 8x8。

另一方面,我们通过反向钩子获得的梯度代表了每个特征图对最终预测的重要性。通过计算梯度和激活的逐元素乘积,我们得到特征图的加权和,突出显示图像中最相关的部分。

最后,通过计算加权特征图的全局平均值,我们得到一个单一的热图,指示出对模型预测最重要的图像区域。这种技术被称为 Grad-CAM,提供了对模型决策过程的视觉解释,有助于我们解读和调试模型的行为。

6. 结合原始图像和热图

以下代码将一张图像叠加在另一张图像上。

from torchvision.transforms.functional import to_pil_image
from matplotlib import colormaps
import numpy as np
import PIL

# Create a figure and plot the first image
fig, ax = plt.subplots()
ax.axis('off') # removes the axis markers

# First plot the original image
ax.imshow(to_pil_image(img_tensor, mode='RGB'))

# Resize the heatmap to the same size as the input image and defines
# a resample algorithm for increasing image resolution
# we need heatmap.detach() because it can't be converted to numpy array while
# requiring gradients
overlay = to_pil_image(heatmap.detach(), mode='F')
                      .resize((256,256), resample=PIL.Image.BICUBIC)

# Apply any colormap you want
cmap = colormaps['jet']
overlay = (255 * cmap(np.asarray(overlay) ** 2)[:, :, :3]).astype(np.uint8)

# Plot the heatmap on the same axes, 
# but with alpha < 1 (this defines the transparency of the heatmap)
ax.imshow(overlay, alpha=0.4, interpolation='nearest', extent=extent)

# Show the plot
plt.show()

这是结果。由于这是正常的 X 光片,模型主要关注于正常 X 光片中预期的正常结构。图片由作者提供。

在另一个例子中,我们有一张肺炎的 X 光片。Grad-CAM 正确地显示了医生必须检查的胸部 X 光片区域,以确认肺炎的存在。图片由作者提供。X 光图像来自kaggle 胸部 X 光数据集

最后,要从你的模型中移除钩子,你只需在每个句柄中调用remove方法。

backward_hook.remove()
forward_hook.remove()

结论

我希望这篇文章有助于澄清 Grad-CAM 的工作原理、如何使用 Pytorch 实现它,以及如何通过使用前向和反向钩子而不改变原始模型的前向函数来实现它。

我要感谢Stepan Ulyanin的文章,并感谢他帮助我更好地理解 Grad-CAM。我希望我也能为读者贡献一些东西。

我还想推荐 Python 库 torch-cam 作为参考。它有 Grad-CAM 的其他实现,因此你不需要从头开始做这个。

[## GitHub - frgfm/torch-cam: 用于 PyTorch 模型的类别激活图(CAM、Grad-CAM、Grad-CAM++…

在 PyTorch 中利用卷积层的类别特定激活的简单方法。来源:woopets 的图片…

github.com](https://github.com/frgfm/torch-cam?source=post_page-----7eba5e38d569--------------------------------)

在 Excel 中进行梯度提升线性回归

原文:towardsdatascience.com/gradient-boosted-linear-regression-in-excel-a08522f13d6a

为了更好地理解梯度提升

Angela 和 Kezhan ShiTowards Data Science Angela 和 Kezhan Shi

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

--

梯度提升是一种通常应用于决策树的集成方法。它如此常见,以至于我们通常用梯度提升来指代梯度提升决策树。例如,在 scikit-learn 中,估算器 GradientBoostingRegressor 或 GradientBoostingClassifier 使用决策树。然而,作为一种集成方法,它也可以应用于其他基础模型,如线性回归。但有一个显而易见的结论,你可能已经知道:

梯度提升线性回归就是,嗯,线性回归。

但实现它仍然很有趣,而且我们将通过 Excel 来完成它,因此即使你不熟悉编程复杂算法,你也能理解这些算法步骤。

机器学习的三步法

我写了 一篇文章来始终区分机器学习的三个步骤,以有效学习,让我们将这个原则应用到梯度提升线性回归中,这里是三个步骤:

  • 1. 模型:线性回归 是一种机器学习模型,因为它接受输入(特征)来预测输出。

  • 1bis. 集成方法:梯度提升 是一种集成方法,它本身并不是一个模型(因为它不直接接受输入来预测目标变量的输出)。它必须应用于某个基础模型来创建一个元模型。这里我们将创建一个梯度提升的线性回归元模型。

  • 2. 模型拟合:线性回归必须进行拟合,这意味着其系数必须针对给定的训练数据集进行优化。梯度下降是一种可以应用于线性回归的拟合算法。但它并不是唯一的。在线性回归的情况下,有一个可以用数学公式表示的精确解。还值得注意的是,集成方法没有拟合算法。

  • 3. 模型调整包括优化模型或元模型的超参数。在这里,我们将遇到两个超参数:梯度提升算法的学习率和步骤数。

这是三步搞定的机器学习!

作为基础模型的简单线性回归

在这里,我们将使用一个简单的线性回归作为基础模型,并使用一个包含十个观察值的简单数据集。我们将重点关注梯度提升部分,对于拟合,我们将使用 Google Sheet 中的一个函数(在 Excel 中也适用):LINEST 来估计线性回归的系数。

梯度提升的线性回归 \ 简单数据集与线性回归 — 作者图像

我将使用 Google Sheet 来演示本文中梯度提升的实现过程。如果你想访问这个表格以及我开发的其他表格——例如带有梯度下降的线性回归、逻辑回归、具有反向传播的神经网络、KNN、k-means 等——请考虑在 Ko-fi 上支持我。你可以在以下链接找到所有这些资源:ko-fi.com/s/4ddca6dff1

梯度提升算法

这是梯度提升算法的主要步骤

  1. 初始化:我们将选择平均值作为梯度提升算法的第一步。

  2. 残差误差计算:我们计算预测值(对于第一步,它是平均值)和训练数据中的实际值之间的残差误差。

  3. 拟合残差的线性回归:我们创建一个线性回归模型来拟合残差。

  4. 将新模型添加到集成中:将之前的模型和新模型结合起来,创建一个新的集成模型。在这里,我们必须将学习率或收缩作为超参数应用于弱模型。

  5. 重复过程:重复步骤 2–4,直到达到指定的提升阶段数或直到误差收敛。

就这样!这是应用于线性回归的梯度提升的基本过程。我想以简单的方式描述,并且我们可以写一些方程式来说明每次迭代:

  • 步骤 1:f0 = 实际 y 的平均值

  • 步骤 2:resd1 = y — f0

  • 步骤 3:resdfit1 = a0 x + b0 以预测 y — f0

  • 步骤 4:f1 = f0 — learning_rate * (a0 x + b0)

  • 步骤 2–2:resd2 = y — f1

  • 步骤 3–2:resdfit2 = a1 x + b1 以预测 y — f1

  • 步骤 4–2:f2 = f1-learning_rate * (a1 x + b1),可以展开为:f0 — learning_rate * (a0 x + b0) — learning_rate * (a1 x + b1)

与梯度提升树的比较

如果你仔细查看上一节的算法,你可能会注意到两个奇怪的现象。

首先,在第 2 步中,我们将线性回归拟合到残差上,这需要时间和算法步骤来实现模型拟合步骤,而不是将线性回归拟合到残差上,我们可以直接将线性回归拟合到 y 的实际值上,我们已经会找到最终的最佳模型!

其次,当将一个线性回归添加到另一个线性回归时,它仍然是线性回归。

例如,我们可以将 f2 重写为:f2 = f0 — learning_rate * (b0+b1) — learning_rate * (a0+a1) x

那就是线性回归!

对于决策树,这两种奇怪的情况不会发生,因为将一棵树添加到另一棵树上与将一棵树进一步生长是不一样的。

在进入实现部分之前,还有一个问题:如果我们将学习率设置为 1,会发生什么?梯度提升线性回归会怎样?

梯度提升线性回归实现

在 Google Sheet 或 Excel 中实现这些公式是直接的。

下表展示了训练数据集以及梯度提升步骤的不同阶段

在 Excel 中进行的梯度提升线性回归 — 作者图像

对于每一步拟合,我们使用 Excel 函数 LINEST

带有系数估计公式的梯度提升线性回归 — 作者图像

我们只进行 2 次迭代,我们可以猜测更多迭代的情况。下面是一个图形,展示了每次迭代的模型。不同的红色阴影展示了模型的收敛情况,我们还展示了通过梯度下降直接应用于 y 找到的最终模型。

梯度提升线性回归 — 作者图像

调整超参数

我们可以调整两个超参数:迭代次数和学习率。

对于迭代次数,我们只实施了两个,但很容易想象更多的情况,我们可以通过检查残差的大小来停止。

对于学习率,我们可以在 Google Sheet 中进行调整,看看会发生什么。当学习率很小时,“学习过程”将会很慢。如果学习率为 1,我们可以看到收敛在第 1 次迭代时就达到了。

学习率 =1 的梯度提升线性回归 — 作者图像

并且第 1 次迭代的残差已经是零。

学习率 =1 的梯度提升线性回归 — 作者图像

如果学习率高于 1,模型将会发散。

梯度提升线性回归的发散—— 作者提供的图像

与梯度下降算法的比较

梯度提升中的学习率和迭代次数的工作方式与梯度下降非常相似……哦,等等,它们非常相似,实际上是相同的算法!在经典梯度下降的情况下,该算法应用于模型的参数,如线性回归的权重或系数。而在梯度提升的情况下,该算法应用于模型。

即使“提升”这个词只意味着“添加”,它与经典梯度下降算法中的步骤完全相同,都是从初始(随机选择的)起点一步步添加下降步骤。

如果你还不信服,可以阅读这篇关于梯度下降与梯度提升的对比:逐步比较的文章。

结论

我希望你对梯度提升的工作原理有了更多的了解。以下是主要要点。

  • Excel 是理解算法如何工作的绝佳方式。

  • 梯度提升是一种集成方法,可以应用于任何基础模型。

  • 梯度提升在某种意义上与梯度下降相同,因为它们是相同的算法,但应用于不同的对象:参数与函数或模型。

  • 梯度提升可以应用于线性回归,但这只是为了理解算法,因为在实际应用中你并不需要这样做,因为梯度提升线性回归就是线性回归。

我写关于机器学习和数据科学的文章,并尝试以清晰的方式简化复杂的概念。请通过下面的链接关注我,并获得我文章的完整访问权限:medium.com/@angela.shi/membership

梯度提升:预测中的银弹

原文:towardsdatascience.com/gradient-boosting-a-silver-bullet-in-forecasting-5820ba7182fd

我们展示了梯度提升在时间序列预测中非常强大,并尝试解释原因

Davide BurbaTowards Data Science Davide Burba

·发表于 Towards Data Science ·6 分钟阅读·2023 年 7 月 20 日

--

“梯度提升”,由 Giulia Roggia。经许可使用。

  • 什么是梯度提升?

  • 梯度提升作为银弹

  • 为什么梯度提升如此出色?

  • 注意事项

  • 附录:竞赛和发布解决方案列表

时间序列预测在许多领域中都非常重要,包括金融、销售和天气预测。尽管经典的时间序列模型和深度学习技术已被广泛使用,但有越来越多的证据表明,梯度提升通常优于其他方法。

什么是梯度提升?

梯度提升 是一种机器学习技术,通过按顺序组合一组弱学习者来构建预测模型。它旨在通过迭代最小化前一个模型的误差来创建一个强学习者。其核心思想是将后续模型拟合到前一个模型的残差上,通过每次迭代逐步提高预测精度。

图片由作者提供。

LightGBMXGBoost 是两个著名的实现梯度提升算法的库。它们因高效、可扩展和卓越的性能而受到欢迎。

尽管梯度提升并非专门为时间序列数据设计,但我们可以通过特征工程步骤将其用于预测。你可以查看这篇文章以获取具体的示例。

梯度提升作为银弹

我们可以通过检视竞赛的获胜解决方案来评估某一领域内最强的模型。获胜的解决方案有时会因为过于复杂且难以在生产环境中复制而受到批评。然而,当某个特定模型在不同竞赛中 consistently 出现于获胜解决方案中时,它展示了应对复杂挑战的能力。

最著名的数据科学竞赛平台是 Kaggle。让我们来看看过去 10 年中进行的货币预测竞赛(见下图)。

这是从 Kaggle 截取的屏幕截图,拍摄于 2023 年 5 月。

顶级解决方案通常会在竞赛结束后发布在讨论区。在本分析中,我们考虑了那些进入前 10 的发布解决方案(你可以在附录中查看详情)。在这些解决方案中,我们看到:

  • 所有竞赛都有使用梯度提升的顶级解决方案(通常是 LightGBM)

  • 如果我们只考虑每个竞赛的单一顶级发布解决方案,我们会发现它们 在 8 个案例中有 6 个是基于梯度提升的(另外两个基于深度学习模型,即 MLBWikipedia

我们不得不放弃最早的两个竞赛,因为没有发布解决方案,以及“M5 不确定性”竞赛,因为一些解决方案基于“M5 准确性”(这些解决方案通常基于 LightGBM,包括获胜者)。

为什么梯度提升如此出色?

有几个因素使得梯度提升成为一个强大的模型:

  1. 处理非线性关系:传统的时间序列模型通常难以捕捉数据中的非线性模式。另一方面,梯度提升可以自动学习变量之间的复杂关系。特别是,基于树的结构能够学习突变,这在表格数据中很常见,并且对于深度学习模型而言更难以学习。

  2. 全局模型:梯度提升允许训练全局模型,即在多个时间序列上训练单个模型。这通常比使用每个时间序列一个模型的方式(传统时间序列模型使用的方式)能得到更好的预测效果。有关局部模型与全局模型的更多信息,你可以查看这篇文章

  3. 分类特征:LightGBM 的一个优点是能够处理分类特征,而无需进行 one-hot 编码,这通常效率不高,或者由于内存限制甚至不可行。

  4. 微调:除了特征工程外,我们可以微调的选项范围很广,以改善基于梯度提升的模型,包括选择自定义损失函数和参数,以提高对无信息特征的鲁棒性。

  5. 快速且可扩展:LightGBM 和 XGBoost 在设计时考虑了效率和可扩展性。这些库采用了先进的优化技术和并行处理,使得处理大规模数据成为可能。

有多个资源对梯度提升进行基准测试,并尝试解释为何梯度提升在表格数据上表现优异,这些资源通常也适用于时间序列预测。以下是一些这些资源:

警告

我们看到梯度提升是时间序列预测的强大模型。然而,有些情况下使用其他模型可能更为合适:

  • 小而规则的数据:如果数据非常小(例如仅有少数样本的单一时间序列)且平滑,传统方法如指数平滑或 ARIMA 可能更为合适。原因在于梯度提升仍需要最少量的数据来拟合和调整超参数。另一方面,传统时间序列模型对生成数据的随机过程有较强的假设,这使得参数估计更为容易。在实践中,这种数据在实际问题中很少见。

  • 大规模数据:梯度提升相当可扩展。然而,如果我们谈论的是数十亿的数据样本,可能更倾向于使用深度学习方法。除了深度学习模型在大数据上表现优越外,它们还可以不断训练,而梯度提升模型通常需要从头开始训练。此外,它们在分类特征方面通过嵌入提供了更好的可扩展性。

  • 极低的信噪比:虽然梯度提升对噪声相当鲁棒,但在一些极端情况下(例如股票价格预测),可能会导致过拟合。如果是这样,可能值得尝试一些较不强大但更为鲁棒的模型(例如线性模型)。

附录:竞赛和已发布解决方案列表

下面你可以找到为本文考虑的竞赛列表和顶级公开解决方案。

[## 2023 年三月机器学习狂欢]

预测 2023 年 NCAA 篮球锦标赛

www.kaggle.com

[## G-Research 加密货币预测

运用你的机器学习专业知识预测真实的加密货币市场数据

www.kaggle.com

[## MLB 球员数字互动预测

预测棒球选手数字内容的粉丝互动

www.kaggle.com

[## M5 预测 — 准确性

估计沃尔玛零售商品的单位销售

www.kaggle.com](https://www.kaggle.com/competitions/m5-forecasting-accuracy?source=post_page-----5820ba7182fd--------------------------------)

[## M5 预测 — 不确定性

估计沃尔玛单位销售的不确定性分布。

www.kaggle.com](https://www.kaggle.com/competitions/m5-forecasting-uncertainty?source=post_page-----5820ba7182fd--------------------------------)

跳过:基于 M5 准确性的解决方案。

[## Recruit 餐厅访客预测

预测餐厅未来将接待多少访客

www.kaggle.com](https://www.kaggle.com/competitions/recruit-restaurant-visitor-forecasting?source=post_page-----5820ba7182fd--------------------------------)

[## Corporación Favorita 超市销售预测

你能准确预测大型超市链的销售吗?

www.kaggle.com](https://www.kaggle.com/competitions/favorita-grocery-sales-forecasting?source=post_page-----5820ba7182fd--------------------------------)

[## 网络流量时间序列预测

预测维基百科页面的未来流量

www.kaggle.com](https://www.kaggle.com/competitions/web-traffic-time-series-forecasting?source=post_page-----5820ba7182fd--------------------------------)

[## Rossmann Store Sales

使用商店、促销和竞争对手数据进行销售预测

www.kaggle.com](https://www.kaggle.com/competitions/rossmann-store-sales?source=post_page-----5820ba7182fd--------------------------------)

[## 流感预测

预测流感的发生时间、地点和强度

www.kaggle.com](https://www.kaggle.com/competitions/genentech-flu-forecasting?source=post_page-----5820ba7182fd--------------------------------)

讨论页面无法加载。

[## AMS 2013–2014 太阳能预测竞赛

使用天气模型的集成预测每日太阳能

www.kaggle.com](https://www.kaggle.com/competitions/ams-2014-solar-energy-prediction-contest/overview?source=post_page-----5820ba7182fd--------------------------------)

无已发布的解决方案。

喜欢这篇文章? 看看我的其他文章 并关注我获取更多内容! 点击这里 阅读无限文章,并在不增加额外费用的情况下支持我❤️

从理论到实践的梯度提升(第一部分)

原文:towardsdatascience.com/gradient-boosting-from-theory-to-practice-part-1-940b2c9d8050

理解流行梯度提升算法背后的数学原理以及如何在实践中使用它

Dr. Roi YehoshuaTowards Data Science Dr. Roi Yehoshua

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

--

图片由Jens Lelie提供,出处:Unsplash

梯度提升是一种广泛使用的机器学习技术,基于提升梯度下降的结合。

Boosting 是一种集成方法,通过将多个弱学习者(或基本学习者)结合起来创建强预测模型。基本模型是按顺序训练的,每个模型侧重于纠正前一个模型的错误。

梯度提升中,每个基本模型被训练以预测相对于前一个模型预测的损失函数的负梯度。因此,将新训练的基本学习者添加到集成中,会朝着损失最小值的最陡下降方向前进。这个过程类似于梯度下降,但它在函数空间中操作而不是参数空间中。因此,它被称为函数梯度下降

当弱学习者是决策树时,所得的方法称为梯度提升决策树(GBDT)或梯度提升机(GBM)。

梯度提升是当前处理结构化(表格)数据的最佳算法之一,并在许多标准分类基准上提供了最先进的结果。与深度学习一起,它是 Kaggle 竞赛中最常用的算法之一。

梯度提升算法最初由 Jerome Freidman 于 2001 年开发[1]。从那时起,它演变成了一系列算法,包括 XGBoost、CatBoost 和 LightGBM。这些算法的变体包含了各种改进,进一步提升了梯度提升的性能和可扩展性。

本文将深入探讨梯度提升的理论和实现。在文章的第一部分,我们将关注梯度提升的理论概念,展示算法的伪代码,并在一个小的数值示例中演示其使用。在第二部分中,我们将探索 Scikit-Learn 中实现梯度提升的类,并使用它们来解决不同的回归和分类任务。

直观介绍

作为提醒,在监督机器学习问题中,我们给定了一个 n 个标记样本的训练集: D = {(x₁, y₁), (x₂, y₂), … , (xₙ, yₙ)},其中 x 是一个 m 维向量,包含样本 i 的特征, yᵢ 表示该样本的标签。我们的目标是建立一个模型,其预测尽可能接近真实标签。

在我们的初步讨论中,我们假设学习问题是回归问题,即目标 yᵢ 是连续值。

梯度提升的基本思想是构建一个弱模型的集合,每个模型都被训练来预测前一个模型的残差。这个过程可以描述如下:

  1. 将基础模型 h₁(x) 拟合到给定标签 y 上。

  2. 将初始集合设置为 F₁(x) = h₁(x)。

  3. 将基础模型 h₂(x) 拟合到残差 yF₁(x) 上。

  4. 将这两个模型组合成一个新的集合: F₂(x) = h₁(x) + h₂(x)。 F₂(x) 的预测应比 F₁(x) 更接近目标。

  5. 将基础模型 h₃(x) 拟合到残差 yF₂(x) 上。

  6. 将这三个模型组合成一个新的集合: F₃(x) = h₁(x) + h₂(x) + h₃(x)。 F₃(x) 的预测应比 F₂(x) 更接近目标。

  7. 继续这个过程 M 步。

  8. 返回 Fₘ(x) 作为最终假设。

我们可以通过手动构建一系列回归树来在 Python 中演示这个过程,每棵树都被训练来预测前面树的残差。

首先生成一些有噪声的二次训练集:

n_samples = 100
X = np.random.rand(n_samples, 1) - 0.5
y = 5 * X[:, 0] ** 2 + 0.1 * np.random.randn(n_samples)

plt.scatter(X, y, s=20)

训练数据

我们的基础学习器将是最大深度为 2 的决策树。第一个决策树 h₁ 适合给定的数据集:

h1 = DecisionTreeRegressor(max_depth=2)
h1.fit(X, y)

第一个集合 F₁ 由这棵单独的树组成,它在训练集上的 R² 分数为:

F1 = [h1]  # ensemble of one tree
F1_pred = h1.predict(X)
print(f'R2 score of F1: {r2_score(y, F1_pred):.4f}')
R2 score of F1: 0.7819

第二棵树 h₂ 被拟合到第一棵树的残差错误上:

h2 = DecisionTreeRegressor(max_depth=2)
y2 = y - F1_pred
h2.fit(X, y2)

h₂ 被添加到集成中以创建第二个集成 F₂。我们可以通过简单地将两棵树的预测结果相加来使用 F₂ 进行预测:

F2 = [h1, h2] # ensemble of two trees
F2_pred = sum(h.predict(X) for h in F2)
print(f'R2 score of F2: {r2_score(y, F2_pred):.4f}')
R2 score of F2: 0.8802

最后,一个第三棵树 h₃ 被拟合到第二个集成 F₂ 的残差上,然后添加到集成中:

h3 = DecisionTreeRegressor(max_depth=2)
y3 = y - F2_pred
h3.fit(X, y3)

F3 = [h1, h2, h3] # ensemble of three trees
F3_pred = sum(h.predict(X) for h in F3)
print(f'R2 score of F3: {r2_score(y, F3_pred):.4f}')
R2 score of F3: 0.9124

注意,随着我们将更多树添加到集成中,模型的 R² 分数是如何逐渐增加的。

让我们在每次迭代后绘制残差和集成的预测结果:

fig, axes = plt.subplots(2, 3, sharex=True, sharey=True, figsize=(12, 7))
X_test = np.linspace(-0.5, 0.5, 500).reshape(-1, 1)

for i, h, residuals in zip([0, 1, 2], [h1, h2, h3], [y, y2, y3]):
    ax = axes[0, i]
    y_test_pred = h.predict(X_test)
    ax.scatter(X, residuals, c='k', s=20, marker='x', label='Residuals')
    ax.plot(X_test, y_test_pred, 'r', linewidth=2)
    ax.set_title(f'$h_{i + 1}(x)$')
    ax.legend(loc='upper center')

for i, ensemble in enumerate([F1, F2, F3]):
    ax = axes[1, i]
    y_test_pred = sum(h.predict(X_test) for h in ensemble)
    ax.scatter(X, y, s=20, label='Training set')
    ax.plot(X_test, y_test_pred, 'm', linewidth=2)
    ax.set_title(f'$F_{i + 1}(x)$')
    ax.legend(loc='upper center')

每次提升迭代后的基础学习器和集成

我们可以看到,随着更多树的加入,集成的预测逐渐改进。

梯度提升对其他类型问题(例如分类问题)和其他损失函数的推广来源于观察到残差 hₘ(x) 与平方损失函数关于 Fₘ₋₁(x) 的负梯度成比例:

因此,我们可以通过使用损失函数的负梯度而不是残差,将此技术推广到任何可微损失函数。

梯度提升算法

我们现在将推导出适用于任何可微损失函数的一般梯度提升算法。

提升通过 加法扩展(集成)的形式来逼近特征到标签 y = f(x) 的真实映射:

其中 hₘ(x) 是来自某个类别 H 的基础学习器(通常是固定大小的决策树),M 是学习器的数量。

给定损失函数 L(y, F(x)),我们的目标是找到一个近似 F(x) 以最小化训练集上的总损失:

目标函数 J 包括作为参数的函数(函数 hₘ),因此不能使用传统优化方法如梯度下降进行优化。相反,模型以加法方式训练,每次添加一个基础学习器。

我们从一个最小化目标的常数函数模型 F₀ 开始:

例如,如果损失函数是平方损失(用于回归问题),F₀(x) 将简单地是目标值的均值。

然后,我们以贪婪的方式逐步扩展模型:

新添加的基础学习器 hₘ 被拟合以最小化集成 Fₘ 的期望损失:

寻找适用于任意损失函数 L 的最佳函数 hₘ 在计算上是不可行的,因为这需要我们枚举 H 中的所有可能函数并挑选最佳函数。相反,我们使用迭代方法:在每次迭代中,我们选择一个基学习器 hₘ,使其指向损失函数的负梯度方向。因此,将 hₘ 添加到集成中将使我们更接近最小损失。

这个过程类似于梯度下降,但它在函数空间中操作,而不是在参数空间中,因为在每次迭代中,我们在假设空间 H 中移动到不同的函数,而不是在特定函数 h 的参数空间中迈出一步。这允许 h 成为一个非参数的机器学习模型,例如决策树。这个过程被称为函数梯度下降

在函数梯度下降中,我们的参数是 F(x) 在点 x₁, …, x 处的值,我们试图最小化每个 x 处的 L(yᵢ, F(x))。每个点 x 处损失函数的最佳最陡下降方向是其负梯度:

gₘ(x) 是损失相对于其第二个参数的导数,在 Fₘ₋₁(x) 处进行评估。

因此,向量

给出了在 Fₘ₋₁(x) 的 n 维数据空间中最佳的最陡下降方向。然而,这个梯度仅在数据点 x₁, …, x 上定义,不能推广到其他 x 值。

在连续情况下,其中 HR 上任意可微分函数的集合,我们可以简单地选择一个函数 hₘH,使得 hₘ(x) = -gₘ(x)。

在离散情况下(即,当集合 H 是有限的时),我们选择 hₘ 作为 H 中最接近 gₘ(x) 在数据点 x 处的函数,即 hₘ 最与向量 -gRⁿ 中平行。这个函数可以通过将基学习器 hₘ 拟合到训练集 {(x, ỹᵢₘ)} 来获得,标签为

这些标签被称为伪残差。换句话说,在每次提升迭代中,我们都将一个基学习器拟合到预测损失函数相对于前一次迭代的集成预测的负梯度。注意,这种方法是启发式的,并不一定能给出优化问题的精确解。

算法的完整伪代码如下:

梯度树提升

梯度树提升是梯度提升算法的一种专门化,适用于基学习器 h(x) 是回归树的情况(有关回归树的更多细节,请参见这篇文章)。

在每次迭代m中,一个回归树hₘ(x)会拟合伪残差,即我们建立一棵树来预测伪残差,给定我们的数据点。树是使用均方误差作为分裂标准以自上而下的贪婪方式构建的。

问题是树应该给定叶节点分配哪个标签?对回归问题,使用该叶子样本的均值效果很好,但对于其他类型的问题则不然。因此,我们需要找到一种通用的方法来为每个叶子节点分配输出值,以最小化模型对任何可微损失函数的预期损失。

Jₘ表示树中叶子的数量。树将输入空间划分为Jₘ个不相交的区域:R,…,Rⱼₘ,并预测一个常量值。

每一个中的γⱼₘ

其中 1(⋅)是指示函数,其值为 1(如果其参数为真),否则为 0。

我们的目标是在每个区域中找到系数γⱼₘ,以最小化由属于该区域的点引起的总损失:

即,我们试图找到每个叶子节点区域中的最优常量更新,以便添加到之前的集成Fₘ₋₁(x)的预测中。

例如,让我们找到最小二乘回归情况下的最优γ。在这种情况下,我们的目标是最小化以下平方损失的总和:

我们现在对E关于γ进行求导并设其为 0:

即,最优的γ就是第m次迭代中jth 叶节点的残差均值:

分类的情况将在后续讨论。

一旦我们找到叶节点的最优输出值,当前的近似Fₘ₋₁(x)将在每个相应区域中分别更新:

正则化

与标准决策树相比,梯度提升树对过拟合相当稳健。尽管如此,仍有几种常用的正则化技术可以帮助控制梯度提升树的复杂性。

首先,我们可以使用与标准决策树相同的正则化技术,例如限制树的深度、叶子数量或分裂节点所需的最小样本数量。我们还可以使用后剪枝技术来移除那些未能通过预定义阈值减少损失的树枝。

其次,我们可以控制提升迭代的数量(即,集成中的树木数量)。增加树木数量减少了集成在训练集上的误差,但也可能导致过拟合。最佳的树木数量通常通过早停法来确定,即算法在验证集上的评分在指定的迭代次数内没有改善时终止。

最后,Friedman [1, 2] 提出了以下更具体于梯度提升树的正则化技术:

缩减

缩减 [1] 通过一个常数因子ν来缩放每个基础学习器的贡献:

参数ν(0 < ν ≤ 1)称为学习率,因为它控制梯度下降过程的步长。类似于随机优化中的学习率,缩减减少了每个个体学习器的影响,并为未来的学习者改进模型留出了空间。

实证研究发现,使用小学习率(例如,ν ≤ 0.1)可以显著提高模型的泛化能力。然而,更小的学习率也需要更多的提升迭代以保持相同的训练误差,从而增加了训练和预测的计算时间。

随机梯度提升(子采样)

在随后的论文 [2] 中,Friedman 提出了随机梯度提升,它将梯度提升与装袋相结合。

在每次迭代中,基础学习器仅在训练集的一个子集(通常为 0.5)上训练,该子集是随机抽取的且不替换。这种子采样过程为算法引入了随机性,并有助于防止模型过拟合。

与装袋方法类似,子采样也允许我们使用袋外样本(未参与构建下一个基础学习器的样本)来评估模型的性能,而不是使用独立的验证数据集。袋外估计通常低估了模型的实际性能,因此仅在交叉验证花费时间过长时使用。

另一种减少模型方差的策略是随机采样每个树节点中考虑进行拆分的特征(类似于 随机森林)。

带有缩减的梯度树提升的伪代码如下所示:

分类的梯度树提升

相同的梯度树提升算法也可以用于分类任务。然而,由于树的和Fₘ(x)可以是任何连续值,因此需要将其映射为概率(介于 0 和 1 之间的值)。这种映射依赖于分类问题的类型(二分类或多分类)。

请注意,梯度提升树始终是回归树,即使它们被用于分类问题(因为它们是为了逼近损失函数的负梯度而构建的)。

二分类

在二分类问题中,我们使用的是 sigmoid 函数

σ(x) 用于建模样本属于正类的概率(类似于 逻辑回归):

这意味着 F(x) 代表预测的对数几率,即对数几率比:

初始模型 F₀(x) 由训练集中正类的对数几率给出:

与逻辑回归类似,我们使用二元对数损失作为我们的损失函数:

其中 pᵢ = σ(Fₘ₋₁(x)) 代表之前集成模型预测的样本 x 属于正类的概率。

我们可以使用链式法则计算关于 Fₘ₋₁(x) 的损失梯度:

我们在这里使用了 sigmoid 函数的导数:

因此,点 x 处的伪残差为:

即,伪残差就是x的实际标签减去之前集成模型预测的该样本属于正类的概率。

以回归树作为基础学习器,我们需要为每个叶节点找到最优输出值 γⱼₘ

这个优化问题没有封闭形式的解(请注意 γ 被添加到对数几率中,而不是直接添加到类别概率中)。因此,我们通过执行单次 牛顿-拉夫森 步骤来逼近解。

作为提醒,牛顿方法试图通过从初始猜测x₀ ∈ R开始构建一个序列 {xₖ} 来寻找一个二次可微函数f 的最小值:RR。这个序列是通过围绕元素xₖf 的二阶泰勒近似构造的。

fxₖ 周围的二阶泰勒展开为:

序列 xₖ₊₁ 中的下一个元素被选择为最小化 t 中的二次展开。最小值可以通过将该展开的导数设为 0 来找到:

因此,最小值可以通过以下公式得到:

因此,牛顿方法执行以下迭代:

同样,我们可以写出围绕点 Fₘ₋₁(x) 的损失函数的二阶泰勒展开:

对这个表达式对γ求导得到:

因此,由Rⱼₘ区域中的样本引起的总损失的导数是:

将这个导数等于 0 给出我们Rⱼₘ区域的最佳输出值:

这个表达式的分子是Rⱼₘ区域内数据点(它们的伪残差)损失函数导数的总和,而分母是相同点损失函数二阶导数的总和。

我们已经找到了对数损失的第一导数:

我们现在需要找到它的第二导数。这个导数可以通过对对数损失的第一导数进行求导来简单计算:

因此,我们可以写出:

在一个玩具数据集上的示例

假设我们得到以下数据集用于二分类问题:

目标是根据三个属性预测客户是否会购买某个产品:客户的年龄、收入水平(低、中或高)和教育水平(高中或大学)。

为了解决这个问题,我们将使用一个最大深度为 2 的梯度提升树集成,学习率为ν = 0.5(我们使用相对较大的学习率以便于说明)。这些树将仅使用二元分裂(如 CART 算法)。

首先,我们用一个常数值初始化模型,该值是正类的对数几率:

接下来,我们计算伪残差。对于对数损失函数,这些只是实际标签减去预测标签:

现在我们将回归树拟合到伪残差上。我们首先找到根节点的最佳分裂。在回归树中,我们在每个节点选择一个分裂,使得节点上存储的值的方差(在我们这里是伪残差)降低最大。

根节点的残差均值为:

因此,方差只是它们平方值的平均数:

我们将计算每个特征中每个可能分裂所能实现的方差减少量。我们从两个类别属性开始:

对于年龄属性,我们按年龄对伪残差进行排序,然后考虑两个连续年龄之间的每个中点作为候选分裂点:

提供最大方差减少的分裂点是 Age < 26.5。因此,树的第一层如下所示:

左子节点的残差方差为 0,因此无需再分裂。我们现在需要找到右子节点的最佳分裂点。

首先,让我们计算右子节点残差的方差:

现在我们考虑属于右子节点的四个样本(样本 2、4、5、6)的所有可能的分裂点:

在这种情况下,我们有多个候选分裂点,这些分裂点能最大程度减少方差(0.0835)。我们可以随意选择分裂点 Income = Medium。得到的回归树是:

接下来,我们计算叶节点的最佳输出值(γ 系数)。请注意,由于这是我们的第一棵树,所有样本的最新预测概率是 p = σ(F₀(x)) = 0.667。

最左边叶子的输出值是:

同样,其他两个叶子的输出值是:

因此,我们从第一棵回归树中得到以下预测:

我们现在将这些预测值按学习率进行缩放,并将其加到上一轮的预测值中。然后,我们使用新的预测值计算下一轮的伪残差:

我们现在对伪残差拟合另一棵回归树。按照与之前相同的过程,我们得到以下树(验证这确实是结果树!):

接下来,我们计算叶节点的输出值。请注意,这一次最新的预测概率 p = σ(F₁(x)) 对所有样本并不相同。从最左边的叶节点到最右边的节点,我们得到:

因此,我们从第二棵树中得到以下预测:

我们现在将这些预测值按学习率进行缩放,并将其加到之前模型的预测值中:

我们可以看到经过三次迭代,我们的集成方法正确地分类了训练集中的所有样本。太棒了!

让我们看看如何使用这个集成方法对新样本进行预测。假设我们有一个新客户,属性如下:年龄 = 33,收入 = 中等,教育水平 = 高中。

我们首先需要找出样本在每棵树中的哪个终端区域。在第一棵树中,样本属于区域 R₂₁(因为 [年龄 < 26.5] 为假,[收入 = 中等] 为真),而在第二棵树中,它属于区域 R₂₂(因为 [教育 = 高中] 为真,[年龄 < 30] 为假)。

因此,该客户购买产品的预测对数几率为:

预测的概率为:

由于 p < 0.5,我们的预测是该客户不会购买该产品。

多类别分类

在多类别分类问题中,每次迭代的 M 轮中都会构建 K 棵树(对应 K 类)。x 属于类别 k 的概率被建模为 Fₘ,ₖ(x) 值的 softmax:

在这种情况下,初始模型由每个类别的先验概率给出,损失函数是交叉熵损失。γⱼₘ 系数的推导留给读者作为练习。

最终说明

你可以在我的 GitHub 上找到本文的代码示例:github.com/roiyeho/medium/tree/main/gradient_boosting

本文的第二部分可以在 这里 找到。

感谢阅读!

参考文献

[1] Friedman, J.H. (2001). 贪婪函数逼近:一种梯度提升机器. 统计年鉴, 29, 1189–1232.

[2] Friedman, J.H. (2002). 随机梯度提升. 计算统计与数据分析, 38, 367–378.

理论与实践中的梯度提升(第二部分)

原文:towardsdatascience.com/gradient-boosting-from-theory-to-practice-part-2-25c8b7ca566b

使用 Scikit-Learn 中的梯度提升类解决不同的分类和回归问题

Dr. Roi YehoshuaTowards Data Science Dr. Roi Yehoshua

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

--

照片由Luca Bravo拍摄,来源于Unsplash

第一部分中,我们介绍了梯度提升算法,并展示了其伪代码实现。

在这部分文章中,我们将探讨 Scikit-Learn 中实现这一算法的类,讨论它们的各种参数,并演示如何使用它们解决几个分类和回归问题。

尽管 XGBoost 库提供了一个更优化和高度可扩展的梯度提升实现,但对于小到中等规模的数据集,使用 Scikit-Learn 中的梯度提升类通常更为简便,因为它们有更简单的接口和显著更少的超参数需要调整。

Scikit-Learn 中的梯度提升

Scikit-Learn 提供了以下实现梯度提升决策树(GBDT)模型的类:

  1. GradientBoostingClassifier用于分类问题。

  2. GradientBoostingRegressor用于回归问题。

除了决策树的标准参数,如criterion, max_depth(默认设置为 3)和min_samples_split,这些类还提供以下参数:

  1. 损失 — 需要优化的损失函数。在GradientBoostingClassifier中,这个函数可以是'log_loss'(默认值)或'exponential'(这将使梯度提升表现得像AdaBoost)。在 GradientBoostingRegressor 中,这个函数可以是'squared_error'(默认值)、'absolute_error''huber'或‘quantile’(有关这些损失函数之间的差异,请参见这篇文章)。

  2. n_estimators — 提升迭代的次数(默认值为 100)。

  3. 学习率 — 缩小每棵树贡献的因子(默认值为 0.1)。

  4. 子样本 — 用于训练每棵树的样本比例(默认值为 1.0)。

  5. max_features — 搜索每个节点最佳分割时要考虑的特征数量。选项包括指定特征数量的整数、指定特征比例的浮点数、使用特征的平方根的‘sqrt’、使用特征的对数的‘log2’,以及使用所有特征的 None(默认值)。

  6. 验证比例 — 用作早期停止的验证集的训练集比例(默认值为 0.1)。

  7. n_iter_no_change — 当验证评分在前* n_iter_no_change 次迭代中未改善至少tol时终止训练(默认值为 0.0001)。默认情况下,n_iter_no_change* 设置为 None,这意味着早期停止被禁用。

梯度提升估计器还有以下属性,它们是从数据中学习到的:

  • n_estimators_ — 通过早期停止确定的拟合树数量(如果指定,否则设置为n_estimators)。

  • 估计器 — 已拟合的树集合。

  • 特征重要性 — 模型估计的特征重要性(将在本文后面讨论)。

  • oob_scores — 每次迭代时的袋外样本损失值(仅在subsample < 1.0 时可用)。

  • train_score — 每次迭代时训练集上的损失值。

GradientBoostingClassifier

例如,我们可以对Iris 数据集进行梯度提升分类器的拟合,使用每朵花的前两个特征(萼片宽度和萼片长度)。作为提醒,我们在这个数据集上使用随机森林能够获得 81.58%的测试准确率(经过超参数调整)。让我们看看梯度提升是否能做得更好。

我们首先加载数据集:

from sklearn.datasets import load_iris

iris = load_iris()
X = iris.data[:, :2] # we only take the first two features
y = iris.target

然后我们将数据集分为训练集和测试集(使用与之前实验相同的随机种子):

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

现在我们创建一个 GradientBoostingClassifier 模型,使用其默认设置(即,一个由 100 棵树组成的集成,max_depth=3),并将其拟合到训练集:

from sklearn.ensemble import GradientBoostingClassifier

clf = GradientBoostingClassifier(random_state=42)
clf.fit(X_train, y_train)

我们固定梯度提升分类器的随机状态,以便结果可以重复。模型的性能是:

print(f'Train accuracy: {clf.score(X_train, y_train):.4f}')
print(f'Test accuracy: {clf.score(X_test, y_test):.4f}')
Train accuracy: 0.9554
Test accuracy: 0.7895

这些结果与之前通过随机森林获得的结果相同,但在超参数调整之前。

调整超参数

让我们在一些梯度提升超参数上运行随机搜索,以找到更好的模型。为了公平比较,我们使用与随机森林相同的搜索迭代次数(50 次)。

from sklearn.model_selection import RandomizedSearchCV

params = {
    'n_estimators': [10, 50, 100, 200, 500],
    'max_depth': np.arange(3, 11),
    'subsample': np.arange(0.5, 1.0, 0.1),
    'max_features': ['sqrt', 'log2', None]    
}

search = RandomizedSearchCV(GradientBoostingClassifier(random_state=42), params, n_iter=50, cv=3, n_jobs=-1)
search.fit(X_train, y_train)

print(search.best_params_)

随机搜索找到的最佳模型是:

{'subsample': 0.6, 'n_estimators': 10, 'max_features': 'sqrt', 'max_depth': 3}

即,最佳模型由 10 棵决策树组成,max_depth = 3,每棵树在训练集的 60%随机子样本上进行训练。该模型在训练集和测试集上的准确性是:

best_clf = search.best_estimator_
print(f'Train accuracy: {best_clf.score(X_train, y_train):.4f}')
print(f'Test accuracy: {best_clf.score(X_test, y_test):.4f}')
Train accuracy: 0.8125
Test accuracy: 0.8684

测试集上的准确性显著高于调整后随机森林的结果(86.84%对 81.58%)。

让我们来检查这个分类器找到的决策边界:

梯度提升分类器在鸢尾数据集上找到的决策边界

与随机森林分类器找到的决策边界相比,我们可以看到梯度提升分类器能够捕捉到更多的紫花地丁区域,而不会对异常值过拟合。

GradientBoostingRegressor

为了演示梯度提升回归模型,我们将使用加州住房数据集。该数据集的目标是基于该区的 8 个不同特征(如中位收入或每户平均房间数),预测加州某个区(房块)的中位房价。

我们首先获取数据集:

from sklearn.datasets import fetch_california_housing

data = fetch_california_housing()
X, y = data.data, data.target
feature_names = data.feature_names

然后,我们将数据集分为 80%的训练集和 20%的测试集:

from sklearn.model_selection import train_test_split

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

接下来,我们使用默认设置(即,一个由 100 棵树组成的集成,max_depth=3)拟合一个 GradientBoostingRegressor 到训练集:

from sklearn.ensemble import GradientBoostingRegressor

reg = GradientBoostingRegressor(random_state=0)
reg.fit(X_train, y_train)

该模型的R²得分是:

train_score = reg.score(X_train, y_train)
print(f'R2 score (train): {train_score:.4f}')

test_score = reg.score(X_test, y_test)
print(f'R2 score (test): {test_score:.4f}')
R2 score (train): 0.8027
R2 score (test): 0.7774

这是一个略逊于我们在调整前使用随机森林得到的结果(测试集上的R²得分=0.798)。然而,请注意,默认情况下 RandomForestRegressor 中的树不会被修剪(它们的最大深度没有限制),而 GradientBoostingRegressor 中的树的默认最大深度仅为 3,同时两种估计器的默认树数相同(100)。

调整超参数

让我们通过运行以下随机搜索来调整梯度提升回归模型的超参数:

from sklearn.model_selection import RandomizedSearchCV

params = {
    'n_estimators': [10, 50, 100, 200, 500],
    'max_depth': np.arange(3, 11),
    'subsample': np.arange(0.5, 1.0, 0.1),
    'max_features': ['sqrt', 'log2', None]    
}

search = RandomizedSearchCV(GradientBoostingRegressor(random_state=0), params, n_iter=50, cv=3, n_jobs=-1)
search.fit(X_train, y_train)

print(search.best_params_)

随机搜索找到的最佳模型是:

{'subsample': 0.7999999999999999, 'n_estimators': 500, 'max_features': 'log2', 'max_depth': 7}

也就是说,最佳模型使用 500 棵树,最大深度为 7,每棵树在训练集的 80%的随机子样本上进行训练,每个节点分裂使用特征的对数数量。

该模型在训练集和测试集上的R²分数是:

best_reg = search.best_estimator_
print(f'R2 score (train): {best_reg.score(X_train, y_train):.4f}')
print(f'R2 score (test): {best_reg.score(X_test, y_test):.4f}')
R2 score (train): 0.9849
R2 score (test): 0.8519

在测试集上的R²分数明显高于随机森林回归器调优后的结果(0.8166)。

学习曲线

我们还可以绘制每次提升迭代中的训练误差和测试误差。训练误差存储在估计器的train_score_属性中。测试误差可以通过调用staged_predict()方法获得,该方法返回一个生成器,按每次迭代返回给定数据集上的模型预测。

from sklearn.metrics import mean_squared_error as MSE

test_score = np.zeros(best_reg.n_estimators_)
for i, y_test_pred in enumerate(best_reg.staged_predict(X_test)):
    test_score[i] = MSE(y_test, y_test_pred)

plt.plot(np.arange(best_reg.n_estimators), best_reg.train_score_, label='Training loss')
plt.plot(np.arange(best_reg.n_estimators), test_score, 'r', label='Test loss')

plt.xlabel('Boosting iterations')
plt.ylabel('MSE')
plt.legend()

加州住房数据集上梯度提升回归器的学习曲线

我们可以看到,最小测试误差在大约 100 次迭代后达到,也就是说,该数据集的最佳树木数量约为 100。此外,随着我们向集成中添加更多树,测试误差保持稳定,这表明模型不容易过拟合。

另一种找到最佳树木数量的方法是使用早停法。我们来运行相同的随机搜索,但这次不改变估计器的数量,而是将其设置为固定的 500,并通过将n_iter_no_change设置为 5 来启用早停法。这会自动将训练集的 10%留作验证集,并在验证分数在 5 次迭代内没有提高时终止训练。

from sklearn.model_selection import RandomizedSearchCV

params = {    
    'max_depth': np.arange(3, 11),
    'subsample': np.arange(0.5, 1.0, 0.1),
    'max_features': ['sqrt', 'log2', None]    
}

search = RandomizedSearchCV(GradientBoostingRegressor(random_state=0, n_estimators=500, n_iter_no_change=5), 
                            params, n_iter=50, cv=3, n_jobs=-1)
search.fit(X_train, y_train)

print(search.best_params_)

reg = search.best_estimator_
print(f'R2 score (train): {reg.score(X_train, y_train):.4f}')
print(f'R2 score (test): {reg.score(X_test, y_test):.4f}')
{'subsample': 0.8999999999999999, 'max_features': 'log2', 'max_depth': 7}
R2 score (train): 0.9227
R2 score (test): 0.8402

与之前的模型相比,这个估计器在测试集上的准确度稍差,这可以通过搜索的随机化以及它只使用了 90%的训练集来构建集成来解释。

我们可以检查在启用早停法之前实际构建了多少棵树,通过检查n_estimators_属性来实现:

reg.n_estimators_
118

特征重要性

与其他树基集成方法类似,梯度提升树可以提供数据集中特征的重要性估计,即每个特征对模型预测的贡献程度。这对于模型解释以及进行特征选择都很有用。

单棵决策树中特征的重要性由特征在树中的位置(位于树顶部的特征对树的预测贡献更大)和使用该特征分裂节点所实现的节点纯度减少来决定。在树基集成方法中,如随机森林和梯度提升树,我们对集成中的所有树的特征重要性进行平均。

例如,我们可以绘制我们梯度提升回归器在加州住房数据集中找到的特征重要性:

# Sort the features by their importance
feature_importance = best_reg.feature_importances_
sorted_idx = np.argsort(feature_importance)

# Plot the feature importances
pos = np.arange(len(feature_importance))
plt.barh(pos, feature_importance[sorted_idx])
plt.yticks(pos, np.array(feature_names)[sorted_idx])
plt.xlabel('Feature importance')

加州住房数据集中特征的重要性

在这个数据集中,最重要的特征是 MedInc(中位收入)、房屋位置(经度和纬度)以及 AveOccup(家庭成员的平均数量)。

基于直方图的梯度提升

Scikit-Learn 0.21 引入了两种基于直方图的梯度提升实现:HistGradientBoostingClassifierHistGradientBoostingRegressor,它们类似于 LightGBM [1] 中使用的基于直方图的算法。

这些估计器首先将数据集中的连续特征离散化为整数值的区间(默认为 255 个区间)。在训练过程中,这些区间用于根据达到树中特定节点的样本值构建特征直方图。然后,基于这些直方图找到该节点的最佳分割点。

这种离散化具有以下优点:

  1. 显著减少了在树的每个节点上考虑的分割点数量。

  2. 这避免了在每个节点上对连续特征值进行排序的需要(请参阅这篇文章中的“处理连续特征”部分,了解为什么需要从一开始就进行排序)。

此外,直方图估计器的许多部分都进行了并行化。例如,梯度计算在样本之间进行并行化,而在节点上寻找最佳分割点则在特征之间进行并行化。

离散化和并行化共同使得基于直方图的估计器在样本数量较大(n > 10,000)时运行速度远快于标准梯度提升估计器。

此外,基于直方图的估计器内置支持缺失值和分类特征,这避免了在数据预处理时使用填补器或独热编码器的需要。

大多数基于直方图的估计器的参数与 GradientBoostingClassifier 和 GradientBoostingRegressor 相同,唯一的变化如下:

  • 估计器的参数被称为 max_iter,而不是 n_estimators

  • 树的默认大小已被修改:max_depth 默认为 None(而不是 3),max_leaf_nodes 设置为 31,而 min_samples_leaf 设置为 20。

  • 当样本数量超过 10,000 时,早期停止功能会自动启用。

此外,添加了以下参数:

  • max_bins 表示要使用的最大区间数。不得大于 255。一个额外的区间保留用于缺失值。

  • categorical_features 是一个整数列表,表示数据集中分类特征的位置。

  • interaction_cst 指定交互约束,即在子节点分裂中可以相互交互的特征集(默认为 None)。

HistGradientBoostingClassifier 示例

例如,让我们比较 HistGradientBoostingClassifier 和 GradientBoostingClassifier 在一个人工生成的数据集上的表现。

我们将使用 Scikit-Learn 中的函数 make_hastie_10_2,它生成一个二分类的 10 维数据集,与 Hastie 等人[2]中的示例 10.2 相同。

数据集包含 10 个遵循标准高斯分布的特征,目标是由以下定义的二进制标签:

也就是说,负类位于一个半径为 9.34 的 10 维球体内。

让我们首先生成一个包含 50,000 个样本的数据集:

from sklearn.datasets import make_hastie_10_2

X, y = make_hastie_10_2(n_samples=50000, random_state=0)

然后我们将其分为 80% 的训练集和 20% 的测试集:

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

现在我们来训练一个 GradientBoostingClassifier 并测量它的训练时间:

from sklearn.ensemble import GradientBoostingClassifier

clf = GradientBoostingClassifier(random_state=0)
%timeit clf.fit(X_train, y_train)
12.6 s ± 358 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

训练这个模型平均需要 12.6 秒。它在训练集和测试集上的表现是:

print(f'Train accuracy: {clf.score(X_train, y_train):.4f}')
print(f'Test accuracy: {clf.score(X_test, y_test):.4f}')
Train accuracy: 0.9392
Test accuracy: 0.9231

现在我们来训练一个 HistGradientBoostingClassifier 在相同的数据集上:

from sklearn.ensemble import HistGradientBoostingClassifier

clf = HistGradientBoostingClassifier(random_state=0)
%timeit clf.fit(X_train, y_train)
1.53 s ± 120 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

训练这个模型平均只需 1.53 秒(比 GradientBoostingClassifier 快 8 倍以上)。它在训练集和测试集上的表现是:

print(f'Train accuracy: {clf.score(X_train, y_train):.4f}')
print(f'Test accuracy: {clf.score(X_test, y_test):.4f}')
Train accuracy: 0.9725
Test accuracy: 0.9467

测试集上的准确率显著提高(94.67% 代替 92.31%)。

总结

让我们总结一下梯度提升与其他监督学习模型相比的优缺点。

优点

  • 提供高度准确的模型,通常在结构化数据集上表现最佳。

  • 通过组合多个弱模型,能够捕捉数据集中的复杂交互和模式。

  • 能够通过自动选择相关特征有效处理高维数据集。

  • 相较于其他模型,对异常值的敏感性较低,因为每个基础模型从前一个模型的残差中学习。

  • 可以处理异质数据类型,包括数值和类别特征。

  • 可以处理缺失值而无需插补。

  • 提供特征重要性的度量。

  • 可以应用于回归和分类任务,并支持多种损失函数。

缺点

  • 训练可能会消耗大量计算资源,特别是在处理大型数据集或当集成模型有很多树时。此外,基础模型的训练无法并行化(例如,与随机森林不同)。

  • 比决策树或线性回归等简单模型更难解释,因为解释模型的决策需要跟踪多个树的路径。

  • 需要调整多个超参数,包括树的数量、每棵树的大小和学习率。

  • 如果没有适当的正则化,或者提升迭代次数过多,可能会对训练集过拟合。

  • 与其他模型相比,预测可能会更慢,因为它需要遍历多个树并聚合它们的预测。

最终备注

所有图片均为作者提供,除非另有说明。

你可以在我的 GitHub 上找到本文的代码示例:github.com/roiyeho/medium/tree/main/gradient_boosting

鸢尾花数据集信息:

引用: Fisher, R. A. (1988). Iris. UCI 机器学习库。 doi.org/10.24432/C56C76.

许可证: Creative Commons CC BY 4.0。

加利福尼亚住房数据集信息:

引用: Pace, R. Kelley 和 Ronald Barry (1997), Sparse Spatial Autoregressions, Statistics and Probability Letters, 33, 291-297。

许可证: Creative Commons CC0: 公共领域。

参考资料

[1] Ke 等人 (2017), “LightGBM: A Highly Efficient Gradient Boosting Decision Tree”

[2] T. Hastie, R. Tibshirani 和 J. Friedman (2009), “Elements of Statistical Learning Ed. 2”, Springer。

梯度提升树:是早期停止还是不早期停止?

原文:towardsdatascience.com/gradient-boosting-to-early-stop-or-not-to-early-stop-5ea67ac09d83?source=collection_archive---------3-----------------------#2023-03-23

利用早期停止方法优化 LightGBM、XGBoost 和 CatBoost

Diogo LeitãoTowards Data Science Diogo Leitão

·

关注 发表在 Towards Data Science ·7 分钟阅读·2023 年 3 月 23 日

--

图片来源:Julian Berengar Sölter

梯度提升决策树(GBDTs)目前在表格数据问题上超越了深度学习,流行的实现如 LightGBM、XGBoost 和 CatBoost 主导了 Kaggle 竞赛[1]。早期停止一种在深度学习中常用的技术—也可以在训练和调整 GBDTs 时使用。然而,常见的做法是明确调整 GBDT 集成中的树的数量,而不是使用早期停止。在本文中,我展示了早期停止可以将训练时间缩短一半,同时保持相同的性能,与明确调整树的数量相媲美。

通过减少训练时间,早期停止可以降低计算成本减少实践者等待模型运行时的空闲时间。这样的节省在大型 GBDT 应用行业中尤为重要,如内容推荐、金融欺诈检测或信用评分。但早期停止如何在不损害性能的情况下减少训练时间呢?让我们深入探讨。

梯度提升决策树

梯度提升决策树(GBDTs)目前在基于(异质)表格数据的分类和回归问题中取得了最先进的表现(具有多种列类型的二维数据集)。虽然深度学习技术在自然语言处理和计算机视觉中表现出色,但尚未在表格数据领域夺得桂冠[2, 3, 4, 5]。

梯度提升决策树(GBDTs)。

GBDTs 通过顺序地将决策树添加到一个集成中来工作。与随机森林不同,GBDTs 中的树不是独立的。相反,它们被训练来纠正之前树的错误。因此,只要有足够的树,GBDT 模型在训练集上可以实现完美的表现。然而,这种行为——称为过拟合——已知会损害模型对未见数据的泛化能力。

超参数调整与早期停止

为了优化对训练数据的拟合程度,实践者调整几个关键的超参数:树的数量、学习率、每棵树的最大深度等。为了找到最佳的值集,会在一个单独的验证数据集中测试几个配置;在保留数据中表现最佳的模型被选为最终模型。

另一个有助于对抗过拟合的工具是早期停止。早期停止在深度学习中常见,是一种如果保留数据上的性能没有改善,则停止学习过程的技术。在 GBDTs 中,这意味着在这一点之后不再构建更多的树。

早停法在验证集中的损失停止减少时中止训练。

尽管在深度学习中很普遍,但早停法在 GBDT 用户中并不那么流行。相反,常见的是看到从业者通过上述搜索过程调整树木数量。但是如果使用早停法的效果等同于显式调整树木数量呢?毕竟,这两种机制的目标都是找到 GBDT 集合的最佳大小,给定学习率和其他超参数。如果是这样的话,这可能意味着通过使用早停法可以在大大减少的搜索时间内实现相同的性能,因为它会停止那些耗时且不具前景的迭代。让我们来测试这个假设。

实验设置

为此,在作者许可下,我使用了公共银行账户欺诈数据集,该数据集最近在 NeurIPS ’22 上发布[6]。它由一个真实的欺诈检测数据集的合成副本组成,由隐私保护的 GAN 生成。对于 GBDT 的实现,我选择了LightGBM,因其速度快且性能领先[1, 7]。所有在此实验中使用的代码可以在这个 Kaggle 笔记本中找到。

如上所述,为了找到最佳的超参数集,最常见的方法是尝试多种配置。最终,在验证集中表现最好的模型被选为最终模型。我遵循这种方法,在每次迭代中从合理的分布中随机抽取超参数。

为了检验我的假设,我运行了两个并行的随机搜索过程:

  1. 在没有早停法的情况下,树木数量参数在 10 到 4000 之间均匀测试。

  2. 使用早停法,最大树木数量设置为 4000,但最终由早停准则定义。早停法监控验证集中的交叉熵损失。训练过程仅在 100 次没有改进的迭代后(即耐心参数)被中止,此时将其重置为最佳版本。

以下函数用于在Optuna研究中运行每次随机搜索试验(为清晰起见已截断;完整版见 上述笔记本)*:

def _objective(t, dtrain, dval, early_stopping):
    params = {
        'boosting_type': t.suggest_categorical(['gbdt', 'goss']),
        'learning_rate': t.suggest_float(0.01, 0.5, log=True),
        'min_split_gain': t.suggest_float(0.00001, 2, log=True),
        'num_leaves': t.suggest_int(2, 1024, log=True),
        'max_depth': t.suggest_int(1, 15),
        'min_child_samples': t.suggest_int(2, 100, log=True),
        'bagging_freq': t.suggest_categorical([0, 1]),
        'pos_bagging_fraction': t.suggest_float(0, 1),
        'neg_bagging_fraction': t.suggest_float(0, 1),
        'reg_alpha': t.suggest_float(0.00001, 0.1, log=True),
        'reg_lambda': t.suggest_float(0.00001, 0.1, log=True),
    }
    model = lgb.train(
        **params, dtrain,
        num_boost_round=(
            4000 if early_stopping
            else trial.suggest_int('num_boost_rounds', 10, 4000)
        ),
        valid_sets=dval if early_stopping else None,
        callbacks=(
            [lgb.early_stopping(stopping_rounds=100)] if early_stopping
            else None))

性能

由于早停法监控验证集上的性能,所有模型都在未见过的测试集上进行评估,从而避免了偏见结果。

测试集上的结果。为了视觉清晰度,底部 20%的试验结果被移除。

是否要早停?两种方法的结果相似。 这一结果在测量交叉熵损失(早停监控的指标)和 5% FPR 下的召回率(一个在此数据集领域特别相关的二分类指标)时都一致[6]。在第一个标准上,无早停策略取得了略微更好的结果,而在第二个标准上,则是早停策略占优。

总之,这次实验的结果未能否定我的假设,即使用早停与显式调节 GBDT 中的树木数量之间没有显著差异。当然,更为稳健的评估需要在多个数据集、超参数搜索空间和随机种子上进行实验。

训练时间

我的假设的一部分是早停通过停止添加无前景的树木来减少平均训练时间。是否可以测量出有意义的差异?

训练时间的分布(以秒为单位)。

结果证实了我的假设的第二部分:使用早停时的训练时间明显减少。即使使用耐心值高达 100 次迭代的策略,平均训练时间也减少了一半,从 122 秒降至 58 秒。这意味着总训练时间从 3 小时 23 分钟减少到 1 小时 37 分钟。

尽管早停机制需要额外的计算来监控验证集上的交叉熵损失,但这些额外计算已在上述测量中考虑在内。

结论

梯度提升决策树(GBDTs)在处理表格数据的问题中目前处于最先进水平。我发现,在训练这些模型时使用早停将训练时间减少了一半,同时保持了与显式调节树木数量相同的性能。这使得像 LightGBM、XGBoost 和 CatBoost 这样的流行 GBDT 实现对大规模行业应用(如数字营销和金融)更具优势。

未来,验证在其他数据集和不同 GBDT 实现中的发现将是重要的。调节耐心参数也可能有益,尽管其最佳值可能会因数据集而异。

除非另有说明,所有图片均由作者提供。

参考文献

[1] H. Carlens. 2022 年竞争性机器学习现状。 ML Contests, 2023.

[2] Y. Gorishniy, I. Rubachev, V. Khrulkov 和 A. Babenko,重新审视用于表格数据的深度学习模型,第 35 届神经信息处理系统会议(NeurIPS 2021)。

[3] R. Shwartz-Ziv 和 A. Armon,表格数据:深度学习并不是你所需的一切,信息融合 81 (2022): 84–90。

[4] V. Borisov, T. Leemann, K. Seßler, J. Haug, M. Pawelczyk 和 G. Kasneci,深度神经网络与表格数据:一项调查,IEEE 神经网络与学习系统汇刊(2022)。

[5] L. Grinsztajn, E. Oyallon 和 G. Varoquaux,为什么树基模型仍然在典型表格数据上优于深度学习?,第 36 届神经信息处理系统会议——数据集和基准追踪(NeurIPS 2022)。

[6] S. Jesus, J. Pombal, D. Alves, A. Cruz, P. Saleiro, R. Ribeiro, J. Gama 和 P. Bizarro,扭转局面:用于机器学习评估的偏倚、不平衡和动态表格数据集,第 36 届神经信息处理系统会议——数据集和基准追踪(NeurIPS 2022)。

[7] G. Ke, Q. Meng, T. Finley, T. Wang, W. Chen, W. Ma, Q. Ye, T. Liu,LightGBM:一种高效的梯度提升决策树,第 31 届神经信息处理系统会议(NIPS 2017)。

梯度下降算法 101

原文:towardsdatascience.com/gradient-descent-algorithm-101-c226c69d756c

面向初学者的指南

了解在机器学习和深度学习中广泛使用的优化算法

Pol MarinTowards Data Science Pol Marin

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

--

山坡 — 由 Ralph (Ravi) Kayden 拍摄于 Unsplash

想象你是山顶上的一滴水,而你的目标是到达位于山脚的湖泊。那座高山有不同的坡度和障碍,因此沿直线下滑可能不是最佳解决方案。你会如何解决这个问题?最佳方案可能是一步一步地迈出小步,每次都朝着更接近目标的方向前进。

梯度下降(GD)就是执行这个操作的算法,对于任何数据科学家来说理解它是至关重要的。它基本且相当简单,但却至关重要,任何愿意进入这个领域的人都应该能够解释它是什么。

在这篇文章中,我的目标是提供一个全面且适合初学者的指南,让大家理解什么是 GD,它的用途是什么,它是如何工作的,并提到不同的变体。

和往常一样,你会在文章末尾找到 资源 部分。

但首先要做的事。

介绍

使用维基百科的定义[1],梯度下降是一种用于寻找可微函数局部最小值的一阶迭代优化算法。尽管这肯定不是最有效的方法,但它在机器学习和深度学习中被广泛使用,尤其是在神经网络中。

基本上,它用于通过在每次迭代中更新一组参数来最小化函数的值。从数学角度来看,它使用导数(梯度)逐渐减小(下降)其值。

但有一个问题:并非所有函数都是可优化的。我们需要一个函数——无论是单变量还是多变量——可微分,即函数定义域中的每一点都有导数,并且(U 形或类似)。

现在,在这个简单的介绍之后,我们可以开始深入探讨其背后的数学。

实际案例

因为超越理论会更清晰,让我们使用实际的数字和数值来理解它的作用。

让我们使用一个常见的数据科学案例,我们希望开发一个回归模型。

免责声明:我完全发明了这个,没有逻辑依据来使用这些函数,所有内容都是随机的。目标是展示这个过程本身。

在任何数据科学问题中,成本函数或损失函数是我们要优化的函数。由于我们在使用回归,所以我们将使用这个:

随机回归函数 — 图片由作者提供

目标是找到 f(x,y)的最优最小值。让我绘制一下它的样子:

f(x,y)绘制 a=1 — 图片由作者提供

现在我们的目标是找到“x”和“y”的合适值,以便找到这个成本函数的最优值。我们已经可以从图形上看到它:

  • y=0

  • x 为-1 或 1

进入 GD 本身,因为我们希望让我们的机器学会做相同的事情。

算法

如前所述,梯度下降是一个迭代过程,我们计算梯度并向相反方向移动。这样做的理由是,函数的梯度用于确定函数的斜率。由于我们想向下移动,而不是向上移动,所以我们朝相反的方向移动。

这是一个简单的过程,在每次迭代中我们更新 x 和 y,按照以下方法进行:

梯度下降中的参数更新 — 图片由作者提供

用语言解释,在第 k 次迭代时:

  1. 使用 x 和 y 在该迭代中的值计算梯度。

  2. 对于每个变量——x 和 y——将其梯度乘以 lambda (𝜆),这是一个称为学习率的浮点数。

  3. 从 x 和 y 中分别移除第 2 步中计算出的值。

  4. 在下一次迭代中,让 x 和 y 具有新的值。

这个过程会重复进行,直到满足某个条件(今天不重要)。一旦满足条件,训练结束,优化也随之结束。我们(或者应该)达到了一个最小值(无论是局部还是全局)。

现在,让我们将这个理论付诸实践。

我们需要做的第一件事是计算 f(x,y)的梯度。梯度对应于一个偏导数的向量:

f(x,y)的梯度 — 图片由作者提供

现在,使用 Python,我要做的就是创建一个循环,迭代计算梯度——使用相应的 x 和 y——并按照上面指定的方式更新这些参数。

在此之前,我将定义两个额外的值:

  • 学习率 (𝜆) 可以是固定的也可以是可变的。对于这个简单的教程,它将设置为 0.01。

  • 我还会使用一个叫做 eps(epsilon)的值来决定何时停止迭代。一旦两个偏导数都低于这个阈值,梯度下降将停止。我将其设置为 0.0001。

现在,让我们来写一些代码:

import random

# Define constants
eps = 0.0001
lr = 0.01

# Initialize x and y with random values
x = random.uniform(-2, 4)
y = random.uniform(-1, 1)

def f(x,y):
  return (x**2 -1)**2 +y**2

def df_x(x):
  return 4*x*(x**2 - 1)

def df_y(y):
  return 2*y

# Perform gradient descent
while max(df_x(x), df_y(y)) >= eps:
  x = x - lr * df_x(x)
  y = y - lr * df_y(y)

# Print optimal values found
print(f'x = {x}, y = {y}')

一次随机迭代的结果是:

示例 GD 输出 — 图片来源于作者

我们可以看到这些值接近 x=1 和 y=0,它们确实是函数的最小值。

我忘了提到的是 x 和 y 的初始化。我选择了在随机范围内生成一个数字。在实际问题中,通常需要更多的时间来考虑这些问题。学习率、停止条件以及许多其他超参数也是如此。

但对于我们的情况,这已经足够了。

梯度下降法的变体

我相信你现在已经理解了基本算法。然而,市面上存在多个版本,我认为其中一些值得一提。

  • 随机梯度下降法 (SGD)。SGD 是一种变体,在每次迭代时随机选择一个数据点。这减少了计算次数,但显然有其缺点,例如,可能无法收敛到全局最小值。

  • 批量梯度下降法 (BGD)。BGD 在每次迭代时使用整个数据集。这对于大型数据集来说并不完全理想,因为计算开销和速度较慢,但另一方面,理论上保证收敛到全局最小值。

  • 迷你批量梯度下降法 (MBGD)。这可以被视为 SGD 和 BGD 之间的中间点。它既不使用单个数据点,也不使用整个数据集,而是一个子集。在每次迭代中,我们随机选择一定数量的样本(之前定义过)并仅使用这些样本进行梯度下降。

结论

梯度下降算法在机器学习和深度学习中被广泛使用,但在其他领域也有应用。因此,理解它是任何希望成为数据科学家的人的必修课。

我希望这篇文章澄清了它是什么、它做了什么以及它是如何做到的。

 **Thanks for reading the post!** 
            I really hope you enjoyed it and found it insightful.

          Follow me for more content like this one, it helps a lot!
                                  **@polmarin**

如果你想进一步支持我,请考虑通过下面的链接订阅 Medium 的会员:这不会额外花费你任何钱,但会帮助我完成这个过程。非常感谢!

[## 使用我的推荐链接加入 Medium - Pol Marin

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

medium.com](https://medium.com/@polmarin/membership?source=post_page-----c226c69d756c--------------------------------)

资源

[1] 梯度下降法 — 维基百科

深度学习笔记:梯度下降

原文:towardsdatascience.com/gradient-descent-f09f19eb35fb?source=collection_archive---------13-----------------------#2023-11-04

神经网络如何“学习”

Luis MedinaTowards Data Science Luis Medina

·

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

--

图片由 Rohit Tandon 提供 / Unsplash

人工神经网络(ANNs)是 通用函数逼近器。只要提供足够的数据、具有适当的架构,并且经过 足够长时间的训练,它们可以逼近任何复杂的函数。

那么“训练”网络到底是什么意思呢?

之前关于前馈过程的文章 中,我提到训练一个网络意味着调整其权重的值,以获得对我们试图逼近的函数更好的拟合。

在这篇文章中,我将描述梯度下降算法,它用于调整人工神经网络(ANN)的权重。

让我们从基本概念开始。

从山上下降

想象一下,我们在山顶上,需要到达旁边山谷的最低点。

我们没有地图,天气阴霾,天色渐暗,我们丢失了路线,需要尽快到达底部。这不是一个好场景,但它展示了问题的“边界”。

为了安全起见,假设山上没有陡峭的山脊,因此它类似于一个可微函数

从蒙维索峰(Monviso peak)下山。靠近翁奇诺,库内奥的小山谷。图像由作者提供。

当天黑时,我们看不到我们移动的方向。我们唯一能做的就是迈小步,并检查我们是否处于较低的高度。

如果我们注意到自己向上移动了,我们就朝相反的方向前进。如果我们向下移动,我们就继续这样做。我们重复这个过程,直到最终到达底部。

如你所见,这不一定是最佳的方法。我们可能会到达一个小山谷,而不是山底,或者可能会在一个平原上花费大量时间。

这说明了梯度下降的基本工作原理及其主要挑战。我们会回到这个例子,但首先让我们看看更正式的解释。

什么是梯度?

梯度是函数变化率的表示。它指示了最大增减方向。直观地说,这意味着在局部最大值或局部最小值处梯度为零。

对于依赖多个变量(或坐标轴)的函数,梯度是一个向量,其分量是函数在给定点的偏导数。这用符号∇(nabla)表示,代表向量微分算子。

让我们用数学符号来看一下。假设我们有一个 n 维函数 f:

该函数在点 p(由 n 个坐标确定)处的梯度为:

回到山的例子,山上有些地方地势陡峭,比如山坡,还有些地方地势几乎平坦,比如山谷或平原。山谷和平原代表局部最小值,这通常是关键点。

梯度下降方法

对于许多优化问题,我们的目标是最小化损失函数,以实现最准确的结果。

在深度学习和人工神经网络(ANN)中,我们使用的损失函数是可微的:它们在整个定义域内平滑且无不连续性。

这使我们能够使用相对于自变量的损失函数的导数作为是否正在朝着解决方案(全局最小值)移动的指示。

我们在与导数成比例时的步长有多大?这由步长参数η决定(当我们谈论深度学习时,我们可以称之为学习率)。它将乘以梯度,调整步长的大小。

这样一来,更陡的梯度将产生更大的步长。当我们接近局部最小值时,斜率(梯度)将趋近于零。

让我们看一下下面的图,以说明在优化一维函数时这如何工作:

一维问题中梯度下降的简化示例。图片由作者提供。

正如你所看到的,我们从一个任意点(我画了两个例子,A 和 B)初始化我们的“搜索”以寻找最小值。我们逐渐向最近的最小值迈进,θ的变化与斜率成比例。

插图表示以下算法的功能(伪代码)[1]:

θ(0) = θ_init       # Initialization of optimization variable
η = 0.02            # Arbitrary step-size parameter
Є = 0.01            # Optimization accuracy
k = 0               # Iteration counter

while |f(θ(k+1) - f(θ(k))| > Є:
 θ(k+1) = θ(k) - η ∇f(θ(k))
 k = k + 1

这里的损失函数就像我们在黑暗中没有地图的山脉:我们不知道它的样子。我们想知道哪个θ值能使J最小化,但优化算法并不知道J在所有可能的θ输入下的值。

这就是为什么我们用任意的θ值初始化我们的优化算法。例如,图中的点 A 和 B 表示两个不同的初始化值。

梯度下降的潜在问题

梯度下降算法是有效的,因为它可以帮助我们为任何凸函数获得一个近似解。

如果我们尝试优化的函数是凸的,对于任何值ϵ,都有某个步长η使得梯度下降将在ϵ内收敛到θ的真实最优值θ。 [1]

然而,正如你可能猜到的,这并不完美。算法可能会收敛,但这并不保证我们会找到全局最小值。

梯度下降的主要挑战如下:

任意初始化对结果有影响

使用不同的初始化值,我们可能会遇到局部最小值而不是全局最小值。

例如,从上图中的点 B 开始,而不是点 A。

或者更不明显的情况,例如,像下图中的蓝线所示,收敛到一个平台(梯度消失问题)。

香草梯度下降在鞍形表面上的效果。动画显示了不同初始化点可能产生不同结果的情况。图片由作者提供。

💡 如果你对如何创建这种动画感兴趣,可以查看我的文章 在 Python 中创建梯度下降动画。

选择适当的步长需要在收敛速度和稳定性之间做出权衡。

步长或学习率与我们应使用的轮次数之间存在相互作用,以实现准确的结果。

以下面的参数实验结果为例。这些图像来自 Mike X Cohen 的在线课程 深度学习的深刻理解,我强烈推荐给任何对深度学习感兴趣并使用 PyTorch 的人,遵循科学的方法。

在这种情况下,Mike 展示了如何在独立改变学习率和训练轮次(一个参数随时间变化,网格上不同的值)时测试梯度下降优化的结果。我们可以看到这两个参数如何影响这个特定案例的结果。

图像来自 Mike X Cohen 的在线课程 深度学习的深刻理解

函数的真正全局最小值在-1.4 左右。对于较小的学习率,收敛到该结果需要更多的训练轮次。因此,单纯使用更大的学习率似乎可以帮助我们减少计算时间。

但实际上,这并不是一个简单的收敛速度问题。

大步长可能导致非常缓慢的收敛,阻止算法完全收敛(在极小值周围不断振荡),或引发发散行为。

下一张图显示了不同学习率如何影响优化结果,即使我们在同一位置 x = 2 初始化算法。

f(x)= 应用不同步长的随机梯度下降示例。在所有情况下,算法都在 x = 2 处初始化。图像由作者提供,基础代码改编自 Saraj Rival 的笔记本

在这里,很明显大步长能提高收敛速度,但仅到达某一点为止。

将学习率提高一个数量级导致算法陷入困境。η = 1 的结果在 x = 2 和 x = -2 之间振荡,这仅由左侧图中的蓝色水平线表示。

在某些情况下,大步长可能会将结果“射”向无限,导致程序的数值溢出。

另一方面,过小的步长可能导致非常缓慢的收敛或完全不收敛。

神经网络训练的梯度下降

在深度学习的背景下,我们试图优化的函数是我们的损失函数 J。我们将训练损失定义为所有训练数据集损失的平均值:

其中 Dtrain 是我们训练数据集中的样本数量。

因此,我们可以基于以下算法实现梯度下降,其中我们计算训练损失的梯度以执行模型权重的更新,并重复若干次迭代[2]

w = [0, ... ,0]    # Initialize the weights
for k = 1,..., n_iters:          # Repeat for n iterations
  grad = ∇w TrainLoss(w)         # Gradient of the Training losses 

  w[k] <-- w[k-1] - η * grad     # Update the model weights

由于我们计算的是损失函数的梯度平均值,因此我们对它有更好的估计。因此,权重更新更有可能朝着改善模型性能的方向进行。

这种梯度下降实现的问题是其速度较慢。

对于一个有几个点和一个简单函数的玩具示例,它可能效果很好,但想象一下我们在开发一个 ANN,并且我们有一百万个数据点来训练它。

要用这个算法训练 ANN,我们需要计算模型对每个训练数据样本的输出,然后将它们的损失平均作为一个大批量。仅仅为了做一次权重更新。然后不断重复,直到达到收敛。

这被称为批量梯度下降。它每次迭代做一次(准确的)权重更新,但每次迭代可能需要很长时间,因为我们重复模型计算 n 次。

为了克服这个问题,我们可以使用所谓的随机梯度下降算法。

随机梯度下降

为了克服批量梯度下降的慢收敛问题,我们可以基于训练集中的每个样本来更新模型权重。其优点是我们不需要等到遍历整个数据集后才进行一次权重更新。

我们可以通过使用每个个体样本的损失函数来做到这一点,而不是使用考虑整个数据集作为批量的训练损失。

这就是随机梯度下降算法(SGD)的样子

w = [0, ... ,0]                          # Initialize the weights
for k = 1,..., n_epoch:      
    for (x, y) ∈ Dtrain:                 # For each sample 
        grad = ∇w J(x,y,w)              
        w[k] <--  w[k-1] - η(k) * grad   # Update the model weights

注意步长是训练迭代的一个函数。这是因为为了使算法收敛,η必须随着迭代次数的增加而减小。

MIT 6.036 的讲义 [1]中提到以下定理:

定理 4.1。如果 J 是凸的,并且 η(t) 是一个满足

然后 SGD 以概率 1 收敛到最优θ

人们采取不同的方法来在训练过程中降低η的值,这通常被称为“退火”:

  • 根据训练轮次调整学习率(例如,η(t) = 1/t),或者在达到某个学习轮次后将其设为更小的值。这个方法效果很好,但与模型性能无关。这被称为“学习率衰减”,是处理此问题的行业标准。

  • 将学习率乘以损失函数的梯度:这种方法很好,因为它对问题是自适应的,但需要仔细调整。这已经被纳入了 RMSprop 和 Adam 梯度下降的变体中。

  • 将学习率乘以损失:优点是这种方法同样对问题自适应,但也需要缩放。

SGD 在仅访问部分数据后可能表现良好。这种行为对相对较大的数据集很有用,因为我们可以减少所需的内存量,并且与“原生”梯度下降实现相比,总运行时间较短。

我们可以说,批量实现较慢,因为它需要遍历所有样本才能进行一次权重更新。

SGD 在每个样本上执行更新,但更新的质量较低。我们可能有噪声数据或一个非常复杂的函数需要用我们的 ANN 拟合。

使用大小为 Dtrain 的批量很慢,但准确,使用大小为 1 的批量很快,但不够准确。

在两者之间有一个术语,称为“mini-batch”梯度下降。

Mini-Batch 梯度下降

图片由Sebastian Herrmann提供,来源于Unsplash

如果我们将数据分成大小相等的更小批量,我们可以做批量梯度下降所做的事情,但针对每个mini-batch

假设我们将数据分成 100 个更小的部分。

我们将数据分成 100 步。在每一步中,我们仅查看当前 mini-batch 中数据的训练损失,并改进我们的模型参数。我们重复这一过程,直到查看所有样本,然后重新开始循环。

每个循环称为周期。我之前更宽泛地使用了这个术语来指代优化过程中的迭代次数,但通常的定义指的是每次遍历训练数据集。对于批量梯度下降,迭代就是一个周期。

当我们设置训练 ANN 的周期数时,我们是在定义通过训练数据集的遍历次数。

使用 mini-batch 的优点是我们在每个 mini-batch 上更新模型参数,而不是在查看整个数据集后再更新。

对于批量梯度下降,批量大小是训练数据集中样本的总数。对于 mini-batch 梯度下降,mini-batch 通常是 2 的幂次:32 个样本、64、128、256 等。

当 mini-batch 大小减少到训练数据集中的单个示例时,SGD 将是一个极端情况。

使用 mini-batch 梯度下降的缺点是我们引入了一定程度的变异性——虽然比随机梯度下降(SDG)轻微。这并不保证每一步都会使我们更接近理想参数值,但总体方向仍然是向最小值靠近。

这种方法是行业标准之一,因为通过找到最佳批量大小,我们可以在处理非常大的数据集时在速度和准确性之间进行折衷。

感谢阅读!我希望这篇文章对你有趣,并且帮助你澄清了一些概念。我还分享了撰写这篇文章时使用的资料,供你参考,以便深入了解更正式的内容。

在未来的帖子中,我将写关于 更高级的梯度下降方法(那些人们在实际应用中使用的方法)以及我们如何在训练过程中实际更新模型权重,使用反向传播,因为梯度下降只是整个过程的一部分。

与此同时,你可能会对阅读我之前的文章感兴趣,关于前馈人工神经网络:

## 前馈人工神经网络

基本概念解释

medium.com

参考文献

[1] MIT 开放学习图书馆:6.036 机器学习导论。第六章:梯度下降

[2] 斯坦福在线:人工智能与机器学习 4 — 随机梯度下降 | 斯坦福 CS221 (2021)

[3] 在线课程 深入理解深度学习,由 Mike X Cohen 提供( sincxpress.com

[4] 斯坦福在线:CS231 视觉识别中的卷积神经网络

最初发表于 https://www.makerluis.com 于 2023 年 11 月 4 日。

梯度下降:优化与初始化解释

原文:towardsdatascience.com/gradient-descent-optimisation-and-initialisation-explained-1f945e9943bd?source=collection_archive---------6-----------------------#2023-01-14

高层次的优化介绍,7 分钟阅读

Jamie McGowanTowards Data Science Jamie McGowan

·

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

--

图片来源于 vackground.comUnsplash

训练一个深度学习模型涉及一组模型参数的调整,将这些参数逐步逼近某个最优值集合。最优值集合被定义为模型在执行某个任务时达到最佳表现的点。

直观上,这可以被认为是我们学习新技能时的情况。例如,如果你决定尝试一项新运动,你很可能在第一次玩的时候表现得相当差劲(当然忽略任何天才儿童)。

然而,随着时间的推移,你会提高并学习如何将你自己的参数(在大脑中)向某个最佳值转变,以改善这项运动。

这些参数如何移动?

设想我们有一个度量值,它定义了我们在某项运动中的差劲程度。这个值越高,我们就越差;值越低,我们就越好。有点像高尔夫中的差点。

图像由Steven Shircliff提供,Unsplash上的图片

我们可以进一步设想,调整这些参数将对该度量值产生一些影响,即随着我们向最佳参数集合移动,度量值会降低(我们在任务中表现得更好)。

希望这能让你明白……但如果没有也没关系!我们会查看一个图示,试图解释这种情况。

可视化优化

图示展示了提高高尔夫水平的优化高级描述。图像由作者提供。

以上图为例,我们的高尔夫差点在A 点(情况相当糟糕——初学者水平),这就是我们开始向泰格·伍兹水平进发的地方!

我们应该朝哪个方向移动以达到 B 点(高尔夫的职业水平)?

你说左边?正确!

从数学上讲,这涉及到在 A 点找到梯度并沿着最陡下降的方向移动。

“等等,等等……提醒我一下梯度是什么”

梯度定义了函数变化的最陡速率。

该图展示了在函数不同点上梯度估计的局部性。图像由作者提供。

由于这个梯度仅在局部计算,即 A 点的梯度仅在 A 点正确,所以我们不希望在距离此点太远的地方信任这个梯度。例如,在图片中,点 X 和 Y 的梯度非常不同

因此,在实际操作中,我们将梯度乘以学习率,它告诉我们向 B 点移动的距离。(我们稍后会再回来讨论这个!)

这是局部性论点,它是现代梯度下降优化算法的支柱之一。

局部性与学习率

想象你正在开车,你知道你想到达哪里,但不知道怎么走(也没有导航)。

你可以做的最好的事情是依靠指示标志来指导你朝着目标前进。

照片由 Brendan Church 提供,来源于 Unsplash

然而,这些指示牌只在出现的地方有效。例如,继续直行的指示不一定在 2 英里后仍然有效。

这些指示牌有点像我们在优化算法中的梯度计算。它们包含关于旅行方向(或函数形状)在特定点的局部信息

根据你多么谨慎(或冒险),你可能更喜欢每 200 米设置一个指示牌,或者你可能满意每两英里设置一个。完全取决于旅程的情况!

例如,如果是一条长而直的道路,我们可以用很少的指示牌。但如果是一个复杂的旅程,充满了许多转弯,我们很可能需要更多指示牌。

显示不同类型函数的小学习率和大学习率之间差异的两个图。图片由作者提供。

这就是我们如何看待学习率。如果我们有一个像左边的那样的函数,我们可能能够使用较大的学习率(类似于一条长直的道路)。

然而,如果我们有右边的这个,这将需要一个更小的学习率,因为我们可能会超越我们的目标(错过一个转弯)。

还值得一提的是,我们很可能无法仅通过一个方向从 A 点到 B 点(除非我们已经非常接近)。因此,实际上,梯度下降通常是一个迭代过程,我们在旅程中的路标上接收指示(A 到 B 变成 A 到 C 到 D 到 E 到 B)。

因此,希望我们能够对学习率和路标数量之间的关系建立一些直觉

将所有内容整合起来…

好的,希望我们已经对优化的目标和一些需要考虑的概念有了一个清晰的了解!

使用上述信息,我们现在可以定义梯度下降算法

回到我们之前的图片,我们将点 A 的参数标记为(⍬₀),最终参数标记为点 B ⍬。

在从点 A 到第一个路标(点 C)的第一次迭代中,我们可以写出一个方程来描述参数更新。为此我们需要考虑:

  • 在点 A 的性能指标 L 的梯度(相对于参数)

  • 一个学习率

  • 初始参数 ⍬₀

  • 更新的参数 ⍬₁

单次梯度下降更新的方程。图片由作者提供。

以下参数更新是类似地计算的,因此我们能够写出通用公式为:

一般梯度下降步骤的方程。图片由作者提供。

初始值

好吧,标题承诺了关于初始值的一些讨论。

对于那些对目前没有提及这方面内容感到非常愤怒的人……非常抱歉!但希望这一部分能让你满意!

从上述所有描述来看,很容易理解初始值如何融入整体图景。

图示说明了不同初始值下的数值差异。图片由作者提供。

我之前提到了某个神童?我们称她为 Pam。就本文中的第一张图片而言,这在某种程度上相当于 Pam 在点 P 处拥有一些初始参数,而不是点 A。顺便说一下,Pam 是那个戴着皇冠和带着得意笑容的人——她知道自己很棒!

初始值的高层次解释是,你从哪里开始你的优化。

一个好的初始值可以减轻优化算法的压力,而一个好的优化算法也可以为初始值做出相同的贡献。实际上,一个好的初始值可以在训练具有大量参数的深度学习模型时节省数百小时的计算时间。

由于这一点,许多不同的研究领域专注于开发更好的初始值****技术。这之所以非常困难,本质上就像是在对未来进行预测,而对我们所处的环境了解不多。

初始值重要的另一个原因与我们可能在优化之后所处的位置有关。

图示展示了不同初始值下梯度下降优化的不同结果。图片由作者提供。

请考虑上面的新优化表面。它有许多不同最小值——其中一些比其他的更好!

在这张图中,很明显我们的起始点会严重影响我们最终的结果。这也是为什么对于机器学习从业者来说,实验不同的初始值以及调整超参数(如学习率)如此重要,以便在特定任务中找到最佳模型。

结论

在这篇文章中,我们介绍了一些关于梯度下降、优化和初始值的高层次解释。我们可视化了优化和初始值的目标,图形化地研究了这些概念,介绍了学习率的概念,甚至写下了梯度下降的公式!

希望这能帮助你建立对这些重要概念的直觉,并加深对梯度下降公式来源的理解!

感谢阅读,敬请关注更多与优化技术相关的文章!

一如既往,欢迎告知我任何问题或评论!

梯度下降与梯度提升:逐一对比

原文:towardsdatascience.com/gradient-descent-vs-gradient-boosting-a-side-by-side-comparison-7067bb3c5712

从初始化到收敛的简单英语

Angela and Kezhan ShiTowards Data Science Angela and Kezhan Shi

·发表于Towards Data Science ·阅读时长 5 分钟·2023 年 2 月 28 日

--

引言

梯度下降和梯度提升是两种流行的机器学习算法。尽管它们的处理方法和应用不同,但这两种算法都基于梯度计算,并且共享若干相似步骤。本文的主要目的是详细比较这两种算法,帮助读者更好地理解它们的相似之处和不同之处。

图片由Gregoire Jeanneau提供,来源于Unsplash

梯度下降

梯度下降是机器学习中常用的优化算法,用于最小化成本函数。目标是找到一组最佳参数,以最小化预测值与实际值之间的误差。该过程开始时随机初始化模型的权重或系数。然后,它通过计算成本函数相对于每个参数的梯度,迭代地更新权重,沿着成本函数的最大下降方向进行。

梯度提升

梯度提升是一种集成方法,通过结合多个弱模型来创建一个更强的预测模型。它通过迭代地将新模型拟合到前一个模型的残差错误来进行工作。最终的预测是所有模型预测值的总和。在梯度提升中,重点是前一个模型所犯的错误。

不同却相似

为了提供梯度下降和梯度提升的全面比较,我们将首先分别解释这两种算法,然后逐步比较每种算法的优化方法。这种方法将帮助读者更好地理解这两种算法的相似性和差异。

梯度下降算法的简单英语解释

以下是一些用简单英语解释梯度下降的步骤:

  1. 选择起点:梯度下降从随机或预定义的模型权重或系数开始。

  2. 计算梯度:梯度是函数的最陡上升或下降方向。在梯度下降中,我们计算成本函数相对于每个参数的梯度。成本函数衡量模型对训练数据的拟合程度。

  3. 更新权重:一旦我们获得梯度,就更新模型的权重,方向与梯度相反。更新的大小由学习率决定,学习率控制每次迭代中权重的调整幅度。

  4. 直到收敛:我们重复步骤 2 和 3,直到达到成本函数的最小值,这对应于模型的最佳权重集。收敛标准可能有所不同,例如达到一定的迭代次数或当成本函数的变化变得足够小时。

通过迭代地调整权重,朝着成本函数的最陡下降方向,梯度下降旨在找到最佳的参数集,以最小化预测值和实际值之间的误差。

梯度提升算法的简单英语解释

以下是一些用简单英语解释梯度提升的步骤:

  1. 训练一个弱模型:我们从训练一个弱模型开始,比如决策树或回归模型,使用训练数据。弱模型可能单独表现不佳,但可以进行一些预测。

  2. 计算误差:我们计算弱模型的预测值和实际值之间的误差。这个误差成为下一个模型的目标。

  3. 训练新模型:我们训练一个新模型来预测前一个模型的错误。这个新模型在前一个模型的残差或错误上进行拟合。

  4. 合并模型:我们将所有模型的预测结果合并以进行最终预测。最终预测是所有模型预测结果的总和。

  5. 直到收敛:我们重复步骤 2 到 4,向集成中添加新模型,直到达到预定义的模型数量或在验证集上的性能停止提升。

通过迭代地将新模型拟合到前一个模型的残差中,梯度提升旨在提高模型的准确性。最终预测是所有模型预测的组合,每个模型都纠正前一个模型的错误。梯度提升可以有效地处理非线性关系、缺失值和异常值。

并排比较

这里是梯度下降和梯度提升每一步的并排比较:

1. 初始化:

  • 梯度下降:随机或预定义初始化模型的权重或系数。

  • 梯度提升:在训练数据上训练一个弱模型,例如决策树或回归模型。

2. 错误计算:

  • 梯度下降:计算模型在整个训练集上的预测值与实际值之间的误差或损失。

  • 梯度提升:计算弱模型在训练集上的预测值与实际值之间的误差或残差。

3. 更新或拟合:

  • 梯度下降:根据学习率和成本函数的梯度,沿着梯度的反方向更新模型的权重。

  • 梯度提升:拟合一个新模型以预测前一个模型的残差错误,基于弱模型的错误和训练数据。

4. 组合:

  • 梯度下降:不需要组合,因为目标是优化单个模型的参数。

  • 梯度提升:结合所有模型的预测结果以做出最终预测。最终预测是所有模型预测结果的总和。

5. 收敛:

  • 梯度下降:重复步骤 2 到 4,直到达到收敛,这可能取决于如迭代次数或成本函数的变化等标准。

  • 梯度提升:重复步骤 2 到 4,向集成中添加新模型,直到达到预定数量的模型或在验证集上的性能停止改善。

结论

梯度下降和梯度提升都依赖梯度计算来优化模型,但它们在方法和目的上有所不同。梯度下降专注于最小化单一模型的成本函数,而梯度提升则旨在提高模型集成的准确性。

尽管梯度下降和梯度提升具有不同的优化目标,但它们共享基于梯度下降的共同算法基础。在梯度下降中,算法优化单个模型的参数以最小化成本函数。相比之下,梯度提升旨在通过迭代添加新模型来优化模型集成,以最小化集成的成本函数。然而,这两种算法都使用梯度下降作为基本优化技术。

图卷积网络:GNNs 简介

原文:towardsdatascience.com/graph-convolutional-networks-introduction-to-gnns-24b3f60d6c95?source=collection_archive---------0-----------------------#2023-08-14

使用 PyTorch Geometric 的逐步指南

Maxime LabonneTowards Data Science Maxime Labonne

·

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

--

作者提供的图像

图神经网络(GNNs)是深度学习领域中最吸引人和迅速发展的架构之一。作为处理图结构数据的深度学习模型,GNNs 带来了显著的多样性和强大的学习能力。

在各种类型的 GNN 中,图卷积网络(GCNs)已经成为最普遍且广泛应用的模型。GCNs 因其能够利用节点的特征及其局部信息进行预测而具有创新性,提供了一种有效处理图结构数据的方法。

在本文中,我们将深入探讨 GCN 层的机制,并解释其内部工作原理。此外,我们还将探索其在节点分类任务中的实际应用,使用PyTorch Geometric作为我们的工具。

PyTorch Geometric 是 PyTorch 的一个专门扩展,专为 GNNs 的开发和实现而创建。它是一个高级但用户友好的库,提供了一整套工具来促进基于图的机器学习。为了开始我们的旅程,我们需要安装 PyTorch Geometric。如果你使用 Google Colab,PyTorch应该已经安装好了,因此我们只需要执行几个额外的命令。

所有代码都可以在Google ColabGitHub上找到。

!pip install torch_geometric
import torch
import numpy as np
import networkx as nx
import matplotlib.pyplot as plt

现在 PyTorch Geometric 已经安装好了,让我们探索一下本教程中将使用的数据集。

🌐 I. 图数据

是表示对象之间关系的重要结构。你可以在许多现实世界的场景中遇到图数据,例如社交和计算机网络、分子化学结构、自然语言处理和图像识别等。

在这篇文章中,我们将研究臭名昭著且广泛使用的扎卡里的空手道俱乐部数据集。

图片作者提供

扎卡里的空手道俱乐部数据集体现了 1970 年代 Wayne W. Zachary 观察到的空手道俱乐部内部形成的关系。这是一种社交网络,其中每个节点代表一个俱乐部成员,节点之间的边代表发生在俱乐部环境之外的互动。

在这个特定的场景中,俱乐部成员被分为四个不同的组。我们的任务是根据他们的互动模式给每个成员分配正确的组(节点分类)。

让我们使用 PyG 的内置函数导入数据集,并尝试了解它使用的Datasets对象。

from torch_geometric.datasets import KarateClub
# Import dataset from PyTorch Geometric
dataset = KarateClub()# Print information
print(dataset)
print('------------')
print(f'Number of graphs: {len(dataset)}')
print(f'Number of features: {dataset.num_features}')
print(f'Number of classes: {dataset.num_classes}')
KarateClub()
------------
Number of graphs: 1
Number of features: 34
Number of classes: 4

该数据集仅包含 1 个图,其中每个节点具有 34 维的特征向量,并且属于四个类别中的一个(我们的四个组)。实际上,Datasets对象可以看作是Data(图)对象的集合。

我们可以进一步检查我们独特的图,以了解更多信息。

# Print first element
print(f'Graph: {dataset[0]}')
Graph: Data(x=[34, 34], edge_index=[2, 156], y=[34], train_mask=[34])

[Data](https://pytorch-geometric.readthedocs.io/en/latest/modules/data.html)对象特别有趣。打印它可以很好地总结我们正在研究的图:

  • x=[34, 34]节点特征矩阵,其形状为(节点数,特征数)。在我们的例子中,这意味着我们有 34 个节点(我们的 34 个成员),每个节点都与一个 34 维特征向量相关联。

  • edge_index=[2, 156]表示图的连通性(节点如何连接),其形状为(2,定向边的数量)。

  • y=[34]节点真实标签。在这个问题中,每个节点被分配到一个类别(组),因此我们对每个节点有一个值。

  • train_mask=[34]是一个可选属性,用于指定哪些节点应用于训练,列表中包含TrueFalse

让我们打印这些张量以了解它们存储了什么。我们从节点特征开始。

data = dataset[0]
print(f'x = {data.x.shape}')
print(data.x)
x = torch.Size([34, 34])
tensor([[1., 0., 0.,  ..., 0., 0., 0.],
        [0., 1., 0.,  ..., 0., 0., 0.],
        [0., 0., 1.,  ..., 0., 0., 0.],
        ...,
        [0., 0., 0.,  ..., 1., 0., 0.],
        [0., 0., 0.,  ..., 0., 1., 0.],
        [0., 0., 0.,  ..., 0., 0., 1.]])

在这里,节点特征矩阵x是一个单位矩阵:它不包含任何相关信息关于节点。它本可以包含诸如年龄、技能水平等信息,但在这个数据集中并非如此。这意味着我们只能通过查看节点之间的连接来对它们进行分类。

现在,让我们打印边索引。

print(f'edge_index = {data.edge_index.shape}')
print(data.edge_index)
edge_index = torch.Size([2, 156])
tensor([[ 0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  1,  1,
          1,  1,  1,  1,  1,  1,  1,  2,  2,  2,  2,  2,  2,  2,  2,  2,  2,  3,
          3,  3,  3,  3,  3,  4,  4,  4,  5,  5,  5,  5,  6,  6,  6,  6,  7,  7,
          7,  7,  8,  8,  8,  8,  8,  9,  9, 10, 10, 10, 11, 12, 12, 13, 13, 13,
         13, 13, 14, 14, 15, 15, 16, 16, 17, 17, 18, 18, 19, 19, 19, 20, 20, 21,
         21, 22, 22, 23, 23, 23, 23, 23, 24, 24, 24, 25, 25, 25, 26, 26, 27, 27,
         27, 27, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30, 31, 31, 31, 31, 31,
         31, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 32, 33, 33, 33, 33, 33,
         33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33, 33],
        [ 1,  2,  3,  4,  5,  6,  7,  8, 10, 11, 12, 13, 17, 19, 21, 31,  0,  2,
          3,  7, 13, 17, 19, 21, 30,  0,  1,  3,  7,  8,  9, 13, 27, 28, 32,  0,
          1,  2,  7, 12, 13,  0,  6, 10,  0,  6, 10, 16,  0,  4,  5, 16,  0,  1,
          2,  3,  0,  2, 30, 32, 33,  2, 33,  0,  4,  5,  0,  0,  3,  0,  1,  2,
          3, 33, 32, 33, 32, 33,  5,  6,  0,  1, 32, 33,  0,  1, 33, 32, 33,  0,
          1, 32, 33, 25, 27, 29, 32, 33, 25, 27, 31, 23, 24, 31, 29, 33,  2, 23,
         24, 33,  2, 31, 33, 23, 26, 32, 33,  1,  8, 32, 33,  0, 24, 25, 28, 32,
         33,  2,  8, 14, 15, 18, 20, 22, 23, 29, 30, 31, 33,  8,  9, 13, 14, 15,
         18, 19, 20, 22, 23, 26, 27, 28, 29, 30, 31, 32]])

在图论和网络分析中,节点之间的连通性通过多种数据结构进行存储。edge_index就是这种数据结构之一,其中图的连接存储在两个列表中(156 条定向边,相当于 78 条双向边)。这两个列表的原因在于一个列表存储源节点,而第二个列表标识目标节点。

这种方法称为坐标列表(COO)格式,本质上是一种高效存储稀疏矩阵的方式。稀疏矩阵是高效存储大部分为零元素的矩阵的数据结构。在 COO 格式中,仅存储非零元素,从而节省内存和计算资源。

相反,更直观和简洁的表示图连通性的方法是通过邻接矩阵 A。这是一个方阵,其中每个元素Aᵢⱼ s指定图中从节点i到节点j的边的存在与否。换句话说,非零元素Aᵢⱼ 表示从节点i到节点j的连接,而零表示没有直接连接。

作者提供的图片

然而,邻接矩阵在稀疏矩阵或边较少的图中并不像 COO 格式那样节省空间。然而,为了清晰和易于解释,邻接矩阵仍然是表示图连通性的热门选择。

邻接矩阵可以通过edge_index和一个工具函数to_dense_adj()来推断。

from torch_geometric.utils import to_dense_adj
A = to_dense_adj(data.edge_index)[0].numpy().astype(int)
print(f'A = {A.shape}')
print(A)
A = (34, 34)
[[0 1 1 ... 1 0 0]
 [1 0 1 ... 0 0 0]
 [1 1 0 ... 0 1 0]
 ...
 [1 0 0 ... 0 1 1]
 [0 0 1 ... 1 0 1]
 [0 0 0 ... 1 1 0]]

对于图数据,节点之间密集互连的情况相对较少。正如你所见,我们的邻接矩阵A稀疏的(填充了零)。

在许多现实世界的图中,大多数节点只与少数其他节点连接,导致邻接矩阵中有大量零。存储这么多零是完全不高效的,这就是为什么 PyG 采用了 COO 格式。

相反,真实标签易于理解。

print(f'y = {data.y.shape}')
print(data.y)
y = torch.Size([34])
tensor([1, 1, 1, 1, 3, 3, 3, 1, 0, 1, 3, 1, 1, 1, 0, 0, 3, 1, 0, 1, 0, 1, 0, 0,
        2, 2, 0, 0, 2, 0, 0, 2, 0, 0])

我们存储在 y 中的节点真实标签仅仅编码了每个节点的组号(0, 1, 2, 3),这就是为什么我们有 34 个值。

最后,让我们打印训练掩码。

print(f'train_mask = {data.train_mask.shape}')
print(data.train_mask)
train_mask = torch.Size([34])
tensor([ True, False, False, False,  True, False, False, False,  True, False,
        False, False, False, False, False, False, False, False, False, False,
        False, False, False, False,  True, False, False, False, False, False,
        False, False, False, False])

训练掩码显示了哪些节点应该用 True 语句进行训练。这些节点代表训练集,而其他节点可以视为测试集。这种划分有助于通过提供未见过的数据来进行模型评估。

但我们还没完成![Data](https://pytorch-geometric.readthedocs.io/en/latest/modules/data.html) 对象提供了更多功能。它提供了各种实用函数,使得可以调查图的多个属性。例如:

  • is_directed() 告诉你图是否有向。有向图意味着邻接矩阵不是对称的,即边的方向在节点间的连接中是重要的。

  • isolated_nodes() 检查是否有一些节点没有连接到图的其余部分。这些节点可能在分类等任务中带来挑战,因为它们缺乏连接。

  • has_self_loops() 表示是否至少有一个节点自我连接。这与的概念不同:环意味着一条路径开始和结束于同一个节点,在此过程中遍历其他节点。

在扎卡里武术俱乐部数据集中,所有这些属性返回 False。这意味着图是无向的,没有孤立节点,并且没有节点与自身相连。

print(f'Edges are directed: {data.is_directed()}')
print(f'Graph has isolated nodes: {data.has_isolated_nodes()}')
print(f'Graph has loops: {data.has_self_loops()}')
Edges are directed: False
Graph has isolated nodes: False
Graph has loops: False

最后,我们可以使用 [to_networkx](https://pytorch-geometric.readthedocs.io/en/latest/modules/utils.html?highlight=to_networkx#torch_geometric.utils.to_networkx) 将 PyTorch Geometric 图转换为流行的图库 NetworkX。这对于使用 networkxmatplotlib 可视化小图特别有用。

让我们为每个组绘制不同颜色的数据集。

from torch_geometric.utils import to_networkx
G = to_networkx(data, to_undirected=True)
plt.figure(figsize=(12,12))
plt.axis('off')
nx.draw_networkx(G,
                pos=nx.spring_layout(G, seed=0),
                with_labels=True,
                node_size=800,
                node_color=data.y,
                cmap="hsv",
                vmin=-2,
                vmax=3,
                width=0.8,
                edge_color="grey",
                font_size=14
                )
plt.show()

这个扎卡里武术俱乐部的图显示了我们的 34 个节点、78 条(双向)边和 4 个标签及 4 种不同颜色。现在我们已经了解了使用 PyTorch Geometric 加载和处理数据集的基本内容,我们可以介绍图卷积网络架构。

✉️ II. 图卷积网络

本节旨在从头开始介绍和构建图卷积层。

在传统的神经网络中,线性层对传入的数据应用线性变换。这种变换通过使用权重矩阵 𝐖 将输入特征 x 转换为隐藏向量 h。暂时忽略偏差,这可以表示为:

在图数据中,通过节点之间的连接增加了额外的复杂性。这些连接很重要,因为在网络中,通常假设相似的节点比不相似的节点更可能互相链接,这种现象被称为网络同质性

我们可以通过将节点的特征与邻居的特征合并来丰富我们的节点表示。这个操作称为卷积或邻域聚合。让我们将节点 i 及其邻域表示为 Ñ

与卷积神经网络(CNNs)中的滤波器不同,我们的权重矩阵 𝐖 是唯一的,并且在每个节点之间共享。但还有另一个问题:节点没有像像素那样的固定邻居数量

我们如何处理一个节点只有一个邻居,而另一个节点有 500 个邻居的情况?如果我们简单地将特征向量相加,那么对于拥有 500 个邻居的节点,得到的嵌入 h 将会大得多。为了确保所有节点的值具有相似的范围并便于比较,我们可以根据节点的来归一化结果,其中度是指一个节点的连接数量。

我们快到了!由 Kipf 等人(2016)介绍的图卷积层还有一个最终的改进。

作者观察到,具有大量邻居的节点的特征比那些较孤立节点的特征传播得更容易。为了抵消这种效应,他们建议为邻居较少的节点的特征分配更大的权重,从而平衡所有节点的影响。这个操作可以表示为:

注意,当 ij 拥有相同数量的邻居时,这等同于我们自己定义的层。现在,让我们看看如何在 Python 中使用 PyTorch Geometric 实现它。

🧠 III. 实现一个 GCN

PyTorch Geometric 提供了GCNConv函数,该函数直接实现了图卷积层。

在这个示例中,我们将创建一个基本的图卷积网络,包括一个 GCN 层、一个 ReLU 激活函数和一个线性输出层。这个输出层将产生四个值,对应我们的四个类别,最高值将决定每个节点的类别。

在以下代码块中,我们定义了一个具有 3 维隐藏层的 GCN 层。

from torch.nn import Linear
from torch_geometric.nn import GCNConv 
class GCN(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.gcn = GCNConv(dataset.num_features, 3)
        self.out = Linear(3, dataset.num_classes) def forward(self, x, edge_index):
        h = self.gcn(x, edge_index).relu()
        z = self.out(h)
        return h, zmodel = GCN()
print(model)
GCN(
  (gcn): GCNConv(34, 3)
  (out): Linear(in_features=3, out_features=4, bias=True)
)

如果我们添加了第二个 GCN 层,我们的模型将不仅仅从每个节点的邻居处聚合特征向量,还会从这些邻居的邻居处聚合特征向量。

我们可以堆叠多个图层以聚合更多的远离值,但有一个问题:如果我们添加太多图层,聚合变得如此强烈,以至于所有嵌入最终看起来都一样。这种现象被称为过度平滑,当图层过多时,可能会成为一个实际问题。

现在我们已经定义了 GNN,让我们用 PyTorch 编写一个简单的训练循环。我选择了常规的交叉熵损失,因为这是一个多类分类任务,优化器使用 Adam。在本文中,我们不会实现训练/测试拆分,以保持简单,专注于 GNN 如何学习。

训练循环是标准的:我们尝试预测正确的标签,并将 GCN 的结果与 data.y 中存储的值进行比较。通过交叉熵损失计算错误,并使用 Adam 进行反向传播,以微调 GNN 的权重和偏差。最后,我们每 10 个 epochs 打印一次指标。

criterion = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.02)
# Calculate accuracy
def accuracy(pred_y, y):
    return (pred_y == y).sum() / len(y)# Data for animations
embeddings = []
losses = []
accuracies = []
outputs = []# Training loop
for epoch in range(201):
    # Clear gradients
    optimizer.zero_grad() # Forward pass
    h, z = model(data.x, data.edge_index) # Calculate loss function
    loss = criterion(z, data.y) # Calculate accuracy
    acc = accuracy(z.argmax(dim=1), data.y) # Compute gradients
    loss.backward() # Tune parameters
    optimizer.step() # Store data for animations
    embeddings.append(h)
    losses.append(loss)
    accuracies.append(acc)
    outputs.append(z.argmax(dim=1)) # Print metrics every 10 epochs
    if epoch % 10 == 0:
        print(f'Epoch {epoch:>3} | Loss: {loss:.2f} | Acc: {acc*100:.2f}%')
Epoch   0 | Loss: 1.40 | Acc: 41.18%
Epoch  10 | Loss: 1.21 | Acc: 47.06%
Epoch  20 | Loss: 1.02 | Acc: 67.65%
Epoch  30 | Loss: 0.80 | Acc: 73.53%
Epoch  40 | Loss: 0.59 | Acc: 73.53%
Epoch  50 | Loss: 0.39 | Acc: 94.12%
Epoch  60 | Loss: 0.23 | Acc: 97.06%
Epoch  70 | Loss: 0.13 | Acc: 100.00%
Epoch  80 | Loss: 0.07 | Acc: 100.00%
Epoch  90 | Loss: 0.05 | Acc: 100.00%
Epoch 100 | Loss: 0.03 | Acc: 100.00%
Epoch 110 | Loss: 0.02 | Acc: 100.00%
Epoch 120 | Loss: 0.02 | Acc: 100.00%
Epoch 130 | Loss: 0.02 | Acc: 100.00%
Epoch 140 | Loss: 0.01 | Acc: 100.00%
Epoch 150 | Loss: 0.01 | Acc: 100.00%
Epoch 160 | Loss: 0.01 | Acc: 100.00%
Epoch 170 | Loss: 0.01 | Acc: 100.00%
Epoch 180 | Loss: 0.01 | Acc: 100.00%
Epoch 190 | Loss: 0.01 | Acc: 100.00%
Epoch 200 | Loss: 0.01 | Acc: 100.00%

太好了!毫不奇怪,我们在训练集(完整数据集)上达到了 100% 的准确率。这意味着我们的模型学会了正确地将每个空手道俱乐部的成员分配到正确的组。

我们可以通过动画化图形来生成一个整洁的可视化效果,并观察 GNN 在训练过程中预测的演变。

%%capture
from IPython.display import HTML
from matplotlib import animation
plt.rcParams["animation.bitrate"] = 3000
def animate(i):
    G = to_networkx(data, to_undirected=True)
    nx.draw_networkx(G,
                    pos=nx.spring_layout(G, seed=0),
                    with_labels=True,
                    node_size=800,
                    node_color=outputs[i],
                    cmap="hsv",
                    vmin=-2,
                    vmax=3,
                    width=0.8,
                    edge_color="grey",
                    font_size=14
                    )
    plt.title(f'Epoch {i} | Loss: {losses[i]:.2f} | Acc: {accuracies[i]*100:.2f}%',
              fontsize=18, pad=20)fig = plt.figure(figsize=(12, 12))
plt.axis('off')anim = animation.FuncAnimation(fig, animate, \
            np.arange(0, 200, 10), interval=500, repeat=True)
html = HTML(anim.to_html5_video())
display(html)

最初的预测是随机的,但经过一段时间,GCN 能够完美地标记每个节点。实际上,最终的图形与我们在第一部分末尾绘制的图形相同。但 GCN 到底学到了什么?

通过聚合邻近节点的特征,GNN 学习了网络中每个节点的向量表示(或嵌入)。在我们的模型中,最终层仅学习如何使用这些表示来产生最佳分类。然而,嵌入才是真正的 GNN 产物。

让我们打印出模型学到的嵌入。

# Print embeddings
print(f'Final embeddings = {h.shape}')
print(h)
Final embeddings = torch.Size([34, 3])
tensor([[1.9099e+00, 2.3584e+00, 7.4027e-01],
        [2.6203e+00, 2.7997e+00, 0.0000e+00],
        [2.2567e+00, 2.2962e+00, 6.4663e-01],
        [2.0802e+00, 2.8785e+00, 0.0000e+00],
        [0.0000e+00, 0.0000e+00, 2.9694e+00],
        [0.0000e+00, 0.0000e+00, 3.3817e+00],
        [0.0000e+00, 1.5008e-04, 3.4246e+00],
        [1.7593e+00, 2.4292e+00, 2.4551e-01],
        [1.9757e+00, 6.1032e-01, 1.8986e+00],
        [1.7770e+00, 1.9950e+00, 6.7018e-01],
        [0.0000e+00, 1.1683e-04, 2.9738e+00],
        [1.8988e+00, 2.0512e+00, 2.6225e-01],
        [1.7081e+00, 2.3618e+00, 1.9609e-01],
        [1.8303e+00, 2.1591e+00, 3.5906e-01],
        [2.0755e+00, 2.7468e-01, 1.9804e+00],
        [1.9676e+00, 3.7185e-01, 2.0011e+00],
        [0.0000e+00, 0.0000e+00, 3.4787e+00],
        [1.6945e+00, 2.0350e+00, 1.9789e-01],
        [1.9808e+00, 3.2633e-01, 2.1349e+00],
        [1.7846e+00, 1.9585e+00, 4.8021e-01],
        [2.0420e+00, 2.7512e-01, 1.9810e+00],
        [1.7665e+00, 2.1357e+00, 4.0325e-01],
        [1.9870e+00, 3.3886e-01, 2.0421e+00],
        [2.0614e+00, 5.1042e-01, 2.4872e+00],
...
        [2.1778e+00, 4.4730e-01, 2.0077e+00],
        [3.8906e-02, 2.3443e+00, 1.9195e+00],
        [3.0748e+00, 0.0000e+00, 3.0789e+00],
        [3.4316e+00, 1.9716e-01, 2.5231e+00]], grad_fn=<ReluBackward0>)

如你所见,嵌入不需要具有与特征向量相同的维度。在这里,我选择将维度从 34 (dataset.num_features) 降到三维,以获得更好的 3D 可视化效果。

让我们在训练开始之前,即第 0 轮,绘制这些嵌入。

# Get first embedding at epoch = 0
embed = h.detach().cpu().numpy()
fig = plt.figure(figsize=(12, 12))
ax = fig.add_subplot(projection='3d')
ax.patch.set_alpha(0)
plt.tick_params(left=False,
                bottom=False,
                labelleft=False,
                labelbottom=False)
ax.scatter(embed[:, 0], embed[:, 1], embed[:, 2],
           s=200, c=data.y, cmap="hsv", vmin=-2, vmax=3)plt.show()

我们看到 Zachary 空手道俱乐部中的每个节点及其真实标签(而不是模型的预测)。目前,它们还很分散,因为 GNN 尚未训练完成。但如果我们在训练循环的每一步绘制这些嵌入,我们将能够可视化 GNN 实际上学到了什么。

让我们看看它们随着时间的推移如何演变,随着 GCN 在分类节点方面变得越来越好。

%%capture
def animate(i):
    embed = embeddings[i].detach().cpu().numpy()
    ax.clear()
    ax.scatter(embed[:, 0], embed[:, 1], embed[:, 2],
           s=200, c=data.y, cmap="hsv", vmin=-2, vmax=3)
    plt.title(f'Epoch {i} | Loss: {losses[i]:.2f} | Acc: {accuracies[i]*100:.2f}%',
              fontsize=18, pad=40)fig = plt.figure(figsize=(12, 12))
plt.axis('off')
ax = fig.add_subplot(projection='3d')
plt.tick_params(left=False,
                bottom=False,
                labelleft=False,
                labelbottom=False)anim = animation.FuncAnimation(fig, animate, \
              np.arange(0, 200, 10), interval=800, repeat=True)
html = HTML(anim.to_html5_video())
display(html)

我们的图卷积网络(GCN)有效地学习了将相似节点分组到不同的簇中的嵌入。这使得最终的线性层能够轻松地区分它们为不同的类别。

嵌入并非 GNN 特有:它们在深度学习中无处不在。它们也不一定是三维的:实际上,它们很少是三维的。例如,像BERT这样的语言模型生成的嵌入维度通常是 768 甚至 1024。

额外的维度存储了关于节点、文本、图像等更多的信息,但它们也会创建更大的模型,这些模型更难以训练。这就是为什么尽可能保持低维嵌入是有利的原因。

结论

图卷积网络是一种非常多功能的架构,可以应用于许多背景。在这篇文章中,我们熟悉了 PyTorch Geometric 库以及像DatasetsData这样的对象。然后,我们成功地从头开始重建了一个图卷积层。接下来,我们通过实现一个 GCN 将理论付诸实践,这使我们理解了实际的方面以及各个组件如何相互作用。最后,我们可视化了训练过程,并清楚地了解了这种网络所涉及的内容。

Zachary 的空手道俱乐部是一个简单的数据集,但足够用来理解图数据和 GNN 中的最重要概念。尽管我们在这篇文章中仅讨论了节点分类,但 GNN 还可以完成其他任务:链接预测(例如,推荐朋友)、图分类(例如,标记分子)、图生成(例如,创建新分子)等。

除了 GCN 之外,研究人员还提出了许多 GNN 层和架构。在下一篇文章中,我们将介绍图注意力网络(GAT)架构,它通过注意机制动态计算 GCN 的归一化因子和每个连接的重要性。

如果你想了解更多关于图神经网络的信息,可以通过我的书籍《动手实践图神经网络》深入探索 GNN 的世界。

下一篇文章

## 第二章:图注意力网络:自注意力解析

使用 PyTorch Geometric 的自注意力 GNN 指南

[towardsdatascience.com

通过点击一下了解更多机器学习知识并支持我的工作 — 成为 Medium 会员,请点击这里:

[## 通过我的推荐链接加入 Medium — Maxime Labonne

作为 Medium 会员,你的会员费的一部分将用于支持你阅读的作者,你将获得对每个故事的完全访问权限……

medium.com

如果你已经是会员,你可以 在 Medium 上关注我.

表格数据的图数据科学

原文:towardsdatascience.com/graph-data-science-for-tabular-data-42f7ee7ffc2a?source=collection_archive---------3-----------------------#2023-11-15

图方法比你想象的要更通用

Andrew Skabar, PhDTowards Data Science Andrew Skabar, PhD

·

关注 发表在 Towards Data Science · 7 分钟阅读 · 2023 年 11 月 15 日

--

图片由 Alina Grubnyak 提供,Unsplash

图数据科学方法通常应用于具有某种固有图形特性的数据显示,例如分子结构数据、交通网络数据等。然而,图方法也可以对那些不显示明显图形结构的数据有用,比如用于机器学习任务的表格数据。在这篇文章中,我将简单直观地演示——不涉及任何数学或理论——通过将表格数据表示为图,我们可以开启对这些数据进行推理的新可能性。

为了保持简单,我将以下面的信用批准数据集作为示例。目标是根据其他属性的值预测批准的值。有许多分类算法可以用来做这件事,但让我们探讨一下如何使用图来处理这个问题。

图片

作者创建的信用批准数据集

图形表示

首先要考虑的是如何将数据表示为图。我们希望捕捉这样一个直觉:两个实例之间共享的属性值越多,它们之间的相似度就越高。我们将使用一个节点来表示每个实例(我们称这些节点为实例节点),并为每个可能的属性值使用一个节点(这些是属性值节点)。实例节点和属性值节点之间的连接是双向的,并反映在表格中的信息。为了保持真的简单,我们将省略属性"Employment"和"History"。这是图形表现。

图片

信用批准数据集的图表示。作者提供的图片。

消息传递

针对某个新实例的属性值,有几种使用图方法预测某个未知属性值的方式。我们将使用消息传递的概念。这是我们将使用的过程。

消息传递过程

在起始节点初始化值为 1 的消息,并让该节点将消息传递给其连接的每个节点。任何接收到消息的节点都会将消息(通过因子k膨胀,其中 0 <k<1)传递给其它连接的节点。继续消息传递,直到达到目标节点(即要预测其值的属性对应的节点),或者没有更多节点可传递消息为止。由于消息不能被传回到接收它的节点,该过程保证终止。

当消息传递完成时,图中的每个节点将收到零个或多个值不同的消息。对属于目标属性的每个节点的这些值进行求和,然后对这些(总和)值进行归一化,使它们本身的总和为 1。将归一化值解释为概率。然后可以使用这些概率来预测未知的属性值,或者从分布中抽取一个随机值来估算。在每次传递中膨胀消息值反映了长路径应该比短路径贡献更少于概率估计的直觉。

示例 1

假设我们希望在收入低的情况下预测批准的值。下面图中的箭头说明了消息传递过程的操作,每个箭头的粗细表示消息值(在每一跳中以因子k = 0.5 膨胀)。

图片

估计在收入低的情况下批准的分布。图片由作者提供。

消息从节点收入:低(绿色)启动。该节点将值为 1 的消息传递给实例 1 和实例 2,然后每个实例将消息(扩展值为 0.5)传递给节点教育:大学和批准:否。请注意,由于教育:大学从实例 1 和实例 2 接收消息,它必须将每个这些消息传递给实例 3,扩展值为 0.25。目标变量每个节点的数字显示了接收到的消息值的总和(括号中为百分比的归一化值)。在收入低的条件下,我们得到批准的以下概率:

  • Prob (批准为‘是’ | 收入低) = 20%

  • Prob (批准为‘否’ | 收入低) = 80%

这些概率与从表中基于计数的预测结果不同。由于五个实例中有两个实例的收入低,并且这两个实例的批准为否,因此基于计数的预测将导致批准为否的概率为 100%。

消息传递程序已考虑到属性值教育大学,由实例 1 和 2 拥有的,同时也由实例 3 拥有,实例 3 的批准为是,从而贡献了节点批准:是的总消息值。如果我们在图中加入了额外的属性就业和历史,这可能会进一步增加连接起始节点和目标节点的路径数量,从而利用额外的上下文信息,并改善概率分布的估计。

示例 2

当对多个属性进行条件判断时,也可以使用消息传递程序。在这种情况下,我们只需在与我们条件的属性值对应的每个节点上启动消息,并从每个节点遵循相同的程序。下图显示了在收入低且教育水平为研究生的情况下,预测批准值的结果。来自每个起始节点的消息序列以不同的颜色显示。

估计在收入低且教育水平为研究生的情况下批准的分布。图片由作者提供。

实例 4 的教育值为研究生,因此对节点批准:是的消息值的总和做出贡献。实例 5 也对批准:是的做出进一步贡献,因为它与实例 4 共享高收入。

[注意:在这些示例中,我们将批准作为目标变量;然而,我们也可以用完全相同的方式估计收入或教育的概率分布]

这些示例表明,消息传递的简单概念(一个基本上是基于图的操作),结合适当的图表示,使我们能够进行一般且灵活的推理。具体来说,可以用来估计任何属性的概率分布,条件是一个或多个属性的值

推理过程有些临时性, 设计成尽可能简单来支持我们的论点。有许多变体可以使用。关键点在于图方法使我们能够利用实例之间丰富的关系网络,这是用基于向量的方法难以捕捉的。当然,我们可以尝试用向量来表达消息传递过程(即,参考表而不是图),但这会很复杂和笨重。图方法的简洁和优雅来自于图表示和推理过程的自然配对

我们可以从我们的例子中得出另一个有趣的观察。在上面的分析中,我们没有提到在实例节点处接收到的消息值之和(即,图中左侧节点)。让我们看看这些值可以显示给我们什么信息。在示例 1 中,在消息传递完成时,实例节点 1 到 5 的消息值之和分别为 1.0、1.0、0.5、0.0 和 0.0。将这些值归一化为总和为 1,结果分别为 40%、40%、20%、0% 和 0%。前两个实例具有批准号,它们归一化值的总和为 80%。最后三个实例具有批准是,它们归一化值的总和为 20%。但这些仅仅是我们在原始分析中得到的批准属性的概率。 (您可以验证 Example 2 中的相同情况)。这并非巧合。实例节点上的消息值之和可以解释为表示该实例与我们条件化的属性值的相似度。因此,推理过程可以被认为是加权最近邻的一种形式,其中相似性度量隐含在消息传递过程中。

UNCRi 框架

在 Skanalytix,我们开发了一个基于图的计算框架,名为统一数值/类别表示与推断(UNCRi) 该框架结合了独特的图形数据表示和灵活的推断过程,可以用于估计和从任何类别或数值变量的条件分布中采样。它可以应用于分类和回归、缺失值填补、异常检测以及从完整联合分布或某些条件分布中生成合成数据等任务。该框架对数据的极端情况具有鲁棒性:类别变量可以从二元到高基数;数值变量可以是多峰的、高度偏斜的和任意规模的;缺失值比例可以很高。你可以在skanalytix.com了解更多关于 UNCRi 的信息。(UNCRi 代码是闭源的)。

结论

从一开始,模式识别和机器学习领域就被操作向量的方法主导。向量的普遍存在使得很难想象其他方法。图方法提供了一个强大而灵活的替代方案。当应用于表格数据时,图方法不仅可以预测一个属性的值或用从其估计分布中抽取的随机值填补属性,还可以通过使用概率链式法则生成整个合成数据集,这些数据实例以与源数据相同的方式分布。所有这些都可以通过一个图和一个推断过程完成!虽然在本文中我们只考虑了类别变量,但这些思想也可以扩展到包含数值和类别属性混合的数据集。

图机器学习:概述

原文:towardsdatascience.com/graph-machine-learning-an-overview-c996e53fab90?source=collection_archive---------0-----------------------#2023-04-04

解密图神经网络——第一部分

入门的关键概念

Zach BlumenfeldTowards Data Science Zach Blumenfeld

·

关注 发布于 Towards Data Science ·9 分钟阅读·2023 年 4 月 4 日

--

作者图片

图神经网络(GNN)在数据科学和机器学习中受到关注,但在专家圈外仍然了解不多。要理解这一激动人心的方法,我们必须从更广泛的图机器学习(GML)领域入手。许多在线资源谈论 GNN 和 GML 时,好像它们是可以互换的概念,或者好像 GNN 是一种万灵药,使其他 GML 方法变得过时。事实并非如此。GML 的主要目的之一是压缩大型稀疏图数据结构,以便进行可行的预测和推断。GNN 是实现这一目标的一种方式,也许是最先进的方式,但不是唯一的方式。理解这一点将帮助为本系列的未来部分奠定更好的基础,我们将在其中更详细地讨论特定类型的 GNN 和相关的 GML 方法。

在这篇文章中,我们将:

  • 回顾一下图数据结构的简要概述

  • 介绍 GML 任务及其解决的问题类型

  • 探讨压缩的概念及其在推动不同 GML 方法(包括 GNN)中的重要性

什么是图?

如果你正在阅读这篇文章,你可能已经对图数据结构有一定的了解。如果没有,我建议阅读这个关于属性图的资源这个关于图数据库概念的资源。我将在这里做一个非常简要的回顾:

图由节点和通过关系连接的节点组成。有几种不同的方式来建模图数据。为了简化,我将使用属性图模型,该模型有三个主要组成部分:

  1. 节点,代表实体(有时称为顶点),

  2. 关系,代表节点之间的关联或互动(有时称为边或链接),以及

  3. 属性,代表节点或关系的特征。

作者提供的图片

什么是图机器学习(GML)?

从根本上说,图机器学习(GML)是将机器学习应用于图,专门用于预测和指导任务。GML 在供应链、欺诈检测、推荐、客户 360、药物发现等领域有各种应用。

理解 GML 的最佳方法之一是通过它可以完成的不同类型的机器学习任务。我将监督式无监督式学习分开说明。

监督式 GML 任务

下图概述了监督学习中最常见的三种 GML 任务:

作者提供的图片

进一步扩展:

  1. 节点属性预测: 预测离散或连续的节点属性。可以将节点属性预测视为预测事物的形容词,例如,在金融服务平台上,某账户是否应被分类为欺诈,或者如何在在线零售店中对产品进行分类。

  2. 链接预测: 预测两个节点之间是否存在关系以及该关系的一些潜在属性。链接预测对实体解析等应用非常有帮助,我们希望预测两个节点是否反映了相同的基础实体;推荐系统中,我们希望预测用户接下来可能想购买或互动的内容;以及生物信息学中,用于预测蛋白质和药物的相互作用。在每种情况下,我们关注的是预测实体之间的关联、相似性或潜在的动作或互动

  3. 图属性预测: 预测图或子图的离散或连续属性。在需要将每个实体建模为独立图进行预测的领域,图属性预测非常有用,而不是将实体建模为大型图中的节点,后者表示完整的数据集。用例包括材料科学、生物信息学和药物发现,其中单个图可以代表分子或蛋白质,我们希望对其进行预测。

无监督 GML 任务

以下是无监督学习中最常见的四种 GML 任务:

作者提供的图像

进一步阐述:

  1. 表示学习: 降维同时保持重要信号是 GML 应用中的核心主题。图表示学习通过从图结构生成低维特征来明确地做到这一点,通常用于后续的探索性数据分析(EDA)和机器学习。

  2. 社区检测(关系的聚类): 社区检测是一种用于识别图中密切互连节点组的聚类技术。社区检测在异常检测、欺诈和调查分析、社交网络分析以及生物学中具有各种实际应用。

  3. 相似性: 相似性在 GML 中指的是在图中找到并测量相似节点对。相似性适用于许多用例,包括推荐、实体解析以及异常和欺诈检测。常见的相似性技术包括 节点相似性算法拓扑链接预测K-最近邻 (KNN)

  4. 中心性与路径寻找: 我将这两个概念归为一类,因为它们通常与机器学习任务关系较少,而更多涉及分析测量。然而,它们在技术上仍然适用,因此我会为了完整性而涵盖它们。中心性用于查找图中的重要或有影响力的节点。中心性在许多用例中普遍存在,包括欺诈和异常检测、推荐、供应链、物流和基础设施问题。路径寻找用于找到图中的最低成本路径或评估路径的质量和可用性。路径寻找可以使许多涉及物理系统的用例受益,例如物流、供应链、交通运输和基础设施。

压缩如何成为 GML 的关键

我遇到了这篇有趣的博客文章 by Matt Ranger,它美妙地解释了这一点:GML 的一个重要目标,以及在很大程度上自然语言处理的目标,是在保持重要信号用于预测和推断的同时,压缩大型稀疏数据结构。

考虑一个由邻接矩阵表示的正则图,这是一个方阵,每行和每列代表一个节点。如果从节点 A 到节点 B 的关系存在,则 A 行与 B 列交叉的单元格为 1;否则为 0。下面是一些小型正则图及其邻接矩阵的示例。

作者提供的图像

请注意,上述邻接矩阵中的许多单元格是 0。如果将其扩展到大型图形,特别是现实世界应用中发现的那些图形,零的比例会增加,邻接矩阵将变成大多数是零。

说明示例使用了来自大型图可视化工具和方法的 Last.fm 推荐图可视化以及来自 Beck, Fabian 等的矩阵图像,通过对比多个层级识别模块化模式

这是因为随着这些图的增长,平均度中心性增长得要慢得多,甚至几乎没有增长。在社交网络中,这可以通过像邓巴数字这样的概念得到证实,它是一个人能够维持稳定社会关系的认知极限。你也可以对其他类型的图有直观认识,比如金融交易图或推荐系统的用户购买图。随着这些图的增长,一个人参与的潜在独特交易或购买的数量增长得要比他们的能力快得多。也就是说,如果网站上有六种产品,一个用户购买其中一半是可行的,但如果有数十万种产品,则不那么可行。因此,你最终会得到非常大且稀疏的数据结构。

如果你能直接将这些稀疏数据结构用于机器学习,你就不需要 GNNs 或任何 GML——你只需将它们作为特征插入到传统的 ML 模型中即可。然而,这并不可行。它不会扩展,而且即使超越这一点,它还会引发关于收敛和估计的数学问题,使 ML 模型不准确且不可行。因此,GML 的一个基本关键是压缩这些数据结构;可以说,这正是 GML 的全部意义所在。

如何实现压缩?——图机器学习方法

在最高层次上,存在三种 GML 方法来实现这种压缩。

作者提供的图片

经典图算法

经典图算法包括像PageRankLouvainDijkstra 的最短路径。它们可以独立用于无监督社区检测、相似性、中心性或路径查找。经典算法的结果也可以作为传统下游模型的特征,如线性和逻辑回归、随机森林或神经网络,以执行 GML 任务。

经典图算法往往简单、易于入门,并且相对可解释和解释。然而,它们可能需要比其他 GML 方法更多的手动工作和主题专家(SME)。这使得经典图算法在实验和开发中成为良好的初步选择,有助于了解在你的图上什么效果最佳。它们在生产中也可以在简单问题上表现良好,但更复杂的用例可能需要升级到另一种 GML 方法。

非 GNN 图嵌入

图嵌入是一种表示学习形式。一些图嵌入技术利用 GNN 架构,而另一些则没有。后者,即非 GNN,是这种方法的重点。这些嵌入技术则依赖于矩阵分解/分解、随机投影、随机游走或哈希函数架构。一些示例包括 Node2vec(基于随机游走)FastRP(随机投影和矩阵操作)HashGNN(哈希函数架构)

图嵌入涉及生成数值或二进制特征向量来表示节点、关系、路径或整个图。其中,节点嵌入是最基本且最常用的。基本思想是为每个节点生成一个向量,使得向量之间的相似性(例如点积)近似于图中节点之间的相似性。下面是一个小型 扎卡里空手道俱乐部网络 的示例。请注意,邻接矩阵是如何被压缩为每个节点的 2 维嵌入向量,以及这些向量如何聚集在一起以反映图的社区结构。大多数现实世界的嵌入将具有多于两个维度(128 到 256 或更高),以表示具有数百万或数十亿节点的大型现实世界图,但基本直觉是相同的。

作者提供的图像

上述相同的逻辑也适用于关系、路径和整个图嵌入:嵌入向量中的相似性应当近似于图结构中的相似性。这实现了压缩,同时保持了重要信号,使得嵌入在各种下游机器学习任务中具有用处。

与经典图算法相比,非 GNN 嵌入可以减少手动工作量和对 SME 的需求。尽管非 GNN 嵌入通常需要调整超参数才能得到最佳效果,但它们往往更容易自动化并在不同图上进行泛化。此外,一些非 GNN 嵌入如 FastRPHashGNN 由于不需要模型训练,可以在普通硬件上极好地扩展到大图。这相比于基于 GNN 的方法可以带来巨大的好处。

然而,非 GNN 嵌入也有一些权衡。由于涉及的数学操作更为一般化,它们的可解释性和解释能力不如经典图算法。它们通常是转导式的,不过 Neo4j 图数据科学的近期改进使得其中一些在特定应用中可以有效地表现为归纳式。我们将在本系列后续内容中更深入地讨论转导式和归纳式设置;这与对新未见数据的预测能力有关,是 GML 的一个重要考虑点。

图神经网络(GNNs)

图片来源:作者

GNN 是一种神经网络模型,它以图数据作为输入,将其转换为中间嵌入,然后将这些嵌入传递到对齐预测任务的最终层。这个预测任务可以是监督性的(节点属性预测、链接预测、图属性预测)或无监督的(聚类、相似性,或仅仅是用于表示学习的最终输出嵌入)。因此,与经典算法和非 GNN 嵌入不同,后者将结果作为特征传递给下游机器学习模型,特别是在监督任务中,GNNs 是完全端到端的图原生解决方案。

GNNs 作为完整的端到端解决方案,具有多种好处。值得注意的是,在训练过程中学习到的中间嵌入理论上能够自动推断图中的最重要信息。最近的大多数 GNNs 也因拥有经过训练的模型而具备归纳能力。

GNNs(图神经网络)也存在一些弱点,包括高复杂性、扩展困难,以及低可解释性和解释能力。GNNs 由于过度平滑和其他数学原理,还可能在深度方面存在局限性。

我将在下一个博客中进一步讨论 GNNs,《GNNs:它们是什么以及为何重要》。与此同时,如果你想开始图机器学习,请查看 Neo4j Graph Data Science。数据科学家和工程师可以在这里找到入门的技术文档。

总结

本文的主要收获:

  • 图机器学习(GML)是一个广泛的领域,涵盖了许多用例应用,包括多个不同的监督和无监督机器学习任务。

  • GML(图机器学习)的主要目的之一是压缩大型稀疏图结构,同时保持预测和推断所需的重要信号。

  • GNNs 是实现这种压缩的多种 GML 方法之一。

图形机器学习 @ ICML 2023

原文:towardsdatascience.com/graph-machine-learning-icml-2023-9b5e4306a1cc?source=collection_archive---------0-----------------------#2023-08-06

图形机器学习的新动态?

最近的进展和热门趋势,2023 年 8 月版

Michael GalkinTowards Data Science Michael Galkin

·

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

--

壮丽的海滩和热带夏威夷风光🌴并没有阻止勇敢的科学家们参加在檀香山举办的国际机器学习大会并展示他们的最新研究成果!让我们一起看看我们最喜欢的图形机器学习领域的新动态。

图片作者。

感谢 Santiago Miret 校对本文。

为了让帖子不那么枯燥,我在檀香山拍了一些照片📷

目录(可点击):

  1. 图形变换器:更稀疏、更快且有方向

  2. 理论:GNN 的 VC 维,深入探讨过度挤压

  3. 新的 GNN 架构:延迟和半跳

  4. 生成模型 — 分子稳定扩散,离散扩散

  5. 几何学习:几何 WL,克利福德代数

  6. 分子:2D-3D 预训练,MD 中的不确定性估计

  7. 材料与蛋白质:用于蛋白质的 CLIP,Ewald 消息传递,对称增强

  8. 酷应用:算法推理、归纳 KG 完成、用于质谱的 GNN

  9. 总结的 Meme 部分

图转换器:更稀疏、更快、且有方向

我们大约一年前 提出了 GraphGPS,很高兴看到许多 ICML 论文基于我们的框架并进一步扩展 GT 能力。

➡️ ExphormerShirzad, Velingker, Venkatachalam 等人 添加了图动机的稀疏注意力的缺失部分:与 BigBird 或 Performer(最初为序列设计)不同,Exphormer 的注意力基于 1-hop 边、虚拟节点(与图中的所有节点相连)以及一个精巧的 扩展边 想法。扩展图具有固定度,并被证明可以近似完全连接的图。所有组件结合起来,注意力成本为 O(V+E) 而不是 O(V²)。这使得 Exphormer 能够在几乎所有地方超越 GraphGPS,并扩展到最多 16 万个节点的非常大图。令人惊叹的工作,Exphormer 有很大机会成为 GT 中标准的稀疏注意力机制 👏。

➡️ 与图转换器并行,扩展图已经可以用来增强任何 MPNN 架构的性能,如 Deac, Lackenby, 和 VeličkovićExpander Graph Propagation 中所示。

类似地,Cai 等人 证明了具有虚拟节点的 MPNN 可以近似线性 Performer-like 注意力,因此即使是经典的 GCN 和 GatedGCN 只要加入虚拟节点,也能在长范围图任务中表现出相当的 SOTA 性能(我们发布了 LGRB 基准测试来测量 GNN 和 GT 的长范围能力)。

来源:Shirzad, Velingker, Venkatachalam 等人

➡️ 一些受视觉模型启发的 GT 的 基于补丁 的子采样方法:He 等人“ViT/MLP-Mixer 在图上的推广” 将输入分成多个补丁,使用 GNN 将每个补丁编码为一个令牌,并对这些令牌运行变换器。

来源:“ViT/MLP-Mixer 在图上的推广” 由 He 等人

GOAT 中,由 Kong 等人 提出,节点特征被投影到 K-Means 的 K 个簇的代码本中,并且每个节点的采样的三跳邻域都关注这个代码本。GOAT 是一个单层模型,并且可以扩展到数百万节点的图中。

➡️ 有向图 也受到了一些 Transformer 的喜爱 💗。“Transformers Meet Directed Graphs”Geisler 等人 引入了磁拉普拉斯 —— 非对称邻接矩阵的拉普拉斯的泛化。磁拉普拉斯的特征向量与有向随机游走结合,成为 Transformer 的强大输入特征,在 OGB Code2 图属性预测数据集上设置了新的 SOTA,超过了现有方法很多!

🏅 最后但并非最不重要的是,我们在社区标准 ZINC 数据集上有了一个新的 SOTA GT — GRIT,由 Ma, Lin, 等人 提出,其全 d-维随机游走矩阵被称为相对随机游走概率(RRWP),作为边特征用于注意力计算(相比之下,流行的 RWSE 特征只是这个矩阵的对角元素)。RRWP 明显比最短路径距离特征更强大,在 ZINC 上取得了创纪录的低 0.059 MAE(比 GraphGPS 的 0.070 低)。GRIT 在其他基准测试中通常也优于 GPS 👏。同样地,Eliasof 等人 提出了一个巧妙的思路,将随机和谱特征结合为位置编码,在超越 RWSE 的同时并未尝试过与 GTs 结合。

图片由作者提供。

理论:GNNs 的 VC 维,深入探讨过度压缩

➡️ VC 维 衡量了模型的容量和表达能力。它已经广泛应用于经典机器学习算法的研究中,但令人惊讶的是,从未应用于研究 GNNs。在 “WL meet VC” 中,Morris 等人 最终揭示了 WL 测试和 VC 维之间的联系 — 原来 VC 维可以由 GNN 权重的比特长度界定,即 float32 权重意味着 VC 维为 32。此外,VC 维对给定任务中唯一 WL 颜色的数量以对数方式依赖,对深度和层数则多项式依赖。这是一个重要的理论结果,我鼓励你深入了解!

来源:“WL meet VC” by Morris 等人

🍊🖐️ 过度压缩效应——尝试将来自过多邻居节点的信息塞入会导致信息丢失——是 MPNN 的另一个常见问题,我们尚未完全理解如何妥善处理。今年,有 3 篇论文专门讨论了这个话题。也许最基础的是Di Giovanni 等的工作,解释了 MPNN 的宽度、深度和图拓扑如何影响过度压缩。

来源:Di Giovanni 等

结果表明,宽度可能有帮助(但存在泛化问题),深度实际上没有什么帮助,而图拓扑(由节点间的通勤时间表征)起着最重要的作用。我们可以通过各种图重连策略(基于空间或谱特性添加和移除边)来减少通勤时间,这些策略有很多(你可能听说过基于 Ricci 流的重连,该研究在 ICLR 2022 上获得了杰出论文奖)。事实上,还有一项后续研究对此进行了更深入的探讨,推导了一些关于过度压缩和 MPNN 属性的不可能性声明——我强烈推荐阅读!

➡️ 有效电阻是空间重连策略的一个例子,Black 等对此进行了详细研究。基于 Ricci 流的重连方法与图的曲率相关,进一步研究见于Nguyen 等的工作中。

➡️ 子图 GNN 继续受到关注:两项工作(Zhang, Feng, Du 等Zhou, Wang, Zhang)同时推导了最近提出的子图 GNN 的表现力等级及其与 1 阶及更高阶 WL 测试的关系。

图像作者提供。

新型 GNN 架构:延迟和半跳

如果你厌倦了 GCN 或 GAT 的各种变体,这里有一些新鲜的想法,可以与任何你选择的 GNN 搭配使用:

⏳ 正如我们在理论部分所知,重新连接有助于对抗过度挤压。Gutteridge 等人 介绍了“DRew: 动态重连的延迟消息传递”,它在后期 GNN 层中逐渐稠密化图,使得长距离节点能够看到先前节点的原始状态(原始 DRew),或者这些跳跃连接是基于延迟来添加的——取决于两个节点之间的距离(vDRew 版本)。例如( 🖼️👇),在 vDRew 延迟消息传递中,来自层 0 的起始节点将在层 1 上向 2-hop 邻居展示其状态,并将在层 2 上向 3-hop 邻居展示其状态。DRew 显著提高了普通 GNN 处理长距离任务的能力——实际上,启用 DRew 的 GCN 是来自 长距离图基准 的 Peptides-func 数据集上的当前 SOTA 👀

来源: Gutteridge 等人

🦘 另一个有趣的想法来自Azabou 等人,即通过在每条边上插入具有特殊连接模式的新慢节点来减慢消息传递——仅从起始节点来的一个输入连接和与目标节点的对称边。慢节点在异质性基准测试中大幅提升了普通 GNN 的性能,也可以通过创建具有不同慢节点位置的视图来实现自监督学习,针对相同的原始图。HalfHop 是一个无需深思熟虑的自监督学习组件,它提升了性能,并且应该成为许多 GNN 库的标准套件 👍。

来源: Azabou 等人

图片由作者提供。

生成模型 — 分子稳定扩散,离散扩散

➡️ 扩散模型可以在特征空间(例如,图像生成中的像素空间,如原始 DDPM)或潜在空间(如稳定扩散)中工作。在特征空间中,必须设计噪声过程以尊重特征空间的对称性和等变性。在潜在空间中,可以简单地向(预训练的)编码器生成的特征添加高斯噪声。大多数 3D 分子生成模型在特征空间中工作(如开创性的EDM),而由Xu 等人(著名的GeoDiff的作者)提出的新GeoLDM模型是首个为 3D 分子生成定义潜在扩散的模型。也就是说,在训练 EGNN 自编码器之后,GeoLDM 在去噪目标上进行训练,其中噪声从标准高斯分布中采样。GeoLDM 相比于 EDM 和其他非潜在扩散方法带来了显著的改进👏。

GeoLDM。来源:Xu 等人

➡️ 在非几何图的领域(仅具有邻接关系和可能的类别节点特征)中,由DiGress(ICLR’23)开创的离散图扩散似乎是最适用的选项。Chen 等人提出了EDGE,这是一种由节点度分布引导的离散扩散模型。与 DiGress 不同,EDGE 中的最终目标图是没有边的断开图,前向噪声模型通过伯努利分布移除边,反向过程将边添加到最近的活跃节点(活跃节点是指在前一步中度数发生变化的节点)。由于度引导引入的稀疏性,EDGE 可以生成高达 4k 节点和 40k 边的大型图!

图生成与 EDGE。来源:Chen 等人

➡️ 最后,“图形结构扩散模型”Weilbach 等人提出,弥合了连续生成模型与在感兴趣问题中引入特定结构的概率图模型之间的差距——通常此类问题具有组合性质。核心思想是将问题的结构编码为尊重排列不变性的注意力掩码,并在 Transformer 编码器的注意力计算中使用该掩码(根据定义,除非使用位置嵌入,否则对输入标记排列是等变的)。GSDM可以处理二进制连续矩阵分解、布尔电路,生成数独,并执行排序。特别有趣的是论文中蕴含的一丝讽刺🙃。

GSDM 任务到注意力偏差。来源:“图形结构扩散模型”Weilbach 等

作者提供的图片

几何学习:几何 WL,Clifford 代数

几何深度学习蓬勃发展!有很多有趣的论文展示,这将占据整个帖子,因此我只会重点介绍几个。

➡️ 几何 WL 终于在 Joshi, Bodnar 等 的工作中出现。几何 WL 扩展了 WL 测试的概念,加入了几何特征(例如坐标或速度),并推导出表达力层级,直到 k-order GWL。关键点:1️⃣ 等变 模型比 不变 模型更具表现力(注意在完全连接的图中差异消失),2️⃣ 张量阶 的特征提升了表现力,3️⃣ 体序 的特征提升了表现力(见下图 👇)。也就是说,球面 > 笛卡尔坐标 > 标量,以及 多体交互 > 仅距离。论文还展示了一个令人惊叹的学习资源 Geometric GNN Dojo,你可以从基本原理推导并实现大多数 SOTA 模型!

来源:Joshi, Bodnar 等

➡️ 超越向量到 Clifford 代数,Ruhe 等 推导了 几何 Clifford 代数网络(GCANs)。Clifford 代数通过双向量、三向量和(一般)多向量自然支持高阶交互。关键思想是 Cartan-Dieudonné 定理,即每个正交变换都可以分解为在超平面上的 反射,而几何代数将数据表示为 Pin(p,q,r) 群的元素。GCANs 引入了线性层、归一化、非线性等概念,以及它们如何用神经网络进行参数化。实验包括流体动力学建模和 Navier-Stokes 方程。

来源:Ruhe 等

实际上,已经有一篇后续工作介绍了等变 Clifford 神经网络——你可以了解更多关于 Clifford 代数基础以及微软研究院支持的最新论文 CliffordLayers

💊 等变 GNN(EGNN)是几何深度学习中的阿司匹林,几乎适用于所有任务,并且已经看到相当多的改进。Eijkelboom 等 将 EGNN 与 单纯形网络 结合,这些网络在高阶结构(即单纯形复合体)上操作,形成 EMPSN。这是第一个结合几何和拓扑特征的例子之一,具有很大的改进潜力!最后,Passaro 和 Zitnick 推导出一种巧妙的技巧,将 SO(3) 卷积简化为 SO(2),将复杂度从 O(L⁶) 降到 O(L³),同时提供了数学等价性保证 👀。这一发现使得几何模型在像 OpenCatalyst 这样的大型数据集上得以扩展,并已被应用于 Equiformer V2 —— 很快将在许多其他几何模型库中出现 😉

图片来源:作者。

分子:2D-3D 预训练,MD 中的不确定性估计

➡️ Liu, Du 等 提出了 MoleculeSDE,这是一个新的框架,用于在分子数据上进行 2D-3D 联合预训练。除了标准的对比损失外,作者还添加了两个 生成 组件:通过基于分数的扩散生成重建 2D -> 3D 和 3D -> 2D 输入。使用标准的 GIN 和 SchNet 作为 2D 和 3D 模型,MoleculeSDE 在 PCQM4M v2 上进行了预训练,并在下游微调任务中表现良好。

来源:MoleculeSDE Github 仓库

➡️ Wollschläger 等 对 GNN 中的不确定性估计进行了一项全面研究,重点关注分子动力学和力场。识别关键的物理信息和应用导向原则,作者提出了一种 局部神经核,这是一种基于高斯过程的扩展,适用于任何几何 GNN,处理不变和等变量(已在 SchNet、DimeNet 和 NequIP 上进行尝试)。在许多情况下,LNK 从一个模型的估计与需要训练多个模型的昂贵集成效果相当或更好。

来源:Wollschläger 等

图片来源:作者。

材料与蛋白质:CLIP 用于蛋白质,Ewald 消息传递,等变增强

CLIP 及其后代已经成为文本到图像模型中的标准工具。我们能否对文本到蛋白质做同样的事情?可以!

➡️ Xu, Yuan 等 提出了 ProtST,一个用于学习文本蛋白质描述(通过 PubMedBERT)和蛋白质序列(通过 ESM)联合表示的框架。除了对比损失外,ProtST 还有一个多模态掩码预测目标,例如,掩盖文本和蛋白质序列中的 15% 令牌,并基于潜在表示联合预测这些令牌,以及基于序列或语言单独的掩码预测损失。此外,作者设计了一个新的 ProtDescribe 数据集,包含 550K 对齐的蛋白质序列-描述对。 ProtSTPEER 基准测试中在许多蛋白质建模任务上表现出色,包括蛋白质功能注释和定位,同时还允许从文本描述中进行零样本蛋白质检索(见下例)。看起来 ProtST 具有成为许多蛋白质生成模型背后核心的光明前景 😉

来源: Xu, Yuan 等

实际上,ICML 展示了几项蛋白质生成工作,如 Lin 和 AlQuraishiGENIEYim, Trippe, De Bortoli, Mathieu 等FrameDiff — 这些工作尚未依赖文本描述,因此将 ProtST 融入其中似乎是提升性能的明智选择 📈

Gif 来源: SE(3) Diffusion Github

⚛️ 分子上的 MPNNs 具有严格的局部性偏差,这会抑制对长程交互的建模。 Kosmala 等 推导出了 Ewald 消息传递,并应用了 Ewald 求和 的思想,该思想将相互作用势能分解为短程和长程项。短程交互由任何 GNN 建模,而长程交互则是新的,通过 3D 傅里叶变换 和傅里叶频率上的消息传递来建模。结果表明,这一长程项相当灵活,可以应用于任何建模周期性和非周期性系统(如晶体或分子)的网络,如 SchNet、DimeNet 或 GemNet。该模型在 OC20 和 OE62 数据集上进行了评估。如果你对更多细节感兴趣,可以查看 Arthur Kosmala 的 1 小时讲座

来源: Kosmala 等

PotNet中,林等人使用了与 Ewald 求和类似的思想来处理 3D 晶体,其中长程连接使用不完全贝塞尔函数建模。PotNet 在 Materials Project 数据集和 JARVIS 上进行了评估,因此阅读这两篇论文可以很好地理解 Ewald 求和为许多与晶体相关的任务带来的好处 😉

来源:林等人

➡️ 另一种方法是通过杜瓦尔、施密特等人FAENet中赋予任何 GNNs 晶体和分子等效性的看法。一种标准的方法是将某些对称性和等效性直接嵌入 GNN 架构中(例如在 EGNN、GemNet 和 Ewald Message Passing 中)— 这是一种安全但计算昂贵的方式(特别是涉及球谐函数和张量积时)。另一种选项通常用于视觉 — 展示相同输入的许多增强,模型最终应该学习到增强中的相同不变性。作者选择第二条路径,并设计了一种严格的方法来采样 2D / 3D 数据的不变或等变增强(例如用于能量或力的增强),所有这些增强都有花哨的证明 ✍️。为此,数据增强管道包括将 2D / 3D 输入投影到基于距离协方差矩阵的 PCA 的规范表示中,从中我们可以均匀采样旋转。

提出的 FAENet 是一个简单的模型,只使用距离,但在使用随机帧平均数据增强时表现非常好,同时速度快 6 到 20 倍。同样适用于晶体结构!

增强和随机帧平均。来源:杜瓦尔、施密特等

图片由作者提供。

酷炫应用:算法推理,归纳 KG 完成,质谱 GNN

本节中的几篇论文不属于上述任何一篇,但仍值得关注。

➡️ “神经算法推理与因果正则化” Bevilacqua 等人 解决了图学习中的一个常见问题——在测试时对更大输入的 OOD 泛化。研究算法推理问题中的 OOD 泛化时,作者观察到在某一步骤中存在许多不同的输入产生相同的计算。与此同时,这也意味着某些输入子集不会(或不应该)影响预测结果。这个假设使得可以设计一个自监督目标(称为Hint-ReLIC),该目标更倾向于一个“有意义”的步骤而不是一堆不影响预测结果的步骤。这个新目标显著提高了许多 CLRS-30 任务的表现,达到 90%以上的 micro-F1。是否可以在一般消息传递中利用相同的原则来改进其他图学习任务中的 OOD 转移是一个有趣的问题 🤔

来源:“神经算法推理与因果正则化” Bevilacqua 等人

如果你对神经算法推理更感兴趣,可以查看知识与逻辑推理研讨会的会议记录,其中包含更多相关工作。

➡️ “InGram: 通过关系图的归纳知识图嵌入” Lee 等人 似乎是 ICML’23 上为数不多的知识图谱论文之一(根据我的搜索)。InGram 是首批能够在测试时对未见实体和未见关系进行归纳泛化的方法之一。之前的归纳 KG 模型需要以某种形式学习关系嵌入以对新节点进行泛化,而在这种范式下,新的未见关系很难建模。InGram 在原始的多关系图上构建了一个关系图,即关系类型的图,并通过运行 GAT 基于这个图学习关系的表示。实体表示是从随机初始化和 GNN 编码器中获得的。拥有实体和关系表示后,应用 DistMult 解码器作为评分函数。InGram 在未见关系方面有很大机会与GraIL (ICML 2020)对未见实体的影响相当 😉。

来源:“InGram: 通过关系图的归纳知识图嵌入” Lee 等人

🌈 “利用图神经网络高效预测高分辨率质谱”Murphy 等人提出,是一个将 GNNs 应用于预测质谱这一实际物理问题的酷应用。主要发现是,大部分质谱信号可以通过少数几个成分(产物离子和中性丧失公式)来解释。并且有可能从训练数据中挖掘出这些公式的词汇。因此,这个问题可以被框架为图分类(或图属性预测),在给定分子图的情况下,我们预测与某些质谱值对应的词汇项。这种方法,GRAFF-MS,通过 GIN 构建带有边特征的分子图表示,通过 SignNet 获得拉普拉斯特征,并与协变量特征进行汇总。与基线 CFM-ID 相比,GRAFF-MS 的推断时间为约 19 分钟,而 CFM-ID 则为 126 小时,性能显著提升👀。

来源: “利用图神经网络高效预测高分辨率质谱”Murphy 等人提出

结论性表情包部分

同一张照片上的四位 Michaels(背景中还有一个ε)!

2022 年的表情包终于汇聚到了Michael Bronstein

图形机器学习在 2023 年的现状

原文:towardsdatascience.com/graph-ml-in-2023-the-state-of-affairs-1ba920cb9232?source=collection_archive---------0-----------------------#2023-01-01

最前沿动态

热点趋势和重大进展

Michael Galkin数据科学前沿 Michael Galkin

·

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

--

2022 年已经结束,是时候坐下来回顾一下在图形机器学习(Graph ML)方面取得的成就,并对 2023 年的可能突破进行假设了。敬请关注 🎄☕

背景图像由 DALL-E 2 生成,文本由作者添加。

这篇文章由 Hongyu Ren (斯坦福大学)、 Zhaocheng Zhu (Mila 和蒙特利尔大学)共同撰写。我们感谢 Christopher Morris Johannes Brandstetter 在理论和偏微分方程部分的反馈和帮助。请关注 Michael Hongyu Zhaocheng、* Christopher Johannes 在 Medium 和 Twitter 上,以获取更多与图机器学习相关的讨论。

目录:

  1. 生成模型:分子和蛋白质的去噪扩散模型

  2. DFTs、机器学习力场、材料和天气模拟

  3. 几何学与拓扑学与偏微分方程

  4. 图变换器

  5. 大型图

  6. GNN 理论:Weisfeiler 和 Leman 的前景,子图 GNN

  7. 知识图谱:归纳推理接管

  8. 算法推理和对齐

  9. 酷炫的 GNN 应用

  10. 硬件:IPU 和 Graphcore 赢得 OGB LSC 2022

  11. 新的会议:LoG 和分子机器学习

  12. 课程和教育材料

  13. 新的数据集、基准和挑战

  14. 软件库和开源

  15. 加入社区

  16. 2022 年的网络迷因

生成模型:分子和蛋白质的去噪扩散模型

生成扩散模型在视觉语言领域是 2022 年深度学习世界的头条话题。尽管生成图像和视频无疑是尝试不同模型和采样技术的酷炫领域,我们认为

在 2022 年,扩散模型最有用的应用实际上是在几何深度学习领域中创建的,重点关注分子和蛋白质

在我们最近的文章中,我们在思考是否 “去噪扩散模型就是你所需的一切?”。

## 去噪扩散生成模型在图机器学习中的应用

去噪扩散模型就是你所需的一切吗?

towardsdatascience.com

在那里,我们回顾了最新的生成模型用于图生成(DiGress)、分子构象生成(EDM、GeoDiff、Torsional Diffusion)、分子对接(DiffDock)、分子连接(DiffLinker)和配体生成(DiffSBDD)。一旦帖子公开,几种令人惊叹的蛋白质生成模型也随之发布:

Chroma来自 Generate Biomedicines,允许施加功能性和几何约束,甚至可以使用自然语言查询,比如“生成一个具有 CHAD 结构域的蛋白质”,这要归功于一个小型的 GPT-Neo,经过蛋白质标注的训练;

Chroma 蛋白质生成。来源: Generate Biomedicines

RoseTTaFold Diffusion(RF Diffusion)来自 Baker Lab 和 MIT,具有类似功能,还支持文本提示,如“生成一个能够结合 X 的蛋白质”,并且能够进行功能性基序支架、酶活性位点支架和de novo蛋白质设计。强项:用 RF Diffusion 生成的 1000 种设计在实验室中被合成和测试

RF Diffusion。来源: Watson 等 BakerLab

Meta AI FAIR 团队在蛋白质设计领域通过语言模型取得了惊人的进展:2022 年中,ESM-2发布了,这是一种仅在蛋白质序列上训练的蛋白质语言模型,远超 ESM-1 及其他基准模型。而且,后来显示编码的语言模型表示是获得蛋白质实际几何结构的非常好的起点,而无需多重序列比对(MSAs)——这可以通过ESMFold实现。特别感谢 Meta AI 和 FAIR 发布了该模型及其权重:它在官方 GitHub 仓库HuggingFace上也可以找到!

扩展 ESM-2 可以获得更好的折叠预测。来源:Lin, Akin, Rao, Hie 等

🍭 随后,来自 ESM 团队的更多好消息传来:Verkuil 等发现 ESM-2 可以生成de novo蛋白质序列,这些序列实际上可以在实验室中合成,并且更重要的是,它们在已知的自然蛋白质中没有任何匹配。Hie 等提出了一种全新的蛋白质设计编程语言(可以把它看作是 ESMFold 的查询语言)——生产规则以约束函数的语法树形式组织。然后,每个程序被“编译”为一个控制生成过程的能量函数。Meta AI 还发布了最大的Metagenomic Atlas,但更多内容请参见本文的数据集部分。

在抗体设计领域,IgLM 采用了类似的基于 LM 的方法,如 Shuai、Ruffolo 和 Gray 所述。IGLM 生成基于链和物种 ID 标签的抗体序列。

最后,我们要特别提到 Jian Tang 实验室在 Mila 的一些工作。由 Liu 等人 提出的 MoleculeSTM 是一个类似 CLIP 的文本到分子模型(以及一个新的大型预训练数据集)。MoleculeSTM 能做到两件令人印象深刻的事情:(1)通过文本描述如“噻唑衍生物”检索分子,并从给定的 SMILES 分子中检索文本描述;(2)根据文本提示进行分子编辑,如“使分子在水中溶解且渗透性低”——模型根据描述编辑分子图谱,令人瞠目结舌 🤯

然后,Shi 等人 提出的 ProtSEED 是一个同时生成蛋白质序列 结构的模型(例如,大多数现有的蛋白质扩散模型一次只能处理其中之一)。ProtSEED 可以基于残基特征或残基对进行条件设置。从模型的角度看,它是一个具有改进的三角注意力的对称迭代模型。ProtSEED 在抗体 CDR 共同设计、蛋白质序列-结构共同设计和固定骨架序列设计中进行了评估。

从文本输入中进行分子编辑。来源:Liu 等人

除了生成蛋白质结构外,还有一些工作致力于从结构生成蛋白质序列,这被称为逆折叠。不要忘记查看 Meta 的 ESM-IF1 和 Baker 实验室的 ProteinMPNN

2023 年的预期:(1)扩散模型的性能改进,例如更快的采样和更高效的求解器;(2)更强大的条件蛋白质生成模型;(3)对 生成流网络(GFlowNets,查看 教程)在分子和蛋白质中的更成功应用。

DFTs、ML 力场、材料和天气模拟

AI4Science 成为对称 GNN 研究及其应用的前沿。通过将 GNN 与 PDEs 配对,我们现在可以处理更复杂的预测任务。

2022 年,这一前沿领域扩展到了用于 分子动力学材料发现 的基于 ML 的 密度泛函理论(DFT)和 力场 近似。另一个不断增长的领域是 天气模拟

我们推荐 Max Welling 的 讲座,以获得关于 AI4Science 的更广泛概述,以及深度学习在科学中的应用现状。

从模型开始,2022 年见证了等变 GNN 在分子动力学和模拟中的激增,例如,基于NequIP、由Musaelian、Batzner 等人提出的Allegro或由Batatia et al.提出的MACE。这些模型的设计空间非常大,因此可以参考Batatia、Batzner 等人的最新综述以获得概述。对于大多数模型来说,一个关键组件是e3nn库(Geiger 和 Smidt的论文)和张量积的概念。我们强烈推荐 Erik Bekkers 提供的一个出色的新课程,以理解数学基础并跟上最新论文。

⚛️ 密度泛函理论(DFT)计算是分子动力学的主要工具之一(并且在大型集群中占据了大量计算时间)。然而,DFT 的时间复杂度是 O(n³),那么机器学习能否有所帮助呢?在学习到的力场已准备好用于基态催化剂发现中,Schaarschmidt et al.展示了学习势能模型的实验研究——结果表明 GNNs 在 O(n)线性时间内可以表现得非常好!Easy Potentials方法(基于 Open Catalyst 数据训练)证明是一个非常好的预测器,特别是与后处理步骤配合时。模型方面,它是一个带有Noisy Nodes自监督目标的 MPNN。

《力量不足》中,Fu et al.提出了一个新的分子动力学基准——除了 MD17,作者们还添加了液体(水)、肽(丙氨酸二肽)和固态材料(LiPS)的数据集。更重要的是,作者们考虑了广泛的物理属性,如模拟的稳定性、扩散性和径向分布函数。包括 SchNet、ForceNet、DimeNet、GemNet(-T 和-dT)以及 NequIP 在内的大多数 SOTA 分子动力学模型都被探讨了。

来源: Fu et al.

在晶体结构建模中,我们要特别提到由Kaba 和 Ravanbakhsh提出的等变晶体网络——一种用来构建具有晶体对称性的周期性结构表示的巧妙方法。晶体可以用晶格单位胞来描述,基向量可以进行群变换。概念上,ECN 创建了与对称群对应的边索引掩码,对这些掩码索引进行消息传递,并聚合多个对称群的结果。

来源: Kaba 和 Ravanbakhsh

关于材料发现的更多消息可以在最近的 AI4Mat NeurIPS workshop 会议记录中找到!

☂️ 基于机器学习的天气预报也取得了巨大的进展。特别是,DeepMind 的 GraphCast 和华为的 Pangu-Weather 展示了出色的结果,大大超越了传统模型。虽然 Pangu-Weather 利用 3D/视觉输入和视觉变换器,GraphCast 则采用了网格 MPNN,其中地球被分割为多个层级的网格。最深层有大约 40K 节点,具有 474 个输入特征,模型输出 227 个预测变量。MPNN 遵循“编码器-处理器-解码器”结构,并具有 16 层。GraphCast 是一个自回归模型,相对于下一个时间步预测,即它利用前两个状态预测下一个状态。GraphCast 可以在单个 TPUv4 上在 <60 秒内建立 10 天的预测,并且比非机器学习预测模型准确得多。👏

GraphCast 中的编码器-处理器-解码器网格 MPNN。来源:Lam, Sanchez-Gonzalez, Willson, Wirnsberger, Fortunato, Pritzel 等

2023 年的期待:我们期望看到更多关注 GNN 的计算效率和可扩展性。目前基于 GNN 的力场取得了显著的准确性,但仍比经典力场慢 2-3 个数量级,并且通常只部署在几百个原子上。为了使 GNN 在材料科学和药物发现中真正发挥变革性影响,我们将看到许多人致力于解决这个问题,无论是通过架构进步还是更智能的采样方法。

几何学 & 拓扑学 & PDEs

在 2022 年,1️⃣ 我们对 GNN 中的过度平滑和过度压缩现象及其与代数拓扑的关系有了更好的理解;2️⃣ 使用 GNN 进行 PDE 建模现在已经成为主流。

1️⃣ Michael Bronstein 的实验室对这个问题做出了巨大贡献 —— 查看那些关于神经切片扩散和将 GNN 视为梯度流的优秀帖子。

## 神经切片扩散在图上的深度学习

细胞切片理论,作为代数拓扑的一个分支,为图神经网络的工作原理提供了新的见解…

[towardsdatascience.com

以及关于 GNN 作为梯度流的内容:

## 图神经网络作为梯度流

GNN 作为梯度流的衍生物,通过最小化描述吸引力和排斥力的可学习能量来优化…

[towardsdatascience.com

2️⃣ 使用 GNNs 进行 PDE 建模已成为主流话题。一些论文需要 🤯 数学警报 🤯 提示,但如果你对 ODEs 和 PDEs 的基础知识很熟悉,这应该会更容易。

消息传递神经 PDE 求解器Brandstetter, Worrall, 和 Welling 描述了消息传递如何帮助解决 PDE,具有更好的泛化能力,并摆脱手动启发式。此外,MP-PDEs 在表示上包含经典求解器,如有限差分。

来源: Brandstetter, Worrall, 和 Welling

该主题通过许多近期工作得到了进一步发展,包括使用隐式神经表示进行连续预测 (Yin et al.)、支持混合边界条件 (Horie and Mitsume),或 PDE 的潜在演变 (Wu et al.)

2023 年的期待:神经 PDE 及其应用有可能扩展到更多与物理相关的 AI4Science 子领域,特别是计算流体动力学(CFD)在未来几个月可能会受到基于 GNN 的替代品的影响。经典的 CFD 被广泛应用于多个领域的研究和工程问题,包括空气动力学、高超声速和环境工程、流体流动、视频游戏中的视觉效果,或上述天气模拟。基于 GNN 的替代品可能会增强/替代传统的经过验证的技术,如有限元方法 (Lienen et al.)、重网格算法 (Song et al.)、边值问题 (Loetsch et al.),或与三角化边界几何的交互 (Mayr et al.)。

神经 PDE 社区正在开始建立强大且常用的基准和框架,这将反过来帮助加速进展,例如 PDEBench (Takamoto et al.) 或 PDEArena (Gupta et al.)

图神经网络变换器

绝对是 2022 年的主要社区推动力之一,图神经网络变换器(GTs)在有效性和可扩展性方面有了很大进展。2022 年发布了几个杰出的模型:

👑 GraphGPSRampášek et al. 提出的,因其结合了局部消息传递、全局注意力(可选地,线性以提高效率)和位置编码,最终在 ZINC 和许多其他基准上设立了新的 SOTA 标杆,被誉为 “2022 年的 GT”。查看有关 GraphGPS 的专门文章

## GraphGPS: 导航图形变换器

最佳图形变换器的烹饪秘诀

towardsdatascience.com

GraphGPS 作为 GPS++ 的核心,获胜 OGB 大规模挑战赛 2022 模型在 PCQM4M v2(图回归)上表现突出。GPS++ Graphcore、Valence Discovery 和 Mila 创建,整合了更多特性,包括 3D 坐标,并利用了稀疏优化的 IPU 硬件(更多信息见下一节)。GPS++ 权重已在 GitHub 上可用

GraphGPS 直观理解。来源:Rampášek et al

Transformer-MLuo et al. 提出的方法也启发了许多顶级 OGB LSC 模型。Transformer-M 将 3D 坐标添加到整洁的 2D-3D 预训练混合中。在推理时,当 3D 信息未知时,该模型将推断出一部分 3D 知识,从而显著提高 PCQM4Mv2 的性能。代码可用

Transformer-M 2D-3D 预训练方案。来源: Luo et al.

TokenGTKim et al 提出的方法更加明确,并将输入图的所有边(除了所有节点)添加到喂给 Transformer 的序列中。使用这些输入,编码器需要额外的标记类型来区分节点和边。作者证明了几个很好的理论性质(尽管代价是较高的计算复杂度 O((V+E)²),在完全连接图的最坏情况下可能达到四次方)。代码可用

TokenGT 将节点和边都添加到输入序列中。来源:Kim et al

2023 年的预期:在来年,我们预计 1️⃣ GTs 在数据和模型参数两个维度上扩展,从小于 50 节点的分子到数百万节点的图,以见证如文本和视觉基础模型中的突现行为 2️⃣ 类似于 BLOOM 由 BigScience Initiative 提供的用于分子数据的大型开源预训练等变 GT,也许在 Open Drug Discovery 项目中。

大图

🔥 我们在 2022 年最喜欢的一篇是Chamberlain, Shirobokov 等“基于子图草图的链接预测图神经网络”——这是算法与 ML 技术的巧妙结合。众所周知,SEAL 类的标记技巧相比于标准 GNN 编码器可以显著提升链接预测性能,但却面临巨大的计算/内存开销。在这项工作中,作者发现利用哈希 (MinHashing) 和基数估计 (HyperLogLog) 算法,可以高效地获取查询边两个节点之间的距离。本质上,消息传递是在 minhashinghyperloglog 单节点的初步草图(min 聚合用于 minhash,max 用于 hyperloglog 草图)上进行的——这就是ELPH 链接预测模型的核心(带有简单的 MLP 解码器)。然后,作者设计了一个更具可扩展性的 BUDDY 模型,其中 k-hop 哈希传播可以在训练前预计算。实验表明,ELPH 和 BUDDY 可以扩展到以前过于庞大或资源消耗过大的图中。出色的工作,绝对是未来所有链接预测模型的坚实基线!👏

计算子图哈希以估计邻域和交集基数的动机。来源:Chamberlain, Shirobokov 等

在图采样和迷你批处理方面,Gasteiger, Qian, 和 Günnemann 设计了基于影响力的迷你批处理 (IBMB),这是个很好的例子,说明个性化 PageRank (PPR) 如何解决图批处理问题!IBMB 旨在创建对节点分类任务影响最大的最小迷你批次。实际上,影响力分数等同于 PPR。实际上,给定一组目标节点,IBMB (1) 将图划分为永久集群,(2) 在每个批次内运行 PPR,选择 top-PPR 节点以形成最终的子图迷你批次。生成的迷你批次可以发送到任何 GNN 编码器。IBMB 对图的大小几乎是常数 O(1),因为分区和 PPR 可以在预处理阶段预计算。

尽管生成的批次是固定的,并且在训练过程中不会改变(不够随机),但作者设计了类似动量的优化项来缓解这种非随机性。IBMB 可以在训练和推理中使用,速度提升可达 17 倍和 130 倍 🚀

基于影响力的迷你批处理。来源:Gasteiger, Qian, 和 Günnemann

本小节的副标题可以是“由谷歌提供”,因为大多数论文的作者都来自谷歌 😉

Carey 等 创建了Stars,一种在数十万亿边的规模下构建稀疏相似性图的方法🤯。成对的 N²比较显然不可行——Stars 使用了两个跳数的spanner 图(这些图中相似的点通过最多两个跳数连接)和SortingLSH,两者结合使得接近线性的时间复杂度和高稀疏性成为可能。

Dhulipala 等 创建了ParHAC,一种用于非常大图的近似(1+𝝐)并行层次聚类(HAC)算法及其广泛的理论基础。ParHAC 具有 O(V+E)复杂度和多对数深度,在具有数百亿边的图上运行速度比基线快高达 60 倍(这里是具有 1.7B 节点和 125B 边的超链接图)。

Devvrit 等 创建了S³GC,一种可扩展的自监督图聚类算法,使用单层 GNN 和对比训练目标。S³GC 使用图结构和节点特征,能够扩展到高达 1.6B 边的图。

最后,Epasto 等 创建了 PageRank 的差分隐私修改版本!

LoG 2022 包含了两个关于大规模 GNN 的教程:生产中 GNN 的扩展由 Da Zheng、Vassilis N. Ioannidis 和 Soji Adeshina 主讲,以及并行和分布式 GNN由 Torsten Hoefler 和 Maciej Besta 主讲。

2023 年值得期待:进一步降低计算成本和推理时间,以应对非常大的图。也许 OGB LSC 图的模型可以在普通机器上运行,而不是在大型集群上?

GNN 理论:Weisfeiler 和 Leman 的足迹,子图 GNN

年度游客!原始肖像来源:几何深度学习 IV:GNN 的化学前体 由 Michael Bronstein 主讲

🏖 🌄 Weisfeiler 和 Leman,图 ML 和 GNN 理论的奠基人,度过了非常多产的一年!继之前访问过NeuralSparseTopologicalCellular场所后,2022 年我们在几个新地方见到了他们:

  • WL Go 机器学习Morris 等 对 WL 测试的基础知识、术语及各种应用的综合调查;

  • WL Go 关系 —— Barcelo et al 首次尝试研究在多关系图和知识图中使用的关系 GNNs 的表现力。结果表明,R-GCN 和 CompGCN 的表现力相同,且受限于关系 1-WL 测试,最具表现力的消息函数(聚合实体-关系表示)是 Hadamard 乘积;

  • WL Go Walking by Niels M. Kriege 研究了随机游走核的表现力,发现 RW 核(经过小的修改)与 WL 子树核一样具表现力。

  • WL Go 几何Joshi, Bodnar et al 提出了几何 WL 测试(GWL)来研究等变和不变 GNNs(对于某些对称性:平移、旋转、反射、排列)的表现力。结果表明,等变 GNNs(例如 E-GNNNequIPMACE)在理论上比不变 GNNs(例如 SchNetDimeNet)更强大。

  • WL Go 时间Souza et al 提出了时间 WL 测试来研究时间 GNNs 的表现力。作者随后提出了一种新型的单射聚合函数(以及 PINT 模型),这应该是最具表现力的;

  • WL Go 渐进Bause 和 Kriege 提议用非单射函数修改原始 WL 颜色细化,其中不同的多重集合可能被分配相同的颜色(在某些条件下)。因此,它使得颜色细化过程更加渐进,并且收敛到稳定着色的速度更慢,最终保留了 1-WL 的表现力,但在过程中获得了一些区分特性。

  • WL Go 无限Feldman et al 提议用从拉普拉斯的热核衍生的谱特征或拉普拉斯的 k 最小特征向量(对于大图)来改变初始节点着色,这与拉普拉斯位置编码(LPEs)非常接近。

  • WL Go 双曲Nikolentzos et al 指出 WL 测试的颜色细化过程会产生颜色的树状层次结构。为了保持这些颜色所编码的节点相对距离,作者建议将每层/迭代的输出状态映射到双曲空间,并在每次更新后调整。最终的嵌入应该保留节点距离的概念。

📈 在比 1-WL 更具表达力的架构领域中,子图 GNNs 是最大的趋势。其中有三种方法脱颖而出:1️⃣ 子图联合网络(SUN),由Frasca, Bevilacqua 等提出,提供了子图 GNNs 设计空间和表达力的全面分析,显示它们受限于 3-WL;2️⃣ 有序子图聚合网络(OSAN),由Qian, Rattan 等提出,设计了一种子图增强 GNNs(k-OSAN)的层次结构,并发现 k-OSAN 与 k-WL 不可比,但严格受限于(k+1)-WL。OSAN 的一个特别酷的部分是使用隐式 MLE(NeurIPS’21),这是一种可微分的离散采样技术,用于采样有序子图。️3️⃣ SpeqNetsMorris 等提出,设计了一种图网络的置换等变层次结构,平衡了可扩展性和表达力。4️⃣ GraphSNNWijesinghe 和 Wang提出,基于子图同构和子树同构的重叠推导出表达性模型。

🤔 一些研究重新审视 WL 框架作为 GNN 表达力的一种通用手段。Geerts 和 Reutter定义了k 阶 MPNNs,可以通过张量语言(WL 与张量语言之间的映射)进行表征。一个新的匿名 ICLR’23 提交提出利用图的双连通性并定义了广义距离 WL算法。

如果你想更深入地研究这个主题,查看 Fabrizio Frasca、Beatrice Bevilacqua 和 Haggai Maron 的精彩LOG 2022 教程及其实际例子吧!

2023 年的期待1️⃣ 更多致力于创建时间和内存高效的子图 GNNs。2️⃣ 更好地理解 GNNs 的泛化能力。3️⃣ Weisfeiler 和 Leman 访问 10 个新地方!

知识图谱:归纳推理占据主导地位

去年,我们观察到了 KG 表示学习的重大转变:传导性方法正被积极淘汰,取而代之的是归纳模型,这些模型可以为新的、未见过的节点构建有意义的表示,并执行节点分类和链接预测。

在 2022 年,该领域沿着两个主要轴线扩展:1️⃣ 归纳链接预测(LP)2️⃣ 和归纳(多跳)查询回答,将链接预测扩展到更复杂的预测任务。

1️⃣ 在链路预测中,大多数归纳模型(如NBFNetNodePiece)通过假设关系类型的集合在训练期间是固定的且不会随时间变化,从而转移到未见节点,并学习关系嵌入。当关系集合也发生变化时会发生什么?在最困难的情况下,我们希望能够转移到具有完全不同节点关系类型的知识图谱。

到目前为止,所有支持未见关系的模型都依赖于元学习,这种方法既慢又消耗资源。在 2022 年,黄,任和莱斯科维奇首次提出了CSR(连接子图推理器)框架,它在实体关系类型上都是归纳的不需要任何元学习!👀 通常,在推理时,对于新关系,模型至少看到k个示例三元组(因此是 k-shot 学习场景)。从概念上讲,CSR 提取每个示例周围的子图,试图学习共同的关系模式(即优化边缘掩码),然后将掩码应用于查询子图(预测缺失的目标链路)。

支持具有未见实体和关系类型的知识图谱的归纳 CSR。来源:黄,任和莱斯科维奇

Chen 等人ReFactor GNNs是另一项有关浅层 KG 嵌入模型归纳特性的有洞察力的工作——特别是,作者发现浅层因子分解模型如 DistMult 在反向传播的视角下,与无限深度的 GNNs 类似,节点如何从邻近节点和非邻近节点更新其表示。理论上,任何因子分解模型都可以转变为归纳模型!

2️⃣ 从归纳表征学习也开始涉及到复杂的逻辑查询回答。 (厚颜无耻的插入广告)事实上,这是我们团队今年的重点之一 😊 首先,在Zhu 等人的研究中,我们发现神经贝尔曼-福特网络从简单的链接预测成功地推广到了复杂的查询任务,这是在一个新的GNN 查询执行器(GNN-QE)模型中,其中一个基于 NBF-Net 的 GNN 进行了关系投影,而其他逻辑运算通过模糊逻辑t-范数来实现。 然后,在知识图中的归纳逻辑查询回答中,我们研究了⚗️ 归纳性的本质 ⚗️并提出了两种解决方案,以在推理时回答关于未知实体的逻辑查询,即通过(1)与仅用于推理的解码器配对的归纳节点表示(性能较差但可扩展),或通过(2)类似于 GNN-QE 中的归纳关系结构表示(质量更高但需要更多资源并且难以扩展)。 总的来说,我们能够在推理时处理拥有数百万节点和 500k 个未见节点以及 5m 未见边缘的图中进行归纳查询设置。

图片

透过节点表示(NodePiece-QE)和关系结构表示(GNN-QE)进行归纳逻辑查询回答。 来源:Galkin 等人

这个领域中的另一项重要工作是SMORE来自任、戴等人 — 这是一个大规模(仅限传导)的系统,可对大约 90M 个节点和 300M 条边缘的完整 Freebase 进行复杂查询回答👀。 除了 CUDA、训练和管道优化,SMORE 还实现了一个双向查询采样器,使得训练查询可以在数据加载器中即时生成,而无需创建和存储大量数据集。 不要忘记从 LOG 2022 中查看关于大规模图推理的全新实践教程!

最后但并非最不重要,杨、林和张提出了一篇有趣的论文,重新思考了知识图完成的评估。 他们指出知识图倾向于是开放世界的(即,有些事实并未被知识图编码),而不是大部分作品所假设的闭世界。 因此,在闭世界假设下观察到的指标对真实指标呈现出了对数趋势——这意味着如果你的模型得到了 0.4 的 MRR,那么测试知识图很可能是不完整的,而你的模型已经做得相当不错👍。也许我们可以设计一些新的数据集和评估来缓解这个问题?

2023 年的预期:一个可以完全迁移到不同知识图谱的新集合的归纳模型,例如在 Wikidata 上进行训练,并在 DBpedia 或 Freebase 上运行推理。

算法推理与对齐

2022 年是算法推理领域取得重大突破和里程碑的一年。

1️⃣ 首先,CLRS 基准测试Veličković 等人 提供,现在可以作为设计和评估算法推理模型和任务的主要平台。CLRS 已经包括了 30 个任务(如经典排序算法、字符串算法和图算法),但仍然允许你带入自己的公式或修改现有公式。

2️⃣ 然后,Ibarz 等人 和 DeepMind 的通用神经算法学习器表明,可以训练一个单一的处理器网络,在不同算法上以多任务模式进行训练——以前,你需要为每个 CLRS 问题训练一个单独的模型。论文还描述了模型架构和训练过程中的若干修改和技巧,以使模型更好地进行泛化并防止遗忘,例如,类似于三角注意力的三元组推理(在分子模型中常见)和 边变换器。总体而言,新模型带来了比基线高出 25% 的绝对增益,并且以 60%+ 的微观 F1 分数解决了 30 个 CLRS 任务中的 24 个。

来源:Ibarz 等人

3️⃣ 去年,我们讨论了关于算法对齐的工作,并看到 GNNs 可能与动态规划良好对齐的迹象。在 2022 年,Dudzik 和 Veličković 通过范畴论、抽象代数以及推前拉回操作的概念证明了GNNs 是动态规划器。这是应用范畴论的一个绝妙例子,许多人认为这是“抽象无聊”的😉。范畴论可能会在 GNN 理论和图 ML 中产生更大的影响,因此可以查看新的课程 Cats4AI 以获得对该领域的温和介绍。

4️⃣ 最后,Beurer-Kellner 等人 的工作是神经算法推理框架的首批实际应用之一,这里将其应用于配置计算机网络,即互联网核心的路由协议如 BGP。在这项工作中,作者展示了将路由配置表示为图形,可以将路由问题框架化为节点属性预测。这种方法相比传统的基于规则的路由方法带来了惊人的👀 490x 👀加速,并且仍然保持 90%以上的规范一致性。

来源:Beurer-Kellner 等

如果你想更深入地了解算法推理,不要错过 Petar Veličković、Andreea Deac 和 Andrew Dudzik 最新的 LoG 2022 教程

2023 年的展望: 1️⃣ 算法推理任务可能会扩展到成千上万个节点的图,以及代码分析或数据库等实际应用, 2️⃣ 更多的基准算法, 3️⃣ 最不可能——会出现一个能够解决 quickselect 的模型 😅

酷炫的 GNN 应用

👃 使用 GNN 学习嗅觉。 早在 2019 年,Google AI 开始了一个关于嗅觉表示学习的 项目。从基础化学知识我们知道,芳香性取决于分子结构,例如环状化合物。事实上,整个“芳香烃”组被命名为 芳香的 是因为它们实际上有一些气味(与许多无机分子相比)。如果我们有一个分子结构,我们可以在其基础上使用 GNN 来学习一些表示!

最近,Google AI 发布了 一篇新博客文章Qian 等人 的论文,描述了项目的下一阶段——主要气味地图,能够将分子分组为“气味簇”。作者进行了三项有趣的实验:对 400 种之前从未闻过的新分子进行分类,并与一组人类评审员的平均评分进行比较;将气味质量与基础生物学关联;以及探测芳香分子的驱蚊特性。基于 GNN 的模型表现非常出色——现在我们可以自信地说 GNN 可以嗅觉!期待 GNN 在香水行业的变革。

气味的嵌入。来源:Google AI 博客

GNNs + 足球。 如果你认为用于建模轨迹的复杂 GNN 仅用于分子动力学和深奥的量子模拟,那就不要担心!这里有一个非常有潜力的实际应用:Graph ImputerOmidshafiei 等 提出的,DeepMind 和利物浦足球俱乐部预测足球运动员(以及足球)的轨迹。每场比赛图包含 23 个节点,通过标准消息传递编码器和特殊的时间依赖 LSTM 进行更新。数据集也非常新颖——它包含了 105 场英超比赛(每场比赛平均 90 分钟),所有球员和足球的运动被以 25 帧每秒的速度跟踪,得到的训练轨迹序列编码了大约 9.6 秒的比赛过程。

这篇论文易于阅读,并有大量的足球插图,快去看看吧!体育技术在这些年里迅速发展,足球分析师现在可以更深入地研究他们的对手。英超俱乐部会在即将到来的转会窗口中竞争 GNN 研究人员吗?是时候为 GNN 研究人员创建一个 transfermarkt 了😉

足球比赛模拟就像分子动力学模拟一样!来源:DeepMind

🪐 银河系与天体物理学。 对天体物理学爱好者而言:MangroveJespersen 等 提出的应用 GraphSAGE 于暗物质的合并树,以预测各种银河属性,如恒星质量、冷气体质量、星形成率,甚至黑洞质量。尽管这篇论文在天体物理学术语上有些复杂,但在 GNN 参数化和训练方面相对简单。Mangrove 的速度比标准模型快 4 到 9 个数量级。实验图表就像艺术品一样,可以挂在墙上 🖼️。

Mangrove 将暗物质晕呈现为合并树和图。来源:Jespersen 等

🤖 GNNs 用于代码。像 AlphaCode 和 Codex 这样的代码生成模型具有令人惊叹的能力。虽然 LLMs 是这些模型的核心,但 GNNs 在几个巧妙的方面确实有帮助:指令指针注意力 GNNs(IPA-GNNs)首次由Bieber et al提出,用于预测运行时错误在竞赛编程任务中——这几乎就像一个虚拟的代码解释器!由Pashakhanloo et al.提出的CodeTrek建议将程序建模为关系图,并通过随机游走和 Transformer 编码器进行嵌入。下游应用包括变量误用、预测异常和预测被遮蔽的变量。

来源:Pashakhanloo et al.

硬件:IPUs 和 Graphcore 赢得 OGB 大规模挑战赛 2022

🥇 2022 年为GraphcoreIPUs带来了巨大的成功——这些硬件专门优化了处理图形时非常需要的稀疏操作。第一个成功故事是优化了 IPUs 上的 Temporal Graph Nets (TGN),取得了巨大的性能提升(查看 Michael Bronstein 博客中的文章)。

加速和扩展在 Graphcore IPU 上的 Temporal Graph Networks

GPU 是 GNNs 的最佳硬件选择吗?与 Graphcore 一起,我们探讨了新 IPU 的优势……

towardsdatascience.com

此后,Graphcore 在 OGB LSC’22 的排行榜上大放异彩,在 3 个赛道中赢得了 2 个:WikiKG90M v2知识图谱上的链接预测和PCQM4M v2分子数据集上的图回归。除了强大的计算能力外,作者还做出了一些巧妙的模型决策:对于链接预测,采用了平衡实体采样和共享 (BESS)来训练一个浅层 LP 模型的集合(查看 Daniel Justus 的博客文章了解更多细节),对于图回归任务采用了 GPS++(我们在 GT 部分中讨论了 GPS++)。你可以尝试在 Paperspace 上使用 IPUs 支持的虚拟机来使用预训练模型。祝贺 Graphcore 及其团队!👏

PyG 与 NVIDIA (post) 和 Intel (post) 合作,分别提高了 GPU 和 CPU 上核心操作的性能。类似地,DGL 纳入 了最近 0.9 版本中的新 GPU 优化。对于稀疏矩阵乘法和采样程序都有大幅提升,因此我们建议你更新到最新版本的环境!

2023 年的期望: 主要 GNN 库可能会扩展对硬件后端的支持,如 IPU 或即将推出的 Intel Max 系列 GPU。

新会议:学习图形(LoG)和分子 ML(MoML)

今年我们见证了两个图形和几何 ML 会议的启动:学习图形会议(LoG)分子 ML 会议(MoML)。

LoG 是一个更通用的全方位 GraphML 会议(今年以虚拟形式举行),而 MoML(在 MIT 举办)具有更广泛的使命和对 AI4Science 社区的影响,其中图和几何仍然扮演着重要角色。这两个会议都得到了极好的反响。MoML 吸引了 7 位顶级讲者和 38 个海报,LoG 有大约 3000 个注册、266 个投稿、71 个海报、12 个口头报告和 7 个精彩的教程(所有口头报告和教程的录音已在 YouTube 上)。此外,LoG 为评审引入了丰厚的奖金激励,显著提高了评审质量!在我们看来,LoG 的评审质量通常优于 NeurIPS 或 ICML 的评审。

这是图形 ML 社区的一次巨大胜利和庆典,祝贺所有在图形和几何机器学习领域工作的人员,拥有了一个新的“家”会议!

2023 年的期望: LOG 和 MoML 将成为包括在提交日历中的主要 Graph ML 会议,此外还有 ICLR / NeurIPS / ICML。

课程和教育材料

新数据集、基准和挑战

  • OGB 大规模挑战 2022: 第二届大规模挑战,在 NeurIPS2022 上举办,涵盖节点、边、图级预测的大规模和现实图机器学习任务。

  • 开放催化剂 2022 挑战: 第二届挑战,在 NeurIPS2022 上举办,任务是设计新的机器学习模型,以预测催化剂模拟的结果,以了解其活性。

  • CASP 15: 由 AlphaFold 在 CASP 14 中引发的蛋白质结构预测挑战。详细分析尚待公布,但似乎 MSAs 卷土重来,表现最好的模型仍然依赖于 MSAs。

  • 长距离图基准: 用于测量 GNNs 和 GTs 在图中捕捉长距离交互的能力。

  • 图基准分类图学习索引器: 对图机器学习数据集景观的深入研究,概述了基准测试和结果可信度中的开放挑战。

  • GraphWorld: 一个框架,用于分析 GNN 架构在数百万个合成基准数据集上的表现。

  • Chartalist — 一系列区块链图数据集。

  • PEER 蛋白质学习基准: 一个多任务蛋白质序列理解基准,包括 17 个蛋白质理解任务,分为 5 个任务类别。

  • ESM 宏基因组图谱: 一个综合性数据库,包含超过 6 亿个预测蛋白质结构,提供了漂亮的可视化和搜索界面。

软件库和开源

  • 主流图机器学习库:PyG 2.2(PyTorch),DGL 0.9(PyTorch、TensorFlow、MXNet),TF GNN(TensorFlow)和 Jraph(Jax)

  • TorchDrugTorchProtein: 用于药物发现和蛋白质科学的机器学习库。

  • PyKEEN: 用于训练和评估知识图谱嵌入的最佳平台。

  • Graphein: 提供多种基于图的蛋白质表示的包。

  • GRAPEMarius: 可扩展的图处理和嵌入库,处理超大规模图。

  • MatSci ML 工具包: 用于在 opencatalyst 数据集上进行深度学习的灵活框架。

  • E3nn:用于 E(3) 等变神经网络的首选库

加入社区

2022 年的流行文化现象 🪓

由 Michael Galkin 和 Michael Bronstein 创建

比利·科根网络图谱:使用 Python 的 NetworkX 库分析和绘制社会关系——第四部分

原文:towardsdatascience.com/graphing-billy-corgans-network-analyzing-and-mapping-social-relationships-with-python-s-networkx-724de1e431ac

继续学习如何使用 NetworkX 和 Python 进行社会网络分析

Christine EganTowards Data Science Christine Egan

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

--

在我们对比利·科根影响力领域的调查开始时,我们介绍了社会网络分析以及像节点和边这样的基本概念。在第二部分中,我们通过绘制 Smashing PumpkinsZwan 乐队成员之间的关系,扩展了对社会网络分析的理解。接着,我们考察了度数中心性和中介中心性等指标,以调查不同乐队成员之间的关系。同时,我们讨论了领域知识如何帮助我们理解结果。

第三部分中,我们介绍了第三种中心性度量——接近中心性。我们还开始讨论社区和子群体的概念,并展示了不同的社区图以及我们如何利用接近中心性来指导我们的解释。通过使用曾经是 Zwan 和 Smashing Pumpkins 乐队成员的音乐网络,我们对成员之间的关系做出了推测。

这一次,我们将通过扩展网络和添加更多乐队使结果更有趣。同时,我们将扩展对中心性度量和社区概念的理解,同时提升 Matplotlib 技能,使你的 NetworkX 图表更加引人入胜。

Tool — By Lugnuts — 自制作品, CC BY-SA 4.0

向网络中添加复杂性

在之前的内容中,我们涵盖了社交网络分析中的三个基本指标:度中心性中介中心性接近中心性。我们还讨论了社区的概念,并描述了如何将这一框架应用于理解构成 Billy Corgan 网络的社区/乐队之间的网络动态。

还记得上次的内容吗?这是我们用 Matplotlib 创建的 NetworkX 图。

尽管即使是小型音乐团体也能展示有趣的网络动态,但网络的复杂性不足使我们的结果变得不那么有趣,也没有提供超出常识的见解。这一次,我们将向网络中添加更多的乐队和音乐人,以观察社区的出现以及音乐人之间的连接如何影响网络动态。

另一种影响范围

另一个在 1990 年代出现的受欢迎的摇滚乐队是Tool——以其独特和前卫的摇滚音乐风格闻名。Tool 常常结合另类金属、前卫摇滚和艺术摇滚元素,形成了独特而实验性的声音。他们的歌曲常常融入隐喻和寓言式的叙事,留给听众解读的空间。

从数据科学的角度来看,Tool 是一个有趣的选择,因为他们在音乐中以各种方式融入了数学主题。虽然他们没有明确使用数学公式或方程(换句话说——我们不能完全称他们为数学摇滚乐队),但他们经常使用复杂的拍号和节奏模式,这些可以被视为受数学启发的。Tool 音乐中更著名的例子之一是歌曲Lateralus中的斐波那契数列和黄金比例。

Maynard James Keenan © Markus Felix | PushingPixels (联系我), CC BY-SA 4.0, 通过维基媒体公用领域

粉丝们会注意到,Tool 和 A Perfect Circle 的主唱 Maynard James Keenan 还有其他项目,我们将留到未来的内容中。但这些与 Billy Corgan 有什么关系呢?让我们用中心性度量来调查一下。

开始之前…

  1. 你对Python有基本了解吗?如果不熟悉, 从这里开始。*

  2. 怎么样 Pandas?如果不熟悉, 从这里开始

  3. 你对社会网络分析的基本概念*如节点和边缘是否熟悉?如果不熟悉, 从这里开始**。

  4. 你对社会网络分析的概念是否感到舒适 比如 度中心性 介数中心性?如果不熟悉, 从这里开始!怎么样 社区?如果不熟悉, 从这里开始

[## 使用我的推荐链接加入 Medium - Christine Egan

作为 Medium 会员,你的一部分会员费用将用于支持你阅读的作者,你将可以完全访问每一篇故事……

christineegan42.medium.com](https://christineegan42.medium.com/membership?source=post_page-----724de1e431ac--------------------------------)

使用 Python 和 NetworkX 计算中心性测量

在下面的代码单元中,我们将通过将 ToolA Perfect Circle 添加到我们的 1990 年代音乐家网络中来扩展我们的网络。然后,我们将使用中心性测量来分析网络。

1. 构建社区

在下面的代码单元中,我们导入 NetworkX 和 Matplotlib。就像我们在 第三部分 中所做的那样,我们使用 NetworkX 创建一个图对象,并列出每个乐队的所有成员。然后,我们将所有乐队合并,并将其分配给变量 communities

注意:当前和前成员都包括在成员列表中。

接下来,我们将使用 for 循环遍历乐队成员列表。然后,我们使用 .add_nodes_from() 将每个乐队成员添加到网络图中。

为了连接同一 community(即乐队)的成员,我们输入一个包含每个乐队成员的列表推导式,并消除任何重复(例如,(Billy Corgan, Billy Corgan))到 .add_edges_from() 中。

2. 计算中心性测量

数据准备好,网络图填充完成后,我们可以调用 NetworkX 中的方法来计算中心性测量并分析网络。结果将存储在 Pandas 数据框中。

输出应类似于下面的样子:

这比我们上一集的结果有趣得多,上集的结果如下:

在我们使用 Matplotlib 构建 NetworkX 图的可视化之前,让我们消化一下关于我们生成的表格的观察结果。为此,我们将描述每个中心性指标,然后根据我们的数据结果进行解释。

1. 节点介数中心性

(量化一个节点在信息或交互流动中作为桥梁或中介的频率。)

当我们的网络仅包含两个乐队(Smashing Pumpkins 和 Zwan)时,Billy Corgan 和 Jimmy Chamberlain 在节点介数中心性中排名最高。由于 Tool 和 A Perfect Circle 共享一个共同的主唱,我们看到 Maynard James Keenan 取代了一些介数,降低了 Billy 和 Jimmy 之间的介数。然而,新的最高介数中心性的乐队成员是 James Iha。这表明 James Iha 是连接这些子组的节点。利用领域知识,这一判断是正确的。作为 Smashing Pumpkins 和 A Perfect Circle 的成员,他是 Billy Corgan 的乐队成员和 Maynard James Keenan 的乐队成员之间的共同联系。

2. 度中心性

(衡量一个节点(个体)在网络中具有的直接连接(边)的数量。)

James Iha 拥有最高的度中心性分数(0.782609),这表明他与网络中其他个体有着最多的直接连接。这是因为与 Zwan 和 Tool 不同,Smashing Pumpkins 和 A Perfect Circle 有一个轮换的成员阵容,所有这些成员都与 James Iha 有联系。

James Iha by Tiffany Bauer, CC BY-SA 3.0, via Wikimedia Commons

3. 接近中心性

接近中心性衡量一个节点与网络中所有其他节点的接近程度,考虑了最短路径。在这种情况下,James Iha 拥有最高的接近中心性分数(0.821429),这表明他能够迅速有效地接触到网络中的其他个体。

这些中心性指标提供了一种识别关键个体的方法,这些个体具有重要的联系,充当桥梁,并能够有效地接触到网络中的其他个体。

现在我们已经计算了中心性指标,我们可以使用 Matplotlib 创建一个 NetworkX 图。这一次,我们将通过添加颜色、标签、标题等,使图表更加引人入胜。

使用 Matplotlib 可视化 NetworkX 图

我们在上一节中创建的表格提供了有关网络中有影响力的音乐家的重要信息。为了使这些结果更直观,我们可以使用 Matplotlib 和 NetworkX 创建图形的可视化。在之前的章节中,我们保持了图形的相对简单。这一次,我们将使用一些高级功能来使我们的可视化特别吸引人。

提醒一下,这就是我们之前网络图的样子:

来自 Closeness 和 Communities 的 Smashing Pumpkins 和 Zwan 的 NetworkX 图

1. 使用 Matplotlib 添加颜色

如果你查看 NetworkX 文档 中的 .draw_networkx_nodes() 方法,你会注意到 node_color 参数接受 colorarray of colors。在我们的情况下,我们想使用 array of colors 以便可以根据成员所在的乐队为每个节点分配颜色。

在进入这些细节之前,让我们选择一些颜色。 🎨

在使用 Matplotlib 创建图表时,你可以使用 命名颜色或十六进制代码。有关颜色选择和可用的多种技术的更多信息,请查看 文档(他们甚至提供了代码,以便你可以自己生成这个图表 🤩)。

用于 Matplotlib 的命名颜色

在我们的案例中,目标是创建一个乐队名称的字典,这些乐队名称映射到我们选择的颜色。然后,我们希望遍历节点,并将该节点的选定颜色添加到列表中。由于我们想强调社区之间的连接,我们将为出现在多个乐队中的成员选择一种特殊颜色。

为了实现这一点,我们使用 for-loop 迭代网络中的每个节点。然后,我们使用一系列 if-else 语句将特殊颜色分配给在 flat_communities 中出现多次的成员。如果该成员的节点仅出现一次,则根据成员所属的乐队分配颜色。

使用上面的代码块,我们生成了二十四种颜色的列表。这直接映射到网络中的二十四个节点列表。

这个输出将作为 .node_colors() 的输入,当我们调用 .draw_networkx_nodes() 开始填充我们图形的可视化时使用。

2. 绘制图形和计算节点位置

为了开始绘制可视化图,我们调用 Matplotlib 的 .figure() 并将 figsize 设置为 20 x 12,为我们的网络图提供空间。

然后,我们使用 NetworkX 的 .spring_layout() 来计算网络中每个节点的位置。这将使我们能够在可视化中绘制节点。

3. 绘制节点和边

接下来,我们将使用 .draw_networkx_nodes().draw_networkx_edges() 来绘制每个节点及其连接。让我们讨论每个参数的意义。

.draw_networkx_nodes()

  • G:NetworkX 图对象(G

  • pos:我们上面生成的每个节点的位置(pos

  • node_color:我们生成的颜色列表(node_colors

  • node_size:每个节点应该有多大?

  • alpha:每个节点的透明度应该是多少?

.draw_networkx_edges()

  • G:NetworkX 图对象(G

  • pos:我们上面生成的每个节点的位置(pos

  • edge_color:边的颜色应该是什么?

  • alpha:每条边的透明度应该是多少?

在 Python 中,它的样子是这样的

4. 为 NetworkX 图添加标签

要添加标签,我们可以使用 NetworkX 方法 .draw_networkx_labels

让我们逐一了解每个参数的意义。我们将跳过最近回顾过的参数,如 Gpos。然后,我会向你展示如何将它们结合起来。

  1. font_size:此参数设置节点标签的字体大小。它指定了代表节点标签的文本应该有多大或多小。

  2. font_color:此参数设置节点标签的颜色。你可以使用各种格式来指定颜色,如命名颜色(‘red’,‘blue’ 等)或十六进制值(‘#RRGGBB’)。

  3. verticalalignment:此参数控制节点标签相对于节点的垂直对齐。例如,将其设置为 ’top’ 会将标签对齐到节点的上方。

  4. bbox:此参数是一个字典,包含节点标签的边界框(bbox)的属性。它通常用于定义一个背景框或一个围绕标签的矩形,以提高可见性和可读性。

  5. labels:这是一个可选参数,允许你指定一个包含自定义节点标签的字典。字典的键应为节点 ID(通常为整数),值应为你想要分配给每个节点的标签。如果未提供此参数,将使用默认的节点标签(通常为节点 ID)。

这段代码将会是这样的:

5. 将图例添加到 Matplotlib 可视化中

最后,我们想要添加一个图例来为观众提供上下文。为此,我们需要创建三个列表(legend labelslegend colorslegend markers),然后将这些列表作为参数传递给 plt.legend()。我们还将使用可选参数 locfontsizeframeonborderpadborderaxespad 来美化图例。

让我们回顾一下我们选择的每个可选参数的目的。

  1. loc:图例应该位于屏幕的哪个位置?

  2. fontsize:确定图例的文字大小。

  3. frameon:在图例周围添加边框。

  4. borderpad:在边框和文本之间添加一点填充空间。

  5. borderaxespad: 图例边框和坐标轴之间的间距,单位为字体大小。

这些只是你可以用来美化 matplotlib 图例的一些可选参数。查看文档以回顾所有参数,以及你可以提供的可能值。

对于 legend labels,我们只需提供每个乐队的名称。对于 legend_colors,我们引用我们创建的字典 color_palette,并使用键值来定义每个乐队的颜色。接下来,legend_markers 将标记位置排列在一条直线上,并定义它们的大小和形状。

最后,我们用 plt.legend() 将所有内容整合在一起。这是它的样子:

6. 添加最后润色

为了给我们的 NetworkX 图添加一个总结标题,我们可以通过调用 plt.title() 来添加标题,然后使用 fontsizefontweightpad 来美化标题。

通过使用 off 参数与 plt.axis(),我们可以去除网络图的边框,从而实现更均匀的间距和可读性。

调用 plt.tight_layout() 自动调整图形元素的位置,以获得更好的间距和可读性。

最后,我们只需调用 plt.show() 来显示我们的可视化!

这是最终产品:

使用 Matplotlib 创建的 NetworkX 图“比利·科根的音乐人网络”

解读 NetworkX 图

在本教程中,我们使用比利·科根的网络来演示社会网络分析的基础概念,通过构建第一部分、第二部分和第三部分中的网络。这一次,我们通过添加两支额外的乐队 Tool 和 A Perfect Circle,使网络变得更加复杂。这使我们能够观察到网络中额外个体(节点)的影响,以及这对中心性度量的影响。

我们发现詹姆斯·伊哈是比利·科根(Smashing Pumpkins 和 Zwan)关联的音乐人社区与梅纳德·詹姆斯·基南(Tool 和 A Perfect Circle)之间的重要中介。这个结果很有趣,因为它表明主唱不一定是大网络中最有影响力的参与者。如果我们考虑如何在大规模网络中使用这种技术,我们可以欣赏 NetworkX 图的实用性,以及它们如何帮助我们快速识别网络中的重要节点。然后,我们可以使用度中心性、中介中心性和接近中心性等指标来突出我们的发现中的细微差别。最后,我们结合领域知识对结果进行解释。

所以,如果你在另类音乐方面是专家,你对这些结果怎么看?如果你在添加了 Tool 和 A Perfect Circle 之后发现网络中有任何有趣的变化,请务必在回复 💬中提到!

如果您想要本教程的 注释笔记本 ,请访问我的 GitHub!给它一个 ⭐️ 以便于参考。 👩🏻‍💻 Christine Egan | medium | github | linkedin

[## 通过我的推荐链接加入 Medium - Christine Egan

作为 Medium 的会员,您的一部分会员费用会分配给您阅读的作者,并且您可以完全访问每个故事……

christineegan42.medium.com

数据科学软技能

原文:towardsdatascience.com/great-applied-data-science-work-5739daf13dd3?source=collection_archive---------4-----------------------#2023-08-23

什么帮助解决从业务需求到结果展示的实际问题

Lars RoemheldTowards Data Science Lars Roemheld

·

关注 发表在 Towards Data Science ·14 min read·2023 年 8 月 23 日

--

行业内的高级数据科学工作有时也被称为“应用科学”,反映了它不仅仅是关于数据的现实,以及许多前学者在该领域工作的事实。我发现“应用科学”有着不同于研究科学的期望。因此,我写下了在我看来有助于产生出色应用科学工作的内容。我将其作为数据科学工作的“完成定义”,但许多要点也将对分析师、工程师和其他技术角色有所帮助。

目标:什么是成功的应用科学工作?

伟大的应用科学家通过巧妙地利用数据和模型,解决有价值的实际问题,从头到尾。有时,第一步是发现最具影响力的商业问题,这些问题可能提供可行的科学解决方案;有时,商业问题已经被很好地理解,科学工作则从制定明确的技术问题陈述开始。

无论哪种情况,成功的科学工作都始于理解实际问题。科学家需要充分理解复杂的商业挑战,以便将其转化为可以在有限时间内解决的技术公式。他们穿透模糊性,创建适当的结构假设以实现解决方案。

成功的科学工作能够找到技术上合适且务实的解决方案:这可能意味着最先进的深度学习,但非常高级的科学工作也可能由一些巧妙的 SQL 查询组成。优秀的科学家知道如何为任务选择合适的工具。

伟大的科学家理解,陷入糟糕的技术方法很容易。为了避免这种情况,他们将工作分阶段进行:他们能够将大型问题拆解为较小的子问题;通过生成中间结果验证单个方法,并积极从同行那里获取反馈。

优秀的科学工作会纳入反馈,因为良好的归纳偏见能显著加快学习过程。但优秀的科学家也知道如何识别经验性问题,并坚持使用数据来回答这些问题。

伟大的科学工作意味着记录再现解决方案所需的步骤,并以适合观众的方式呈现结果。同时,还要确保结果被使用——无论是软件的变化、战略决策,还是发表的论文。因为只有这样,宝贵的实际问题才能从头到尾得到解决。

总结:4 个原则

以下四个原则支撑了这些关于行业应用科学工作的建议:

  1. 所有权:我们的工作是解决模糊的问题,从头到尾。

  2. 高效的好奇心:我们喜欢学习。理想情况下,比通过盲目实验更高效。

  3. 三思而后行:在探索性工作中,明确的规划可以防止迷失方向。

  4. 迭代结果:频繁的反馈减少了模糊性。

路径:什么使科学工作更成功?

以下章节提供了根据我的经验能够改善科学工作的具体建议,这些建议是按照科学过程结构化的。

1. 应用科学的角色

应用科学的工作本质上是社会性的。要产出出色的成果,应用科学家需要在团队中表现出色。

与人合作

很多科学工作需要与他人协作;理解之前的工作,寻找相关的数据集,要求解释,将进展传达给你的利益相关者,说服你的团队成员支持你的项目。最终你负责交付结果——管理协作是工作的一部分。这可能意味着你需要说服团队成员审查你的 Pull Request,也可能意味着你需要找到将数据工程需求纳入另一个团队待办事项中的方法。当这个过程陷入困境时,你可以升级问题,领导可以帮助明确优先级——但你有责任完成这一工作。

跟进

作为科学工作的一部分,团队经常在小组讨论或一对一的情况下头脑风暴。这些会议可以非常有价值,帮助产生比任何一个人单独完成的更高质量的工作。但如果头脑风暴会议没有直接与后续工作相关联,它们通常是一种浪费时间。你需要避免后者——这意味着要主动跟进讨论的想法和任务:如果团队达成了一致,认为某事应该完成,就去做(并沟通结果)。如果立即做完太多,可以为这些想法写一个工单(并沟通工单)。在头脑风暴会议中,主动询问具体的收获和谁负责下一步是非常有帮助的——任何人都可以这样做,并与团队分享他们的笔记。

透明度和信任

并非一切都按照计划进行,科学工作的性质决定了大多数实验会失败。计划没有如预期那样顺利进行是可以预期的。这使得科学工作成为一种高变异性的活动:即使你可以准确预测你工作的“期望值”,意外也是很可能发生的。

当事情没有按计划进行时,要完全透明。最重要的是,当发现问题时立即通知你的团队。这可以更高效地协调路线图和交付物。作为回报,期待信任:失败的实验是我们日常工作的组成部分。

最令人烦恼的实验结果是“结论不明确”:实验并没有完全失败,但也没有成功。这些也是科学工作的一部分,它们也值得展示和分享:我们可以假设为什么科学问题无法解决吗?如果我们从头再来,我们会做得不同吗?

在团队合作时,自然不会理解演示、对话或任务中的所有内容。然而,“随声附和”是有问题的——当其他人假设你理解了你其实没理解的内容时,这很可能导致误解和工作结果的不一致。作为科学家,“为什么”应该是我们最喜欢的问题:当某些事情不清楚时,继续提问直到感到有了共同理解。如果有疑问,用自己的话重新表述某些内容是检查你是否真正理解的有效工具。

2. 了解商业问题

正确理解商业问题往往比纯粹的研究科学更“复杂”:概念定义不明确,量度不可测量,利益相关者的目标不一致。然而,误解待解决的问题会导致失望,减少应用科学工作的价值——没有科学的复杂性可以挽救这种情况。

继续提问直到你完全理解商业问题。在某些情况下,你的问题可能会导致商业问题表述的细化甚至转变。

在将商业问题用技术术语表述时,我们通常需要做出一些基本假设:我们需要选择某一概念的具体定义,我们忽略边缘情况,我们需要决定哪些潜在副作用超出范围。记录这些假设,以便后续回顾。

由于做出假设,你可能会解决错误的问题,因为一个合理的假设证明是不准确的。预防这种情况的最有效方法是渐进工作,并频繁验证增量是否朝着正确方向发展。极端情况下,构建一个模拟解决方案(例如一个快速的电子表格)通常可以帮助生成有价值的商业反馈。

在开始深入建模工作之前,仔细检查你的方法是否能解决正确的商业问题。但不要忘记在过程中反复验证这一点。如果有疑问,选择频繁且较小的迭代。

3. 研究路线图是假设树

一旦开始深入数据和模型,就容易迷失方向。写下清晰的研究路线图可以帮助避免这种情况。经验丰富的科学家通常会有意识或无意识地遵循明确的假设结构,将模糊问题分解为顺序可解的子问题。我建议在编写第一行代码之前,先写一个完整的假设结构草稿,并获取反馈。

重要的是你要写下一些形式的计划,并在进展过程中在其中定位自己。我发现的一个有用格式是思维导图或以假设树结构明确规定的项目符号。这是创建一个的粗略“算法”:

  1. 对你的问题进行一些不同的思考;不要忘记查找来自其他团队的现有解决方案、开源项目和已发表的材料。将它们记录为“候选方案”。这里的想法是广度优先:收集许多粗略的想法。

  2. 粗略估计“验证”某种方法所需的工作量:这不是解决问题所需的工作量,而是判断某种方法是否很可能有效的工作量。

  3. 按照估计的验证工作量对你的方法进行排序。

  4. 从最低验证工作量的方法开始,头脑风暴如何验证这种方法,并最终使用它来解决你的(子)问题。递归地继续你的树(即对于每个子问题,从 1 重新开始)。根据需要对树的各个层次重复这一过程。

使用这种方法,你最终理想的结果是一个结构良好、优先级排序的计划,明确首先尝试什么,成功后怎么做(沿着那一分支),以及失败后怎么做(继续在当前分支下的下一个分支上工作)。你树的“叶子”理想情况下应该是相对容易测试、可以通过数据回答的问题。你的计划结构还应该使描述进展和获取中期结果反馈变得更加容易。

增量工作受益于出色的直觉。

最难捉摸的建议:伟大的科学家对哪些方法可能有效、哪些方法不值得进一步考虑具有非凡的直觉。这有时会给人一种特定的人“总是能让一切奏效”的印象——更准确的说法是,这些人知道什么不要尝试,他们大多数时间都花在了富有成效的想法上。当然,建立这种水平的直觉是困难的,是一个终身的事业。好的直觉意味着你将大多数时间花在富有成效的假设上——这很重要,因为潜在的假设和想法的宇宙是巨大的,直觉减少了你假设树中的搜索空间。

直觉是社会性的。

当你找到一个你信任其直觉的人时,请向他们询问该跟随哪些方法。请他们为该建议提供理由。尽量理解他们如何思考问题-解决方案的映射,超越直接的技术问题。

构建直觉特别受益于互动学习:考虑与同事进行配对编程的日子,并互相解释概念。尽量面对面交流,而不仅仅是远程交流:至少我尚未找到能完全替代白板或几张白板以及同处一室的替代品。

扎实的基础有帮助。

投资于理解基础知识:你应该建立事物运作的心理模型。这些模型需要“足够正确”,但又足够简单,以便应用于现实世界情况。你应该能够在架构层面进行“黑箱思维”,并在处理细节时理解黑箱的内部工作。为了更具体地说明:在处理图像或文本数据时,使用“嵌入”的想法是一个很好的直觉,可以帮助你快速建立潜在模型的心理架构。但要准确判断这些方法的可行性,你需要完全理解嵌入是如何训练的,以及结果信息的编码方式。

好奇心

对类似但不同的问题保持好奇。考虑它们相似的地方和不同的地方。思考你问题的解决方案如何适用于这些类似的问题。一些例子:可替代产品的实验与社交网络的实验(“溢出效应”)相关。时尚定价与航空公司定价(“易腐烂商品”)相关。产品/实体匹配与音乐版权执法(“粗略+精确匹配步骤”)相关。

反思你之前的工作:当你不得不尝试某些东西时,因为你没有强烈的直觉,你可以从实验中学到什么?是否有从实验中学到的普遍真理可以帮助你提高直觉?

积极寻求对你方法的批评:无论是作为“研究路线图审查”的一部分,还是作为你对完成项目的反思,异议意见可以帮助你磨练直觉和发现盲点。

5. 用干净的代码解决问题

在应用于探索/实验性工作时,干净的代码是一项特殊的挑战,这在应用科学中非常典型。但它同样重要:干净的代码可以避免错误,部分是因为它强制进行清理,部分是因为阅读你代码的人更容易发现错误,部分是因为它使你在第一次实验不可避免地失败时更容易迭代思想。变量名称比大多数大学课程所建议的要重要得多。函数、类和模块的封装可以帮助处理不同层次的细节和抽象。

过早的“生产化”可能会拖慢进度:在解决方案明确之前,应该容易替换你方法中的部分。

编写时考虑读者

当你编写分析笔记本时,要为读者编写,而不仅仅是为了自己。解释你在做什么。使用好的变量名称。清理图表。标记单元格是发明有原因的

思考DRY 代码。当你发现自己从以前的调查中复制/粘贴代码时,这通常是一个重新构造的好时机。

当进行探索性工作时,考虑到读者的需求,可以像处理其他代码一样将其作为 Pull Request 进行审查。实际上,最终分析答案所需的所有步骤都应该经过第二双眼的审查。请为你的审阅者提供方便,在提交审查之前,移除(或明确标记)纯粹的探索性代码。

文档

组织和更新中央知识库是我所知技术组织中最普遍的问题之一。我不知道简单的解决方案。但我知道,投资于良好的文档从长远来看是值得的。对于中央知识,应该有一个(而且只有一个!)中央来源。这个文档应该是真相的来源:如果代码与文档不符,代码是错的(而不是文档)。这需要频繁且便捷地更新文档:写得差,但正确且完整的文档远胜于写得好但过时的文档。投资文档是值得推广的工作,我相信它的影响。

6. 展示,而不仅仅是展示,结果

演讲是一个大步后退,反思你工作在宏观层面上意义的机会。这对于最终结果的展示是正确的,但对于中期结果的展示可能更为适用。

每次你展示结果时,都要考虑观众的期望。对于你提出的每一点(每一张幻灯片;每一节文本),你应该回答观众心中的隐含“那又怎样?”不同的观众在这里会有不同的期望:高级业务领导可能对一个直接叙述最感兴趣,这个叙述捕捉了你的发现的精髓,并且可以轻松地与其他高级领导共享。你的经理可能最关心的是了解如何解决一个特定问题以及何时解决。一个同事可能最感兴趣的是他们可以从你的方法中学到什么,以便用于自己的工作。一个利益相关者或客户将想知道你的工作使他们能够做出哪些新的决策或采取哪些行动。

我发现许多科学家倾向于在展示中遵循他们的发现过程,从第一个实验开始。我强烈建议不要这样做,因为这通常会导致在你甚至还没到达有趣的部分时就失去观众的注意力:相反,从你试图回答的原始业务问题开始,以及你对原始问题的最佳答案开始。然后描述你的高层次方法,并解释为什么你认为你的答案是你能给出的最佳答案。预见你的展示会提出哪些问题,并准备好答案。最重要的是,回答你提出的每一点“那又如何?”的问题。

有趣的实验,如果最终未对你的答案产生贡献,应放在附录中——它们可能对深入讨论有帮助,但不需要在主要展示中出现。

推论:为展示准备材料常常感觉像是在忙碌工作。相反,我发现频繁制作可展示的中期结果有助于保持专注和思维清晰,因为你迫使自己退后几步。制作清晰的图表和展示用叙述对保持专注于最终目标以及得出明确结论极为有用;但优化幻灯片布局并非如此。因此,对于中期结果,形式服从功能。重要的是清楚传达关键信息。设计是否完美并不重要。例如,手绘图形并展示其照片是完全可以接受的。

清晰的可视化

所有图表应自解释,并传达明确的信息。我强烈建议即使是在你仅为自己创建的图表中,也要遵循一些基本原则。

  1. 用自解释的描述标记你的轴:使用单词,而不仅仅是字母。

  2. 使用清晰的图表标题,解释所展示的内容 主要信息(再一次,“那又如何?”)。

  3. 将展示的数据减少到必要的部分:例如,数据中可能包含一个明显不打算有用的“虚拟”类别。不要让这些在图表中占据视觉空间。

  4. 当展示多组仅通过颜色区分的数据时,请确保颜色图例清晰区分(如果考虑到色盲友好会更好)。

  5. 可视化有助于理解数据中的模式。如果一个图表仅展示了一个混乱的点云,它可能是多余的(除非你想证明某个特定点)。

  6. 对数尺度通常有助于清理(正)计数数据的图表。

清晰的数值结果

每当展示数值结果时(例如,在表格中):

  1. 针对并展示一个合适的成功指标进行优化。许多应用科学家在这方面花的时间太少:了解何时使用 RMSE/MAPE/MAE、对数尺度、F1 与 ROC 与精准度-召回曲线下面积的区别。

  2. 几乎所有现实世界的问题都涉及加权成功度量,然而大多数机器学习课程几乎不涉及这个话题:例如,销售预测的成功度量可能需要根据使用情况加权价格、库存价值或包装尺寸。

  3. 如果成功意味着估计反事实(“假如”分析),请明确表达,并找到一个明确的推理,说明你的成功度量如何捕捉这样的反事实。(自然)实验是一个流行的选择。

  4. 对于你呈现的任何数字,请提供合理的基准。通常,确定“正确”的基准需要深思熟虑,但这总是值得的。你拟合了一个复杂的机器学习模型?它比线性回归好多少?你建立了下周的预测?它比假设下周等于这周好多少?你正在呈现 A/B 测试结果?相对于我们的月收入,或者相对于上次改进,增长了多少?

清晰的语言

把想法表达出来可能比听起来更难,而且做得好对技术人员来说是一种真正的超能力。沟通风格也是主观和文化的,因此你需要找到适合自己的风格。无论如何,我建议你有意识地改进沟通技巧:注意他人的表达方式;定义你喜欢的方式;批评并让他人批评你现有的沟通;编辑你自己的写作;写出如何解释一个复杂想法以准备在演示中实际解释;录制自己解释一个想法并听听它;寻求风格反馈。我们一直在沟通:如果你愿意,就有大量练习的机会。

谢谢你阅读到这里!我很想听听你的反馈:什么让你产生共鸣?你的经验在哪里不同?还有什么其他帮助解决有价值的现实世界问题的方法?

资源

下面是我发现启发性的进一步阅读资源。大多数资源是“永恒”的,但有些技术资源可能在你阅读时已经过时。请在评论中分享你自己的推荐!

文化

  1. 丹·纳:“用乐观和善良克服摩擦,因为这是工作。”(30 分钟视频)

  2. Netflix 宣言:“我们是一支职业运动队,而不是一个家庭”,以及许多关于高绩效团队的想法

  3. 一个受咨询启发的关于“那么”和假设树的解释

  4. 更具问题解决能力的工程师的愿景(而不是“只做票中事项”)

  5. 亚马逊科学的一篇非常类似的博客文章

科学与工程:

  1. 对 ML、统计学、经济学相关观点的精彩总结 — 强烈推荐! Matt Taddy 的商业数据科学

  2. 独特的现代统计学复习:Statistical Rethinking 2022

  3. 为迭代工作辩护的一个想法,最初来自软件工程:“追踪子弹”(来自下面链接的《实用程序员》一书)

  4. Lisa Muth 的易懂的 数据可视化教程

  5. 现代 pandas(和/或直接阅读 Wes McKinney 的书

  6. 超现代 Python:数据科学与良好的工程实践结合

  7. 宇宙 Python:软件架构与开发流程

  8. 成为一名 实用程序员

沟通

  1. Dan Luu 的写作 — 从工程角度看有效的写作。

  2. Ciechanowski 的博客 — 极其美丽的技术写作和插图

图片由 NASA 提供,发布在 Unsplash

绿色 AI:改进 AI 可持续性的方法和解决方案

原文:towardsdatascience.com/green-ai-methods-and-solutions-to-improve-ai-sustainability-861d69dec658

对一个长期被忽视的话题进行技术性的审视

Federico PecciaTowards Data Science Federico Peccia

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

--

照片由 Benjamin Davies 提供,来源于 Unsplash

介绍

如果你打开了这篇文章,你可能已经听说过关于当前大型语言模型(LLMs)安全性和可信度的争议。由计算机科学界知名人物如史蒂夫·沃兹尼亚克加里·马库斯斯图尔特·拉塞尔签署的公开信表达了他们对这一问题的担忧,并要求暂停 LLMs 的训练 6 个月。但还有另一个话题正在慢慢引起大量关注,这可能会在不久的将来促使另一封公开信的出现:AI 模型训练和推理的能源消耗和碳足迹。

估计仅训练流行的 GPT-3 模型,这一 1750 亿参数的 LLM,便排放了大约 502 吨二氧化碳[1]。甚至有在线计算器可以估算训练某个模型的排放量。但训练步骤并不是唯一消耗能源的环节。训练后,在推理阶段,AI 模型每天会被执行数千次或数百万次。即便每次执行消耗的能源较少,经过数周、数月甚至数年的累计消耗,也可能成为一个巨大的问题。

这就是为什么绿色人工智能(Green AI)这一概念越来越受欢迎的原因。它的主要焦点是寻找解决方案和开发技术,以提高人工智能的可持续性,减少其能源消耗和碳足迹。在本文中,我旨在介绍一些正在积极研究的技术和方法,它们可以用来改进这一概念,并且这些内容通常不会以易于理解的方式讨论。本文末尾,你将找到与所讨论主题相关的资源和参考文献。

尽管本文重点讨论了在部署人工智能算法时实现节能的技术方法,但即使你不是研究人员,也需要对这些方法有一个大致了解。你是否负责训练公司人工智能算法?那么,也许你可以在训练过程中考虑一些优化,以改善算法部署后的能源消耗。你是否负责选择将要部署算法的硬件?那么请留意本文提到的概念,因为它们可能是前沿优化硬件的标志。

计算机架构基础

为了理解本文,基本了解计算机架构以及软件和硬件如何相互作用是至关重要的。这是一个非常复杂的话题,但在进入本文的主要部分之前,我将尝试提供一个简要的总结。

你可能听说过比特(bit),它是任何计算机中最简单的信息单位,也是数字世界存在的原因。比特只能取两种状态:0 或 1。8 个比特组成一个字节(byte)。为了便于讨论,我们可以将任何计算机架构视为 2 个硬件组件,它们操作和存储这些字节:计算单元和内存。

计算单元负责将一组字节作为输入,并生成另一组字节作为输出。例如,如果我们想要计算 7 x 6,我们会将表示 7 的字节插入到乘法器的一个输入端,将表示 6 的字节插入到另一个输入端。乘法器的输出将给我们表示数字 42 的字节,即乘法的结果。这种乘法操作需要一定的时间和能源,直到结果在乘法器的输出端可用。

内存是存储未来使用字节的地方。从内存中读取和写入字节(也称为“访问”内存)需要时间能量。在计算机架构中,内存层次结构通常有多个“层级”,接近计算单元的层级访问时间最快,每字节读取的能量消耗也较少,而离得较远的层级则是最慢且能量消耗最大的。内存的这种层次组织的主要思想是数据重用。使用非常频繁的数据从最后一级内存传输到最接近的一级,并尽可能多次重用。这一概念称为“缓存”,而这些更快且最接近的内存称为 L1 和 L2 缓存。

软件负责协调数据从内存到计算单元的移动,然后将结果存储到内存中。因此,软件的决策可以真正影响系统的能量消耗。例如,如果软件请求的数据在 L1 缓存中不可用,硬件首先需要从 L2 级别甚至从最后一级中获取数据,从而带来时间延迟和更多的能量消耗。

绿色人工智能的技术

现在计算机架构的基础已经建立,我们可以专注于绿色人工智能中使用的具体技术和方法。这些技术分为两个不同的类别:

  1. 硬件优化,例如电压/频率调整或近似计算。这些技术作用于电子电路的实际物理设计和特性。

  2. 软件优化,例如剪枝、量化、微调等。

DVFS:动态电压和频率调整

标准硅基电子电路的功耗直接与电路中使用的电压和工作频率相关。在相同的操作条件下,如果降低这些参数中的任何一个,功耗也会随之减少。我们是否可以利用这种行为使人工智能算法的执行更绿色

当然!设想我们有一个小型嵌入式设备连接到电池上,接收多个请求(每个请求有其自身的重要性和约束),用人工智能算法处理这些请求,然后将结果发送回去。我们希望人工智能算法的处理消耗尽可能少的能量,以便让电池尽可能长时间运行,对吧?我们是否可以在处理不那么重要的任务时动态调整设备的电压和工作频率,然后在需要处理关键任务时恢复到正常工作状态?

根据执行 AI 算法的设备,这确实是一个完全有效的选项!实际上,这是一个活跃的研究领域。如果你对进一步阅读感兴趣,我建议你查看 Kim [2] 的《AutoScale:使用强化学习进行随机边缘推理的能效优化》或 Hao [3] 的《通过 DNN 解耦的多智能体协作推理:中间特征压缩与边缘学习》,这些论文提供了如何利用该技术减少 AI 算法能耗的良好示例。

近似计算

在 CPU 或 GPU 上执行数学运算时,我们通常期待得到所请求计算的精确结果,对吧?这在使用消费级硬件时通常是这样的。由于乘法是 AI 算法中最常用的数学操作之一,我们希望在乘以两个整数时得到精确结果,而在乘以两个浮点数时得到一个非常好的近似(这个近似通常如此精确,以至于对基础用户程序没有问题)。我们为什么还要考虑插入两个整数而未获得正确的数学结果的可能性呢?

但在过去几年里,一种新的方法正在积极研究。问题很简单:是否可以通过牺牲乘法结果的准确性,设计出更简单的乘法器,从而减少物理面积和能量消耗?更重要的是,这些新乘法器是否可以在实际应用中使用,而不会显著影响性能?这两个问题的答案实际上都是肯定的。这就是被称为近似计算的计算范式。

这真是太吸引人了!目前已经有研究展示了一些近似乘法器,这些乘法器能够提供两个整数相乘的精确结果,且仅对少数输入组合提供不正确的结果,但能在执行整个模型时实现大约 20%的能量减少。如果你对这种令人难以置信的技术感兴趣,我鼓励你查看 Zervakis [4] 的《用于机器学习的近似计算:最新进展、挑战与展望》,这本书提供了关于这一主题的具体工作的良好概述。

剪枝与量化

对于熟悉 AI 算法训练,尤其是神经网络的人,这两种技术应该很熟悉。对于那些不熟悉这些术语的人,这些概念非常值得阅读。

剪枝是一种基于这样一个观点的方法:神经网络中的参数存在大量冗余,这些参数包含了网络的知识。因此,许多参数可以被移除,而不会实际影响网络的预测。

量化意味着使用更少的字节表示网络的参数。记得我们说过计算机使用一定数量的字节表示数字吗?通常,网络使用一种称为“浮点”的表示方式进行训练,其中每个数字可以占用 4 或 8 个字节。但实际上有技术可以只使用 1 个字节(“整数”表示法)来表示这些参数,并且仍然能保持类似甚至相同的预测质量。

我相信你已经在想象这两种技术如何帮助减少神经网络的能源消耗了。对于剪枝,如果处理一个输入所需的参数更少,会发生两件事来改善算法的能源消耗。首先,需要在计算单元中执行的计算更少。其次,由于计算较少,从内存中读取的数据也更少。对于量化,使用仅一个字节表示的整数来乘以两个数字需要一个更小、更简单的硬件乘法器,这反过来需要更少的能量来进行实际的乘法运算。最后,如果每个参数的大小从 8 个字节减少到 1 个字节,这意味着需要从内存中读取的数据量也减少了 8 倍,从而大大减少了处理一个输入所需的能源消耗。

想了解更多吗?看看 Zhi [5] 的“轻量级参数剪枝以实现节能深度学习:一种二值化门控模块方法”或 Widmann [6] 的“为电力优化:通过神经网络剪枝优化物联网的能源效率”以获取当前该主题的工作示例。

微调

由于许多最新的 LLM 具有封闭的特性,通常需要大量的计算能力来复制这些模型的结果。如果这些模型向公众开放,可以应用被称为微调的技术。这是一种在微调训练过程中只修改预训练模型的一部分参数的方法,以将网络专门化为特定任务。这个过程通常需要较少的训练迭代,因此比从头开始重新训练整个网络消耗更少的能源。

这就是为什么向公众开放这些模型不仅会帮助那些试图利用它们构建产品的人,还会帮助那些从头开始重新训练这些模型的研究人员,从而节省大量可以节省的能源。

结论

我希望你觉得这些技术和方法与我一样令人着迷。了解到有人积极研究这些技术并尽可能改进在能源节省和碳足迹这样重要的主题上,感到安心和慰藉。

但我们不能坐下来放松,把寻找优化解决方案的责任完全交给从事这些课题的研究人员。你是否正在启动一个新项目?首先检查是否可以微调一个预训练模型。你的硬件是否优化以运行剪枝算法,但你没有有效应用此技术的专业知识?去学习它,或者找一个已经具备技能的人。从长远来看,这将是值得的,不仅对你和你的公司,而且对我们地球整体。

欢迎在TwitterLinkedIn上关注我,并告诉我你对这篇文章的看法,或者如果你真的喜欢这篇文章,可以请我喝杯咖啡

感谢阅读!

参考文献

[1] “估算 BLOOM 的碳足迹,一个 1760 亿参数的语言模型”

[2] “AutoScale:使用强化学习优化随机边缘推断的能效”

[3] “通过 DNN 解耦的多智能体协作推断:中间特征压缩和边缘学习”

[4] “机器学习的近似计算:最新进展、挑战与愿景”

[5] “轻量级参数剪枝以实现能效深度学习:二值化门控模块方法”

[6] “优化能效:通过神经网络剪枝提高物联网能效”

带误差条的分组条形图(R 语言)

原文:towardsdatascience.com/grouped-barplot-with-error-bars-in-r-ee87b112204d?source=collection_archive---------4-----------------------#2023-11-01

数据准备和可视化的全面指南

Bhaskara Govinal BadigerTowards Data Science Bhaskara Govinal Badiger

·

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

--

图片由Paulius Dragunas提供,来源于Unsplash

条形图因多种原因成为一个重要且广泛使用的可视化工具。它们非常适合可视化数值分类变量之间的关系,使得理解类别或组之间的差异变得简单。条形图可以表示计数、频率、比例和百分比,使其在各种数据类型中都具有很大的灵活性。

在 R 中,我们能够分析分类数据并通过条形图表示它。然而,初学者在进入 R 编程时,常常在估计均值、标准误差和创建带有误差条的分组条形图方面遇到挑战。为了应对这些挑战,必须对数据类型、数据结构和数据分析所需的操作有基本的了解。

在本教程中,我们从创建一个简单的数据集开始,以理解不同类型的数据以及如何将它们转换为适合数据分析的格式。然后,我们将深入探讨估计均值和标准误差的过程。随后,我们将创建一个带有误差条的分组条形图。为了帮助初学者,我会逐步剖析代码,以确保对编程过程有透彻的理解。

我假设我们的读者已经熟悉安装和加载 R 包的过程。如果不熟悉,请参阅 STHDA 获取指导。

让我们直接开始创建数据集、修改它并进行可视化。首先加载所需的库,如下所示。

library(tidyverse)
library(ggthemes)
library(ggpubr)

tidyverse 是一个核心的 R 包集合,包括 [**dplyr**](https://r4ds.hadley.nz/data-transform.html) 用于数据操作和分析,以及 [**ggplot2**](https://r4ds.hadley.nz/data-visualize) 用于数据可视化。 ggthemes 提供了替代主题和主题组件,以样式化 ggplot2 图表。ggpubr 提供 **ggsave()** 用于以可自定义的尺寸、分辨率和文件格式保存图表。要进一步探索这些包,只需点击提供的超链接即可。

创建一个数据框

在下面的代码中,**data.frame()** 函数初始化了一个名为 **df** 的数据框,其中包含三列:**Animals****Category****Counts**

df <- data.frame(Animals = c("cats", "dogs", "cows", 
                             "cats", "dogs", "cows",
                             "cats", "dogs", "cows",
                             "cats", "dogs", "cows"),
                Category = c("Domestic", "Domestic", "Domestic",
                             "Domestic","Domestic","Domestic",
                             "Stray", "Stray", "Stray", 
                             "Stray", "Stray", "Stray"),
                  Counts = c("28", "22", "45", 
                             "30", "18", "43", 
                             "40", "65","10",
                             "35", "72", "8"))

💡 或者在 Excel 中创建数据框并将文件导入到 R 中。

我们可以使用 **view(**) 函数查看数据框 **df**

view(df)

**Animals** 列包含不同种类动物的名称。该列中有三种独特的动物: “cats”、 “dogs”和“cows”。我们对每种动物进行了四次采样,共得到 12 行数据。**Category** 列将动物分类为“Domestic”或“Stray”。其中有六个“Domestic”和六个“Strays”。**Counts** 列表示在给定的‘Category’和‘Animals’列中每种动物的计数。

数据类型和数据操作

在 R 中,了解数据框中列(变量)的数据类型至关重要,因为不同的数据类型可能适用于不同的操作和函数。在**df**数据框中,**Animals****Category**列具有字符数据类型,而**Counts**列具有数值数据类型。字符数据类型对于处理文本数据(如名称、标签、描述和文本信息)非常重要。数值数据类型表示数值,包括整数和实数(浮点值)。

我们使用**glimpse()**函数获取数据框的摘要,以了解数据框**df**中变量的数据类型。

glimpse(df)
Rows: 12
Columns: 3
$ Animals  <chr> "cats", "dogs", "cows", "cats", "dogs", "cows", "cats", "dogs", "cows", "…
$ Category <chr> "Domestic", "Domestic", "Domestic", "Domestic", "Domestic", "Domestic", "…
$ Counts  <chr> "28", "22", "45", "30", "18", "43", "40", "65", "10", "35", "72", "8"

我们可以观察到**df**数据框包含 12 行(观测值)和 3 列(变量)。值得注意的是,**Animals****Category**变量被识别为字符数据类型,标记为**<chr>**。这些变量中的值被双引号括起来,使其成为字符字符串。然而,值得注意的是,**Counts**变量也被识别为字符数据类型。这是意外的,因为该变量中的值实际上是数字,而不是字符字符串。这种数据类型的误分类可能会在进行数值操作时引发问题。为了解决这个问题并便于数值操作,我们必须采取必要步骤将**Counts**变量转换为数字数据类型。

你可能注意到,当我最初创建数据框**df**时,我故意将数值用双引号括起来,从而将**Counts**变量设为字符数据类型。现在我们已经清楚了解数据框的结构,让我们继续解决这个问题。我们将通过将**df**数据框中的**Counts**变量从当前的字符数据类型转换为数字数据类型来完成这项任务。

df$Counts <- as.numeric(df$Counts)

**df$Counts**选择**df**数据框中的**Counts**列。

**as.numeric()**将输入转换为数字数据类型。

glimpse(df)
Rows: 12
Columns: 3
$ Animals  <chr> "cats", "dogs", "cows", "cats", "dogs", "cows", "cats", "dogs", "cows", "…
$ Category <chr> "Domestic", "Domestic", "Domestic", "Domestic", "Domestic", "Domestic", "…
$ Counts  <dbl> 28, 22, 45, 30, 18, 43, 40, 65, 10, 35, 72, 8

现在**Counts**变量被识别为数值数据类型,标记为**<dbl>**,通常简称为“双精度”。该变量中的值没有用引号括起来,表明它们是数值,而非字符字符串。

**<dbl>** 代表“双精度浮点数”。这是一种用于存储实数并对实数(包括小数)执行算术运算的数值数据类型。

估算均值和标准误差

以下代码通过对现有数据框**df**执行数据汇总和聚合,创建了一个名为**mean_df**的新数据框。

mean_df <- df %>% 
  group_by(Animals, Category) %>% 
  summarise(mean= mean(Counts),
            se = sd(Counts)/sqrt(n()))

让我们一步步解析代码:

  1. 管道 (**%>%**) 操作符将现有的数据框 **df** 传递给代码的下一部分。它将 **df** 设为后续操作的输入。在 R 中,**%>%** 操作符将左侧一个函数的结果传递给右侧下一个函数的第一个参数,从而以清晰和顺序的方式将一系列操作连接在一起。

  2. **group_by()** 函数根据 **Animals****Category** 列对数据框进行分组,在数据框内创建基于这两个变量的唯一组合的子组。

  3. **summarise()** 函数估算均值和标准误差,并将其分别分配给新变量:**mean****se**

  • **mean()** 函数计算由 **group_by()** 操作创建的每个子组的 **Counts** 列的均值 (平均值)。

  • **sd()** 函数计算每个子组的 **Counts** 列的标准误差 (se)。标准误差通过将 Counts 列的标准偏差 (**sd()**) 除以样本大小的平方根 (**sqrt(n())**) 来计算。

让我们查看数据框 **mean_df** 的摘要

glimpse(mean_df)
Rows: 6
Columns: 4
$ Animals  <chr> "cats", "cats", "cows", "cows", "dogs", "dogs"
$ Category <chr> "Domestic", "Stray", "Domestic", "Stray", "Domestic", "Stray"
$ mean     <dbl> 29.0, 37.5, 44.0, 9.0, 20.0, 68.5
$ se       <dbl> 1.0, 2.5, 1.0, 1.0, 2.0, 3.5

一切准备就绪!我们现在有四列或变量,包括两个新变量,基于我们的分组标准为 **Counts** 列计算的 **mean****se**。让我们使用这个新的数据框 **mean_df** 进行数据可视化。

可视化数据

下面的代码将数据框 **mean_df** 传递给 **ggplot2**,使用 **%>%** 操作符创建一个分组条形图,并将其分配给对象 “barplot”。

barplot <- mean_df %>% 
  ggplot(
    aes(x = Animals, y = mean, fill = Category))+
    geom_col( position = "dodge", width = 0.5, alpha = 0.5, color = "black", size = 0.1)

💡请注意,尽管管道操作符 (%>%) 对于在数据操作和分析中连接多个操作非常有用,但在数据可视化过程中,它不能用于在 ggplot2 函数中添加层。相反,我们使用 ‘+’ 符号,如上所示。

让我们逐步解析代码:

  1. **aes()** 函数,缩写自 ‘aesthetic’,用于指定数据集中的变量如何映射到图中的视觉美学。在这种情况下,它指定 x 轴应表示 **Animals** 变量,y 轴应表示 **mean** 变量,并根据 **Category** 变量填充条形颜色。

  2. **geom_col()** 根据 **mean** 列中的值计算每个条形的高度。

下面是 **geom_col()** 函数中使用的参数的详细说明:

  • **position = "dodge"**:它指定条形应根据 **Category** 变量并排分组(dodged)。这就是创建分组条形图的方式。

  • **width = 0.5**:它决定条形的宽度。在这种情况下,条形的宽度为 0.5。

  • **alpha = 0.5**:它控制条形的透明度。alpha 值为 0.5 使条形略显透明。

  • **color = "black"**:它将条形的边框颜色设置为黑色。

  • **size = 0.1**:它指定了条形图边框的大小。在这种情况下,边框将相对较细。

这些参数的组合自定义了条形图的外观,使其变得更细、略微透明、带有黑色边框,并且不同类别的条形图并排排列,这可以增强数据的视觉表现。

让我们来看一下图表:

plot(barplot)

太好了!我们已经成功创建了一个条形图,但目前条形图缺少标准误差。让我们将标准误差加入到图表中。

将标准误差添加到条形图并自定义条形图颜色

首先复制并粘贴上述代码,然后使用 **+** 符号将 **geom_errorbar()** 函数添加到其中,如下所示。

barplot <- mean_df %>% 
  ggplot(aes(x = Animals, y = mean, fill = Category))+
  geom_col( position = "dodge", width = 0.5, alpha = 0.7, color = "black", size = 0.1)+
  geom_errorbar(aes(ymin = mean-se, ymax = mean+se),
                position =  position_dodge(width = 0.5), width = 0.2) 

让我们详细拆解 **geom_errorbar()** 函数中使用的组件:

  1. **ymin****ymax** 定义了基于计算的均值和标准误差的误差条的下限和上限值。

  2. **position** 参数设置为 **position_dodge()**,用于将误差条并排放置(dodged),而 **width = 0.5** 设置了 dodge 的宽度。可以调整宽度参数,以注意条形图位置的变化。

  3. **width = 0.2** 设置了误差条的宽度。在这种情况下,误差条的宽度将为 0.2。

让我们来看一下图表:

plot(barplot)

太好了!我们现在创建了一个带有误差条的分组条形图。这个图表几乎准备好发表了,但一些小的调整可以大大改善其外观。例如,你可能已经注意到条形图和 x 轴标签之间有一个间隙。让我们将其去除。此外,改变条形图颜色、将图例放置在图表内部以及提供预定义的主题以获得简洁的视觉效果,将改善其整体表现。

创建出版质量的图表

让我们开始复制上述代码,然后使用 **+** 添加额外的图层,如下所示。

barplot <- mean_df %>% 
  ggplot(aes(x = Animals, y = mean, fill = Category))+
  geom_col( position = "dodge", width = 0.5, alpha = 0.7, color = "black", size = 0.1)+
  geom_errorbar(aes(ymin = mean-se, ymax = mean+se),
                position =  position_dodge(width = 0.5), width = 0.2)+
  scale_y_continuous(expand = expansion(0),
                     limits = c(0,100))+
  scale_fill_manual(values = c("blue", "gray"),
                    name = NULL)+ # NULL removes the legend title "Category".
  theme_par()+
  theme(legend.position = c(0.2, 0.80))

在这里,我们将 **scale_y_continuous()** , **scale_fill_manual()** , **theme_par()** , **theme()** 函数,这些函数都是 **ggplot2** 包的一部分,融入到了现有的代码中。

以下是这些函数中使用的参数的详细说明。

  1. scale_y_continuous 允许我们修改 y 轴的各个方面,例如范围、刻度、标签等。在上述代码中,**scale_y_continuous()****expand** 参数设置为 expansion(0),以去除 y 轴限制周围的任何填充,而 **limits** 参数设置为 **c(0,100)**,以将 y 轴限制设置为 0 到 100。

  2. **scale_fill_manual()** 允许我们手动指定填充不同类别或变量水平的颜色。注意,在代码的第 2 行中,填充美学设置为**fill = Category** 因此,在这种情况下,参数**values = c("blue", "gray")**根据**Category**中的值填充条的颜色:**"Domestic"**的条为蓝色,**"Stray"**的条为灰色。

  3. **theme_par()** 允许我们自定义图的主题,提供了干净的白色背景和紧凑的图形。它有其自身的优缺点。

  4. **legend.position**参数**theme()**被设置为**c(0.2, 0.80)**以指定图例在图中的位置。尝试这些值以更好地理解这个参数。

💡 **theme_par()** 等预定义主题将覆盖其后应用的主题组件。我们在 **theme_par()** 函数之后添加了 **theme(legend.position)** 来解决这个问题。

让我们看看图:

plot(barplot)

这是一个视觉上吸引人并适合发表的带误差条的条形图。该图有效地展示了动物种群的变化。在“流浪”类别中,狗的数量超过了猫和牛,而在“家庭”类别中,牛的数量明显超过了猫和狗。

保存图以用于发表

下面的代码利用了**ggsave()**函数来自**ggpubr**库。为了将图保存为图像,我们需要指定要保存的对象(在这种情况下是“barplot”)并定义所需的文件名及所选的图形格式。在本例中,我们选择了文件名“barplot_animals.tiff”,这将把图保存为 TIFF 图像。此外,我们设置了以英寸为单位的**width****height**的尺寸,以及指定为**dpi**(每英寸点数)的分辨率。

ggsave(barplot, filename = "barplot_animals.tiff", width = 5, height = 4, dpi = 300)

现在你已准备好创建适合发表的条形图,并带有标准误差条。享受创造的过程,尽情绘制你的数据吧!🎉✨

Grover 的量子搜索算法

原文:towardsdatascience.com/grovers-quantum-search-algorithm-54c427315768

量子计算

量子算法之一的视觉解释

Dan JacksonTowards Data Science Dan Jackson

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

--

量子计算机 IBM 低温冷却系统的特写图像。图像来源:IBM/Graham Carlow

Grover 算法 是最早提出的量子算法之一,它展示了量子相对于经典算法的优势,在这种情况下是二次‘加速’。该算法由洛夫·格罗弗 [1] 在 1996 年开发,是量子计算领域的一项突破,继类似算法如 Shor 算法Deutsch-Jozsa 算法 之后。在本文中,我们将通过视觉方式解释 Grover 算法的工作原理,并通过数学展示它相对于最佳经典搜索算法的量子‘加速’

无结构搜索问题

首先,让我们介绍一下 Grover 算法所解决的问题。假设我们有一个无结构数据库,或列表,其中包含 N 元素,每个元素由一个唯一的 n 位字符串 ID 代表 x。因此,该列表最多可以包含 N = 2 个元素。我们的任务是从数据库中找到一个特定的“标记”元素,其位字符串为 x₀

N 元素数据库的简化示意图,其中标记的元素用红色标出。图像来源:作者。

为了找到标记的元素,我们需要通过查询数据库来获取数据库中某个元素的 ID 号 x 并检查它是否等于目标 ID 号 x₀。如果它相等,那么我们通过一次查询就成功找到了标记的元素!

然而,被查询的元素很可能不会在第一次尝试时与我们寻找的目标匹配,尤其是当数据库中包含很多元素(即 N 很大)时。因此,我们将不得不继续反复查询数据库,直到出现匹配的 x = x₀,并成功找到目标元素。

注意: 在“检查”元素的 ID 号时,我们不知道目标 ID x₀是什么。我们只是有一些方法来确定查询的 ID 号 x 是否等于 x₀。因此,“找到标记元素”也等同于确定 x₀ 实际上是什么。

经典无结构搜索算法

为了“检查”某个元素的 ID 号 x,假设我们可以访问一个函数 f(x),该函数接收一个 n 位字符串 x,如果它不等于目标比特串,则输出 ‘0’,如果相等则输出 ‘1’。由于我们不知道目标比特串 x₀ 实际上是什么,函数被描述为黑箱函数。我们可以如下定义该函数:

现在,为了找到标记元素,我们逐个查询数据库中的每个元素,并应用 f(x) 来检查它是否是目标。

  • 最佳情况下,我们在一次查询后找到目标。

  • 最坏情况下,我们必须查询数据库 N 次,即我们必须查询数据库中的每个元素。

  • 平均而言,我们必须进行 N/2 次查询才能找到标记元素。

一般来说,我们可以说,如果我们对数据库进行 k 次不同查询,那么找到标记元素的概率是:

因此,如果我们希望以 𝝐 的概率成功,那么我们必须进行 k ≥ 𝝐N 次查询。总体而言,我们可以使用 Big-O 记法定义经典搜索算法的查询复杂度为:

Grover 的量子搜索算法

在这一部分,我们将深入探讨 Grover 量子搜索算法如何在一个无结构的数据库中找到标记元素的理论,其查询复杂度为:

该算法描述了如何在量子电路上对初始处于零态n 个量子比特应用一组量子算符或 量子门

量子电路将初始n 量子比特状态转化为最终n 量子比特状态,该状态等于目标量子态,并且具有高概率。然后,测量最终量子状态将返回目标 ID 比特串 x₀(具有高概率)。

量子门与算符

预言机算符

预言算子是经典算法中黑箱函数 f(x) 的量子门等效物。预言算子将作用于 n 量子比特态 |x 上,并且如果它等于目标态 |x₀添加负相位,否则保持不变:

要了解这如何与黑箱函数 f(x) 相关,我们还可以将预言操作表示为:

如果我们仔细考虑,可以发现预言算子等同于对角单位算子(在矩阵形式中只有对角项等于 1),其对应于目标态 |x₀ 的元素具有负号。因此,我们可以将预言算子写为:

通过快速检查可以验证该表示确实等于上述的两个表示。

预言算子是算法的核心,定义了解决的计算问题。本质上,它只是验证给定问题的潜在解决方案。因此,Grover 算法可以用来解决任何可以用黑箱函数表示的问题,所以它不仅限于无结构搜索问题。

相位反转算子

相位反转算子类似于预言算子,不同之处在于,如果状态等于目标态 |x₀,它会添加负相位;如果状态等于n-量子比特零态 |0,它会添加负相位。其他情况下,状态保持不变。

相位反转算子也可以表示为对角单位算子,其对应于零态的元素具有负相位:

Grover 的算子

Grover 的算子 D 通过在应用相位反转算子之前和之后对所有 n 个量子比特应用一个Hadamard 算子 并添加负相位,即添加负号,从而获得。它可以表达为:

其中 Hadamard 算子将所有 n 个量子比特置于等概率叠加的可能 N = 2ⁿ 状态。我们可以代入相位反转算子的替代表示,以获得:

在零态的单个量子比特上,Hadamard 操作的效果是将其置于以下单量子比特叠加态:

反转和反射算子表示

为了更清晰、更直观地理解预言算符、相位倒置算符和 Grover 的 D 算符对 n 量子比特状态的作用,让我们首先稍作绕行,探讨两类一般算符,即倒置和反射算符

正如你可能猜到的,倒置和反射算符执行对某个其他量子状态 |𝜓⟩ 的‘倒置’‘反射’。它们表示如下:

为了观察这两种形式的算符对状态的作用,让我们考虑它们对一个任意状态的作用,该状态被分解为正交组件

倒置

很容易检查,将倒置算符应用于上述任意状态会得到以下结果:

我们可以看到,状态的 |𝜓⟩ 组件前面的符号被翻转了。这对应于对整体状态 |𝜙⟩ 关于正交状态 |𝜓⟩ 的‘反射’。我们可以如下可视化:

任意状态 |𝜙⟩ 上的倒置操作动画。黄色轴表示反射轴。Gif 作者提供。

反射

同样,如果我们将反射算符应用于任意状态,则会得到:

我们现在发现正交组件的符号被翻转了。这种符号翻转对应于对整体状态 |𝜙⟩ 关于 |𝜓⟩ 状态的反射。我们可以如下可视化:

任意状态 |𝜙⟩ 上的反射操作动画。黄色轴表示反射轴。Gif 作者提供。

预言算符、相位倒置算符和 Grover 的 D 算符

基于我们对倒置和反射算符的新理解,让我们将预言算符、相位倒置算符和 Grover 的 D 算符表示如下:

量子算法

现在我们已经对 Grover 算法涉及的核心概念和量子算符有了深入的理解,我们可以开始研究算法的工作原理。完整算法描述如下:

描述该算法的完整 单位算符 因此是:

因此,算法结束时我们 n 量子比特所处的最终量子状态是:

如果我们的算法正常工作,则最终状态应该等于目标状态 |x₀,具有较高的概率。然后,测量每个量子比特的最终状态应该给我们目标 ID 比特串 x₀

代表该算法的量子电路如下所示:

对应于 Grover 算法的量子电路图。图片作者提供。

Grover 迭代

每个 T ‘Grover 迭代’n-qubit 状态的作用由括号中的算符描述。我们将它们组合成一个 Grover 算符 G

我们可以用 反演和反射算符 来表达:

其中,倒数第二项表示对正交的 |+ⁿ 状态的反射。我们可以说 |x₀ 和 |+ⁿ 状态 张成了 Hilbert 空间的二维子空间 (我们 n-qubits 的总 N = 2ⁿ 状态空间),每次 Grover 迭代中的反射操作都发生在这个二维子空间中。

算法的几何解释

让我们考虑每次 Grover 迭代 G 在二维子空间中对任意状态 |𝜁⟩ 的作用,该子空间由 |x₀ 和 |+ⁿ 张成。我们可以将任意状态 |𝜁⟩ 表达为状态 |+ⁿ 及其正交对应物:

同样,目标状态 |x₀ 可以写成:

初始状态 |𝜁⟩ 和目标状态 |x₀ 可以在表示正交 |+ⁿ 组件的二维子空间图中绘制。下图展示了状态之间的相关角度。

展示由某些初始状态和目标状态张成的二维子空间。图片作者提供。

如果我们检查 Grover 迭代算符:

每次 Grover 迭代对某些初始状态的作用可以通过图形化的方式理解如下:

  1. |𝜁⟩ 状态关于 |x₀ 状态的 反射

  2. 结果状态关于正交轴的 反射

因此,如果我们初始状态和目标状态之间的角度为 𝜽,我们首先需要在 |x₀ 状态关于的 反射减去 -2𝜽,然后在关于垂直正交轴的反射后 加上 +2(𝜽 — 𝛾)。最终角度因此由下式给出:

换句话说:

每次 Grover 迭代中 n-qubit 量子状态 |𝜁⟩ 和目标状态 |x₀ 之间的角度减少 2𝛾。

使用下面的动画可以更清楚地理解这一点:

Grover 迭代对某些初始任意状态的作用动画。Gif 作者提供。

因此,为了将初始量子状态 |𝜁⟩ 转换为目标状态 |x₀,我们只需 反复应用 Grover 迭代,然后在状态 |𝜁⟩ 尽可能接近 目标状态 |x₀ 时停止。

然而,如前所述,我们并不是从某个初始任意态 |𝜁⟩ 开始,并应用我们的 Grover 迭代。如 步骤 1 和 2 所示,我们实际上从初始状态开始:

初始状态 |𝜁⟩ 和目标状态 |x₀ 之间的 初始角度 𝜽 通过 内积 找到:

类似地,我们还可以很容易地展示目标状态 |x₀ 与正交轴之间的角度 𝛾 如下所示:

因此,随着数据库大小 N 的增加,即 在非常大的集合大小 N 的极限下, 我们得到如下结果:

类似地,我们也可以说 (使用 小角度近似) 在大 N 的极限下:

查询复杂度分析

那么,使用这些初始角度,大约需要多少次 Grover 迭代才能使初始状态接近或尽可能接近目标状态 |x₀⟩?

由于每次 Grover 迭代都将状态与目标状态之间的角度减少 2𝛾,而初始角度为 𝜋/2,我们可以展示:

其中 T 代表 Grover 迭代次数,因此也代表 对 oracle 的查询次数 k。然而,我们应该注意这 只是一个近似值。关于 Grover 迭代次数的更精确推导将在接下来的章节中介绍。不过,查询复杂度 可以用 大 O 符号*** 表示:***

相对于经典查询复杂度的二次加速!下图比较了经典查询复杂度与量子查询复杂度,并指出了量子优势的点。

经典查询复杂度与量子查询复杂度,量子优势点用黄色标出。图片由作者提供。

成功概率分析

然而,成功到达目标状态 |x₀ 的概率强烈依赖于总系统大小 N 和应用的 Grover 迭代次数 T

实际上,成功概率并不会随着 Grover 迭代次数的增加而趋近于 1,而是 振荡! 要了解原因,我们首先考虑最终状态 |𝜁⟩ 在 目标状态 |x₀ 中的概率:

我们知道 内积 可以用更为熟悉的几何形式表示:

使用最终角度,即状态|𝜁⟩与目标状态|x₀之间的角度,在T次 Grover 迭代后。我们也已经知道这个角度由以下公式给出:

使用以下关系:

给出以下最终角度的表达式:

我们可以将其代入内积公式得到:

最终给出以下成功将最终状态置于目标状态的概率表达式:

使用这个方程,我们可以绘制成功概率作为 Grover 迭代次数的函数,对于某些系统规模N,以观察成功概率是否确实存在振荡。

成功概率作为 Grover 迭代次数的函数。图像由作者提供。

为了理解这种振荡现象,我们可以再次将状态|𝜁⟩和目标状态|x₀绘制在图上,并应用许多 Grover 迭代,以观察状态在多次T迭代中的行为

状态在多次 Grover 迭代中的行为动画。Gif 由作者提供。

我们可以看到,当状态以每次 Grover 迭代 2𝛾的角速度接近目标状态时,实际上会达到一个最小点,此时状态与目标状态之间的角度符号发生反转。因此,状态开始“螺旋”远离目标状态。

在每个系统规模N下,都存在一个最大化成功概率的最优 Grover 迭代次数T

我们可以通过将成功概率设为 1 并重新排列以找到T来确定这个最优数字。

然而,这个最优的迭代次数必须是整数值,因此通常会因舍入到最近整数而存在一些小的截断误差

我们可以通过绘制成功概率作为集合大小 N 的函数并使用四舍五入到最近整数的最优 Grover 迭代次数来观察这种截断误差的影响。

使用最近整数的最优 Grover 迭代次数T值作为系统规模 N 的函数的成功概率。图像由作者提供。

其中蓝色曲线显示了成功概率,红色曲线显示了成功概率的包络线近似作为集合大小 N 的函数。如图所示,成功概率的变化规律如下:

因此,随着集合大小N增大,成功概率接近于 1!

如果我们再次绘制成功概率的振荡图像,作为 Grover 迭代次数的函数,但这次仅显示T的整数值,我们或许能更清楚地理解成功概率为何出现这种趋势。对于集大小N=7,我们得到以下结果:

成功概率作为截断的最接近整数 Grover 迭代次数的函数,设定集大小 N=7。图像由作者提供。

我们可以看到,成功概率的振荡‘频率’是这样的,在上述第一个振荡周期中,最接近整数的 Grover 迭代次数(垂直线)与振荡的峰值不对齐。因此,成功概率无法达到 1。

然而,如果我们对集大小N=100进行相同的操作,我们得到以下结果:

成功概率作为截断的最接近整数 Grover 迭代次数的函数,设定集大小 N=100。图像由作者提供。

增加集大小N有效地降低了振荡的‘频率’,使得最优的最接近整数 Grover 迭代次数可以更接近振荡的峰值。 如果我们进一步增加集大小到N=800,这种模式会变得更加明显,如下所示:

成功概率作为截断的最接近整数 Grover 迭代次数的函数,设定集大小 N=800。图像由作者提供。

因此,随着N趋向于无穷大,最优的最接近整数 Grover 迭代次数 T 接近振荡的峰值,而成功概率趋向于 1。

结论

在这篇文章中,我们详细探讨了 Grover 量子搜索算法如何实现比传统经典搜索算法更快的二次加速的理论。然而,理论上 Grover 算法可以应用于比非结构化搜索更广泛的算法。更一般地说,该算法可以加速任何黑箱问题,这涉及到满足一些由 oracle 操作符检查的约束,或任何固有地涉及穷举搜索的问题。因此,该算法的一个潜在应用是量子密码学,特别是 Grover 算法可能为许多暴力攻击算法中固有的穷举搜索提供量子加速。

然而,Grover 算法的查询复杂性只有在特定系统规模 N 实现后才会超过其经典对手。目前,近期的量子计算机远未能够提供实现这种量子优势所需的大量无噪声量子比特。不过,随着未来几十年量子计算和量子工程领域的持续进展,长期的容错量子计算机似乎有可能达到实现 Grover 量子搜索算法承诺所需的复杂性和精密度。

感谢阅读! 如果你喜欢这篇文章并想阅读更多关于物理学、量子力学和量子计算的内容,请关注我并查看我的其他文章!如果你真的喜欢这篇文章,你还可以在 bmc.link/danjackho5 请我喝一杯咖啡(如果你愿意的话),我会非常感激!

如果你想继续在 Medium 上阅读关于数学和物理的内容,那么为什么不通过以下链接注册成为 Medium 会员呢?medium.com/@danjackho/membership

[## 薛定谔方程的各种形式

也许在量子力学中,没有哪个方程像薛定谔方程那样普遍存在。

量子物理学 101:薛定谔方程的各种形式 [## 量子谐振子:狄拉克的研究方法

这个物理学基本模型如何被量子化,以及我们如何应用狄拉克的‘阶梯方法’来找到其能量…

量子谐振子:狄拉克的研究方法 [## 量子传送的数学解释

量子态如何‘转移’到远处粒子的数学原理…

量子计算传送的数学原理

参考文献

[1] Grover, Lov K. “一种快速的量子机械算法用于数据库搜索。” 第二十八届年度 ACM 计算理论研讨会论文集。1996 年。

[2] Linden, Noah. 讲义, “量子计算”。2022 年。

使用 Python 和 Plotly Express 创建树状图

原文:towardsdatascience.com/grow-a-treemap-with-python-and-plotly-express-963706a2bd74

将政府 PDF 转换为财务规划工具

Lee VaughanTowards Data Science Lee Vaughan

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

--

图片来源:Robert Murray,来自 Unsplash!

层级数据是一种数据模型,其中项目通过父子关系相互链接,形成树状结构。一些显而易见的例子包括家谱和公司组织结构图。

树状图是一种使用嵌套矩形表示层级数据的图示。每个矩形的面积对应其数值。树状图已经存在大约 30 年了。早期的应用是可视化硬盘使用情况,如下图所示。

硬盘空间分配通过树状图进行可视化(Carnivore1973 via Wikimedia Commons

通过树状图,你可以捕捉到单独类别的价值以及层级的结构。它们对于以下场景非常有用:

  • 在类别数量超出条形图承载能力时显示层级数据。

  • 突出显示单个类别与整体之间的比例。

  • 使用不同的大小和颜色区分类别。

  • 发现模式、主要贡献者和异常值。

  • 为数据可视化带来新鲜的视角。

在这个快速成功数据科学项目中,我们将使用 Python 创建一个树状图,以帮助人们制定预算。我们首先会使用tabula-py库将基于网页的劳动统计局PDF 转换为 pandas DataFrame。然后,我们将使用Plotly Express库将 DataFrame 转换为一个吸引人且互动的基于区域的可视化图表。

消费者支出调查

随着疫情和随后的通货膨胀激增,消费者支出引起了广泛关注。个人需要了解如何有效预算,政策制定者需要了解哪些行业对潜在选民造成了最大的财务负担。

为了跟踪收入和支出,人口普查局使用消费者支出调查收集美国消费者的支出、收入和人口特征信息。随后,劳工统计局(BLS)将这些统计数据汇编成每年在九月份发布的年度报告

这些 BLS 报告属于公共领域,对政策制定者和个人都有用。刚刚起步的年轻人可以利用这些表格作为准备预算和储蓄计划的起点。年长者可以利用这些表格来帮助规划退休。你可以在这里查看完整的表格,并在下面的图示中查看部分内容。

2021 年 BLS 消费者支出调查表 1300 的前几行(来自劳工统计局)

消费支出表将支出分为 14 种不同的类型,如下所示:

支出类型解释(来自劳工统计局)

现金捐赠类别包括对离家上大学的学生的资助、赡养费和抚养费,以及个人现金礼物,例如生日或婚礼礼物。它还包括对宗教、慈善或政治组织的捐赠。

Plotly Express 和 Tabula 库

Plotly ExpressPlotly图形库的高级版本。它让你可以轻松制作出具有许多内置功能的吸引人的图形。

Treemaps 的一个缺点是小矩形可能没有标签,或者标签可能难以辨认。Plotly Express 通过提供一个交互式的“悬停窗口”来帮助克服这一限制,当光标停留在矩形上时,该弹出窗口会显示详细信息,这在图表上直接显示是不切实际的。

tabula-py库是 tabula-java 的 Python 封装,它可以从 PDF 中提取表格。提取的数据可以转换成 DataFrame 列表,或 CSV、TSV 或 JSON 文件。

你可以通过 pip 或 conda 安装 Plotly Express 和 tabula-py。Plotly Express 需要 Plotly 作为依赖项。以下是 conda 安装示例:

conda install plotly plotly_express tabula-py

过程

我们的任务是将 BLS “表格 1300” PDF 转换为 pandas DataFrame,然后转换为树图。虽然 tabula-py 提供了多种 PDF 格式转换,但我们将在这里使用 CSV 格式:

  1. 使用 tabula-py 从网络读取 PDF 并将其转换为 CSV。

  2. 使用 pandas 将 CSV 转换为 DataFrame。

  3. 使用 pandas 清理和准备 DataFrame 以供绘图。

  4. 使用 Plotly Express 绘制树图。

导入库

以下代码导入我们所需的库,设置 Jupyter Notebook 以显示 DataFrame 中的最大行数,并将 DataFrame 中的浮点值四舍五入到两位小数。

import string

import pandas as pd
import plotly.express as px
import tabula 

# Permit display of entire DataFrame and set decimal precision:
pd.set_option('display.max_rows', None)
pd.set_option('display.float_format', '{:.2f}'.format)

将 PDF 表格转换为 DataFrame

为了将基于 Web 的 PDF 转换为 DataFrame,我们将首先使用 tabula-py 将其转换为 CSV 文件,然后使用 pandas 将 CSV 文件转换为 DataFrame。

# Convert web-based PDF into a CSV file:
URL = 'https://www.bls.gov/cex/tables/calendar-year/\
aggregate-group-share/reference-person-age-ranges-2021.pdf'
tabula.convert_into(URL, "output.csv", output_format="csv", pages='all')

# Convert CSV file into a DataFrame and inspect:
df = pd.read_csv('output.csv', header=1)
df.head()

初始 DataFrame 的前几行(图片由作者提供)

数据处理

现在我们需要清理 DataFrame。第 4 行包含每个年龄段的消费者单位的数量,以千为单位,存储在第 3 到第 9 列中。我们需要每个年龄段的消费者单位数量,以便稍后确定平均支出。以下代码提取单位信息,移除标点符号,将结果转换为整数,并乘以 1,000。

units = df.iloc[4, 3:10].apply(lambda x: int(x.translate(
                             str.maketrans('', '', string.punctuation))) * 1000)

接下来,我们创建一个字典,以年龄段作为键和值——基于 DataFrame 列标题——以及每个年龄段的消费者单位数量。

# Define age brackets based on DataFrame column headers and create a dictionary:
brackets = ['25-34', '35-44', '45-54', '55-64', '65 years', '65-74', '75 years']
num_units_per_age = dict(zip(brackets, units))

print(num_units_per_age)
{'25-34': 21024000, '35-44': 22921000, '45-54': 22276000, '55-64': 24751000, '65 years': 36016000, '65-74': 21479000, '75 years': 14537000}

准备一个用于绘图的年龄段

我们希望一次绘制一个单一年龄段。以下代码指定年龄段(25–34 岁),为该年龄段生成一个新的 DataFrame,进行更多数据清理和重命名,并计算每种支出类型的百分比份额和平均支出。

支出类型名称,如“食品”和“住房”,是通过浏览表格 1300 找到的。这些代表了更详细的支出信息的高层总结。年龄段列表示该年龄组的总支出百分比

# Designate age bracket to examine (must match column header name): 
AGE = '25-34'

#Prepare DataFrame:
df = df.iloc[4:, :]
df.columns = ['Expenditure Type', 'Aggregate'] + list(df.columns[2:])
df = df[['Expenditure Type', 'Aggregate', AGE]]

# Strip punctuation and leading and trailing whitespace from columns:
df['Expenditure Type'] = df['Expenditure Type'].str.replace(
                                           '[^\w\s]','', regex=True).str.strip()
df['Aggregate'] = df['Aggregate'].str.replace('[^\w\s]','', regex=True)

# Make top-level 'Expenditure Type' the index and keep only selected rows:
df = df.set_index('Expenditure Type', drop=True)
df = df.loc[['Food', 'Housing', 'Transportation', 'Healthcare', 'Education',
             'Reading', 'Alcoholic beverages', 'Apparel and services',
             'Entertainment', 'Personal care products and services', 
             'Tobacco products and smoking supplies', 'Miscellaneous', 
             'Cash contributions', 'Personal insurance and pensions'], :]

# Rename columns for smaller Treemap labels:
df.rename(index={'Alcoholic beverages': 'Alcohol', 
                 'Apparel and services': 'Apparel',
                 'Personal care products and services': 'P-Care',
                 'Tobacco products and smoking supplies': 'Smoking',
                 'Miscellaneous': 'Misc.',
                 'Cash contributions': 'Cash Contr.',
                 'Personal insurance and pensions': 'Insurance'}, inplace=True)

# Calculate percent share and average expenditure (in $) for each type:
df['Expenditure'] = df['Aggregate'].astype(float) * df[AGE].astype(float) / 100
df['Expenditure Percent'] = (df['Expenditure'] / df['Expenditure'].sum()) * 100
df['Ave Expenditure'] = (df['Expenditure'] * 1000000) / num_units_per_age[AGE]
df['Ave Expenditure'] = df['Ave Expenditure'].astype(int)
df.head(15)

准备好绘图的 DataFrame(图片由作者提供)

绘制树图

Plotly Express 提供了超过 30 个函数来一次性创建整个图形。treemap() 函数接受 DataFrame、“顶级”类别(在此情况下为“支出类型”)、剩余类别(df.index)以及值和颜色的列名称。我们还将指定一个颜色比例(“portland”)和图形的尺寸。

# Create treemap:
fig = px.treemap(df, path=[px.Constant('Expenditure Type'), df.index],
                 values='Expenditure Percent',
                 color='Ave Expenditure',
                 color_continuous_scale='portland',
                 width=900, height=500)

# Update layout:
title = f'Average Expenditure for {AGE} year-olds \
(Bureau of Labor Statistics 2021)'
fig.update_layout(title=title, margin=dict(t=40, l=10, r=10, b=25))

25–34 岁群体的平均支出树图(图片由作者提供)

你可以看到这张图表对财务规划师解释预算给客户可能非常有用。与几页文字相比,它容易看懂。而且使用 Plotly Express,你可以通过简单地将光标悬停在矩形上来深入查看详细值,如下所示的医疗保健部分。

显示医疗保健支出详细信息的悬停功能示例(图像由作者提供)

虽然这不会改变表格的外观,但你可以通过切换valuescolor参数的值来显示“支出百分比”,如下所示:

fig = px.treemap(df, path=[px.Constant('Expenditure Type'), df.index],
                 values='Ave Expenditure',
                 color='Expenditure Percent',
                 color_continuous_scale='portland',
                 width=900, height=500)

25-34 岁人群的平均支出百分比树形图(图像由作者提供)

根据之前的树形图,可以看出 25-34 岁的人群往往将大部分收入用于住房、交通和保险。但那老年人呢?

如果你将AGE变量更改为'75 years'并重新运行代码,你会看到医疗保健变得更加重要,现金捐赠也增加了,这可能表现为慈善捐款和家庭礼物。颜色条还表明支出通常低于 25-34 岁的人群。

75 岁以上人群的平均支出树形图(图像由作者提供)

关于树形图的一个最终观察:由于数据是排序的,你可以区分类似值的类别。在前面的图中,尽管“食品”和“交通”的大小相似,但你可以确定“食品”因其在层级中的位置而更大。

买者自慎!

BLS 表中的数值代表全国平均值。可能会出现地方差异。例如,大型城市的住房和交通成本无疑高于小型农村镇。此外,所有支出估算都可能存在误差。因此,这些数据应当方向性使用,而非绝对性使用于任何财务规划目的。

总结

tabula-py 模块大大简化了将 PDF 表格转换为 CSV 等有用格式的过程。同样,Plotly Express 与 pandas DataFrame 配对,能够轻松生成互动的树形图。使用树形图,你可以轻松地可视化和传达层级数据中的关系。

树形图很有趣,但并不是唯一的选择。要查看使用桑基图呈现的相同基本数据,请查看Visual Capitalist中的这个帖子

谢谢!

感谢阅读。如果你喜欢这篇文章,可以查看我的书籍,不切实际的 Python 项目现实世界的 Python,以获取更多编码点子。并关注我,未来会有更多 快速成功的数据科学 项目。

ChatGPT 高级设置指南——Top P、频率惩罚、温度等

原文:towardsdatascience.com/guide-to-chatgpts-advanced-settings-top-p-frequency-penalties-temperature-and-more-b70bae848069

通过优化扩展配置,如 Top P、频率和存在惩罚、停止序列和最大长度,来解锁 ChatGPT 的隐藏潜力。

Kenneth LeungTowards Data Science Kenneth Leung

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

--

图片来源:Volodymyr HryshchenkoUnsplash

虽然 ChatGPT 在默认设置下已经能产生令人印象深刻的结果,但其高级参数中蕴藏着巨大的未开发潜力。

通过调整设置,如Top P、频率惩罚、存在惩罚、停止序列、最大长度和温度,我们可以引导文本生成以满足复杂的需求,带来新的创造力和具体性水平。

在本文中,我们探讨这些高级设置,并学习如何有效调整它们。

目录

(1)温度(2)最大长度(3)停止序列(4)Top P(5)频率惩罚(6)存在惩罚

介绍

使用 ChatGPT 很简单——只需输入提示并接收响应。然而,我们可以配置许多高级参数以丰富生成的输出。

OpenAI Playground 让我们与语言模型互动,同时提供各种配置选项,如下所示:

OpenAI Playground 登陆页面,右侧面板中的高级设置 | 图片来源:作者

这些高级设置也可以在 API 代码中配置:

response = openai.ChatCompletion.create(
         messages = [
            {"role": "user",
             "content": ""
             }],
         model = "gpt-4"
         temperature = 0,
         max_tokens = 100,
         stop = ["goodbye"],
         top_p = 0.5,
         frequency_penalty = 0,
         presence_penalty = 0)

除了模式和模型的显而易见的参数外,我们还深入探讨其他六个参数。

(1) 温度

温度控制响应中的随机性程度,其值范围在 0 到 2 之间。

在零温度下,输出更加可预测,并将紧密贴近最可能的词汇。如果我们想要一致的答案,零温度是理想选择,特别是在使用这些模型进行有依据的响应时。

如果我们想要更具创意和不可预测的响应,可以提高温度。考虑以下句子:

“猫坐在垫子上并开始 ___”

  • 低温度(例如 0)下,模型将选择像“purr”或“sleep”这样的高概率词汇。

  • 中等温度(例如 1)下,模型可能会引入稍微不那么预期但合理的词汇,如“groom”或“stretch”。

  • 高温度(例如 2)下,模型可能生成更多样化和不那么可预测的结果,如“contemplate”或“brainstorm”。

在较高的温度下,模型更倾向于冒险,从而产生更广泛的可能完成。然而,高温度可能导致无意义的输出,如下所示:

最大温度值的胡言乱语输出示例 | 作者提供的图片

从技术角度来看,更高的温度会使概率分布变平,使得通常不常见的令牌现在变得和更常见的令牌一样可能被生成。另一方面,较低的温度会使分布偏向,使得更常见的令牌生成的概率更高。

(2) 最大长度

最大长度涉及将生成的令牌数量的最大值。

对于英文文本, 1 个令牌大约是 0.75 个词汇 (或 4 个字符)。查看 OpenAI 的令牌计数器 以计算文本中的令牌数量。

一点需要注意的是,最大长度 包括输入提示。如果我们将最大长度设置为 1,000,而我们的输入包含 300 个令牌,则输出将限制为 1,000 – 300 = 700 个令牌。

此外,最大长度的上限是特定于模型的。例如,GPT-4 模型可以达到 8,191 个令牌。

有了这个,我们可以生成符合不同使用场景的自定义令牌限制的响应。例如,在营销活动中,我们创建符合 160 个字符(约 40 个令牌)限制的短信。

假设我们有以下 20 个令牌的提示:

“为一家位于伦敦的本地面包店 Delights 生成一条短信营销信息,该面包店正在提供折扣。”

为了生成符合短信限制的消息,我们将最大长度设置为40 + 20 = 60 个令牌。输入提示后,我们得到这个简洁的短信消息:

Hey! Delicious savings at Delights Bakery, London! 
Enjoy 20% off exquisite pastries & delightful bread. 
Pop in & make your day a bit sweeter. Hurry, offer ends soon!

(3) 停止序列

“停用序列”参数指示模型在达到特定字符串时停止生成。这在我们希望输出在特定点结束时非常有用,确保响应简洁且省略不需要的信息。

假设我们在要求 ChatGPT 生成辞职信模板后得到以下输出:

Subject: Resignation Notice

Dear [Manager's Name],

I hereby resign from my position at [Company Name], 
effective [Last Working Day, typically two weeks from the date of the email].

**Best,
[Your Name]**

如果我们想排除底部的签名,可以将字符串“Best”设置为其中一个停用序列。这样,重新生成的输出将在停用序列处截断,如下所示:

OpenAI Playground 中的停用序列输出 | 作者提供的图片

返回的输出不包括停用序列本身,每次执行可以定义多达个字符串序列。

(4) Top P

Top P 与 top-p 采样技术(也称为核心采样)相关。回顾一下,GPT 模型通过为词汇表中的所有可能下一个词分配概率来生成下一个词。

使用 top-p 采样时,不是考虑整个词汇表,而是从一个较小的词汇集合中进行采样,这些词汇的累积概率总和高于 Top P 值。

Top P 范围从 0 到 1(默认),较低的 Top P 意味着模型从更窄的词汇选择中采样。这使得输出较少随机和多样,因为更可能的标记将被选中。

例如,如果 Top P 设置为 0.1,则只考虑前 10% 概率质量中的标记。

鉴于 Top P 影响输出的随机性, OpenAI建议调整 Top P 或温度,但不建议同时调整这两者。不过,尝试同时调整两者也没有害处。

以下展示了不同 Top P 值下的输出结果:

“写一个关于鲸鱼的极具创意的短篇简介”

不同 Top P 值的输出比较 | 作者提供的图片

上述示例显示,较低的 Top P 值0.01生成的输出在描述上显得不那么有创意和华丽

技术细节

如果 Top P 设置为 0.1,这并不严格意味着考虑的是前 10% 概率质量中的标记。相反,模型找到的是累积概率超过 10% 的最小可能标记集

从最可能的标记开始,并按递减概率添加其他标记,直到满足 Top P。有时,如果没有单一标记具有非常高的概率且分布相对平坦,这可能涉及许多标记。

(5) 频率惩罚

频率惩罚解决了文本生成中的一个常见问题:重复。通过对频繁出现的词汇施加惩罚,模型被鼓励多样化语言使用。

正的频率惩罚值根据 token 在文本中当前的频率来惩罚新 token,减少模型逐字重复相同句子的可能性。

根据 API 文档,频率惩罚的范围是 -2 到 2 (默认为 0)。然而,Playground 上的范围是 0 到 2。我们将遵循 API 文档中的范围。

以下展示了基于该提示的不同频率惩罚的输出:

“写一首每个词都以 Z 开头的诗”

不同频率惩罚的输出比较 | 图片作者提供

上述示例表明,更大的频率惩罚会导致更少的重复词和更大的多样性,以至于我们甚至得到不以 ‘Z’ 开头的词。

频率惩罚的合理值大约是 0.1 到 1。我们可以进一步增加它以强力抑制重复,但这可能会降低输出质量。也可以设置负值来增加重复,而不是减少它。

(6) 存在惩罚

与频率惩罚类似,存在惩罚旨在减少 token 的重复。

正的存在惩罚值会根据新 token 是否已在文本中出现过来惩罚新 token,从而增加模型讨论新主题的可能性。

根据 API 文档,存在惩罚的范围是 -2 到 2(默认为 0),而 Playground 上的范围是 0 到 2

频率惩罚和存在惩罚之间的区别是什么?

细微的区别主要在于对重复 token 的惩罚程度。频率惩罚成比例的(即,相对标记),与特定 token 的生成频率相关。

另一方面,存在惩罚 是一次性的(加性) 惩罚,应用于至少出现过一次的 token,类似于 布尔(1/0)标记。

这些惩罚的影响在以下方程式中体现,用于计算 token j 的 logit(未归一化的对数概率)μ

显示第 j 个 token 的 logit 减去两个惩罚项的方程式 | 图片作者提供

c[j] 指代一个 token 之前生成的次数,α 值是惩罚系数(即介于 -2 和 2 之间)。

存在惩罚的合理值与频率惩罚相同。

总结

理解每个参数的作用后,我们可以更自信地调整这些高级设置,以满足我们的需求。

调整这些参数是艺术与科学的微妙结合,因此建议尝试不同的配置,以了解哪些最适合各种用例。

在你离开之前

我欢迎你加入我的数据科学探索之旅! 关注这个 Medium 页面,并访问我的 GitHub 以获取更多有趣且实用的内容。同时,尽情享受 ChatGPT 的高级设置带来的实验乐趣!

## 在 CPU 上本地运行 Llama 2 进行文档问答

清晰解释了如何使用 LLama 2、C Transformers 和 GGML 在 CPU 上运行量化 LLM 应用的指南 [## Bark 的文本到音频生成,清晰解释

探索 Bark 的能力,这是一个开源的 GenAI 模型,用于文本到音频的转换](https://betterprogramming.pub/text-to-audio-generation-with-bark-clearly-explained-4ee300a3713a?source=post_page-----b70bae848069--------------------------------)

处理 Python 中分类变量的指南

原文:towardsdatascience.com/guide-to-handling-categorical-variables-in-python-854d0b65a6ae

如何处理分类变量以用于机器学习和数据科学的指南

Andrea D'AgostinoTowards Data Science Andrea D'Agostino

·发布于 Towards Data Science ·13 分钟阅读·2023 年 6 月 16 日

--

图片由 Thomas Haas / Unsplash 提供

在数据科学或机器学习项目中处理分类变量并非易事。这种工作需要对应用领域有深入的了解,并且需要对多种可用方法有广泛的理解。

出于这个原因,本文将重点解释以下概念

  • 什么是分类变量及如何将它们划分为不同类型

  • 如何根据它们的类型将它们转换为数值

  • 处理这些变量的工具和技术主要使用Sklearn

适当处理分类变量可以大大改善我们预测模型或分析的结果。事实上,学习和理解数据的大部分相关信息可能都包含在可用的分类变量中。

想想表格数据,按变量gender或某种color进行划分。这些划分基于类别数量,可以揭示组之间的显著差异,从而为分析师或学习算法提供信息。

我们从定义分类变量是什么以及它们如何表现自己开始。

分类变量的定义

分类变量是用于统计学和数据科学中的一种变量,用于表示定性或名义数据。这些变量可以定义为一种类别或数据类别,这些数据无法连续量化,而只能离散表示。

例如,一个分类变量的例子可能是一个人的眼睛颜色, 可能是蓝色、绿色或棕色。

大多数学习模型无法处理分类格式的数据。我们必须先将它们转换为数字格式,以便信息得以保留。

分类变量可以分为两种类型:

  • 名义型

  • 有序

名义变量是没有严格顺序限制的变量。性别、颜色或品牌就是名义变量的例子,因为它们不能排序。

有序变量是将分类变量划分为逻辑上可排序的级别的变量。数据集中包含第一、第二和第三等级别的列可以被认为是有序的分类变量。

你可以通过考虑二元变量和循环变量来深入了解分类变量的分解。

二元变量很简单理解:它是一个只能取两个值的分类变量。

循环变量则具有值的重复特征。例如,星期几是循环的,季节也是如此。

如何转换分类变量

现在我们已经定义了分类变量是什么以及它们的样子,让我们通过一个实际的例子来解决转化它们的问题——一个名为cat-in-the-dat的 Kaggle 数据集。

数据集

这是一个开源数据集,作为对分类变量管理和建模的入门竞赛基础,称为分类特征编码挑战 II。你可以从下面的链接直接下载数据。

[## 分类特征编码挑战 II

二元分类,每个特征都是分类的(以及交互!)

www.kaggle.com](https://www.kaggle.com/c/cat-in-the-dat-ii?source=post_page-----854d0b65a6ae--------------------------------)

这个数据集的特殊性在于它仅包含分类数据。因此,它成为本指南的完美使用案例。它包括名义型、序数型、循环型和二元变量。

我们将看到将每个变量转换为学习模型可用格式的技术。

数据集如下所示:

作者提供的图片。

由于目标变量只能取两个值,因此这是一个二元分类任务。我们将使用 AUC 指标来评估我们的模型。

现在我们将应用处理分类变量的技术,使用上述提到的数据集。

1. 标签编码(映射到任意数字)

将分类转换为可用格式的最简单方法是将每个类别分配一个任意数字。

例如,ord_2列包含的类别

array(['Hot', 'Warm', 'Freezing', 'Lava Hot', 'Cold', 'Boiling Hot', nan],
      dtype=object)

可以使用 Python 和 Pandas 进行这样的映射:

df_train = train.copy()

mapping = {
    "Cold": 0,
    "Hot": 1,
    "Lava Hot": 2,
    "Boiling Hot": 3,
    "Freezing": 4,
    "Warm": 5
}

df_train["ord_2"].map(mapping)

>> 
0         1.0
1         5.0
2         4.0
3         2.0
4         0.0
         ... 
599995    4.0
599996    3.0
599997    4.0
599998    5.0
599999    3.0
Name: ord_2, Length: 600000, dtype: float64

然而,这种方法有一个问题:你必须手动声明映射。对于少量类别这不是问题,但对于大量类别可能会有问题。

为此,我们将使用 Scikit-Learn 和 LabelEncoder 对象以更灵活的方式实现相同的结果。

from sklearn import preprocessing

# we handle missing values
df_train["ord_2"].fillna("NONE", inplace=True)
# init the sklearn encoder
le = preprocessing.LabelEncoder()
# fit + transform
df_train["ord_2"] = le.fit_transform(df_train["ord_2"])
df_train["ord_2"]

>>
0         3
1         6
2         2
3         4
4         1
         ..
599995    2
599996    0
599997    2
599998    6
599999    0
Name: ord_2, Length: 600000, dtype: int64

映射由 Sklearn 控制。我们可以这样可视化它:

mapping = {label: index for index, label in enumerate(le.classes_)}
mapping

>>
{'Boiling Hot': 0,
 'Cold': 1,
 'Freezing': 2,
 'Hot': 3,
 'Lava Hot': 4,
 'NONE': 5,
 'Warm': 6}

注意上面的代码片段中的 .fillna(“NONE")。实际上,Sklearn 的标签编码器不处理空值,如果发现任何空值,应用时会产生错误。

正确处理分类变量时最重要的一点是始终处理空值。实际上,大多数相关技术在未处理这些值时无法正常工作。

标签编码器将任意数字映射到列中的每个类别,而无需显式声明映射。这很方便,但对某些预测模型引入了一个问题:如果列不是目标列,它引入了需要对数据进行缩放的需求

实际上,机器学习初学者经常问标签编码器和 one hot 编码器之间的区别,我们很快会看到。标签编码器从设计上应应用于标签,即我们想要预测的目标变量,而不是其他列。

说到这一点,一些在领域内非常相关的模型即使使用这种类型的编码也能很好地工作。我指的是树模型,其中 XGBoost 和 LightGBM 脱颖而出。

所以,如果你决定使用树模型,可以自由使用标签编码器,但否则,我们必须使用 one hot 编码。

2. One Hot 编码

正如我在我的文章中提到的机器学习中的向量表示,one hot 编码是一种非常常见且著名的向量化技术(即将文本转换为数字)。

它的工作原理是:对于每个存在的类别,创建一个仅可能包含 0 和 1 的方阵。这个矩阵通知模型,在所有可能的类别中,这一观察到的行具有由 1 表示的值。

一个例子:

 |   |   |   |   |   |   
-------------|---|---|---|---|---|---
 Freezing    | 0 | 0 | 0 | 0 | 0 | 1 
 Warm        | 0 | 0 | 0 | 0 | 1 | 0 
 Cold        | 0 | 0 | 0 | 1 | 0 | 0 
 Boiling Hot | 0 | 0 | 1 | 0 | 0 | 0 
 Hot         | 0 | 1 | 0 | 0 | 0 | 0 
 Lava Hot    | 1 | 0 | 0 | 0 | 0 | 0 

数组的大小为 n_categories。这是非常有用的信息,因为 one hot 编码通常需要稀疏表示转换后的数据。

这意味着什么?这意味着对于大量类别,矩阵也可能变得非常大。由于矩阵仅由 0 和 1 组成,并且只有一个位置可以被 1 填充,这使得 one hot 表示非常冗余和繁琐。

稀疏矩阵解决了这个问题——只保存 1 的位置,而不保存值为 0 的位置。这简化了上述问题,并使我们能够以极少的内存使用量保存大量的信息。

让我们看看在 Python 中这样一个数组的样子,再次应用之前的代码。

from sklearn import preprocessing

# we handle missing values
df_train["ord_2"].fillna("NONE", inplace=True)
# init sklearn's encoder
ohe = preprocessing.OneHotEncoder()
# fit + transform
ohe.fit_transform(df_train["ord_2"].values.reshape(-1, 1))

>>
<600000x7 sparse matrix of type '<class 'numpy.float64'>'
 with 600000 stored elements in Compressed Sparse Row format>

Python 默认返回一个对象,而不是值列表。要获得这样的列表,你需要使用 .toarray()

ohe.fit_transform(df_train["ord_2"].values.reshape(-1, 1)).toarray()

>>
array([[0., 0., 0., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 1.],
       [0., 0., 1., ..., 0., 0., 0.],
       ...,
       [0., 0., 1., ..., 0., 0., 0.],
       [0., 0., 0., ..., 0., 0., 1.],
       [1., 0., 0., ..., 0., 0., 0.]])

如果你不完全理解这个概念也不用担心:我们很快会看到如何将标签编码和独热编码应用于数据集,以训练预测模型。

标签编码和独热编码是处理分类变量的最重要技术。掌握这两种技术将使你能够处理大多数涉及分类变量的情况。

3. 转换和聚合。

将分类数据转换为数值格式的另一种方法是对变量进行转换或聚合。

通过.groupby()分组,可以将列中存在值的计数用作转换的输出。

df_train.groupby(["ord_2"])["id"].count()

>>
ord_2
Boiling Hot     84790
Cold            97822
Freezing       142726
Hot             67508
Lava Hot        64840
Warm           124239
Name: id, dtype: int64

使用.transform()我们可以将这些数字替换到相应的单元格中。

df_train.groupby(["ord_2"])["id"].transform("count")

>>
0          67508.0
1         124239.0
2         142726.0
3          64840.0
4          97822.0
            ...   
599995    142726.0
599996     84790.0
599997    142726.0
599998    124239.0
599999     84790.0
Name: id, Length: 600000, dtype: float64

也可以使用其他数学操作应用这种逻辑——应测试最能提高模型性能的方法。

4. 从分类变量创建新的分类特征。

我们将 ord_1 列与 ord_2 一起查看。

图片来源于作者。

我们可以通过合并现有变量来创建新的分类变量。例如,我们可以将 ord_1 与 ord_2 合并,以创建一个新特征。

df_train["new_1"] = df_train["ord_1"].astype(str) + "_" + df_train["ord_2"].astype(str)
df_train["new_1"]

>>
0                 Contributor_Hot
1                Grandmaster_Warm
2                    nan_Freezing
3                 Novice_Lava Hot
4                Grandmaster_Cold
                   ...           
599995            Novice_Freezing
599996         Novice_Boiling Hot
599997       Contributor_Freezing
599998                Master_Warm
599999    Contributor_Boiling Hot
Name: new_1, Length: 600000, dtype: object

这种技术几乎可以应用于任何情况。指导分析师的思想是通过向学习模型中添加原本难以理解的信息来提高模型的性能。

5. 将 NaN 作为分类变量使用。

很多时候,空值会被删除。我通常不推荐这样做,因为 NaNs 可能包含对我们的模型有用的信息

一种解决方案是将 NaNs 视为一个独立的类别。

再次查看 ord_2 列。

df_train["ord_2"].value_counts()

>>
Freezing       142726
Warm           124239
Cold            97822
Boiling Hot     84790
Hot             67508
Lava Hot        64840
Name: ord_2, dtype: int64

现在我们尝试应用.fillna(“NONE"),以查看有多少个空单元格存在。

df_train["ord_2"].fillna("NONE").value_counts()

>>
Freezing       142726
Warm           124239
Cold            97822
Boiling Hot     84790
Hot             67508
Lava Hot        64840
NONE            18075

作为百分比,NONE 约占整列的 3%。这是一个相当显著的量。利用 NaN 更有意义,并且可以使用之前提到的独热编码来完成。

跟踪稀有类别。

让我们记住 OneHotEncoder 的作用:它创建一个稀疏矩阵,其列数和行数等于引用列中唯一类别的数量。这意味着我们还必须考虑可能存在于测试集中但可能在训练集中缺失的类别

对于 LabelEncoder 情况类似——测试集中可能存在但训练集中没有的类别,这可能在转换过程中造成问题。

我们通过连接数据集来解决这个问题。这将允许我们将编码器应用于所有数据,而不仅仅是训练数据。

test["target"] = -1
data = pd.concat([train, test]).reset_index(drop=True)
features = [f for f in train.columns if f not in ["id", "target"]]
for feature in features:
    le = preprocessing.LabelEncoder()
    temp_col = data[feature].fillna("NONE").astype(str).values
    data.loc[:, feature] = le.fit_transform(temp_col)

train = data[data["target"] != -1].reset_index(drop=True)
test = data[data["target"] == -1].reset_index(drop=True)

图片来源于作者。

如果我们有测试集,这种方法对我们很有帮助。如果没有测试集,当新类别成为训练集的一部分时,我们将考虑使用类似 NONE 的值。

模型分类数据。

现在让我们开始训练一个简单的模型。我们将按照以下链接中关于如何设计和实现交叉验证的步骤进行 👇

## 什么是机器学习中的交叉验证

了解什么是交叉验证——构建通用模型的基本技术

towardsdatascience.com

我们从头开始,导入数据并使用 Sklearn 的StratifiedKFold创建折叠。

train = pd.read_csv("/kaggle/input/cat-in-the-dat-ii/train.csv")
test = pd.read_csv("/kaggle/input/cat-in-the-dat-ii/test.csv")

df = train.copy()

df["kfold"] = -1
df = df.sample(frac=1).reset_index(drop=True)
y = df.target.values

kf = model_selection.StratifiedKFold(n_splits=5)

for f, (t_, v_) in enumerate(kf.split(X=df, y=y)):
  df.loc[v_, 'kfold'] = f

这段小代码将创建一个包含 5 个组的 Pandas 数据框,以测试我们的模型。

作者提供的图像。

现在让我们定义一个函数,用于测试每个组上的逻辑回归模型。

def run(fold: int) -> None:
    features = [
        f for f in df.columns if f not in ("id", "target", "kfold")
    ]

    for feature in features:
        df.loc[:, feature] = df[feature].astype(str).fillna("NONE")

    df_train = df[df["kfold"] != fold].reset_index(drop=True)
    df_valid = df[df["kfold"] == fold].reset_index(drop=True)

    ohe = preprocessing.OneHotEncoder()

    full_data = pd.concat([df_train[features], df_valid[features]], axis=0)
    print("Fitting OHE on full data...")
    ohe.fit(full_data[features])

    x_train = ohe.transform(df_train[features])
    x_valid = ohe.transform(df_valid[features])
    print("Training the classifier...")
    model = linear_model.LogisticRegression()
    model.fit(x_train, df_train.target.values)

    valid_preds = model.predict_proba(x_valid)[:, 1]

    auc = metrics.roc_auc_score(df_valid.target.values, valid_preds)

    print(f"FOLD: {fold} | AUC = {auc:.3f}")

run(0)

>>
Fitting OHE on full data...
Training the classifier...
FOLD: 0 | AUC = 0.785

我邀请感兴趣的读者阅读有关交叉验证的文章,以更详细地理解所展示代码的工作原理。

现在让我们看看如何应用像 XGBoost 这样的树模型,它也与 LabelEncoder 配合得很好。

def run(fold: int) -> None:
    features = [
        f for f in df.columns if f not in ("id", "target", "kfold")
    ]

    for feature in features:
        df.loc[:, feature] = df[feature].astype(str).fillna("NONE")

    print("Fitting the LabelEncoder on the features...")
    for feature in features:
        le = preprocessing.LabelEncoder()
        le.fit(df[feature])
        df.loc[:, feature] = le.transform(df[feature])

    df_train = df[df["kfold"] != fold].reset_index(drop=True)
    df_valid = df[df["kfold"] == fold].reset_index(drop=True)

    x_train = df_train[features].values
    x_valid = df_valid[features].values

    print("Training the classifier...")
    model = xgboost.XGBClassifier(n_jobs=-1, n_estimators=300)
    model.fit(x_train, df_train.target.values)

    valid_preds = model.predict_proba(x_valid)[:, 1]

    auc = metrics.roc_auc_score(df_valid.target.values, valid_preds)

    print(f"FOLD: {fold} | AUC = {auc:.3f}")

# execute on 2 folds
for fold in range(2):
    run(fold)

>>
Fitting the LabelEncoder on the features...
Training the classifier...
FOLD: 0 | AUC = 0.768
Fitting the LabelEncoder on the features...
Training the classifier...
FOLD: 1 | AUC = 0.765

结论

总之,还有其他值得一提的技术来处理分类变量:

  • 基于目标的编码,即将类别转换为目标变量在对应类别下的平均值

  • 神经网络的嵌入,可用于表示文本实体

总结一下,正确管理分类变量的关键步骤如下

  • 总是处理空值

  • 根据变量的类型和我们想使用的模板应用 LabelEncoder 或 OneHotEncoder

  • 从变量丰富的角度考虑,将 NaN 或 NONE 视为可以告知模型的分类变量

  • 建模数据!

感谢你的时间,

安德烈亚

推荐阅读

对于感兴趣的读者,以下是我推荐的每个机器学习相关主题的书单。这些书在我看来是必不可少的,并且对我的职业生涯产生了重大影响。

免责声明:这些是亚马逊的附属链接。我将从亚马逊获得少量佣金,以推荐这些商品给你。你的体验不会改变,你也不会被多收费,但这将帮助我扩大业务,并制作更多关于 AI 的内容。

有用的链接(由我编写)

如果你想支持我的内容创作活动,请随意通过下面的推荐链接加入 Medium 的会员计划。我将从你的投资中获得一部分,同时你也可以无缝访问 Medium 上关于数据科学等领域的大量文章。

## 通过我的推荐链接加入 Medium - Andrea D'Agostino

作为 Medium 会员,你的部分会员费用将分配给你阅读的作者,并且你可以全面访问每一个故事……

medium.com

数据分析师成功机器学习模型部署指南

原文:towardsdatascience.com/guide-to-successful-ml-model-deployment-for-data-analysts-e5b893260926

照片由 ray rui 提供,来自 Unsplash

数据模型部署如何不同于其他分析项目

Olivia TanuwidjajaTowards Data Science Olivia Tanuwidjaja

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

--

恭喜你创建了一个看似结果可靠的机器学习模型!我从经验中知道,获得可靠的机器学习模型并不容易,特别是在作为数据分析师且接触有限的情况下。然而,一旦模型完成,下一个挑战就是将其投入生产,以便集成到产品中。

对于许多数据分析师来说,机器学习模型的部署可能是未涉足的领域,因为大多数数据建模概念仅用于分析目的,而未在生产中部署。虽然一些公司有机器学习运维团队来帮助这些部署,但分析师了解相关的关键概念和注意事项,以便与机器学习运维团队有效合作是至关重要的。

为了弥补这一知识差距,在这篇文章中,我将分享数据分析师在将模型投入生产时需要了解的关键概念和注意事项。我还将解释机器学习模型的部署如何不同于常规分析项目。了解这些内容后,数据分析师可以确保他们的模型成功部署并提供有意义的结果。

数据分析师的机器学习模型部署注意事项

作为数据分析师,我们习惯于消耗历史数据进行离线分析。大多数时候,我们会花几分钟从在线数据库中查询数据,再花几个小时或几周处理(分析)这些数据。对于每个项目,处理和查询的数据可能有所不同。找到一个一次性的分析项目是不奇怪的,不需要可重复性

当我们谈论用于产品集成的机器学习模型时,情况会变得非常不同。需要考虑几个原则,包括

  • 延迟:模型提供输出所需的时间,从检索特征到交付预测结果的时间。完成工作的时间越长,输出被客户使用的时间也越长,这会影响用户体验。

  • 数据可用性:在模型触发时可用的数据/特征集合。有些数据可能不存在(或可能在延迟限制内被查询)。

  • 鲁棒性:模型在使用新数据与训练数据时性能变化的程度。理想情况下,鲁棒的模型即使面对新的真实数据也会保持稳定的性能。然而,确实有时候模型性能可能会随着时间的推移而下降,需要对模型进行重新训练。

这些因素在一次性分析世界中几乎不存在,但在机器学习部署世界中非常关键。

数据和特征工程管道

准备/处理数据用于分析与建模(部署)之间的一个显著区别是数据管道和随后的特征工程。如上所述,我们在开发数据管道时需要考虑延迟和数据可用性约束。

在模型输出被纳入产品时,延迟要求可能会变得非常严格。假设你正在构建一个ETA 预测模型,模型输出需要在几秒钟内完成,以避免对用户体验的任何干扰。因此,你只能使用在该时间内可以获得的特征,并且需要一个可靠的管道来支持这一点。一些常见的做法包括:

  • 使用特征存储 特征存储是一个集中平台,用于存储来自不同数据源的特征。它有助于(1)可重复性,因为一致的特征可以在训练和推理之间使用,以及(2)特征在多个生产模型中的可重用性。它可以设计用于在线服务,以最大化服务速度。特征存储及其架构在决定性能方面发挥重要作用——无论是服务速度还是准确性。

特征存储示例(来源:feast.dev)

  • 组装预处理步骤。在输入数据被插入模型之前,通常需要进行多个预处理步骤——从数据清洗到填补和归一化。由于需要进行多个步骤,这些步骤存在可读性和调试风险。组装这些步骤,例如使用 sklearn 中的管道 可以帮助组织这些步骤,使其对用户更清晰,减少错误的发生。

持续监控和不断改进

与分析工作不同,在数据建模中,模型部署或集成后,工作并未完全完成。在某些情况下,工作甚至才刚刚开始 😆

就像其他数字产品一样,你需要随着时间的推移来维护和改进数据模型。模型只能理解与其进行训练的数据动态。随着市场和用户行为的变化,模型可能会变得与预测/分类不相关,因为它未能适应这些新的数据分布

ML 模型的性能会随着时间的推移而下降。来源:ML Ops

因此,需要对模型进行持续监控和不断改进,以确保模型预测结果的质量。

需要持续监控的 ML 模型方面包括:

  • 输入数据分布的偏差。 在大多数情况下,模型的不相关性发生是因为环境/市场变化(即用户行为),这导致数据分布的变化。考虑一个电子商务平台,它有一个欺诈检测模型,该模型使用每个客户每天的总订单作为主要预测因素来识别滥用客户。随着平台扩展其产品种类,客户订单量意外增加,这可能导致一些合法客户被模型错误地识别为滥用客户。为避免这种情况,需要持续监控输入特征的统计分布,例如均值或标准差

当变化是持续的,并且偏差开始影响性能时,可能需要考虑一些改进措施。

  • 模型性能。 模型性能(模型准确率、精确度、召回率是最重要的监控方面,但有时也是最难监控的。原因是有些情况下我们没有经过验证的标签来与模型的预测进行比较,因为模型处理的是新数据。为了实现性能监控,我们可以使用(1)代理指标或(2)基准评审。代理指标是可以间接显示模型影响的产品指标,如推荐模型的客户点击/转化率。基准评审是对模型输入和输出的抽样进行审查和手动标记,例如在为模型训练准备的数据中。

  • 系统性能。 部署的数据模型不仅涉及模型本身,还包括数据管道中的其他组件,如特征存储、数据库和产品 API。对于分布式系统来说,常见的指标有流量、延迟、错误和饱和度

在更高级的组织中,持续模型训练、集成和部署可以考虑用来应对数据和业务环境的快速变化。

结束语

随着数据分析师提升技能,成为全栈数据分析师,他们可能会发现自己在将洞察力转化为数据产品,包括数据模型。

然而,必须认识到交付(部署)数据模型交付(展示)数据洞察力是两个完全不同的领域。

在操作化过程中,有必要考虑持续监控和改进要求。通过理解这些概念,数据分析师可以确保交付的模型能够随着时间的推移满足产品/组织的需求。

引导你的受众:在演示中创建引人入胜的叙事

原文:towardsdatascience.com/guide-your-audience-crafting-a-cohesive-narrative-from-the-data-4032538ff252

如何使用叙事技巧结构化你的数据展示

John LenehanTowards Data Science John Lenehan

·发布于 Towards Data Science ·14 分钟阅读·2023 年 11 月 2 日

--

这篇文章是关于制作技术演示的一系列文章中的第二篇,其中涵盖了我用来创建引人入胜的演讲的三个关键概念——了解你的受众、引导你的受众和为回应做准备。要阅读第一篇文章,你可以点击下面的链接:

## 了解你的受众:准备技术演示的指南

一种结构化的方法来创建符合利益相关者需求和关注点的地址

[towardsdatascience.com

图片由 Teemu Paananen 提供,来源于 Unsplash

令人难忘的演示的标志是引人入胜的叙事。演讲者从一开始就吸引观众,保持稳定的演讲节奏,专注于关键细节,并在故事的结尾有明确的总结。这种框架之所以具有如此普遍的吸引力并非秘密——叙事结构在各个文化中都可以找到,超越了种族和经济背景。它们触及了人类认知和情感的基本方面,使它们成为沟通、教育和说服的有效工具。此外,以这种叙事格式呈现的信息由于其结构而更容易记住,通常遵循逻辑顺序——听众更有可能记住作为故事的一部分或形成可识别模式的细节。

Melyna Valle拍摄,来自Unsplash

通过将复杂的信息组织成这些叙事结构,可以将复杂的想法简化,并以连贯的方式呈现,使其更易于理解。将信息以逻辑情节的形式进行背景化,为演讲者提供了一种强有力的方法来吸引观众,保持他们的兴趣,并确保他们在离开讲堂后仍然记住你的演讲。

指导你的观众

在这篇关注技术演示的第二篇文章中,我将描述如何将叙事结构融入你的演示中,以及如何将这个故事传达给你的观众,以保持他们的参与和对你信息的接受。这将主要从数据传递技术的角度出发,例如向可视化中添加叙事元素、用标注和注释对复杂信息进行背景化——尽管我也会涉及保持演讲集中于你的核心信息,以及如何在传递数据时利用语气。此外,我还将描述叙事可视化中使用的不同方法,并展示使用 Matplotlib 开发主导叙事的典型方法。

由 Alexandre Pellaes 拍摄,来自 Unsplash

指导原则

基础要做到位

在深入探讨我通常使用的叙事可视化技术之前,让我们首先明确一些准备演示的指导原则:

  1. 定义你的核心信息: 从一开始就确立你演示的主要目标——你的核心信息概括了你打算传达的基本思想,作为你演示中每一个方面的指导原则。明确的消息使你的目的更加清晰,并确保每一张幻灯片、每一句话和每个视觉元素都协同工作,形成一个统一而有影响力的演示。

  2. 组织你的内容: 组织良好的演示逻辑进展,引导观众通过一个连贯的思想序列。以引人注目的引言开始,抓住他们的注意力,然后是系统地讨论主要观点的主体部分,最后以强有力的总结结束,强化你的关键信息。你演示的每个部分都应无缝地过渡到下一个部分,提供自然的流动性,并增强你信息的整体一致性。

  3. 明智地使用视觉: 视觉是展示信息的强大工具。在审慎使用时,图表等视觉效果可以展示你的关键点,增强整体理解。关键在于选择直接支持你信息的视觉,确保它们清晰、相关且易于解读。视觉应该作为补充你演讲的基本组成部分,为你的展示增添深度。对于数据密集的演示来说,这一点尤为重要——在下一部分将详细讨论这一点。

  4. 保持专注: 尊重观众的时间和注意力,避免偏离主题或深入不必要的细节。专注的演示简洁、相关且直接——它要求思路和表达的清晰,避免可能削弱信息影响的离题。这确保你的观众从你的演讲中获取最重要的信息,加强你核心信息的意义。

掌握演讲交付的基本要点并非易事——这些基本准则构成了有影响力的演示的基础,促进了讲者与听众之间的沟通。现在让我们来看看如何通过使用叙事可视化技巧来提升演示中的图形,为观众提供背景。

图片由airfocus拍摄,发布在Unsplash上。

介绍视觉叙事

一图胜千言

首先,定义一下——叙事可视化(或 NarViz)是数据可视化与叙事技巧的结合,融合了支持讲故事的技巧和数据可视化技术。根据Edmond 和 Bednarz的说法,叙事可视化有 3 种不同的路径:

  1. 引导叙事: 引导叙事是一种结构化的故事情节,明确引导观众对数据的解读。这是一种清晰而有意的叙述,驱动观众的注意力和理解。叙述者的视角是这种方法中的主导观点。

  2. 综合叙事: 综合叙事将故事情节巧妙地融入到可视化中,让观众在研究和解读数据的同时,自己发现潜在的叙事。这种叙述方式强调探索——观众在发现叙事时发挥关键作用,参与度低。像 PowerBI 和 Tableau 这样的商业智能应用是这种讲故事形式的典型例子,互动式仪表盘允许观众自行深入数据。

  3. 支持叙述: 支持叙述提供额外的背景、解释或补充信息,增强观众对数据的理解,而不强加特定的故事情节。通常,支持叙述提供背景信息,而不对内容采取任何特定的观点——从这个意义上讲,这可以说根本不是一种叙述,因为对数据没有采取任何特定的视角(叙述者或观众)。

当涉及将信息传递给更广泛的受众时,传统的方法是使用主导叙述——你(作为叙述者)掌控故事,并可以决定观众对呈现的数据采取的视角。

照片由Matthew Osborn拍摄,发布在Unsplash

举个例子,下面是准备关于气候变化数据的 matplotlib 图表的步骤,这些数据展示为主导叙述的形式。这些数据(以及大量其他气候数据)可以在网站Our world in Data上自由获取,供任何人自行探索。此外,我对这些数据的探索细节可以在我的 github 项目文件夹这里找到。我将使用这些数据展示如何将主导叙述元素迭代地融入你的可视化中,准备数据讲述你希望它讲述的故事,并使其对观众更具可理解性。

首先,让我们使用 matplotlib 创建一个简单的折线图,展示全球平均气温和 CO2 排放量随时间的变化,分别设置两个坐标轴以比较这些数据。

# Creating primary axis (CO2 emissions)
fig, ax1 = plt.subplots(figsize=(15, 9))
ax1.plot(climate_data['Year'], climate_data['CO₂ emissions (billions of tonnes)'],
         color='steelblue',linewidth=2, label='CO2 emissions (billions of tonnes)')
ax1.set_xlabel('Year',weight='bold',fontsize=11)
ax1.set_ylabel('CO2 Emissions (Billions of Tonnes)',color='blue',weight='bold',fontsize=11)

# Adjusting gridlines on primary y-axis
ax1.grid(color='blue', linestyle='--', linewidth=0.5, alpha=0.3)

# Creating secondary y-axis (relative avg temp)
ax2 = ax1.twinx()
ax2.plot(climate_data['Year'], climate_data['avg temp'],
         color='salmon', linewidth=2, label='Average Temperature (°C)')
ax2.set_ylabel('Average Temperature (°C)',color='red',weight='bold',fontsize=11)

# Setting x-axis limits for ax
ax1.set_xlim(1850, 2022)

# Setting y-axis limits for both ax1 and ax2
ax1.set_ylim(0, 1.15 * max(climate_data['CO₂ emissions (billions of tonnes)']))
ax2.set_ylim(0.999 * min(climate_data['avg temp']), 16.1)

# Combining legends for both axes
lines, labels = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax2.legend(lines + lines2, labels + labels2, loc='upper right',fontsize=10)

# Set title
plt.title('Global CO2 Emissions compared to Average Global Temperature, 1850-2021',weight='bold',fontsize=14)

# Show plot
plt.show()

运行此脚本,我们得到以下图表:

简单的图表展示了 CO2 排放量和全球气温随时间的上升趋势(图片由作者提供)

从这个图表中,我们可以看到平均气温的上升似乎与全球 CO2 排放的增加相关——然而,这对被动观察者来说并不立即显而易见,需要观众付出一些努力来识别这种趋势。让我们在图表中引入趋势线,通过对每个数据集近似非线性函数来实现。Numpy 的 polyfit 函数非常适合在 python 中应用——以下代码片段被添加到原始脚本中,以将这些样条曲线添加到图形中(经过一些反复试验,使用了 3 的系数,以获得最佳拟合线):

# Spline for CO2 data
theta1 = np.polyfit(climate_data['Year'], 
                    climate_data['CO₂ emissions (billions of tonnes)'],3)
CO2_spline = theta1[3]+theta1[2]*pow(climate_data['Year'],
                                     1)+theta1[1]*pow(climate_data['Year'],
                                                      2)+theta1[0]*pow(climate_data['Year'],3)

#plotting CO2 spline
ax1.plot(climate_data['Year'], CO2_spline,
         color='blue', linestyle ='dashed', linewidth=2, label=None)

# Spline for temp data
theta2 = np.polyfit(climate_data['Year'], 
                    climate_data['avg temp'],3)
temp_spline = theta2[3]+theta2[2]*pow(climate_data['Year'],
                                      1)+theta2[1]*pow(climate_data['Year'],
                                                       2)+theta2[0]*pow(climate_data['Year'],3)

现在观众更容易看到两个数据集的上升趋势:

在图形中添加样条曲线以显示上升趋势(图片由作者提供)

到目前为止看起来不错——但如何在数据中显示一个“好坏”指标(或在这种情况下是“坏”)?假设你想在图表上展示工业化前的平均温度(这是气候科学中的一个常用标准),以及平均温度增量的限制(在这种情况下为 +1、+1.5 和 +2 摄氏度)。这些限制来自于《巴黎气候协议》,该协议要求各国努力将全球变暖控制在工业化前水平以上不超过 1.5 摄氏度。可以使用 matplotlib 的 axhline 函数来引入这些限制——下面是将其添加到初始脚本中的一个示例:

# Adding horizontal constant line at 14C (pre-industrial mean temp)
ax2.axhline(y=14, color='black', linestyle='--',alpha=cnst_alpha)
pre_ind_label = 'Pre-industrial Temperatures'
ax2.text(1882,14.03,pre_ind_label,color='black',alpha=cnst_alpha)

#Adding horizontal constant line at 15.5C (+1.5C Mean Temp)
ax2.axhline(y=15, color='darkgoldenrod', linestyle='--',alpha=cnst_alpha)
increase_label1 = '+1°C increase'
ax2.text(1882,15.03,increase_label1,color='darkgoldenrod',alpha=cnst_alpha)

# Adding horizontal constant line at 15C (+1C mean temp)
ax2.axhline(y=15.5, color='red', linestyle='--',alpha=cnst_alpha)
increase_label2 = '+1.5°C increase'
ax2.text(1882,15.53,increase_label2,color='red',alpha=cnst_alpha)

#Adding horizontal constant line at 16C (+2C Mean Temp)
ax2.axhline(y=16, color='darkred', linestyle='--',alpha=cnst_alpha)
increase_label3 = '+2°C increase'
ax2.text(1882,16.03,increase_label3,color='darkred',alpha=cnst_alpha)

基于这些限制,我们可以看到图表上我们正接近 1 摄氏度的增幅,全球排放量没有放缓的迹象——如果我们想保持在《巴黎气候协议》规定的 1.5 摄氏度限制以下,这并不好:

向图形中添加 x 轴限制(图像由作者提供)

同样地,假设我们想要在时间轴上展示一些关于气候变化的重要条约和组织发生的时间——我们可以使用 matplotlib 的 axvline 函数应用垂直常数线。在这种情况下,我选择展示气候变化政府间专门委员会(IPCC)成立的日期、《京都议定书》通过的日期,以及《巴黎气候协议》生效的日期:

#Adding vertical constant line at 1988 (IPCC)
plt.axvline(x=1988, color='darkgreen',linestyle='--',alpha=cnst_alpha)
IPCC_label='IPCC (1988)'
ax2.text(1986,13.5,IPCC_label,color='darkgreen',weight='bold',fontsize=10,rotation=90,alpha=cnst_alpha)

#Adding vertical constant line at 1995 (Kyoto)
plt.axvline(x=1997, color='darkgreen',linestyle='--',alpha=cnst_alpha)
kyoto_label='Kyoto (1997)'
ax2.text(1995,13.5,kyoto_label,color='darkgreen',weight='bold',fontsize=10,rotation=90,alpha=cnst_alpha)

#Adding vertical constant line at 2015 (Paris)
plt.axvline(x=2015, color='darkgreen',linestyle='--',alpha=cnst_alpha)
paris_label='Paris (2015)'
ax2.text(2013,13.5,paris_label,color='darkgreen',weight='bold', fontsize=10,rotation=90,alpha=cnst_alpha)

现在图表显示了所有这些重要事件发生的时间,如下所示,这使得政府决定采取一些气候变化行动的时间点变得清晰:

向图形中添加 y 轴限制(图像由作者提供)

在 matplotlib 图形的任一轴上显示重要区域的另一种方法是使用 span 函数来展示范围或时期。对于这个图表,我决定展示被称为第二次工业革命(或技术革命)的时间段,以及当前人类驱动的气候变化时期(人类世)。为此,我在图表上应用了 axvspan 函数,如下片段所示:

# Specifying regions to shade on the x-axis
plt.axvspan(1870, 1910, alpha=0.1, color='darkorange', 
            label='Second Industrial Revolution')
plt.axvspan(1945, 2022, alpha=0.1, color='royalblue', 
            label='The Anthropocene')

现在图表展示了这些重要的历史时期,这有助于展示人类活动如何与气候变化相关联的整体叙述。

向图形中添加阴影区域(图像由作者提供)

最后,假设我们要指出过去两个世纪的关键发展,这些发展导致了碳排放(及随之而来的全球温度)的增加;这可以通过使用注释来完成。下面,我列出了若干个关键事件和日期,然后将这些注释添加到图表中,并指出了该年的平均温度。请注意,对于使用多个坐标轴的图表,有必要指定应用注释的轴,如下所示:

#adding callouts for key dates
# Points to annotate (x, y, annotation_text)
callouts = [(1886, climate_data.loc[climate_data['Year'] == 1886, 'avg temp'].values[0], 'Automobile \ninvented'),
            (1903, climate_data.loc[climate_data['Year'] == 1903, 'avg temp'].values[0], 'Airplane \ninvented'),
            (1921, climate_data.loc[climate_data['Year'] == 1921, 'avg temp'].values[0], 'Automobiles in America\nexceed 10 million'),
            (1938, climate_data.loc[climate_data['Year'] == 1938, 'avg temp'].values[0], 'Rise in global\ntemperatures proven;\nOil discovered in\nSaudi Arabia'), 
            (1968, climate_data.loc[climate_data['Year'] == 1968, 'avg temp'].values[0], 'Melting ice caps \npredicted'), 
            (1985, climate_data.loc[climate_data['Year'] == 1985, 'avg temp'].values[0], 'Ozone hole \ndiscovered'),
            (1994, climate_data.loc[climate_data['Year'] == 1994, 'avg temp'].values[0], '1st climate change \ntreaty signed into law'),
            (2003, climate_data.loc[climate_data['Year'] == 2003, 'avg temp'].values[0], 'European heatwave \nkills 70,000+'),
            (2021, climate_data.loc[climate_data['Year'] == 2021, 'avg temp'].values[0], 'EU climate law\nratified')
           ]

# Iterate over specified points and add minimal annotations
for point in callouts:
    x_point, y_point, annotation_text = point
    ax2.annotate(annotation_text, xy=(x_point, y_point), xytext=(x_point - 35, y_point + 0.7),
                 arrowprops=dict(arrowstyle='-', color='black'),
                 fontsize=9, weight='bold')  # Set font size for annotation text

现在,这张图表讲述了有关气候变化的更详细的故事,以及为何会发生这种变化。在第二次工业革命期间,汽车和飞机的发明、它们的广泛使用以及在中东发现的大量石油,导致了 1945 年人类世时期的开始。二氧化碳排放量和全球温度的急剧上升引发了一些令人担忧的气候预测和事件,促使各国政府通过立法努力遏制和减少气候变化。

向图表添加标注(图片由作者提供)

通过添加注释和结构化数据以帮助理解,即使是最复杂的主题也能被听众轻松理解。这在试图在时间有限的大型群体中描述这些概念时尤为重要,这也是大多数演讲的情况——因此,使用这些引导叙事元素对于智能且富有洞察力的演讲至关重要。

关于语调的一些笔记

不是你说什么,而是你如何说

最后,我想花几分钟来扩展一下在演讲时语调的重要性——这是一个关键点,确保你的听众能完全理解你所讲述的内容。1967 年 Mehrabian 的研究 表明,沟通中只有 7%是通过语言进行的——38%基于声音语调,55%基于面部表情。因此,为了传达你的完整信息,要注意你使用的语调以及你说话时的表情。

在他的书《绝不妥协》中,前 FBI 人质谈判专家克里斯·沃斯谈到了一个叫做“深夜 FM DJ 声音”的概念——这种低沉缓慢的声音非常适合他曾参与的紧张局势,它将讲述者和听众都带入平静而接纳的心理状态。你可能会想,FBI 谈判专家的建议与您的演讲有什么关系——这种语调通常是与利益相关者讨论紧张或有争议的点时所需的方式,清晰、稳重的表达对于使自己清楚地传达信息并向听众展现平静的形象至关重要。

继续我们之前关于绘制气候变化数据的示例——对于一个假设的观众,这些信息应以冷静但乐观的语调呈现,以强调情况的严重性,同时突出迄今为止所采取的步骤以及仍需完成的工作。通过这种方式,观众从这次演讲中得到两个关键点——我们面临的气候紧急情况是真实且严重的威胁,并且仍有措施可以减轻或消除气候变化带来的威胁。

由 Ioann-Mark Kuznietsov 在 Unsplash 上拍摄

总结

总之,本文提供了我在准备演讲时使用的基本原则的高层次概述,包括定义你的关键消息、组织你的要点和保持主题。我描述了从数据中讲述叙事的不同方法,然后逐步讲解了使用 matplotlib 绘制气候数据趋势图来准备引导叙事的关键技巧,使数据背后的故事对观众更为明显。最后,我简要说明了向潜在观众传递这些数据时的语调,确保以合适的节奏和重点传达完整的信息。通过将这些技巧融入到你的演讲中,你将拥有适当引导观众理解数据的工具,并确保你的演讲产生持久的影响。

参考文献

[1] Edmond, C., & Bednarz, T. (2021). 叙事可视化的三种轨迹。视觉信息学, 5(2021), 26–40。

[2] 英国气象局哈德利中心. (2023). 全球平均温度异常. 获取自 ourworldindata.org/grapher/temperature-anomaly

[3] 全球碳预算 (2022). 年度 CO2 排放量. 获取自 ourworldindata.org/co2-emissions#global-co2-emissions-from-fossil-fuels

[4] Hansen, J., 等 (2010). 全球地表温度变化. 地球物理学评论, 48. 获取自 onlinelibrary.wiley.com/doi/10.1029/2010RG000345/abstract (访问时间:2023 年 10 月 21 日)。

[5] 联合国. (2015). 巴黎协定 第 2.1 条(a). 获取自 unfccc.int/sites/default/files/english_paris_agreement.pdf (访问时间:2023 年 10 月 20 日)。

[6] 联合国, 欧洲委员会 (无日期). 气候变化谈判指南. 获取自 www.europarl.europa.eu/infographic/climate-negotiations-timeline/index_en.html (访问时间:2023 年 10 月 21 日)。

[7] Mokyr, J. (1998). 第二次工业革命,1870–1914。检索自 faculty.wcas.northwestern.edu/jmokyr/castronovo.pdf(访问日期:2023 年 10 月 21 日)。

[8] Chua, L., & Fair, H. (2019). 人类世。载于 F. Stein(编),人类学开放百科全书。为剑桥人类学百科全书的首版影印本。检索自 doi.org/10.29164/19anthro(访问日期:2023 年 10 月 22 日)。

[9] Roos, J. (2022). 改变世界的 7 项镀金时代发明。检索自 www.history.com/news/most-important-gilded-age-inventions(访问日期:2023 年 10 月 22 日)。

[10] Statista (1993). 1900 年至 1988 年美国在用乘用车和商用汽车的数量。检索自 www.statista.com/statistics/1246890/vehicles-use-united-states-historical/(访问日期:2023 年 10 月 22 日)。

[11] UK Research and Innovation. (2021). 气候变化发现简史。检索自 www.discover.ukri.org/a-brief-history-of-climate-change-discoveries/index.html#group-section-1990s-2000s-bwnXtJHqpd(访问日期:2023 年 10 月 22 日)。

[12] Mehrabian, A., & Wiener, M. (1967). 不一致交流的解码。人格与社会心理学杂志,6(1),109–114。 doi.org/10.1037/h0024532

[13] Voss, C., & Raz, T. (2017). 绝不妥协。随机房出版社商业图书。

有指导的迁移学习:如何利用“侦察的力量”提升机器学习表现

原文:towardsdatascience.com/guided-transfer-learning-how-to-use-the-power-of-scouts-to-boost-machine-learning-performance-7e478d1ec5e4

对训练神经网络的革命性新方法的独家预览

Katherine MunroTowards Data Science Katherine Munro

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

--

在这一新提议的技术中,小型“侦察模型”被派遣去探索问题领域,并“反馈”给主要模型。图片由 @ansgarscheffold 在 Unsplash 上提供。

我的好朋友和谦逊的天才 Dr Danko Nikolić 最近与我分享了一篇未发表的论文,认为我可能会感兴趣。我确实感兴趣了。阅读它让我感觉像是在目睹一个历史时刻,并且我迫不及待地想要分享。幸运的是,Danko 同意了。所以这是我将一种可能革新深度神经网络训练的方法翻译成日常语言的版本。它甚至还没有发布到 arXiv 上(更新:现在已经发布了!),但 NASA 已经在使用它。所以一旦它爆火,记住:你首先在这里听到的。😉

从问题开始

我相信你知道:机器学习,尤其是深度神经网络,需要大量的数据、计算能力和模型参数。这使得这些技术只有最富有的公司和研究机构才能使用,因此将开发塑造我们技术未来的 AI 技术的权力集中在少数人手中。并不酷。

为什么会有这个问题

当我们为某个任务训练神经网络时,实际上是在通过成千上万个例子教它如何调整自身的权重和偏差,以便输入网络的信息产生另一种所需的信息输出。这些权重和偏差统称为“参数”,一个网络可以拥有数百万甚至数万亿个这样的参数。由于“参数空间”如此庞大,学习到正确的参数非常困难,因为数量实在太多。

由于无法尝试每种可能的参数值组合,我们尝试做出好的猜测。一个典型的机器学习算法会寻找有关每个参数如何变化的“提示”,然后根据这些提示进行调整,再根据调整的成功程度寻找新的提示。这些算法中最著名的是“梯度下降”。

如果你已经对梯度下降有所了解,可以跳过这一段。否则,你可以将其想象为这样工作:问题空间是一个有丘陵和山谷的地形,而我们是一个在这个地形上滚动的球,试图到达最低的山谷底部。我们称之为“全局最小值”。所以我们的算法会根据周围地面的陡峭程度、倾斜方向等信息来决定滚动的方向。希望它朝着最低的点前进。但也有危险:我们可能会滚入一个“并不是特别低”的沟壑中,并陷入困境。我们称之为“局部最小值”,这是一个不好的位置。

神经网络使用梯度下降来学习解决各种问题。不幸的是,一些研究表明,这些网络的性能提升有限,之后需要再次增加规模和数据,而这种趋势似乎遵循一种“幂律”。简单来说,这意味着“小幅度的‘智能’提升需要大量的资源增加”。反过来说,我们投入这些模型的资源回报正在递减。

理论上的希望……和更多的痛苦

理论上,更小的网络也可以完成任务,但必须通过人工构建或由专门为特定任务设计的算法学习。另一方面,梯度下降可以应用于大型模型,学习解决更广泛的问题,但在小模型中找到合适的参数会面临困难。

它之所以感到困难,是因为它无法看到在任何给定的训练周期中对参数的更改将如何影响后续的性能。我们,这个在问题空间中滚动的球,可能会被诱使向右滚动,因为那里地面坡度最陡。但这可能会直接带我们到一个令人害怕的局部最小值!也许继续直行会更好——即使从我们站的位置看起来不那么陡——因为最终会带我们到所有山谷中最深的那个。

梯度下降试图通过增加模型参数的数量来解决这个问题,因为更多的维度只是减少我们陷入困境的统计可能性。然而,更多的参数可能使模型“过拟合”(即记住当前问题的答案,但之后对其他问题的概括能力变得无用)。因此,我们最终不得不增加相应的训练数据,以帮助训练出的模型具有更好的泛化能力。但这种不断增加资源的需求是不可持续的。必须有更好的方法!

指导性迁移学习…来拯救!

Nikolić博士和他的同事 Davor Andrić及 Vjekoslav Nikolić(是的,他们是兄弟!)提出了一个我认为相当巧妙的解决方案。他们将其比作将侦察员送入问题领域,只不过在这里,“侦察员是解决这个领域中更小但相关问题的模型。他们深入探究并带回关于他们成功旅行方向的信息。” 侦察员不太可能陷入局部极小值,因为他们的问题更容易解决。而且由于他们解决的是较小的问题,我们可以提供更多数据给他们工作,这使得他们不容易过拟合。最终,他们将知识转移回主模型。可爱,对吧?

论文中对此的描述最为恰当,所以我将其转述:局部极小值就像山中的湖泊;降雨水仅凭局部视角向下流动,这可能导致其陷入困境。指导性迁移学习(GTL)就像沿着山坡侧行走,而不是直接向最近的山谷下滑。有时山谷是一个陷阱,而侦察员知道这一点。

那么它是如何工作的呢?

侦察员被“派遣”去解决比主模型更简单的问题。例如,如果整体任务是将输入分类为十个不同的类别中的一个,那么每个侦察员模型可能会被分配一个只包含这三个类别的数据子集进行分类。简单的问题减少了侦察员模型过拟合的机会,因此,它们代表主模型获得的“知识”更可靠。

降低过拟合风险的另一种策略是给侦察员更多的数据。这也具有类似于预训练的知识迁移好处,我将在下一部分中介绍。

这次侦察任务的结果是所谓的“指导矩阵”,它告诉主模型对于每个参数,这个参数的重要性。一个低值意味着侦察员在该维度(参数)上没有发现太多变化,因此改变它可能对整体解决方案贡献不大。例如,侦察员走了这条路但坡度保持相当平缓,所以他们放弃了。一个大值意味着更戏剧性和潜在有用的变化。例如,一个侦察员走了一条看起来很平坦的小径,然后几乎绊倒在悬崖边缘!很可能向那个方向移动受影响的参数是个好主意,对吧?

这种方法的好处在于,它在数学和实现代码方面都非常简单。基本上,梯度下降始终忙于计算每个参数变化的值,使用它对周围倾斜地形的所有信息。假设参数是一个网络权重 w。我们称这种变化的值为Δ_w(其中 _ 表示后续字母是下标),或称为‘delta w’。引导矩阵有一个相应的引导值,称为 g_w。因此,引导变化Δw_g,就是:

就这些,我将涵盖的数学就是这些(在论文第 5 页上还有一点点更多,第 7 页上作者以非常简单的方式讨论了计算引导矩阵的不同方法。但我给你的已经是理解这个思想所需的所有数学)。代码也同样简单。如果你不是程序员,放心,下面的代码非常简单易懂:

loss.backward()
for name, param in model.named_parameters():
  param.grad *= guidance_mask[name]
optimizer.step()

但传统的预训练呢?

GPT-4 中的‘PT’代表‘预训练’,所以如果你还不知道,这可是大事。其思想是在某个庞大而通用的数据集和任务上首先训练神经网络,然后在更具体的数据集和任务上进行微调。例如,GPT 和其他大型语言模型通常会预训练以预测句子中缺失的单词(即那些被人类操控者随机移除的单词),使用从几乎整个互联网抓取的文本。这使得模型对不同上下文中最常出现的单词有一个相当好的了解(从统计学角度讲,当然不是认知上的)。之后,模型会在实际任务上进行微调,例如问答任务,使用更小且更具体的数据集。从原始任务中获得的一般‘知识’往往能提升下游任务的表现;我们称之为‘迁移学习’。

迁移学习通过预训练有助于应对许多,但不一定全部,可能存在的局部最小值。因此,引导迁移学习通过帮助探索新的问题领域以避免来补充预训练。这两种方法不一定需要结合,但当它们结合时,效果可能会非常显著。

听起来不错,但有效吗?

论文首先展示了指导转移学习在‘一次性学习’中的好处。这是将一个模型训练成执行一项任务,例如将图像分类为不同类别,然后向其展示一个新类别的单一示例,并期望它能够正确分类更多该类别的示例。作者发现,使用 GTL 作为预训练的补充可以一致地提高性能,但指导矩阵很快达到了‘帮助’的最大程度,此后添加更多的侦察者或提供更多的数据并没有帮助。好的一面是,这也意味着减少侦察者的数量或训练数据的规模对性能影响不大。

这意味着 GTL 是一种低成本、稍微有用的技术,特别是在数据有限的情况下(通常会使用一次性学习或少量学习)。而且目前还处于初期阶段:作者目前只尝试了使用单一指导矩阵,但建议其他实现可能会有用,比如根据侦察者从起点移动的距离来创建不同的矩阵。这在直观上是有意义的:主要模型已经有关于其周围环境的信息,我们希望侦察者帮助看到更远的地方,因此如果我们制作多个矩阵,并对那些距离起点更远的矩阵赋予更大的重要性,这可能会帮助算法‘选择’最佳的下一步行动。

经作者许可转载:A) 示例一次性学习任务:给出一个示例进行训练,然后必须找到其他相同字符的示例。B) 使用预训练和添加 GTL 的分类性能示例。

第二个实验涉及‘XOR 问题’,也叫‘排他性或’。目标是学习一个将两个输入(x1 和 x2)映射到一个输出(y)的函数,使得当 x1 和 x2 不同时,y = 1(或‘真’),否则 y = 0(或‘假’)。使用梯度下降的大型模型通常会在这一挑战中停滞不前,因此 Nikolić、Andrić和 Nikolić应用了未经过预训练的 GTL 来帮助避免这些局部最小值。这一方法非常有效,以至于没有观察到停滞现象。

最后的实验解决了“灾难性遗忘”的问题:即神经网络在新任务上训练时,新数据的影响使得所有已学习的权重大幅更新,从而“忘记”了之前学到的内容。Nikolić等人进行了一系列模型重训练:每一步额外的训练持续固定数量的纪元,并基于一个额外的数据点;每个数据点都是已经学习过的类别的新示例。也就是说,这些数据点是顺序学习的,而不是通常的批量学习。在一次私人谈话中,Danko 将其比作现实生活:存在一种称为“汽车”的物体类别,而今天你正在驾驶一种特定类型的汽车,比如 SUV。即使你现在接触到 SUV 的特性,也不意味着你会忘记以前接触过的所有其他汽车类型。你的新接触应该增加你对汽车的理解,而不是减少它。

尽管仅仅依靠经典的预训练转移学习无法从这些顺序添加的数据点中受益,但添加 GTL 却带来了逐步的性能提升,这表明知识得到了积累。换句话说,模型对遗忘之前的示例更具鲁棒性,尽管最终还是会发生。作者用常识解释了这一点:在特定的问题空间中,会有一些解决方案既适用于现在学习的示例,适用于之前学习的示例;这些解决方案可能接近于模型开始在新示例上训练时的起始点;没有 GTL,模型可能会“偏离”;有了GTL,指导矩阵鼓励模型保持在那个有用的邻域。这就像一个侦察员提醒你不要离营地太远。

那这在实践中意味着什么?

指导转移学习是一种“学习如何学习”的方法,并不是唯一的方法。其他方法也存在,并且可能会产生更好的结果,但它们往往不可扩展,这可能会使它们失去意义。GTL 计算便宜且灵活,这可能使它特别适用于那些已经将所有可用资源榨取出来的模型。大型语言模型和计算机视觉模型(这些模型经常在严格的硬件限制下存在,如嵌入在自动驾驶汽车中)是几个好的例子。

另一方面,如果数据和计算能力非常充足,那么作者承认 GTL 可能就不再需要了。正如他们所说,它不是万能的,但没有任何单一的机器学习解决方案是万能的。正如实验所示,它对那些即使是资源充足的模型也难以解决的难题仍然充满了希望。

作者计划尝试所有可能的指导矩阵范式,我对此感到乐观。所以,请关注这个领域。

指导大型语言模型进行任务特定推理 — 提示设计与软提示

原文:towardsdatascience.com/guiding-a-huge-language-model-lm-to-perform-specific-tasks-prompt-design-and-soft-prompts-7c45ef4794e4?source=collection_archive---------1-----------------------#2023-02-27

了解提示设计和软提示如何用于开发和部署 SOTA 模型。

Prem OommenTowards Data Science Prem Oommen

·

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

--

图片来源:camilo jimenezUnsplash

提示(Prompting)是为训练好的模型提供额外信息的过程,以在预测任务的输出标签时进行条件设置。这是通过提示来完成的,提示是输入到模型中的几行指令,用于执行某项任务,无论是否提供了几个示例。

最近,随着大型 Transformer 模型如GPT-2GPT-3的成功,提示得到了大量关注。通过 GPT-2,OpenAI 证明了扩大模型规模可以相对提高其任务特定性能,而不需要更新模型在下游任务中的权重。通过 GPT-3,他们展示了描述所需任务的提示加上零到少量的结果样本,足以使这些大型模型成功执行任务。这催生了提示工程(Prompt Engineering),其目标是精心策划这些输入提示,以从模型中提取最佳结果。在此基础上,谷歌的研究人员最近推出了提示调优(Prompt Tuning),其思想是学习软提示(Soft Prompts),这些软提示可以指导大型语言模型(LLMs)执行各种任务。这些提示是经过训练的权重,一旦调优后,可以与输入一起提供给冻结的语言模型,以获得所需的结果。

背景

越来越多的趋势倾向于预训练语言模型,以创建任务无关的语言表示,这些表示可以通过特定任务的数据集和架构灵活地适应以执行特定任务。一个相关的例子是谷歌的BERT,它具有多层双向 Transformer 编码器,并在未标注的数据集上使用掩码语言模型(Masked LM)和下一句预测(NSP)进行预训练。然后,模型必须在下游任务中使用标注数据进行微调,以更新其参数。BERT 的一个显著特点是通过在预训练和微调过程中使用统一架构,能够避免需要任务特定架构的问题:

预训练的 BERT 模型可以通过仅添加一个额外的输出层来微调,以创建用于广泛任务(如问答和语言推理)的最先进模型,而无需对任务特定架构进行重大修改。— BERT: 语言理解的深度双向 Transformer 预训练

这显而易见的缺点是需要任务特定的数据集和精细调优方法,这限制了模型的适用性。在面临需要利用训练好的语言模型进行特定任务时,研究人员/开发人员通常会陷入获取大量特定任务标注数据的繁重任务,这需要为每个新任务重复进行。此外,研究表明,大型精细调优模型可能对训练分布过于特定,难以很好地泛化到分布外,导致领域转移的问题。这主要归因于在预训练和精细调优过程中训练的数据量的差异。

1. 使用纯文本进行编程 — 提示设计

多任务学习一直是克服这些限制和提高模型总体性能的一个有前途的框架。实现这一目标的一个最突出的方式是元学习,其中模型在训练过程中被教会执行多个任务,以便它能够发展广泛的技能和模式识别能力,这些能力在推断时可以被利用来生成期望的输出。随着变换器的引入,GPT2 展示了通过学习* p(output | input, task)* 的条件概率,可以实现这一点,使其成为多任务学习者 尽管沿着这些方向已有一些类似的研究,如 MAML(模型无关元学习)和 MQAN(多任务问答网络),变换器的可扩展性和大量数据的可用性使研究人员能够开发出任务无关的模型,这些模型可以与现有的精细调优模型相媲美。此外,MQAN 展示了常见 NLP 任务的特性提供了将它们转化为带有执行每个任务说明的问答任务的灵活性。这是其中之一。

虽然 GPT2 显示出大型模型可以进行多任务学习,但 GPT3,主要在模型大小、数据规模和多样性以及训练时间上与前者有所不同,表明在少量样本设置中(通过文本交互指定)可以改善大型语言模型的下游任务,而无需任何梯度更新或微调。这节省了训练、存储和部署不同任务特定模型的需求。这里的少量样本(K)表示模型只需几个示例,K 从 0 到模型令牌限制能容纳的最大值。为了对比基于 K 的性能差异,图 1 显示了模型在 LAMBADA(语言建模拓展到话语方面)基准数据集上的准确率。该基准的目标是通过要求模型预测段落末尾最可能的词来评估模型理解文本段落的能力。这也可以间接测量模型捕捉长程依赖的能力。有关 GPT-3 在各种语言建模基准上的表现,请参阅其原始论文 这里

图 1. GPT-3 在不同少量样本(K)设置下的 LAMBADA 性能。来源: 语言模型是少样本学习者

不幸的是,提示设计也存在一些关键缺陷。模型性能的质量通常取决于任务描述,并受限于模型输入中可以容纳的条件文本量。构建高质量提示需要人工参与,并涉及对每个任务运行多个实验,测试几种提示设计。尽管已有尝试(如 AutoPrompt)自动生成提示,并有工具(如 promptsource)可用于简化过程,但性能往往无法超越 SOTA。

2. 可学习提示 — 提示调优

另一种技术由 Google 提出,建立在其文本到文本 T5 语言模型之上,是训练一组特定任务的令牌,这些令牌可以附加到冻结的语言模型输入令牌上。提示调优是学习每个下游任务的可调令牌的过程,这些令牌可以预先附加到输入文本中。这是一种高效的冻结模型条件方法,因为即使模型规模较小,也能实现与 SOTA 相当的性能,但与提示设计不同,它们仍需任务特定的数据集来训练这些提示。

虽然用于提示设计的输入令牌的嵌入来自模型自身的嵌入空间,但用于提示调优的令牌嵌入则是从特定任务的数据集中单独学习得到的。此外,与模型调优中更新模型权重的方法不同,此方法仅更新提示权重,同时保持模型权重不变。

图 2. 对比模型调优和提示调优的服务效果。来源:参数高效提示调优的规模力量

图 2所示,这进一步使得通过批处理和向量化来节省资源成为可能。学习到的任务提示可以附加到各种任务输入中,以创建一个多任务批处理,并将其传递给相同的冻结模型。

提示调优的另一个优势是它能够调节模型的输入表示,从而防止模型修改其学习到的语言通用理解。他们认为,这将帮助模型在使用这些数据集时克服分布外错误。为了证明这一点,研究人员调查了提示调优在问答和同义句检测上的零样本领域转移性能。提示在 SQuAD 上进行了训练,结果显示提示调优在大多数常见的领域外数据集上优于模型调优(图 3. 有关详细结果,请参见论文)。这表明,在领域外任务中,用轻量级提示代替重型模型可以以更低的计算和内存成本获得更好的性能。

图 3. 在不同的领域外数据集上对比提示调优和模型调优的 F1 分数。来源:参数高效提示调优的规模力量

由于软提示可以用来影响模型的嵌入空间以适应下游任务,它们提供了一种高效的方式来集成巨大的语言模型而没有相关的开销。提高任务性能的一种方法是使用多个具有不同初始化的微调模型,但在相同数据上训练。这可以通过软提示高效地完成,因为现在可以创建N个具有不同初始化的单独提示,而不是N个微调模型。之前讨论的批处理和向量化的想法也可以在这里使用,以通过对冻结的语言模型进行一次传递来获得结果。

论文进一步报告了消融研究的结果,调查了提示长度、初始化方法以及预训练目标的选择如何影响提示调优的性能,因此我强烈推荐阅读论文以了解更多信息。

关键要点:

图 4. 比较模型调优、提示设计和提示调优。来源:缩放的力量:高效参数提示调优

  • 模型调优 涉及在下游任务中更新任务无关的预训练语言模型的权重,是否更新底层架构都可以。因此,每个应用只能由其自身的模型服务,并且在分布外示例上的表现相当差。

  • 提示设计 适用于大规模语言模型,这些模型将每个 NLP 任务视为一种问答问题,输出标签通常是一系列标记。通过冻结模型权重,这些模型能够通过少量样本提示快速适应任务。然而,由于提示文本大多是手动创建的,输出的质量取决于输入提示和任务描述的质量。

  • 与更新模型权重不同,提示调优 涉及训练一个独立于模型嵌入空间的标记向量,可以根据当前任务调节模型的嵌入。这些提示可以与特定任务的输入一起批处理,并输入到一个冻结的模型中。这种方法对领域偏移表现出强大的弹性,并且能够有效替代神经网络集成。

参考文献:

  • GPT 2: Radford, Alec, 等。“语言模型是无监督的多任务学习者。” OpenAI 博客 1.8 (2019): 9。

  • GPT 3: Brown, Tom, 等。“语言模型是少样本学习者。” 神经信息处理系统进展 33 (2020): 1877–1901。

  • MAML: Finn, Chelsea, Pieter Abbeel, 和 Sergey Levine。“模型无关的元学习用于深度网络的快速适应。” 国际机器学习会议。PMLR, 2017。

  • MQAN: McCann, Bryan, 等。“自然语言十项全能:将多任务学习作为问题回答。” arXiv 预印本 arXiv:1806.08730 (2018)。

  • 提示调优:Lester, Brian, Rami Al-Rfou, 和 Noah Constant。“缩放的力量:高效参数提示调优。” arXiv 预印本 arXiv:2104.08691 (2021)。

破解因果推断:使用 ML 方法的合成控制

原文:towardsdatascience.com/hacking-causal-inference-synthetic-control-with-ml-approaches-7f3c19c7abfa

使用 PCA 测试任何治疗的效果随时间变化

Marco CerlianiTowards Data Science Marco Cerliani

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

--

图片由 Raul Petri 提供,来源于 Unsplash

在文献中提出并被公司大规模采用的标准,用于研究商业行动(如设计更改、折扣优惠和临床试验)的因果影响肯定是 AB 测试。进行 AB 测试时,我们在进行随机化实验。换句话说,我们将受我们控制的人群(病人、用户、客户)随机分成两组:治疗组和对照组。治疗组接受治疗措施,而对照组保持不变。经过一段时间,我们重新记录感兴趣的指标,并分析治疗对人群行为的影响。

闪烁的东西并非金子!已证明 AB 测试存在不同的缺陷。随机试验的主要假设包括:

  • 没有治疗组和对照组之间的互动(即网络效应)。

  • 大规模实验的成本不断增加。

在现实世界中,网络效应很常见,因为我们可能会预期到人们之间的“污染”。例如在营销试验中,社交媒体上的意见分享会影响彼此的选择。为了克服这一不便,解决方案是通过选择不同地区具有不同口味的人来扩大实验规模。尽管增加样本大小可能是一个有效的解决方案,但由于成本呈指数增长,它却是不可行的。在这种情况下,引入了一种叫做合成控制的技术。

在这篇文章中,我们介绍了用于随机实验试验的合成控制方法。这项技术在本文中被介绍[1]。我们的目标不是仅仅展示在实际数据集上的方法实现。我们利用数据中存在的时间动态,提出了一种使用数据科学家工具箱中的工具的合成控制变体

合成控制是什么

合成控制旨在评估案例研究中干预的效果。它类似于任何随机实验。在初步阶段,选择处理组和控制组。与标准因果分析不同,处理人群可以有任意大小!

想象一下你有兴趣验证一个由单一单元组成的群体上的处理效果。采用经典的合成控制方法,我们最终会构建一个多个控制单元的加权平均,以模拟处理案例的行为(人工控制案例)。

合成控制数学公式 [作者提供的图像]

从数学角度来看,这个问题在于找到使上述方程最小化的W(单位权重)的最佳值。这些值代表了每个控制单元在构建人工控制案例中的贡献。

合成控制模拟 [作者提供的图像]

我们希望验证单位之间的关系在处理引入日期(干预)之前和之后如何变化。如果我们在测试期间观察到人工控制案例和实际处理案例之间存在显著差异,我们可以断言处理是成功的。

主成分分析下的合成控制

合成控制是一种革命性的技术,它为研究人员提供了生成控制案例的规则。通过使合成案例看起来像处理案例,能够研究任何处理行动随时间的影响。

合成控制在幕后做的就是研究控制单元和处理单元的时间动态。首先,在构建人工控制案例时,研究控制单元之间的互动。其次,在处理引入之后,外推并验证相同的互动。换句话说,我们在检查干预前后单位之间关系的变化

在机器学习中,我们习惯于检查分布和关系随时间的变化。如果我们在因果背景中应用一种通常用于检测关系变化的技术,以验证处理实验的有效性会怎么样呢?

一个适合此任务的候选方法可能是主成分分析(PCA)。这是一个在机器学习中广泛采用的技术,用于解决各种任务。我们决定使用它来学习处理引入之前的单元关系。

pca = make_pipeline(StandardScaler(), PCA(n_components=1))
pca.fit(df_train)

然后,我们将其应用于重建处理后的单元路径。

df_test_reconstruct = pca.steps[0][1].inverse_transform(
  pca.steps[-1][1].inverse_transform(pca.transform(df_test))
)
df_test_reconstruct = pd.DataFrame(
  df_test_reconstruct, 
  columns=df_test.columns, index=df_test.index
)

最终,我们通过测量重建误差来量化每个单独单元动态可能发生的变化,这可能是由于治疗措施的采用。

reconstruct_errors = (df_test - df_test_reconstruct).mean()

我们应用提出的方法研究 1988 年加州实施的限制措施 (提案 99),以研究对烟草消费的影响。该修正案旨在提高烟草产品的税收以防止香烟消费。

各州多年香烟消费 [作者提供的图片]

这项研究与 Abadie 在这项工作中提出的相同 [2]。实验数据可以通过 这里 下载,并在 公共许可证 下发布。

PCA 拟合的组件多年 [作者提供的图片]

独立分析每个州的重建误差,加州记录了最大的负向减少。对于加州,我们注意到预期消费(由 PCA 重建)与观察到的消费之间有显著偏差。这种行为可能是由于烟草税的增加导致香烟消费的减少。

干预后各州的重建误差 [作者提供的图片]

通过简单的建模策略,我们可以了解研究中的各个单元的路径。我们可以观察它们的预期行为,并分析与正常条件下或引入治疗活动后预期的偏差。

总结

在这篇文章中,我们介绍了合成控制作为从任何治疗措施中提取因果洞察的方法。经典的合成控制方法通过研究对照组单元的关系来构建一个人工对照案例。我们发现可以以一种直接的方式做到这一点,简单地研究单元之间的关系并观察它们随时间的变化。我们通过分析治疗引入后的 PCA 重建误差来实现这一点。

查看我的 GITHUB 仓库

保持联系: Linkedin

参考文献

[1] A. Abadie, J. Gardeazabal, 冲突的经济成本:以巴斯克地区为例 (2003), 美国经济评论

[2] A. Abadie, A. Diamond, J. Hainmueller, 比较案例研究的合成控制方法:估计加州烟草控制计划的效果 (2010),美国统计协会期刊

破解 MySQL 的 JSON_ARRAYAGG 函数以创建动态、多值维度

原文:towardsdatascience.com/hacking-mysqls-json-arrayagg-function-to-create-dynamic-multi-value-dimensions-211074115d28?source=collection_archive---------9-----------------------#2023-07-25

补偿 MySQL 的一个不太为人知的不足

Dakota SmithTowards Data Science Dakota Smith

·

关注 发布于 Towards Data Science ·9 分钟阅读·2023 年 7 月 25 日

--

图片由 Azamat E 拍摄,刊登于 Unsplash。感谢 Azamat!

介绍

让我们假设我们是一个订阅盒子公司的数据团队成员。在 MySQL 数据库中,购买的事务记录被写入名为 subscriptions 的表中。除了元数据之外,该表包含一个 customer_idsubscription 字段,类似于这样:

订阅表。(注意:所有图像,除非另有说明,均由作者提供)

请注意,在这个示例场景中,一个客户可以有多个订阅。每条记录的唯一性由客户 ID 和订阅共同定义,即没有客户可以拥有相同的订阅两次。如果你想将这些测试数据加载到自己的数据库中,你可以在 这里找到相应的代码。

作为一个订阅盒子公司,我们的目标是销售更多的订阅盒子。为此,产品团队最近指出我们当前的所有客户都拥有不止一个订阅。他们对这表明的客户行为感到好奇。他们要求我们的团队提供一个数据模型,展示用户购买的订阅组合,以及哪些组合最为常见。

市场营销团队对这个模型也表现出了兴趣。他们认为这些结果可能对市场营销捆绑产品促销、客户画像以及定向电子邮件活动有用。出于这些相同的原因,他们还希望查看每个客户购买的最常见订阅数量

简而言之,请求的数据模型希望回答一些重要的问题,理想情况下这些问题将最终导致更高的订阅盒子销售。问题是,我们应该如何准确执行?

在这篇文章中,我们将解决一个独特的数据建模挑战,以弥补 MySQL 的一个不太为人所知的缺陷。我们将讨论定性聚合、JSON 数据类型,以及如何强制 MySQL 以一种产生独特、多值维度的方式对值进行排序。

目录

  • 聚合作为维度

  • MySQL 中 JSON 数据类型的简要概述

  • JSON_ARRAYAGG

  • 使用 ROW_NUMBER 强制值的排序

  • 回顾

聚合作为维度

从概念上讲,我们需要做的事情相对简单:我们需要按客户对订阅进行捆绑(分组)。然后,我们需要查看这些捆绑,看看哪些最为常见,以及它们包含了多少订阅。

在数据建模术语中,我们关注的是某种形式的聚合:具体来说,是按客户对订阅进行聚合。

通常会想到定量意义上的聚合函数(SUMCOUNT等),这主要是因为 SQL 中大多数聚合函数的功能。但我们也可以将拼接的字符串值聚合成更长的、类似列表的字符串。

然而,这个挑战在于访问、操控或评估这些连接字符串中的值。MySQL 会将foo, bar, hello, world的值视为文本,而不是列表。

这有什么相关性?主要是因为在我们的假设场景中,我们想要计算每个组合中的订阅数量。我们不希望得到一个长的以逗号分隔的字符串,我们希望得到一个更真正的列表形式。

在 Python 中解决这个问题会很简单——使用 pandas,也许是 polars,甚至只是 Python 本身的数据结构。但有许多情况下这不是一个选项。也许数据团队只使用 dbt;或者更常见的是,你在一个 IT 部门严密锁定本地环境的公司工作。

无论如何,如果你只有 SQL 可以使用,你需要一个能够提供最可读代码和最灵活结果的解决方案。实现这一点并不直观。例如,我遇到这个问题时的第一反应是使用GROUP_CONCAT,这是一个根据你定义的分组连接字符串的函数:

WITH
  subscriptions_grouped AS (
  SELECT
    customer_id,
    GROUP_CONCAT(subscription) AS subscriptions
  FROM 
    subscriptions
  GROUP BY customer_id
  )
SELECT
  subscriptions,
  COUNT(*) AS num_accounts
FROM subscriptions_grouped
GROUP BY subscriptions
;

查询结果

正如你所看到的,聚合有效,从技术上讲,但它并没有按照我们的业务逻辑工作。请查看第一行和最后一行。组合“international_snacks, self_care”的订阅与“self_care, international_snacks”是相同的组合。(第二行和第四行也是如此。)

我们可以在GROUP_CONCAT中使用ORDER BY子句来解决这个特定问题:

WITH
  subscriptions_grouped AS (
  SELECT
    customer_id,
    GROUP_CONCAT(subscription ORDER BY subscription) AS subscriptions
  FROM 
    subscriptions
  GROUP BY 1
  )
SELECT
  subscriptions,
  COUNT(*) AS num_accounts
FROM subscriptions_grouped
GROUP BY subscriptions
;

查询结果

但这仍然留下了一个问题:如何计算每个组合中的订阅数量。

这样做是可行的。但我认为不仅复杂且不太易读,而且还伴随着一些不太明显的陷阱。

关于如何计算 MySQL 中以逗号分隔的字符串中的值数量的快速搜索找到了一个解决方案,对我们来说,相当于这个(subscriptions_grouped CTE 除外):

SELECT
  subscriptions,
  LENGTH(subscriptions) - LENGTH(REPLACE(subscriptions, ',', '')) + 1 AS num_subscriptions,
  COUNT(*) AS num_accounts
FROM subscriptions_grouped
GROUP BY subscriptions
;

这本质上是计算逗号的数量,然后将结果加 1。这是可行的。但这个答案不仅难以一眼理解,还引入了一个潜在的错误:LENGTHCHAR_LENGTH函数计算的内容不同

正如你可能猜到的,这篇文章详细描述了我在工作中遇到的障碍,当时我发现自己处于类似的情况。

最终,解决方案是使用本地 MySQL JSON 数据类型的某种黑客式但非常易懂的变通方法。

MySQL 中 JSON 数据类型的简要概述

MySQL 中的 JSON 数据类型是在 5.7.8 版本中新增的,提供了许多对存储和建模非常有用的功能。

在 JSON 数据类型的伞下(官方称为“JSON 文档”)有两种不同的数据结构:JSON 数组和 JSON 对象。

JSON 数组可以简单地被看作是一个数组(如果你是 Python 爱好者的话,就是一个列表):值用方括号 [ ] 括起来,并用逗号分隔。

  • 一个 MySQL JSON 数组值的示例:[“foo”, “bar”, 1, 2]

JSON 对象可以被看作是一个哈希表(或者用 Python 的术语说,就是一个字典):键值对,用逗号分隔,并用花括号 { } 括起来。

  • 一个 MySQL JSON 对象值的示例:{“foo”: “bar”, 1: 2}

MySQL 提供了许多函数来处理这两种格式—几乎没有函数执行任何形式的聚合。

不过幸运的是,有两个函数是可以的。它们都返回 JSON 文档,这意味着我们可以使用 MySQL 内置的函数来访问其中的值。

JSON_ARRAYAGG

MySQL 函数 JSON_ARRAYAGG 很像 GROUP_CONCAT。最大的区别是它返回一个 JSON 数组,而这个 JSON 数组带有多个有用的内置函数,如上所述。

JSON 数组数据类型以惊人的简单性解决了我们面临的两个问题中的一个:可靠计算组合中订阅数量的问题。这是通过使用 [JSON_LENGTH](https://dev.mysql.com/doc/refman/5.7/en/json-attribute-functions.html#function_json-length) 函数完成的。语法非常简单:

SELECT JSON_LENGTH(JSON_ARRAY("foo", "bar", "hello", "world"));
-- JSON_ARRAY function used here just to quickly create an example array

这个语句的结果是 4,因为生成的 JSON 数组中有 4 个值。

但让我们回到订阅的组合。不幸的是,JSON_ARRAYAGG 不具备 GROUP_CONCAT 的排序功能。在基查询之前即使在 CTE 中排序 subscription 值,也无法返回期望的结果:

WITH
  subscriptions_ordered AS (
  SELECT
    customer_id,
    subscription
  FROM subscriptions
  ORDER BY subscription 
  )
  , subscriptions_grouped AS (
  SELECT
    customer_id,
    JSON_ARRAYAGG(subscription) AS subscriptions,
    JSON_LENGTH(JSON_ARRAYAGG(subscription)) AS num_subscriptions
  FROM 
    subscriptions_ordered
  GROUP BY customer_id
  )
SELECT
  subscriptions,
  COUNT(*) AS num_accounts
  num_subscriptions
FROM subscriptions_grouped
GROUP BY subscriptions
;

查询结果

每个组合中的订阅数量是存在的,这要归功于 JSON_LENGTH 函数——但由于它们的顺序,相同的组合再次被错误地标记为不同的。

使用 ROW_NUMBER 强制值的排序

ROW_NUMBER 是一个窗口函数,用于创建索引。索引必须被定义;也就是说,你需要告诉它从哪里开始,如何递增(方向),以及在哪里结束。

我们可以通过应用 ROW_NUMBER 函数并告诉它按 subscription 字段排序来快速查看一个例子:

SELECT 
  customer_id, 
  subscription, 
  ROW_NUMBER() OVER(ORDER BY subscription) AS alphabetical_row_num
FROM subscriptions
;

查询结果

仔细查看结果。尽管我们在查询的最后没有使用 ORDER BY 语句,但数据还是根据 ORDER BY OVER 子句中的顺序进行了排序。

当然,这还不是我们想要的。接下来我们需要在窗口函数中添加一个 PARTITION BY 子句,以便结果的排序与每个客户 ID 相关(实际上是受每个客户 ID 的限制)。如下所示:

SELECT 
  customer_id, 
  subscription, 
  ROW_NUMBER() OVER(PARTITION BY customer_id ORDER BY subscription) AS alphabetical_order
FROM subscriptions
;

查询结果

你可能已经看出接下来会发生什么了。

如果我们在 CTE 中对这些结果执行 JSON_ARRAYAGG 函数,我们会看到重复的组合现在看起来完全一样,这要归功于 ROW_NUMBER 函数强制按字母顺序排序的订阅:

WITH 
  subscriptions_ordered AS (
  SELECT 
    customer_id, 
    subscription, 
    ROW_NUMBER() OVER(PARTITION BY customer_id ORDER BY subscription) AS alphabetical_order
  FROM subscriptions
  )
SELECT
  customer_id,
  JSON_ARRAYAGG(subscription) AS subscriptions
FROM subscriptions_ordered
GROUP BY 1
ORDER BY 2
;

查询结果

现在我们只需在执行 ROW_NUMBER 的 CTE 后添加分组 CTE,并修改基本查询即可:

WITH 
  subscriptions_ordered AS (
  SELECT 
    customer_id, 
    subscription, 
    ROW_NUMBER() OVER(PARTITION BY customer_id ORDER BY subscription) AS alphabetical_order
  FROM subscriptions
  )
  , subscriptions_grouped AS (
  SELECT
    customer_id,
    JSON_ARRAYAGG(subscription) AS subscriptions,
    JSON_LENGTH(JSON_ARRAYAGG(subscription)) AS num_subscriptions
  FROM subscriptions_ordered
  GROUP BY customer_id 
  )
SELECT
  subscriptions,
  COUNT(*) AS num_customers,
  num_subscriptions
FROM subscriptions_grouped
GROUP BY subscriptions
ORDER BY num_customers DESC
;

这不仅提供了准确的独特订阅组合,还显示了购买这些组合的客户数量,以及每个组合包含的订阅数量:

查询结果

看这里!

总结

  • 我们想知道有多少客户购买了不同组合的订阅,以及每种组合中包含了多少个订阅。这提出了两个问题:如何最好地获取后者,以及如何生成准确独特的订阅组合。

  • 为了获取每个组合中的订阅数量,我们选择了 MySQL 的 JSON 函数之一 JSON_ARRAYAGG。结果的聚合以 JSON 数据类型返回给我们,允许我们使用 JSON_LENGTH 函数。

  • 我们接着需要强制 JSON 数组内部值的排序,以免重复的组合被错误地认为是不同的。为此,我们在基本查询之前使用了 ROW_NUMBER 窗口函数,在 CTE 中按客户 ID 分区,并按字母顺序(升序)排序订阅。

  • 这最终使我们能够准确地聚合独特的订阅组合;通过这个方法,我们能够使用简单的 COUNT 函数查看每种组合有多少客户购买。

感谢阅读!🤓

我希望这对你有所帮助!如果你知道 SQL 中的其他聪明技巧/变通方法(无论方言如何),我很想听听。SQL 长期以来一直是转换结构化数据的事实上的通用语言,但它并不完美。我总是喜欢了解针对现实世界挑战的创新和/或聪明的解决方案。🔥

我定期撰写有关数据工程和分析主题的文章——目标始终是尽可能清晰简洁。如果本文中的任何内容让你感到困惑,请在评论中告诉我。如果你有兴趣阅读更多类似的文章,欢迎 关注我 和/或 在 LinkedIn 上联系我

黑客统计显著性:使用机器学习方法进行假设检验

原文:towardsdatascience.com/hacking-statistical-significance-hypothesis-testing-with-ml-approaches-74ff102c5ff1

在任何上下文中无假设地测试统计显著性

Marco CerlianiTowards Data Science Marco Cerliani

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

--

照片由Christian Stahl提供,发布在Unsplash

数据分析的重要性在各个领域都很清楚。从商业到学术,进行适当的分析是达到前沿结果的关键。在这方面,正确地操作和提取数据中的有意义见解是至关重要的。数据分析师/科学家负责填补理论假设与实际证据之间的差距

为所有可能提出的问题提供分析答案是一个昂贵且艰难的过程。将问题/需求转化为分析语言是开始进行的第一步。这类操作的好坏至关重要,因为它会影响最终结果的正确性。在初步阶段,理解分析目标并指出最佳的数据源、框架和参与人员,以达到最佳结果,是非常重要的。

大多数情况下,分析性地回答问题是通过进行统计检验来完成的。许多统计检验如下所示:

  • 陈述一个原假设,这是描述世界的默认选项。

  • 陈述一个替代且补充的假设。

  • 计算检验统计量(数据的函数)并概述最终结果。

已知检验统计量的分布后,可以轻松计算观察到任何值的概率(p 值)。如果 p 值小于预设的(通常为 0.01 或 0.05)显著性水平,则拒绝原假设,接受替代假设。

统计测试本身没有问题,但我们需要注意一些隐藏的陷阱

  • 对数据的严格假设。大多数时候,基础数据必须遵循正态或已知的分布。正如我们所知,现实世界的现象并不遵循正态分布。

  • 对我们不感兴趣的数量/统计的限制。如果我们想测试一些定制的或更复杂的内容,可能会遇到困难。

在这篇文章中,我们介绍了一些时髦且简单的方法来测试假设并从我们手头的数据中提取有意义的见解。我们不使用标准统计测试得出结论,而是通过模拟和排列来实现

数据准备

为了说明这些方法,我们使用一个包含金县(美国)房屋销售记录的数据集。该数据集可以从Kaggle访问,并且在CC0 公共领域许可证下提供。它包含了 2014 年 5 月到 2015 年 5 月之间的房屋销售价格数据,涉及金县(包括西雅图)。

数据集包含大约 20,000 条销售房屋的记录,具有不同的数值属性:销售价格、卧室数量、浴室数量、生活空间的平方英尺、楼层数、纬度/经度、建筑年份,等等。

在一个标准的预测应用中,预测房屋的销售价格,考虑到它们的特征,会是一个有趣的问题。这里我们并不关注这种应用。我们希望通过一种不同于传统统计测试的方法来分析数据,从而回答一些问题,这种方法同样高效(或者可能更灵活)

模拟

让我们假设我们对房屋的建筑年份和销售价格之间是否存在关联感兴趣。

销售价格的分布与正态分布相差较大。正如我们所预期的,价格和建筑年份之间并不存在明确的线性关系。

价格分布(左)。价格与建筑年份(右)[作者提供的图片]

中位销售价格为 45,000 美元。2015 年建造的房屋(根据我们的数据集,较新的房屋)有更高的中位价格。这似乎是合理的,但了解这种效果是否“由于偶然”会很有趣

每年的最低和最高中位销售价格 [作者提供的图片]

所谓“由于偶然”,指的是我们仅观察到整个总体的一个样本。我们手头的数据仅限于 2014 年至 2015 年间在金县发生的所有房屋交易的一部分。在此期间可能还有更多 2015 年建造的房屋被售出,但未记录在我们的数据集中。

在这种情况下,我们能做的最好的是注意到局限性,并尝试估计真实的中位数。我们可以通过模拟来做到这一点。

作为第一步,我们计算并存储 2015 年建造的房屋的中位数销售价格与我们掌握的所有数据的中位数销售价格之间的观察到的差异。这个值(观察到的差异)代表了我们可以观察到的价格差异,并且我们希望验证这一点。

year = 2015

y = df[df['yr_built'] == year]['price'].agg(['count','median'])

observed_diff = abs(y['median'] - df['price'].median())

此时,我们希望检查我们的观察到的差异是否可能被任何随机销售子组所记录。我们随机抽取与 2015 年房屋相同大小的组,并计算它们的中位数价格与数据集中中位数价格之间的差异。

n_simulation = 1_000

sampling = lambda x,y: x['price'].sample(n=int(y['count']))

sim_diffs = np.asarray([
    abs(sampling(df,y).median() - df['price'].median()) 
    for i in tqdm(range(n_simulation))
])

最后,我们验证模拟价格差异高于我们的观察到的差异的次数。这个值可以解释为成功的概率,并代表我们的估计的p 值

p_value = np.mean(sim_diffs >= observed_diff)

具有较低的 p 值,我们更有信心拒绝原假设并接受替代假设。在我们的情况下,我们更有信心拒绝 2015 年房屋与其他房屋之间没有价格差异的假设。

2015 年和 2012 年的模拟结果 [作者提供的图片]

根据我们的需求,我们可以对所有感兴趣的建筑年份进行测试。下图显示了所有年份的测试结果。

所有建筑年份及其中位数销售价格的模拟结果 [作者提供的图片]

多变量模拟

这是一个令人难以置信的结果!只需几行代码,我们就可以测试和验证任何实证问题。我们的研究验证了不同年份建造的房屋销售价格差异的存在。这是否意味着 2015 年建造的房屋与 80 年代建造的房屋不同?并不完全,因为我们仅验证了价格差异的可能性。可能有很多因素会区分不同年份建造的房屋。希望我们的数据集中还有许多其他特征,我们可以用来进一步验证可能的差异。

如前所述,我们希望检查 2015 年建造的房屋与其他房屋之间是否存在差异。现在我们不仅仅关注销售价格,还考虑所有可用的特征。为了有效地进行这种多维度测试,我们拟合一个二分类模型来区分 2015 年的房屋和其他房屋。我们记录 ROC-AUC 作为一个度量指标(观察到的分数)。

year = 2015

cv_scoring = lambda x,y: np.mean(cross_val_score(
    RandomForestClassifier(10), 
    x, y, cv=5, scoring='roc_auc', n_jobs=-1, 
    error_score='raise'
))

observed_score = cv_scoring(
    df.drop(['yr_built','date','id'], axis=1), 
    (df['yr_built'] == year).astype(int)
)

然后我们检查我们的观察到的分数是否可能被任何随机的房屋销售子组所记录。我们随机抽取与 2015 年房屋相同大小的组,拟合一个二分类器来区分它们,并记录获得的 ROC-AUC。

n_simulation = 1_000

sim_scores = np.asarray([
    cv_scoring(
        df.drop(['yr_built','date','id'], axis=1), 
        (df['yr_built'] == year).sample(frac=1).astype(int)
    )
    for i in tqdm(range(n_simulation))
])

最后,我们可以像之前一样验证观察到的分数是否高于模拟值,并计算相对的 p 值。

p_value = np.mean(sim_scores >= observed_score)

在我们的案例中,我们更有信心拒绝认为 2015 年房屋与其他房屋之间没有整体差异的假设。

2015 年和 2012 年的模拟结果 [作者提供的图片]

总结

在这篇文章中,我们展示了一种基于模拟的方法来回答观察数据时可能出现的任何问题。所提出的方法的灵活性使其适用于任何背景,并且没有特别的前提假设。我们还提出了一种多变量泛化方法,以测试数据子组之间的差异,进一步证明了该方法可以在任何领域扩展,以验证任何假设。

查看我的 GitHub 仓库

保持联系: Linkedin

为了庆祝这些非常有用和实用的文章,本周的《Variable》将聚焦于我们“提示与技巧”栏目中的近期亮点:它们提供了可操作的、经过验证的建议,可以帮助你节省时间和精力,并在项目中取得更好的结果。无论你本周是否已经享受了你的“甜点”(祝庆祝者万圣节快乐!),我们希望这些技巧能激发你寻找新的方法或工具进行尝试。

原文:towardsdatascience.com/had-your-treats-time-for-data-science-tricks-28ad59f9b59d?source=collection_archive---------9-----------------------#2023-11-02

我们在 TDS 对长篇、详尽的指南情有独钟——但我们也欣赏那些针对数据科学家在日常工作中面临的具体挑战和痛点的集中帖子。

·

关注 发布于 Towards Data Science · 以 Newsletter 形式发送 · 阅读需 3 分钟 · 2023 年 11 月 2 日

TDS EditorsTowards Data Science TDS Editors

享受过甜点了吗?是时候了解数据科学技巧了

  • 在探索性数据分析中简化重复任务EDA 有时因为需要经历繁琐的阶段而被人诟病,这一阶段你必须经历,才能进入更有趣的建模和预测工作阶段。Christabelle Pabalan 最近分享了一种聪明的方法,为该过程添加了自动化层,但没有牺牲过程中的细致和精确。

  • 探索 Pydantic V2 的增强数据验证能力Pydantic,“最广泛使用的 Python 数据验证库”,是许多数据从业者的首选工具。Lynn Kwong 对 Pydantic V2 的概述提供了利用其最新改进的具体技巧,包括支持严格模式和在没有模型的情况下验证数据的可能性。

照片由 Ahmad Ossayli 拍摄,来源于 Unsplash

  • 6 个你应该了解的 Pandas 索引相关操作鉴于 Pandas 在数据科学工作流中的普及,深入理解其功能并扩展处理数据框的有效方法从来不是坏主意。Yong Cui 的新文章重点介绍了与索引相关的操作,并通过简单的实际案例进行了详细说明。

  • 如何在数据可视化中使用颜色如果你一直将图表和图形中的颜色选择视为事后考虑,Michal Szudejko 的关于颜色正确使用的提示合集肯定会让你重新考虑你的方法。从可访问性到调色板选项,你将学习到小的调整如何使你的可视化更清晰,并帮助其成为更强大的讲故事工具。

  • 释放 Julia 超类型的力量对于越来越多的 Julia 爱好者,Emma Boudreau 关于抽象及其如何有效地融入代码的实践资源是必读的——它提供了关于如何以最小的努力开始创建我们自己的超类型的详细概述。

我们希望你还有空间容纳一些额外的美味,因为我们不希望你错过这些其他主题的精彩阅读:

  • AI 生成内容的普及将如何影响 LLM 训练的质量?Aicha Bokbot 探讨了一个关于 AI 工具可持续发展的新兴关注点。

  • 音乐与机器学习相遇 在 Emmanouil Karystinaios 的迷人项目中,该项目试图自动化和声分析。

  • 想要构建和发布一个 R 数据包吗?Deepsha Menghani 提供了一个 利用 devtools 的逐步指南 来实现这一目标。

  • 通过使用混合搜索、层级排名和讲师嵌入,Agustinus Nalwan 尝试解决 RAG 在领域特定搜索中的重大挑战。

  • 要对 AI 初创生态系统的现状 有一个清晰的反思,不要错过 Clemens Mewald 最近的深度剖析,解释了为何 LLMs 成功进入主流而 MLOps 工具却没有。

  • 可疑的数据黑客行为不幸地无处不在;Hennie de Harder 提供了一个 关于这些行为背后统计学概念的有益看法。

感谢你支持我们作者的工作!如果你喜欢在 TDS 上阅读的文章,可以考虑 成为 Medium 会员 —— 这将解锁我们整个档案(以及 Medium 上的所有其他帖子)。

直到下一个变量,

TDS 编辑

使用 Delta 表处理缓慢变化的维度(SCD)

原文:towardsdatascience.com/handling-slowly-changing-dimensions-scd-using-delta-tables-511122022e45

使用 Delta 框架处理缓慢变化维度的挑战

Manoj KukrejaTowards Data Science Manoj Kukreja

·发表于 Towards Data Science ·10 分钟阅读·2023 年 1 月 23 日

--

长期以来,Kimball 方法一直是维度数据建模技术的标准。根据 Kimball 的说法“时间的概念渗透到数据仓库的每一个角落”。在数据分析的背景下,这意味着什么?从高层次来看,现代分析可以被视为随着时间的推移对不断变化的数据的聚合。问题在于,不断变化的数据不仅包括新的添加,还包括对以前数据集的更改。

整体维度数据建模将数据分为两大类:

事实 — 这些数据表示无限的数据集,存储实体的测量信息。它包含对定量分析和决策制定至关重要的数据。事实表通常具有与其他表(维度)连接的列,以供参考。

维度 — 这些数据表示相对有限的数据集,提供关于在事实表中进行的测量的描述性信息。与事实表相比,维度的变化速度要慢得多。这就是为什么它们通常被称为“缓慢变化的维度”。

Kimball 的方法涉及基于事实和维度创建星型模式。由于其非规范化的结构,星型模式非常适合分析用例……不需要复杂的连接条件。因此,多年来,星型模式一直是传统数据仓库建模的事实标准。

图片由作者提供

多年来,数据处理人员面临着处理缓慢变化维度的挑战,同时保持其之前的历史记录并保留与事实表的关系参考。Kimball 方法提出了几种有效处理缓慢变化维度的方法。现实情况是,一旦选择了特定的 SCD 方法,在数据仓库中实施它相对容易。对 SQL 和 ACID 事务的支持使其处理起来变得简单。

不幸的是,在数据湖中实现相同的操作是另一回事。有几个原因:

  • 第一个问题是 不变性。根据最佳实践,数据湖中的数据不应更改。

  • 其次,多年来在数据湖中无法进行原子写入。这意味着即使你只做了小的编辑,也需要重写整个表。

Delta Lake 框架解决了上述问题。对 ACID(原子性、一致性、隔离性和持久性)事务的支持现在使得在数据湖中实现 SCD 与在数据仓库中一样轻松。在本文中,我们将学习如何使用 Delta Lake 框架实施处理缓慢变化维度的最常见方法。

下面是一个示例案例:

“一家公司希望跟踪客户维度随时间发生的变化。他们要求数据工程团队提供几种替代方案。经过仔细考虑,数据工程提出了三种管理缓慢变化维度的选项:SCD 类型 1、SCD 类型 2 和 SCD 类型 3。”

在我们深入探讨每个选项之前,让我们尝试理解客户维度的数据结构。在本文中,我们将使用下面的示例数据集。下面的数据集显示了一些示例客户记录。为了说明处理缓慢变化维度的不同选项,我们将重点关注用红色框标出的客户记录(客户名称 = Magee Cash)。

作者提供的图片

Magee Cash 最近更改了她的地址。更改记录以 CDC 记录的形式传送到 OLAP 系统。在数据工程的背景下,CDC 过程旨在从源头捕获增量数据集,并将其合并到企业数据湖中。以下是 Magee Cash 的更改记录,注意地址与上述原始记录不同。

作者提供的图片

Delta Lake 的核心能力使其成为构建现代数据湖仓架构的极其合适的平台。在湖仓架构中,Delta Lake 可用于将变更记录合并到公共数据层(银层)。一旦创建,银层就会作为分析工作负载的基础数据层,包括 BI、数据科学、机器学习和人工智能。因此,银层通常被称为“单一事实来源”。

让我们回到本文的核心目标。现在我们对数据集有了清晰的理解,我们可以开始探索第一个 SCD 方法。

SCD 类型 1

这种类型通常被称为“覆盖”方法。在这种方法中,任何对维度数据的更改都会用相同键的先前数据状态进行覆盖。虽然这种方法实现起来非常简单,但它有一个主要缺点。由于覆盖机制,你不仅会丢失维度的先前历史记录,还会丢失它所附加的事实表的状态。下图展示了使用 SCD 类型 1 方法的客户维度的前后图像。

图片由作者提供

请注意,新的住址是覆盖了之前的地址,之前地址的历史记录丢失了。在维度发生变化时,失去历史记录的后果可能非常严重。如果事实表的聚合受到维度变化的影响,那么没有历史记录的情况下,很难追溯聚合值变化的原因。

我们现在将学习如何使用 Delta 框架实现 SCD 类型 1。从使用湖仓的铜层原始客户数据集创建银层客户维度表(customer_silver_scd1)开始。

图片由作者提供

使用Magee Cash的变更记录创建一个新的数据框架。

图片由作者提供

最后,将地址变更记录合并到customer_silver_scd1银层维度表中。

图片由作者提供

在对银层维度表执行查询后,你会注意到地址变更已经覆盖了其先前的状态。问题在于,这条记录的先前状态无法找到。

考虑一种情况,Magee Cash可能使用旧版本的地址下了电子商务订单。产品尚未发货,但地址在此期间已发生变化。那么,产品应该发往哪里?旧地址还是新地址?

图片由作者提供

让我向你介绍 Delta Lake 框架中一个非常有用的功能。Delta Lake 维护了更改的时间顺序历史,包括插入、更新和删除。在上面的示例中,版本 0的表是在创建customer_silver_scd1银层表时生成的。同样,版本 1的表是在我们为地址变更记录执行数据合并时创建的。此外,Delta Lake 表可以轻松恢复到任何所需的先前版本。

图片由作者提供

由于上述缺陷,现代数据平台中很少使用 SCD 类型 1。因此,我们需要一种更好的方法,能够在进行维度更改时保留先前的引用以供活动使用。总的来说,如果你的计算关心数据的先前状态或其可能引发的后果,可以简单地使用 SCD 类型 1。

SCD 类型 2

也称为“添加新记录”方法。在这种方法中,更改记录作为新记录添加到维度表中,并标记为“当前”或“活动”。此外,记录的先前版本被标记为“过期”或“非活动”。记录的不同版本(当前和历史)通过替代键关联起来。在表级别,SCD 类型 2 通过为维度表中的每一行添加开始日期结束日期时间戳列来实现。此外,添加了一个状态列,以标记记录的当前过期状态。下面展示了使用 SCD 类型 2 方法的客户维度的前后图像。

图片由作者提供

我们现在将学习如何使用 delta 框架实现 SCD 类型 2。从使用 Lakehouse 原始客户数据集创建银层客户维度表(customer_silver_scd2)开始。

图片由作者提供

现在将地址更改记录合并到customer_silver_scd2银层维度表中。

图片由作者提供

请注意,之前的记录被标记为过期,并且更新了结束日期。同时,插入了一条新记录,包含最新地址,其开始日期与之前记录的结束日期相同。使用这种方法,Magee Cash将确保她的电子商务订单被送到正确的地址。

图片由作者提供

使用 SCD 类型 2 方法,你可以按时间顺序跟踪更改历史,并以时间顺序的方式维护对事实表的引用。我必须承认,与 SCD 类型 1 相比,这种实现有些复杂。

作为一个警告,维护维度表的应用程序需要以这样的方式编码,即将带有当前版本的新记录和先前版本的过期合并为一个事务。此外,所有针对维度表的查询都需要过滤status=Current

还有一种更简单的替代方案,我们进一步探讨另一种方法,这在某些方面仅仅是 SCD 类型 1 方法的扩展。

SCD 类型 3

也称为“添加新字段”方法。对于每次更改,先前版本和当前版本作为两个不同的列存储在同一行的维度表中。与 SCD 类型 2 相比,SCD 类型 3 相对更易于实现,历史记录仅包括当前和先前的版本。

作者提供的图片

我们现在将学习如何使用 Delta 框架实现 SCD 类型 3。首先使用湖库中铜层的原始客户数据集创建银层客户维度表 (customer_silver_scd3)。

作者提供的图片

请注意,维度表中的每一列都维护当前先前状态。在创建维度表时,列的当前状态填充了最新的数据,而列的先前状态则留空。

现在将地址变更记录合并到customer_silver_scd3银层维度表中。

作者提供的图片

继续查看在 Delta Lake 合并后记录的状态。

作者提供的图片

请注意,address 字段现在已填充了更改后的记录,地址的先前版本已移动到 previous_address 字段。同样,modifieddate 字段已更新以维护更改的时间顺序。

由于仅有有限的历史记录可用,SCD 类型 3 的使用案例略显有限。但实现的简便性使其具有一定的吸引力。如果您厌恶 SCD 类型 1 的局限性,并且发现 SCD 类型 2 难以实现和管理,那么这是一个不错的折衷方案。

本文中使用的所有代码可以在下面的链接中找到:

[## blogs/scd at master · mkukreja1/blogs

目前无法执行该操作。您在另一个标签页或窗口中已登录。您在另一个标签页或…

github.com

在许多方面,SCD 类型 2 通常被认为是实施缓慢变化维度的主要技术。需要明确的是,SCD 的主要目标不是存储记录的历史,而是保持与事实表的准确关联。此外,在许多方面,缓慢变化维度要求你更新记录,这在一般情况下与数据湖/仓库的不可变性原则相悖。然而,像 Delta Lake 这样的框架的新进展使得以简单轻松的方式实现 SCD 场景成为可能。

希望这篇文章对你有帮助。SCD 作为 Datafence Cloud Academy 提供的 AWS 大数据分析课程的一部分进行了讲解。课程由我在周末在线授课。

使用 Python 处理时区

原文:towardsdatascience.com/handling-time-zones-with-python-a100bfc7f6df?source=collection_archive---------3-----------------------#2023-04-21

Himalaya Bir ShresthaTowards Data Science Himalaya Bir Shrestha

·

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

--

本文通过对 2020 年四个城市的小时太阳辐射数据在不同时间区的比较评估,展示了在 Python 中处理时区的功能。

时区

当我在德国波恩的工作开始的第一天,十月的早上 9 点时,我在尼泊尔奇旺的家乡已经是下午 12:45。我在澳大利亚悉尼的朋友当天已经在晚上 6 点结束了他的工作安排。另一个在美国纽约的朋友还在睡觉,因为那里的时间是凌晨 3 点。这表明这四个地方有不同的时区

世界各地的不同时间区插图。地图 © OpenStreetMap contributors,依据 Open Data Commons Open Database License (ODbl)OpenStreetMap foundation 授权(OpenStreetMap,2023)。标签由作者添加。

时区 是一个区域,该区域为了法律、社会或商业目的而观察统一的标准时间。世界并不是根据经度均匀划分成不同的时区。时区往往根据国家之间和国家内部的边界进行划分。

所有时区都定义为相对于协调世界时 (UTC) 的偏移量。这些值范围从 UTC-12:00 到 UTC+14:00。虽然偏移量通常是整数小时,但有些时区的偏移量还额外增加了 30 分钟或 45 分钟。例如,尼泊尔的时区偏移量为 UTC+05:45。全球共有 38 个时区

如果我有尼泊尔、德国、澳大利亚和美国四个城市的太阳辐射数据,并且这些数据是基于 UTC 时区的,这并不反映每个国家同一时刻的数据。在这篇文章中,我将讨论如何处理数据的时区,以便在 Python 中处理日期时间对象,包括 pandas 数据框。

为此,我将下载这四个城市/国家的 2020 年太阳辐射数据,并在以下情况下对数据进行比较和分析:

  1. 每个国家的数据都在 UTC 时区中

  2. 数据指的是相应国家的时区。

让我们开始吧。

图片由 Luis Cortes 提供,来自 Unsplash

地理编码以检索四个城市的坐标

在第一步中,我检索了四个国家四个城市的坐标,因为我需要这些坐标来提取太阳辐射数据。通过提供地点名称来提取地理坐标的过程称为地理编码。

如下所示,我编写了一个使用 geopy 包进行地理编码的函数。该函数利用了 Nominatim,这是一个开源的地理编码服务,使用 OpenStreetMap 数据通过名称和地址查找地球上的位置。

from geopy.geocoders import Nominatim
geolocator = Nominatim(user_agent="app")

def get_coordinates(place):
    """Return the latitude and longitude of the place."""
    place_details = geolocator.geocode(place)
    coordinates = (place_details[1][0], place_details[1][1])

    return coordinates

我使用该函数提取了各个城市的坐标,并创建了一个 pandas 数据框,如下图所示。

创建了一个包含四个城市的纬度和经度值的数据框。插图由作者提供。

使用 NASA Power API 访问数据

美国宇航局 Power 的应用程序编程接口(API)服务允许检索分析就绪数据(NASA Power, 2023a)。对于这篇文章,我从 NASA Power 数据(NASA Power, 2023b)下载了四个城市的每小时分辨率的太阳辐射数据。我使用的参数是2020年的全天空表面短波下行辐射量(ALLSKY_SFC_SW_DWN),其详细描述见下节。

数据以UTC 时区格式调用,尽管每小时 API默认也允许以本地太阳时间(LST)格式调用数据。

base_url配置如下所示:

base_url = r”https://power.larc.nasa.gov/api/temporal/hourly/point?parameters=ALLSKY_SFC_SW_DWN&community=RE&time-standard=UTC&longitude={longitude}&latitude={latitude}&format=JSON&start=2020&end=2020"

接下来,我遍历由地理编码定义的每个地点的经纬度,这些地点在一个名为places的列表中,并请求 2020 年的每小时太阳辐射数据。该步骤的完整代码如下所示:

参数描述

太阳辐射数据指的是在所有天空条件下,每小时每单位面积(Wh/m²)从太阳获得的总功率(直接+漫射),位于地球表面上的水平面上(NASA Power, 2023c)。

该参数,也称为全球水平辐射(GHI),用于计算满足给定电力需求所需的太阳光伏模块尺寸,如下方公式所示:

方程 1 和 2 指的是计算太阳光伏模块和电池尺寸的公式,以满足给定的电力需求,基于太阳辐射量和其他技术参数。由作者说明。

给定数据的基本统计

下载的全球水平辐射数据,涵盖四个城市。由作者说明。

下载的数据如上图所示。数据显示悉尼在年初和年末的太阳辐射量较高,而在年中较低。这一模式在其他三个城市中正好相反,这可以通过悉尼位于南半球而其他城市位于北半球来解释。

从下载的数据中得到的统计信息。由作者说明。

观察到 2020 年,尼泊尔的奇特旺接收到的年太阳辐射量最高(1669 kWh/m²),其次是澳大利亚悉尼(1631 kWh/m²)、美国纽约(1462 kWh/m²),而德国波恩接收到的最少(1193 kWh/m²)。

然而,特定小时接收到的最大太阳辐射量最高的是悉尼(1061.3 W/m²),其次是奇特旺(997 W/m²)。

每个城市的最小太阳辐射量和第 25 百分位值为零,因为夜间没有太阳辐射。

时区处理

1. 默认的 pandas 数据框,没有“datetime”格式索引

由于 2020 年是闰年,有 366 天,因此数据获取了 8784 小时。

当数据首次下载时,其索引是整数(int64)类型,如下所示:

数据最初从 NASA Power 网站下载。作者插图。

2. 将整数类型索引转换为“原始”日期时间索引

可以使用pd.to_datetime()并指定格式%Y%m%d%H将数据框架索引转换为日期时间类型,分别对应年、月、日和小时。

将整数索引转换为日期时间索引。作者插图。

当数据框绘制时也反映了这一变化,因为 2020 年 1 月至 12 月的月份在 xticks 中可见,如下所示:

用“原始”日期时间索引绘制 pandas 数据框显示了 2020 年 1 月至 12 月的月份在 x 轴上。作者插图。

尽管此数据框具有日期时间索引,但没有任何关于时区和夏令时的信息。因此,数据框索引是一个原始的日期时间对象。通过检查 pandas 数据框架的一个索引的时区信息可见。

检查数据框架第一个日期时间索引的时区信息。作者插图。

3. 将“原始”日期时间对象本地化为“时区感知”日期时间对象

Python 的 datetime 模块可用于访问、检索和操作日期和时间信息。

默认情况下,datetime.now()函数返回当前的“本地”日期和时间信息。但是,由于下面代码段中的time_now.tzinfo返回 None,这意味着它是一个原始的日期时间对象。

截至目前(2023 年 4 月 21 日),我在尼泊尔。因此,我使用pytz包的timezone.localize()模块将当前时间本地化为“Asia/Kathmandu”时区。现在,time_in_nepal是一个时区感知的日期时间对象。

要获取德国当前本地时间,可以使用time_in_nepal.astimezone(timezone("Europe/Berlin")),这也是一个时区感知的日期时间对象。

第一个单元格显示当前本地时间time_now作为一个没有时区信息的原始日期时间对象。第二个单元格显示将time_now本地化为尼泊尔时区。第三个单元格显示将尼泊尔本地时间转换为德国本地时间。作者插图。

4. 本地化 pandas 数据框的时区

接下来,我使用df.tz_localize(tz = "UTC")将 pandas 数据框的原始索引本地化为 UTC 时区,如下图所示。

将原始数据框索引本地化为 UTC 时区。作者插图。

观察到df的索引从天真的索引转换为 UTC 时区的时区感知索引,如上所示。

5. 所有可能的时区地址列表

所有可以参考的可能时区地址列表可通过 pytz 包的all_timezones模块获得。共有 594 个这样的地址。一些地址可以指向相同的时区。例如,Europe/Berlin、Europe/Amsterdam、Europe/Copenhagen 都指向相同的时区。

使用 pytz 模块参考的可能地址列表。插图由作者提供。

6. 为每个城市创建新的数据框,并将 UTC 时区转换为相应的本地时区

df包含四个城市的 UTC 时区的太阳辐射数据。在这一步中,我从df的每一列创建了四个数据框。然后,我将新数据框的时区从 UTC 转换为它所属城市或国家的本地时区。例如,df_chitwan的时区转换使用

df_chitwan.tz_convert(tz = "Asia/Kathmandu")

df的每一列创建不同的数据框。新数据框的时区从 UTC 转换为相应国家/城市的时区。

需要注意的是,对于有夏令时的国家,这在时区转换中会自动考虑。例如,尼泊尔时间全年与 UTC + 05:45 保持一致。然而,对于悉尼,Python 会自动处理夏令时,因为与 UTC 时区的偏移可以根据年份的不同为 10 小时或 11 小时。

7. 比较不同时间区的太阳辐射数据图

在最后一步中,我想比较四个城市的数据在下列情况下的太阳辐射情况:

a. UTC 时区和

b. 每个城市的本地时区。

在下面的代码片段中,我创建了两个子图来绘制四个城市的太阳辐射。在左侧子图中,绘制了基于 UTC 时区的 2020 年 10 月 1 日的太阳辐射数据。在右侧子图中,绘制了基于每个城市本地时间的 2020 年 10 月 1 日的太阳辐射数据。

fig, (ax1, ax2) = plt.subplots(1, 2, figsize = (20, 6))
fig.suptitle("Solar irradiance on October 1, 2020")

ax1.plot(df.loc["2020–10–01"])
ax1.set_title("Based on UTC time zone")
ax1.xaxis.set_ticks(ticks = df.loc["2020–10–01"].index[::4], labels = np.arange(0, 24, 4))

cities = df.columns.tolist()
handles = ax1.get_legend_handles_labels()[0]
ax1.legend(handles, labels = cities, loc = "upper right")
ax1.set_xlabel("Hour of day")
ax1.set_ylabel("W/m$²$")

ax2.plot(df_chitwan.loc["2020–10–01"].values.tolist())
ax2.plot(df_newyork.loc["2020–10–01"].values.tolist())
ax2.plot(df_bonn.loc["2020–10–01"].values.tolist())
ax2.plot(df_sydney.loc["2020–10–01"].values.tolist())
ax2.xaxis.set_ticks(ticks = np.arange(0, 24, 4), labels = np.arange(0, 24, 4))

handles = ax2.get_legend_handles_labels()[0]
ax2.legend(handles, labels = cities)
ax2.set_title("Based on local time zone of each city/country")
ax2.set_xlabel("Hour of day")
ax2.set_ylabel("W/m$²$")

plt.savefig("output/solar irradiance on october 1.jpeg",
 dpi = 300)
plt.show()

图形如下所示:

2020 年 10 月 1 日的太阳辐射。左图:基于 UTC 时区。右图:基于每个城市/国家的本地时区。插图由作者提供。

截至 2020 年 10 月 1 日,与 UTC 时区相比,四个城市的时区如下:奇特旺(UTC+05:45)、纽约(UTC-04:00)、波恩(UTC+02:00)和悉尼(UTC+10:00)。因此,我们可以看到在左图中,奇特旺、纽约、波恩和悉尼的太阳辐射峰值分别出现在 UTC 时区的凌晨 4 点、下午 3 点、上午 10 点和凌晨 3 点。

右侧的图表显示了各城市的太阳辐射在每天的本地时间内有相似的形状。太阳辐射在每天的 5 或 6 点左右开始从零增加,在中午达到峰值,然后在 5 或 6 点前继续下降,最后再次达到零。在这一年的这一天,悉尼接收到的太阳辐射量最高,其次是奇特旺、纽约和波恩。

结论

在本文中,我演示了在处理日期时间对象(包括数据框)时如何处理时区的方法。我使用了四个城市的太阳辐射数据作为示例。这些方法在处理时间序列数据时非常有用,特别是当时区很重要时,例如气象数据。我总结了从本文中学习到的处理 Python 中时区的关键技术,列在以下编号的要点中:

  1. 可以使用tzinfo模块检查日期时间对象的时区。

2. 当日期时间对象不包含任何关于时区和夏令时的信息时,它被称为naive日期时间对象。

3. 使用pytz包的timezone模块,可以将naive time转换为local time。例如,

time_in_nepal =  timezone("Asia/Kathmandu”).localize(datetime.now()) 

4. 新对象现在是时区感知的。可以使用日期时间对象的astimezone模块获取不同时区的时间。例如,

german_timezone = timezone(“Europe/Berlin”)
time_in_germany = time_in_nepal.astimezone(german_timezone)

5. 在处理时间序列数据时,将 pandas 数据框的索引转换为日期时间索引是有意义的。

6. 使用df中的tz_localize模块和指定的时区可以对 naive 数据框索引进行本地化。例如,

df_utc = df.tz_localize(tz = “UTC”)

7. 数据框对象也可以使用dftz_convert模块转换为不同的时区。

df_nepal = df_utc.tz_convert(tz = “Asia/Kathmandu”)

本文的数据、代码和输出图表可以在此 GitHub 代码库中的notebooks/Timezone_handling文件夹中找到。感谢您的阅读!

参考文献

OpenStreetMap, 2023. 版权和许可

NASA Power, 2023a. NASA Power APIs

NASA Power, 2023b. POWER|数据访问查看器

NASA Power, 2023c. 参数定义

实战深度 Q 学习

原文:towardsdatascience.com/hands-on-deep-q-learning-9073040ce841

强化学习

提升你的代理,以赢得更困难的游戏!

Dr. Robert KüblerTowards Data Science Dr. Robert Kübler

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

--

照片由 Sean Stratton 提供,来源于 Unsplash

强化学习是机器学习中最迷人的领域之一。与监督学习不同,强化学习模型可以独立学习复杂的过程,即使没有精美整理的数据。

对我来说,看到 AI 代理赢得视频游戏是最有趣的,但你也可以使用强化学习来解决商业问题。只需将其表达为游戏,就可以开始了!你只需要定义……

  • 你的代理所处的环境,

  • 你的代理可以做出什么决策,以及

  • 成功和失败的表现。

AI 代理掌握游戏的示例。接客并将其送到酒店。图片来源:作者。

在继续之前,请阅读我关于强化学习的介绍文章。它会给你更多背景信息,并展示如何自己进行简单但有效的强化学习。它也为本文提供了基础。

## 实践者的强化学习指南

开始编写获胜的游戏 AI 代理的第一步

实践者的强化学习指南

在这篇文章中,你将了解深度 Q 学习,为什么我们需要它,以及如何自己实现它以掌握看起来比我另一篇文章中的游戏更复杂的游戏。

你可以在 我的 Github找到代码

大型观察空间

在上述链接的文章中,我们进行了 Q 学习,使得一个智能体能够在具有小的离散观察空间的一些简单游戏中进行游戏。例如,在 Frozen Lake 游戏中,你可以在 4x4 地图上的 16 个领域(=状态或观察)中站立。在Blackjack 卡牌游戏的 gymnasium 版本中,有 32 · 11 · 2 = 704 个状态。

Q 学习的低效性

在我提到的简单游戏中,Q 学习效果非常好,因为 Q 表保持得相当小。然而,更大的表意味着算法需要更多工作,因此 Q 学习变得更加低效。

这通常发生在你有过多的观察而不是过多的动作时,因为动作空间通常比观察空间小得多。例如,在 Blackjack 环境中,你只有 2 个动作但有 704 个状态。

卡车杆游戏

或者更糟的是:想象一下你的观察空间是连续的,例如在 gymnasium 提供的卡车杆游戏中。在这个游戏中,智能体必须通过左右转动卡车来学习如何平衡杆子。

平衡那个杆子。图片由作者提供。

虽然这个游戏的动作空间仅由两个动作组成——向左或向右——观察空间由

  • 车的位置,

  • 其速度,

  • 杆的角度和

  • 杆的角速度,

每一个都是实数。这意味着你的观察空间是无限的,创建一个 Q 表对于它来说……是具有挑战性的。

离散化 Q 学习

作为解决方法,你可以离散化空间,即将其划分为有限数量的桶,并将每个连续状态映射到这些桶之一。然后你可以进行正常的 Q 学习。

四个桶用于卡车位置,并非政治光谱。图片由作者提供。

这就提出了一个问题,你必须决定要使用多少个桶。如果你使用的桶太多,Q 表将会过大。如果桶太少,非常不同的状态将被同等对待,这可能导致智能体性能较差。这可能发生在上面的图像中,例如,其中 0 到 3 之间的所有卡车位置都被视为相同。在模型看来,位置 0.1 与位置 2.9 是一样的。

我们可能在另一篇文章中尝试这种方法,但请注意,它基本上是我们所知道的 Q 学习,只是对观察进行了额外的离散化步骤。然而,在这篇文章中,我们想应用 深度 Q 学习,它能够 直接处理连续观察空间

深度 Q 学习

之前,我们为 Q 表中的每个状态 s 和动作 a 保留了一个单元。如果这些表格变得过大,我们可以尝试另一种策略:通过函数建模 Q 值

对于给定的状态 s 和动作 a,我们希望输出 (接近) Q 值。由于我们不能自己构建这个函数——至少我不能——让我们使用一个具有可学习权重的神经网络作为近似。

Q 学习与深度 Q 学习的比较。作者提供的图片。

这不是我编造的,它早在 1993 年就以 QCON(连接主义 Q 学习)的名称在 Long-Ji Lin 的论文 “使用神经网络的机器人强化学习” 中描述过。祝 30 周年快乐!

作者还在其目前的形式中引入了 经验回放 的概念,这种形式在著名的 DeepMind 论文 使用深度强化学习玩 Atari 游戏 中被使用。

注意: 你可能会想知道为什么网络不使用状态 s 和动作 a 作为输入,因为这将是用模型替代表的直接方法。输入状态和动作,接收 Q 值。DeepMind 论文的作者说如下:

有几种可能的方法可以使用神经网络对 Q 进行参数化。由于 Q 将历史-动作对映射到它们的 Q 值的标量估计,一些先前的方法使用历史和动作作为神经网络的输入。[……] 这种架构的主要缺点是需要单独的前向传播来计算每个动作的 Q 值,这导致成本随动作数量线性增长。[……] 我们类型架构的主要优势是能够通过对网络进行单次前向传播来计算给定状态下所有可能动作的 Q 值。

直观理解

在我的另一篇文章中,我告诉你 Q 表中的更新是通过以下公式完成的

作者提供的图片。

用简单的话说,就是我们将旧的价值 Q(s, a) 稍微向目标值 R(s, a) + γ·max Q(s’, a) 的方向移动,其中 s 是状态,s’ 是在采取行动 a 后的下一个状态,R 是奖励函数,γ < 1 是折扣因子。α 是学习率,告诉我们将旧的 Q 值 Q(s, a) 移动到方向 R(s, a) + γ·max Q(s’, a) 的程度。

小示例。 假设 Q(s, a) = 3, R(s, a) + γ·max Q(s’, a) = 4,并且 α = 0.1 那么我们原来的值 3 会更新为 3 + 0.1·(4–3) = 3.1,稍微偏离 3 向 4 的方向。

这意味着这个更新规则逐步改变条目,使得某个时刻我们有 Q(s, a) = R(s, a) + γ·max Q(s’, a),这也称为贝尔曼方程

令人惊叹的观察是,我们可以将这个目标转换为一个简单的机器学习任务给我们的神经网络。所以,假设我们在环境中执行一步。我们从状态 s 开始,执行某个动作 a,然后进入新状态 s’。我们还从环境中获得了奖励 r = R(s, a)。接下来,我们按照以下步骤进行训练:

  1. 计算 N(s) 和 N(s’),它们都是实数向量,每个动作一个向量。

  2. 查看 N(s) 的 a 号动作,称其为 N(s)[a]。

  3. 确保 N(s)[a] 更接近 R(s, a) + γ·max N(s’) 通过执行一个梯度更新步骤来最小化均方误差,这意味着 (R(s, a) + γ·max N(s’) - Q(s, a))²。

然后我们可以在环境中再进行一步,获取新数据 (s, a, s’, r) (阅读: 代理在状态 s 中,执行 a,进入 s’,并获得奖励 r),然后再次执行步骤 1–3。我们这样做直到有一个足够好的代理,或者我们耗尽耐心或资金进行训练。

经验回放

上述直觉有一个缺陷:它并不像那样有效。当我刚开始时,我实现了这个逻辑,但从未得到过好的模型。

原因是我每次只用单个训练样本更新模型。这就像进行纯随机梯度下降。它可能有效,但在优化过程中,模型参数会剧烈波动,可能很难收敛。另一个问题是后续动作高度相关,因为状态通常不会因为单个动作而变化太多。现在,我们知道一种简单的方法来解决这两个问题:使用小批量

花哨的术语 经验回放 仅仅是这样。

你不是在环境中执行单个步骤,而是多个步骤,并将记忆 (s, a, s’, r) 存储到某种数据结构中,即回放记忆。它可以是一个数组,但通常,人们使用 双端队列 来实现有限的回放记忆。

图片由作者提供。

新的记忆会将旧的记忆淘汰,一旦回放记忆的大小限制达到。这是有道理的,因为非常旧的记忆不应该对现在发生的事情产生太大影响。这也使得模型能够适应游戏中的新规则。

7 淘汰 1。图片由作者提供。

伪代码

好了,了解了这些知识后,让我们看看《Playing Atari with Deep Reinforcement Learning》论文中的伪代码。唯一的区别是,他们可能会使用某些函数 φ 预处理状态 s(我们这里没有这样做),并且在代理达到终止状态时,即游戏结束时,他们会移除最大项。

来自他们的论文。

现在我们可以在 Python 中实现这个想法了!

实现

首先,让我们导入一些库并定义一些参数。

import random
from collections import deque, namedtuple

import gymnasium as gym
import numpy as np
import tensorflow as tf
from tqdm.auto import tqdm

n_episodes = 1000 # play 1000 games
eps = 0.4 # exploration rate, probability of choosing random action
eps_decay = 0.95 # eps gets multiplied by this number each epoch...
min_eps = 0.1 # ...until this minimum eps is reached
gamma = 0.95 # discount
max_memory_size = 10000 # size of the replay memory
batch_size = 16 # batch size of the neural network training
min_length = 160 # minimum length of the replay memory for training, before it reached this length, no gradient updates happen
memory_parts = ["state", "action", "next_state", "reward", "done"] # nice names for the part of replay memory, otherweise the names are 0-5

这里没有什么特别的,但如果有不清楚的地方请阅读注释。

回放记忆

现在,我们定义了回放,这只是一个 Python deque 的薄包装器。

Memory = namedtuple("Memory", memory_parts) # a single entry of the memory replay

class ReplayMemory:
    def __init__(self, max_length=None):
        self.max_length = max_length
        self.memory = deque(maxlen=max_length)

    def store(self, data):
        self.memory.append(data)

    def _sample(self, k):
        return random.sample(self.memory, k)

    def structured_sample(self, k):
        batch = self._sample(k)
        result = {}
        for i, part in enumerate(memory_parts):
            result[part] = np.array([row[i] for row in batch])

        return result

    def __len__(self):
        return len(self.memory)

在这里,我们定义了一个易于使用的记忆回放。让我们先玩一下,然后再实际使用它。我们可以进行

r = ReplayMemory(max_length=3)

r.store(("a", "b", "c", "e", "f"))
r.store((1, 2, 3, 4, 5))
r.store((6, 7, 8, 9, 0))

print(r.structured_sample(2)) # get 2 random sampples from the replay memory

# Output (for me):
# {
#   'state': array([1, 6]),
#   'action': array([2, 7]),
#   'next_state': array([3, 8]),
#   'reward': array([4, 9]),
#   'done': array([5, 0])
# }

如果我们添加另一个记忆,我们将丢失第一个记忆:

r.store((0, 0, 0, 0, 0))

print(r.memory)

# Output:
# deque([(1, 2, 3, 4, 5), (6, 7, 8, 9, 0), (0, 0, 0, 0, 0)], maxlen=3)
# no more letters in here!

模型

让我们定义一个简单的模型!我们将在这里使用 TensorFlow,但也可以使用 PyTorch,JAX 或其他工具。

model = tf.keras.Sequential(
    [
        tf.keras.layers.Dense(16, input_shape=(4,), activation="relu"), # state consists of 4 floats
        tf.keras.layers.Dense(16, activation="relu"),
        tf.keras.layers.Dense(16, activation="relu"),
        tf.keras.layers.Dense(16, activation="relu"),
        tf.keras.layers.Dense(2, activation="linear"), # 2 actions: go left or go right
    ]
)
model.compile(
    loss=tf.keras.losses.MeanSquaredError(),
    optimizer=tf.keras.optimizers.Adam(learning_rate=0.01),
)

训练

准备好了,就让我们开始训练吧。

env = gym.make("CartPole-v1")
replay_memory = ReplayMemory(max_length=max_memory_size)

for episode in tqdm(range(n_episodes)): # tqdm makes a nice proress bar
    state, _ = env.reset()
    done = False

    while not done:
        if random.random() < eps:
            action = env.action_space.sample() # random action
        else:
            action = model.predict(state[np.newaxis, :], verbose=False).argmax() # best action according to the model

        next_state, reward, done, _, _ = env.step(action)
        memory = Memory(state, action, next_state, reward, done)
        replay_memory.store(memory)

        if len(replay_memory) >= min_length:
            batch = replay_memory.structured_sample(batch_size) # get samples from the replay memory

            target_batch = batch["reward"] + gamma * model.predict(batch["next_state"], verbose=False).max(axis=1) * (1 - batch["done"]) # R(s, a) + γ·maxₐ N(s') if not a terminal state, otherwise R(s, a)
            targets = model.predict(batch["state"], verbose=False)
            targets[range(batch_size), batch["action"]] = target_batch # set the target for the action that was done and leave the outputs of other 3 actions as they are

            model.fit(batch["state"], targets, verbose=False, batch_size=batch_size) # train for one epoch

        state = next_state

    eps = max(min_eps, eps * eps_decay)

就这样!它看起来有点像 Q-learning 的代码,但表格中单元格的简单更新被替换为一个 fit 步骤的梯度下降。

结果

代码可能会运行几个小时。你也可以在训练期间记录一些数据,例如每个回合的奖励。另一个常见做法是每隔几个回合记录模型, 这样当训练崩溃时,你不会丢失太多工作。不过,保存模型还有另一个好处:通常情况下,最好的模型不是最新的那个。你可以从我的奖励图中看到这一点:

作者提供的图片。

你可以看到最开始,代理表现很差。大约第 100 集时,代理已经获得了大约 200 的奖励,然后又下降了。大约第 230 集时,模型变得异常优秀,但直到第 1000 集时才又变得较差,那时我结束了训练。这很奇怪,但这是一个我在其他人做强化学习时也常见的模式。

来玩吧!

有什么比看到我们辛苦训练的模型实际运作更好的呢?对,没有,所以让我们开始吧!首先,让我们看看随机代理如何竞争。

随机代理的三次不同运行。白色闪光表示游戏结束。图片由作者提供。

哇,随机代理真的很糟糕。中间的运行开始时看起来还不错,但随机性使其无法向左移动。你可以通过以下方式重现这样的悲惨运行:

env = gym.make("CartPole-v1", render_mode="human")

env.reset()
done = False

while not done:
    env.render()
    action = env.action_space.sample()
    _, _, done, _, _ = env.step(action)

env.close()

但你来这里是为了别的,对吧?

新的挑战者出现

让我们加载第 230 集的模型,看看它在实时测试中的表现如何!

model = tf.keras.models.load_model("PATH/TO/MODEL/230") # the model is in my Github, https://github.com/Garve/towards_data_science/blob/main/A%20Tutorial%20on%20Deep%20Q-Learning/230.zip
env = gym.make("CartPole-v1", render_mode="human")

state, _ = env.reset()
done = False
total_reward = 0

while not done and total_reward < 500: # force end the game after 500 time steps because the model is too good!
    env.render()
    action = model.predict(state[np.newaxis, :], verbose=False).argmax(axis=1)[0]
    state, reward, done, _, _ = env.step(action)
    total_reward += reward

env.close()

我们的第 230 集的智能体动作流畅。经过 500 个时间步后,我切断了动画,但智能体并没有失败。图片由作者提供。

看这儿!智能体可以轻松地用最小的动作保持杆子的平衡。然而,你可以看到它略微向右漂移,这在长时间内可能会成为一个问题。让我们将 500 个时间步的人工限制增加到几千个,并查看加速版本:

智能体 230 再次以 60fps 的游戏风格行动。图片由作者提供。

看起来这个智能体表现不错,即使经过了 500 步。太棒了!

结论

在这篇文章中,我们解释了为什么普通的 Q-learning 在处理大规模甚至潜在无限的观察空间时会失败。离散化 Q-learning 在这种情况下可以有所帮助,但你必须考虑你离散化的程度,以找到智能体性能和训练时间之间的平衡。

离散化的 Q-learning 方法完全没问题,但我们选择了另一种成功应用于更复杂游戏的技术——深度 Q-learning。我们不是更新一个可能很大的 Q 表条目,而是训练一个具有固定参数量的模型。

不过,我们还是要诚实。深度 Q-learning 也是在训练时间和智能体性能之间的权衡。像线性回归(没有隐藏层)这样的小型网络可能表现得很糟。如果你有数百万个参数用于摆杆游戏,那就是过度配置了,而且训练需要很长时间。

你也必须找到一个平衡,这不是免费的。

然而,从论文来看,似乎深度 Q-learning 对于像 Atari 游戏 这样更复杂的游戏仍然更有前景。

从感官输入中学习

到目前为止,我们使用了环境的内部状态。在摆杆游戏中,我们获得了汽车的位置、速度、角度等信息。当我们作为人类玩游戏时,通常我们没有这种奢侈。我的意思是,我们无法足够快地处理这些信息以在游戏中表现出色。我们必须依赖视觉反馈——屏幕——来做出快速决策。

如果智能体也只能使用屏幕输出呢?正如《用深度强化学习玩 Atari》的作者所展示的那样,它效果相当好!正如他们所写:

我们将我们的方法应用于来自 Arcade Learning Environment 的七款 Atari 2600 游戏,未对架构或学习算法进行任何调整。我们发现它在六款游戏中优于所有以前的方法,并在三款游戏中超越了人类专家。

作者使用了一个深度卷积神经网络,它接收屏幕,处理它所看到的图像,并输出 Q 值。准确地说,他们不使用单帧图像,而是使用游戏的最后 4 帧。这使模型能够学习屏幕上物体的移动速度和方向。

但我现在先这样,我们可以在另一篇文章中以清新的思维深入探讨这个话题!

希望你今天学到了一些新、趣味且有价值的东西。感谢阅读!

如果你有任何问题,可以在 LinkedIn上联系我!

如果你想更深入了解算法的世界,可以尝试我的新出版物《算法全景》!我仍在寻找作者!

[## 《算法全景》

从直观解释到深入分析,算法通过示例、代码和令人惊叹的内容变得生动。

allaboutalgorithms.com

针对产品和工程领导者的动手 GenAI

原文:towardsdatascience.com/hands-on-genai-for-product-engineering-leaders-6ee6ad94e058?source=collection_archive---------10-----------------------#2023-11-28

通过深入了解基于 LLM 的产品,做出更好的产品决策

Ninad SohoniTowards Data Science Ninad Sohoni

·

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

--

图片由 Bing Image Creator 根据提示“为机器学习驱动的应用程序工作中的产品所有者”生成

介绍

如果你是一个普通司机,你可能不在乎你车的引擎盖下是什么。然而,如果你是设计和执行链条中的一部分,负责打造更好的汽车,了解不同部件是什么以及它们如何协同工作,将帮助你打造更好的汽车。

同样,作为产品负责人、业务领导或负责创建新大型语言模型(LLM)驱动产品的工程师,或将 LLM/生成性 AI 引入现有产品的工程师,了解 LLM 驱动产品的构建模块将帮助你解决与技术相关的战略和战术问题,例如,

  1. 我们的使用案例是否适合 LLM 驱动的解决方案?也许传统的分析、监督式机器学习或其他方法更合适?

  2. 如果 LLM 是可行的,我们的使用案例现在或在不久的将来是否可以通过现成的产品(比如 ChatGPT Enterprise)来解决?这是经典的构建与购买决策。

  3. 我们的 LLM 驱动产品的不同构建模块有哪些?其中哪些已经商品化,哪些可能需要更多时间来构建和测试?

  4. 我们如何衡量解决方案的性能?有哪些杠杆可以提高我们产品输出的质量?

  5. 我们的数据质量是否符合使用案例的要求?我们是否正确地组织了数据,并将相关数据传递给了 LLM?

  6. 我们能否确信 LLM 的回答始终是事实准确的?也就是说,我们的解决方案是否会在生成回答时偶尔出现“幻觉”?

虽然这些问题在文章后面会得到回答,但通过动手实践的目标是建立对 LLM 驱动解决方案的直观理解,这应该有助于你自己回答这些问题,或者至少让你更好地进行进一步研究。

在一篇上一篇文章中,我深入探讨了与构建 LLM 驱动产品相关的一些基础概念。但你不能仅仅通过阅读博客或观看视频来学会驾驶——这需要你亲自上路。好在我们生活的时代提供了免费的工具(这些工具的创建花费了数百万美元),我们可以在不到一小时的时间里构建自己的 LLM 解决方案!因此,在这篇文章中,我建议我们就这样做。这比学习驾驶要容易得多😝。

构建一个允许你与网站“聊天”的聊天机器人

目标:构建一个基于提供的网站信息回答问题的聊天机器人,以更好地理解当前流行的 GenAI 解决方案的构建模块

我们将创建一个基于知识库信息回答问题的问答聊天机器人。这种解决方案模式,称为检索增强生成(RAG),已成为公司中的首选解决方案模式。RAG 之所以受欢迎的一个原因是,它不仅依赖于 LLM 自身的知识,还可以以自动化的方式将外部信息带入 LLM。在实际应用中,外部信息可以来自组织自己的知识库,包含专有信息,以使产品能够回答有关业务、产品、业务流程等问题。RAG 还减少了 LLM 的“幻觉”,即生成的响应是基于提供给 LLM 的信息的。根据 最近的一次演讲

“RAG 将是企业使用 LLM 的默认方式”

-Dr. Waleed Kadous, Chief Scientist, AnyScale

在我们的实操练习中,我们将允许用户输入一个网站,我们的解决方案将“读取”该网站到其知识库中。然后,解决方案将能够根据网站上的信息回答问题。这个网站是一个占位符——实际上,可以调整为从任何数据源如 PDFs、Excel、其他产品或内部系统等获取文本。这种方法也适用于其他媒体——如图像——但它们需要一些不同的 LLM。目前,我们将重点关注来自网站的文本。

作为示例,我们将使用为本博客创建的示例书单网页:Books I’d Pick Up — If There Were More Hours in the Day! 您也可以使用您选择的其他网站。

这就是我们的结果的样子:

LLM 驱动的聊天机器人可以根据网站上的信息智能回答问题。(图像由作者提供)

以下是我们将遵循的步骤来构建我们的解决方案:

0. 设置 — Google Colaboratory 和 OpenAI API 密钥

1. 创建知识库

2. 搜索与问题相关的上下文

3. 使用 LLM 生成答案

4. 添加“聊天”功能(可选)

5. 添加一个简单的预编码 UI(可选)

0.1. 设置 — Google Colaboratory 和 OpenAI API 密钥

要构建一个 LLM 解决方案,我们需要一个编写和运行代码的地方,以及一个生成问题回答的 LLM。我们将使用 Google Colab 作为代码环境,并使用 ChatGPT 背后的模型作为我们的 LLM。

首先设置 Google Colab,这是一个由 Google 提供的免费服务,可以以易于阅读的格式运行 Python 代码 — 无需在计算机上安装任何东西。我发现将 Colab 添加到 Google Drive 中很方便,这样我就可以轻松找到 Colab 笔记本。

为此,导航至 Google Drive(使用浏览器) > 新建 > 更多 > 连接更多应用 > 在 Google Marketplace 中 搜索 “Colaboratory” > 安装。

要开始使用 Colab,你可以选择 新建 > 更多 > Google Colaboratory。这将会在你的 Google 云端硬盘中创建一个新的笔记本,方便你返回继续使用。

Google Colaboratory 可以在 Google 云端硬盘中访问。(图片由作者提供)

接下来,让我们获取对 LLM 的访问权限。有几个开源和专有选项可供选择。虽然开源 LLM 是免费的,但强大的 LLM 通常需要强大的 GPU 来处理输入和生成响应,且 GPU 的运行成本较低。在我们的示例中,我们将使用 OpenAI 的服务来使用 ChatGPT 所用的 LLM。为此,你需要一个 API 密钥,它类似于用户名/密码的组合,用以让 OpenAI 知道是谁在尝试访问 LLM。根据此时的信息,OpenAI 为新用户提供了 $5 的信用额度,足以用于本实践教程。以下是获取 API 密钥的步骤:

访问OpenAI 平台网站> 开始使用 > 注册,使用电子邮件和密码进行注册,或使用 Google 或 Microsoft 帐户注册。你还可能需要一个电话号码进行验证。

登录后,点击右上角的个人资料图标 > 查看 API 密钥 > 创建新密钥。密钥将类似于以下内容(仅供参考的假密钥)。请保存以备后用。

sk-4f3a9b8e7c4f4c8f8f3a9b8e7c4f4c8f-UsH4C3vE64

现在,我们已经准备好构建解决方案了。

0.2. 准备构建解决方案的笔记本

我们需要在 Colab 环境中安装一些软件包以方便我们的解决方案。只需在 Colab 中的文本框(称为“单元格”)中输入以下代码,然后按“Shift + Return(Enter)”。或者,直接点击单元格左侧的“播放”按钮或使用笔记本顶部的“运行”菜单。你可能需要使用菜单插入新的代码单元格以运行后续代码:

# Install OpenAI & tiktoken packages to use the embeddings model as well as the chat completion model
!pip install openai tiktoken
# Install the langchain package to facilitate a most of the functionality in our solution, from processing documents to enabling "chat" using LLM
!pip install langchain
# Install ChromaDB - an in-memory vector database package - to save the "knowledge" relied on by our solution to answer questions
!pip install chromadb
# Install HTML to text package to transform webpage content to a more human readable format
!pip install html2text
# Install gradio to create a basic UI for our solution
!pip install gradio

接下来,我们应该从已安装的软件包中提取代码,以便在编写的代码中使用这些软件包。你可以使用新的代码单元格并再次按“Shift + Return” — 以这种方式继续进行每个后续的代码块。

# Import packages needed to enable different functionality for the solution
from langchain.document_loaders import AsyncHtmlLoader # To load website content into a document
from langchain.text_splitter import MarkdownHeaderTextSplitter # To document into smaller chunks by document headings 
from langchain.document_transformers import Html2TextTransformer # To converrt HTML to Markdown text
from langchain.chat_models import ChatOpenAI # To use OpenAI's LLM
from langchain.prompts import PromptTemplate # To formulate instructions / prompts
from langchain.chains import RetrievalQA, ConversationalRetrievalChain # For RAG
from langchain.memory import ConversationTokenBufferMemory # To maintain chat history
from langchain.embeddings.openai import OpenAIEmbeddings # To convert text to numerical representation
from langchain.vectorstores import Chroma # To interact with vector database
import pandas as pd, gradio as gr # To show data as tables, and to build UI respectively
import chromadb, json, textwrap # Vector database, converting json to text, and prettify printing respectively
from chromadb.utils import embedding_functions # Setting up embedding function, following protocol required by Chroma

最后,将 OpenAI API 密钥添加到一个变量中。请注意,这个密钥类似于你的密码 — 请勿分享。此外,在分享你的 Colab 笔记本前,请务必先删除 API 密钥。

# Add your OpenAI API Key to a variable
# Saving the key in a variable like so is bad practice. It should be loaded into environment variables and loaded from there, but this is okay for a quick demo
OPENAI_API_KEY='sk-4f3a9b8e7c4f4c8f8f3a9b8e7c4f4c8f-UsH4C3vE64' # Fake Key - use your own real key here

现在我们准备开始构建解决方案。以下是接下来步骤的高级视图:

构建 RAG 解决方案的核心步骤(图片由作者提供)

在编码时,我们将使用 LangChain,它已经成为构建此类解决方案的流行框架。它有助于从连接数据源到发送和接收 LLM 信息的每个步骤。LlamaIndex 是另一个简化构建 LLM 驱动应用的选项。虽然并不严格要求使用 LangChain(或 LlamaIndex),并且在某些情况下,高级抽象可能使团队对内部发生的事情一无所知,但我们将使用 LangChain,但仍会经常查看内部情况。

请注意,由于创新的速度如此之快,可能会有代码中使用的包更新,有些更新可能会导致代码停止工作,除非相应地进行更新。我不打算保持代码的最新状态。然而,本文旨在作为演示,代码可以作为参考或起点,您可以根据需要进行调整。

1. 创建知识库

1.1. 确定并读取文档 让我们访问书单并将内容读取到我们的 Colab 环境中。内容最初以 HTML 格式加载,这对网页浏览器很有用。然而,我们将使用 HTML 转文本工具将其转换为更易读的格式。

url = "https://ninadsohoni.github.io/booklist/" # Feel free to use any other website here, but note that some code might need to be edited to display contents properly

# Load HTML from URL and transform to a more readable text format
docs = Html2TextTransformer().transform_documents(AsyncHtmlLoader(url).load())

# Let's take a quick peek again to see what do we have now
print("\n\nIncluded metadata:\n", textwrap.fill(json.dumps(docs[0].metadata), width=100), "\n\n")
print("Page content loaded:")
print('...', textwrap.fill(docs[0].page_content[2500:3000], width=100, replace_whitespace=False), '...')

以下是运行代码在 Google Colab 上生成的内容:

执行上述代码的结果。网站内容被加载到 Colab 环境中。(图片由作者提供)

1.2. 将文档分解为较小的摘录 在我们将博客的信息加载到我们的知识库(即我们选择的数据库)之前,还有一步。文本不应按原样加载到数据库中。它应首先分割成较小的块。这有几个原因:

  1. 如果我们的文本太长,由于超出文本长度阈值(即“上下文大小”),则不能发送给 LLM。

  2. 较长的文本可能包含广泛且松散相关的信息。我们将依赖 LLM 挑选出相关部分——这可能不会总是如预期那样有效。使用较小的块,我们可以利用检索机制来识别仅相关的信息并发送给 LLM,如我们后面将看到的。

  3. LLM 对文本的开始和结束部分关注较强,因此较长的块可能导致 LLM 对后面更多的内容关注较少(称为“在中间迷失”)。

每个用例的适当块大小将根据用例的具体情况而有所不同,包括内容类型、使用的 LLM 及其他因素。明智的做法是尝试不同的块大小并在确定解决方案之前评估响应质量。在本演示中,我们将使用上下文感知的分块,其中书单中的每个书籍推荐都有自己的块。

# Now we split the entire content of the website into smaller chunks
# Each book review will get its own chunk since we are splitting by headings
# The LangChain splitter used here will also create a set of metadata from headings and associate it with the text in each chunk
headers_to_split_on = [ ("#", "Header 1"), ("##", "Header 2"),
    ("###", "Header 3"), ("####", "Header 4"), ("#####", "Header 5") ]
splitter = MarkdownHeaderTextSplitter(headers_to_split_on = headers_to_split_on)
chunks = splitter.split_text(docs[0].page_content)

print(f"{len(chunks)} smaller chunks generated from original document")

# Let's look at one of the chunks
print("\nLooking at a sample chunk:")
print("Included metadata:\n", textwrap.fill(json.dumps(chunks[5].metadata), width=100), "\n\n")
print("Page content loaded:")
print(textwrap.fill(chunks[5].page_content[:500], width=100, drop_whitespace=False), '...')

原始内容拆分后的众多文档块之一。(图片由作者提供)

请注意,如果迄今为止创建的文本块仍然比所需的长度长,可以使用其他文本拆分算法进一步拆分,这些算法可以通过 LangChain 或 LlamaIndex 轻松获得。例如,每本书的评论可以根据需要拆分为段落。

1.3. 将摘录加载到知识库 文本块现在准备好加载到知识库中。这些文本块首先通过嵌入模型转换为一系列捕捉文本意义的数字。然后,实际的文本及其数值表示(即嵌入)将加载到向量数据库——我们的知识库中。请注意,嵌入也由大语言模型(LLMs)生成,只是与聊天 LLM 的类型不同。如果你想了解更多关于嵌入的内容,上一篇文章通过示例展示了这一概念。

我们将使用向量数据库来存储所有信息。这将实现我们的知识库。向量数据库是专门设计用于通过嵌入相似性进行搜索的。如果我们想从数据库中搜索某些内容,搜索词首先通过嵌入模型转换为数值表示,然后将问题的嵌入与数据库中的所有嵌入进行比较。与问题嵌入最接近的记录(在我们的例子中,就是关于每本书的文本块)作为搜索结果返回,只要它们超过了一个阈值。

# We will get embeddings for each chunk (and subsequently questions) using an embeddings model from OpenAI
openai_embedding_func = embedding_functions.OpenAIEmbeddingFunction(api_key=OPENAI_API_KEY)

# Initialize vector DB and create a collection
persistent_chroma_client = chromadb.PersistentClient()
collection = persistent_chroma_client.get_or_create_collection("my_rag_demo_collection", embedding_function=openai_embedding_func)
cur_max_id = collection.count() # To not overwrite existing data

# Let's add data to our collection in the vector DB
collection.add(
    ids=[str(t) for t in range(cur_max_id+1, cur_max_id+len(chunks)+1)],
    documents=[t.page_content for t in chunks],
    metadatas=[None if len(t.metadata) == 0 else t.metadata for t in chunks]
    )

print(f"{collection.count()} documents in vector DB")
#  25 documents in vector DB

# Optional: We will write a scrappy helper function to print data slightly better -
#  it limits the length embeddings shown on the screen (since these are over a 1,000 long numbers).
#  Also shows a subset of text from documents as well as metadatas fields
def render_vectorDB_content(chromadb_collection):
    vectordb_data = pd.DataFrame(chromadb_collection.get(include=["embeddings", "metadatas", "documents"]))
    return pd.DataFrame({'IDs': [str(t) if len(str(t)) <= 10 else str(t)[:10] + '...'for t in vectordb_data.ids],
                         'Embeddings': [str(t)[:27] + '...' for t in vectordb_data.embeddings],
                         'Documents': [str(t) if len(str(t)) <= 300 else str(t)[:300] + '...' for t in vectordb_data.documents],
                         'Metadatas': ['' if not t else json.dumps(t) if len(json.dumps(t))  <= 90 else '...' + json.dumps(t)[-90:] for t in vectordb_data.metadatas]
                        })

# Let's take a look at what is in the vector DB using our helper function. We will look at the first 4 chunks
render_vectorDB_content(collection)[:4]

加载到向量数据库中的前几个文本块及其数值表示(即嵌入)的视图。(图片由作者提供)

2. 搜索问题相关的上下文

我们的终极目标是让我们的解决方案从向量数据库知识库中挑选相关信息,并将其与我们希望 LLM 回答的问题一起传递给 LLM。让我们尝试一下向量数据库搜索,询问问题“你能推荐几本侦探小说吗?”

# Here we are linking to the previously created an instance of the ChromaDB database using a LangChain ChromaDB client
vectordb = Chroma(client=persistent_chroma_client, collection_name="my_rag_demo_collection", 
                  embedding_function=OpenAIEmbeddings(openai_api_key=OPENAI_API_KEY)
                  )

# Optional - We will define another scrappy helper function to print data slightly better
def render_source_documents(source_documents):
    return pd.DataFrame({'#': range(1, len(source_documents) + 1), 
                         'Documents': [t.page_content if len(t.page_content) <= 300 else t.page_content[:300] + '...' for t in source_documents],
                         'Metadatas': ['' if not t else '...' + json.dumps(t.metadata, indent=2)[-88:] for t in source_documents]
                         })

# Here's where we are compiling the question
question = "Can you recommend a few detective novels?"

# Running the search against the vector DB based on our question
relevant_chunks = vectordb.similarity_search(question)

# Printing results
print(f"Top {len(relevant_chunks)} search results")
render_source_documents(relevant_chunks)

问题“你能推荐几本侦探小说吗?”的搜索结果前 4 名(图片由作者提供)

默认情况下,我们得到前 4 个结果,除非我们明确设置不同的值。在这个例子中,排名第一的结果是一本福尔摩斯小说,直接提到了“侦探”一词。第二个结果(《杰克尔的日子》)虽然没有“侦探”一词,但提到了“警察机构”和“揭露阴谋”,这些与“侦探小说”在语义上相关。第三个结果(《卧底经济学家》)提到了“卧底”一词,尽管它是关于经济学的。我认为最后一个结果被获取是因为它与小说/书籍有关,而不是特定的“侦探小说”,因为请求了四个结果。

同样,使用向量数据库并不是绝对必要的。您可以加载嵌入并在其他存储形式中进行搜索。“普通”关系数据库甚至 Excel 都可以使用。但您需要在应用程序逻辑中处理“相似性”计算,当使用 OpenAI 嵌入时,这可以是点积。另一方面,向量数据库为您处理了这些。

请注意,如果我们想通过元数据预筛选一些搜索结果,我们可以这样做。为了演示,我们将根据元数据中从书单加载的“Header 2”过滤。

# Let's try filtering and accessing records that match a specific metadata filter. This can probably be transformed into a pre-filter if needed
pd.DataFrame(vectordb.get(where = {'Header 2': 'Finance'}))

基于应用元数据预筛选的搜索结果,仅显示关键列。(图像由作者提供)

LLM 提供了一个有趣的机会,即利用 LLM 本身来检查用户问题,审查可用的元数据,评估是否需要和可能进行基于元数据的预筛选,并制定预筛选查询代码,这些代码可以在向量数据库上实际预筛选数据。有关更多信息,请参见 LangChain 的 自查询检索器

3. 使用 LLM 生成答案

接下来,我们将向 LLM 添加指令,基本上是说“我将给你一些信息片段和一个问题。请使用提供的信息片段回答问题”。然后,我们将这些指令、向量数据库中的搜索结果和我们的问题打包,并发送给 LLM 进行回应。所有这些步骤都由以下代码完成。

请注意,LangChain 提供了抽象一些代码的机会,因此您的代码不必像以下代码那样冗长。然而,以下代码的目的是展示发送到语言模型的指令。这里也是自定义这些指令的地方——例如,在这种情况下,默认指令被更改为请求 LLM 尽可能简洁地回应。如果默认设置适用于您的用例,您的代码可以完全跳过问题模板部分,LangChain 会在向 LLM 发送请求时使用其包中的默认提示。

# Let's select the language model behind free version of ChatGPT: GPT-3.5-turbo
llm = ChatOpenAI(model_name = 'gpt-3.5-turbo', temperature = 0, openai_api_key = OPENAI_API_KEY)

# Let's build a prompt. This is what actually gets sent to the ChatGPT LLM, with the context from our vector database and question injected into the prompt
template = """Use the following pieces of context to answer the question at the end. 
If you don't know the answer, just say that the available information is not sufficient to answer the question. 
Don't try to make up an answer. Keep the answer as concise as possible, limited to five sentences.
{context}
Question: {question}
Helpful Answer:"""
QA_CHAIN_PROMPT = PromptTemplate.from_template(template)

# Define a Retrieval QA chain, which will take the question, get the relevant context from the vectorDB, and pass both to the language model for a response
qa_chain = RetrievalQA.from_chain_type(llm, 
                                       retriever=vectordb.as_retriever(),
                                       return_source_documents=True,
                                       chain_type_kwargs={"prompt": QA_CHAIN_PROMPT}
                                       )

现在,让我们再次请求侦探小说的推荐,看看我们会得到什么回应。

# Let's ask a question and run our question-answering chain
question = "Can you recommend a few detective novels?"

result = qa_chain({"query": question})

# Let's look at the result
result["result"]

推荐侦探小说的解决方案回应(图像由作者提供)

让我们确认模型是否回顾了我们从向量数据库中获得的所有四个搜索结果,还是仅仅获取了响应中提到的两个结果?

# Let's look at the source documents used as context by the LLM
# We will use our helper function from before to limit the size of the information shown
render_source_documents(result["source_documents"])

与问题一起传递给 LLM 的上下文,以便于响应。(图像由作者提供)

我们可以看到,LLM 仍然访问了所有四个搜索结果,并推断出只有前两本书是侦探小说。

请注意,LLM 的响应每次提问时可能会有所不同,尽管发送的是相同的指令和来自向量数据库的相同信息。例如,在询问关于奇幻书籍推荐时,LLM 有时给出三本书籍推荐,有时则更多——尽管都是来自书单。在所有情况下,最推荐的书籍保持不变。请注意,这些变化发生在将一致性——创造力谱——即“温度”参数配置为 0 以最小化差异的情况下。

4. 添加“聊天”功能(可选)

现在,解决方案具备了必要的核心功能——它能够从网站中读取信息并根据这些信息回答问题。但目前它并未提供“对话式”的用户体验。感谢 ChatGPT,“聊天界面”已成为主流设计:我们现在期望这成为与生成式 AI 尤其是 LLM 互动的“自然”方式 😅。实现聊天界面的第一步涉及向解决方案中添加“内存”。

这里的“内存”是一种假象,LLM 实际上并不记住到目前为止的对话——它需要在每次回合中展示完整的对话记录。因此,如果用户向 LLM 提问后续问题,解决方案将打包原始问题、LLM 的原始答案以及后续问题,并将其发送给 LLM。LLM 阅读整个对话并生成有意义的响应以继续对话。

在问答聊天机器人中,如我们正在构建的这个,通常需要进一步扩展此方法,因为存在中间步骤需要从向量数据库中提取相关信息以制定对用户后续问题的响应。在问答聊天机器人中,“内存”是这样模拟的:

  1. 将所有问题和响应保留(在一个变量中)作为“聊天记录”

  2. 当用户提问时,将聊天记录和新问题发送给 LLM,并要求生成一个独立的问题

  3. 此时,聊天记录不再需要。使用独立的问题在向量数据库上进行新的搜索

  4. 将独立问题和搜索结果传递给 LLM,并附上指令以获得最终答案。这个步骤类似于我们在之前阶段“使用 LLM 生成答案”中实施的步骤

虽然我们可以使用简单的变量来跟踪聊天记录,但我们将使用 LangChain 的一种内存类型。我们将使用的特定内存对象提供了一个很好的功能,即在达到你指定的大小限制时自动截断较旧的聊天记录,通常是选定 LLM 可以接受的文本大小。在我们的案例中,LLM 应该能够接受略超过 4,000 个“tokens”(即词汇部分),这大约是 3,000 个单词或 ~5 页的 Word 文档。OpenAI 提供了相同 ChatGPT LLM 的 16k 变体,可以接受 4 倍的输入。因此,需要配置内存大小。

这是实现这些步骤的代码。同样,LangChain 提供了更高层次的抽象,代码不必如此明确。这个版本只是为了展示发送到 LLM 的底层指令——首先将聊天记录浓缩成一个独立的单一问题,然后用于向量数据库搜索,其次根据向量数据库搜索结果生成对生成的独立问题的响应。

# Let's create a memory object to track chat history. This will start accumulating human messages and AI responses.
# Here a "token" based memory is used to restrict the length of chat history to what can be passed into the selected LLM. 
# Generally, the maximum token length configured will depend on the LLM. Assuming we are using the 4K version of the LLM, 
#  we will set the token maximum to 3K, to allow some room for the question prompt.
#  The LLM parameter is to make LangChain aware of the tokenization scheme of the selected LLM. 
memory = ConversationTokenBufferMemory(memory_key="chat_history", return_messages=True, input_key="question", output_key="answer", max_token_limit=3000, llm=llm)

# While LangChain includes a default prompt to generate a standalone question based on the users' latest question and
# any context from the conversation up to that point, we will extend the default prompt with additional instructions.
standalone_question_generator_template = """Given the following conversation and a follow up question, 
rephrase the follow up question to be a standalone question, in its original language.
Be as explicit as possible in formulating the standalone question, and 
include any context necessary to clarify the standalone question.

Conversation:
{chat_history}
Follow Up Question: {question}
Standalone question:"""
updated_condense_question_prompt = PromptTemplate.from_template(standalone_question_generator_template)

# Let's rebuild the final prompt (again, optional since LangChain uses a default prompt, though it might be a little different)
final_response_synthesizer_template = """Use the following pieces of context to answer the question at the end. 
If you don't know the answer, just say that the available information is not sufficient to answer the question. 
Don't try to make up an answer. Keep the answer as concise as possible, limited to five sentences.
{context}
Question: {question}
Helpful Answer:"""
custom_final_prompt = PromptTemplate.from_template(final_response_synthesizer_template)

qa = ConversationalRetrievalChain.from_llm(
    llm=llm, 
    retriever=vectordb.as_retriever(), 
    memory=memory,
    return_source_documents=True,
    return_generated_question=True,
    condense_question_prompt= updated_condense_question_prompt,
    combine_docs_chain_kwargs={"prompt": custom_final_prompt})

# Let's again ask the question we previously asked the retrieval QA chain
query = "Can you recommend a few detective novels?"
result = qa({"question": query})
print(textwrap.fill(result['answer'], width=100))

解决方案的侦探小说推荐。与之前仅使用“问答”能力而没有“记忆”时收到的响应相同(图片由作者提供)

让我们提出一个后续问题,并查看响应以验证解决方案现在具有“记忆”并且可以对后续问题进行对话式回答:

query = "Tell me more about the second book"
result = qa({"question": query})
print(textwrap.fill(result['answer'], width=100))

对“第二本书”的更多信息的后续问题的响应。解决方案返回更多关于同一本书的信息(图片由作者提供)

让我们看看在引擎盖下发生了什么,以验证解决方案确实经过了本节开头概述的四个步骤。让我们从聊天记录开始,以验证解决方案确实记录了到目前为止的对话:

# Let's look at chat history upto this point
result['chat_history']

提问第二个问题后的聊天记录。注意此时响应也包括在对话中。(图片由作者提供)

让我们看看解决方案除了聊天记录外还跟踪了什么:

# Let's print the other parts of the results
print("Here is the standalone question generated by the LLM based on chat history:")
print(textwrap.fill(result['generated_question'], width=100 ))
print("\nHere are the source documents the model referenced:")
display(render_source_documents(result['source_documents']))
print(textwrap.fill(f"\nGenerated Answer: {result['answer']}", width=100, replace_whitespace=False) )

提问第二个问题后,除聊天记录之外的输出。(图片由作者提供)

解决方案内部使用 LLM 将问题“告诉我更多关于第二本书的事”转换为“你能提供更多关于弗雷德里克·福赛斯的《贼日》‘The Day of the Jackal’的信息吗?”。有了这个问题,解决方案能够在向量数据库中搜索任何相关信息,并这次首先检索到《贼日》的信息。虽然注意到也包括了一些关于其他书籍的无关搜索结果。

快速的可选侧边栏讨论潜在问题

潜在问题 #1 — 独立问题生成不佳: 在我的测试中,聊天解决方案在生成一个好的独立问题时并不总是成功,直到调整了问题生成器提示。例如,对于一个后续问题,“告诉我关于第二本书的事”,生成的后续问题往往是“你能告诉我关于第二本书的什么?”这本身并没有特别的意义,并导致随机的搜索结果,因此 LLM 的生成响应看起来也是随机的。

潜在问题 #2 — 原始问题与跟进问题之间的搜索结果变化: 值得注意的是,即使第二个生成的问题明确提到感兴趣的书籍,向量数据库搜索返回的结果中也包括其他书籍的结果,更重要的是,这些搜索结果与原始问题的结果不同!在这个例子中,这种搜索结果的变化是期望的,因为问题从“侦探小说推荐”变成了特定的小说。然而,当用户提出跟进问题以深入探讨某个主题时,问题表述的变化或 LLM 生成的独立问题可能会导致不同的搜索结果或搜索结果的不同排序,这可能是期望的。

这个问题可能会在一定程度上自动得到缓解,至少是通过对向量数据库进行更广泛的初步搜索——返回更多的结果,而不仅仅是我们示例中的 4-5 个——并对结果进行重新排序,以确保最相关的结果上升到顶部并始终发送到 LLM 以生成最终响应(参见Cohere 的“重新排序”)。此外,应用程序应相对容易识别搜索结果是否发生了变化。可能可以应用一些启发式方法来判断搜索结果的变化程度(通过排序和重叠度量)以及问题变化的程度(通过余弦相似度等距离度量)是否匹配。至少在搜索结果在聊天轮次中出现意外波动的情况下,可以根据用例的关键性以及最终用户的培训或素养,提醒最终用户并让他们参与更详细的检查。

控制这种行为的另一个想法是利用 LLM 来决定跟进问题是否需要再次访问向量数据库,或者这个问题是否可以用之前获取的结果有意义地回答。一些用例可能希望生成两组搜索结果和回答,让 LLM 在答案之间裁定,另一些用例可能通过赋予用户冻结上下文的能力来将控制上下文的责任转交给用户(这取决于用例、用户培训或素养以及其他考虑因素),还有一些用例可能对跟进问题中搜索结果的变化持宽容态度。

正如你可能已经看出,获得一个基本解决方案是相对简单的,但做到完美——这才是难点。这里提到的问题只是冰山一角。好了,回到主要任务 …

5. 添加预编码 UI

最后,聊天机器人的功能已经准备好。现在,我们可以添加一个漂亮的用户界面以改善用户体验。这是(某种程度上)由于 Python 库如 Gradio 和 Streamlit,使得基于 Python 编写的指令构建前端小部件成为可能。在这里,我们将使用 Gradio 快速创建一个用户界面。

以使任何尚未执行代码的人能够跟上进度,并演示一些达到相同结果的变体,以下两个代码块是自包含的,可以在全新的 Colab 笔记本中运行,以生成完整的聊天机器人。

# Initial setup - Install necessary software in code environment
!pip install openai tiktoken langchain chromadb html2text gradio   # Uncomment by removing '#' at the beginning if you're starting here and haven't yet installed anything

# Import packages needed to enable different functionality for the solution
from langchain.document_loaders import AsyncHtmlLoader # To load website content into a document
from langchain.text_splitter import MarkdownHeaderTextSplitter # To document into smaller chunks by document headings 
from langchain.document_transformers import Html2TextTransformer # To converrt HTML to Markdown text
from langchain.chat_models import ChatOpenAI # To use OpenAI's LLM
from langchain.prompts import PromptTemplate # To formulate instructions / prompts
from langchain.chains import RetrievalQA, ConversationalRetrievalChain # For RAG
from langchain.memory import ConversationTokenBufferMemory # To maintain chat history
from langchain.embeddings.openai import OpenAIEmbeddings # To convert text to numerical representation
from langchain.vectorstores import Chroma # To interact with vector database
import pandas as pd, gradio as gr # To show data as tables, and to build UI respectively
import chromadb, json, textwrap # Vector database, converting json to text, and prettify printing respectively
from chromadb.utils import embedding_functions # Setting up embedding function, following protocol required by Chroma

# Add the OpenAI API Key to a variable
# Saving the key in a variable like so is bad practice. It should be loaded into environment variables and loaded from there, but this is okay for a quick demo
OPENAI_API_KEY='sk-4f3a9b8e7c4f4c8f8f3a9b8e7c4f4c8f-UsH4C3vE64' # Fake Key - use your own real key here

在运行下一组代码以呈现聊天机器人 UI 之前,请注意,当通过 Colab 渲染时,应用程序对于任何拥有链接的人公开可访问 3 天(链接在 Colab 笔记本单元输出中提供)。理论上,可以通过将代码中的最后一行更改为demo.launch(share=False)来保持应用的私密性,但那时我无法使应用正常工作。相反,我更倾向于在 Colab 中以“调试”模式运行,这样 Colab 单元会“运行”直到停止,然后终止聊天机器人。或者,在不同的 Colab 单元中运行下面的代码,以终止聊天机器人并删除在 Colab 中加载到 Chroma 向量数据库中的内容。

# To be run at the end to terminate the demo chatbot
demo.close() # To end the chat session and terminate the shared demo

# Retrieve and delete the vector DB collection created for the chatbot
vectordb = Chroma(client=persistent_chroma_client, collection_name="my_rag_demo_collection", embedding_function=openai_embedding_func_for_langchain)
vectordb.delete_collection()

以下是将聊天机器人作为应用运行的代码。大部分代码重复了到目前为止的文章中的代码,所以应该很熟悉。请注意,与之前的代码相比,下面的代码存在一些差异,包括但不限于没有使用之前用过的 LangChain 的‘token’内存对象进行内存管理。这意味着随着对话的继续,历史记录将变得过长,无法传递给语言模型的上下文,应用需要重启。

# Initiate OpenAI embedding functions. There are two because the function protocol is different when passing the function to Chroma DB directly vs using it with Chroma DB via LangChain
openai_embedding_func_for_langchain = OpenAIEmbeddings(openai_api_key=OPENAI_API_KEY)
openai_embedding_func_for_chroma = embedding_functions.OpenAIEmbeddingFunction(api_key=OPENAI_API_KEY)

# Initiate the LangChain chat model object using the GPT 3.5 turbo model
llm = ChatOpenAI(model_name='gpt-3.5-turbo', temperature=0, openai_api_key=OPENAI_API_KEY)

# Initialize vector DB and create a collection
persistent_chroma_client = chromadb.PersistentClient()
collection = persistent_chroma_client.get_or_create_collection("my_rag_demo_collection", embedding_function=openai_embedding_func_for_chroma)

# Function to load website content into vector DB
def load_content_from_url(url):
  # Load HTML from URL and transform to a more readable text format
  docs = Html2TextTransformer().transform_documents(AsyncHtmlLoader(url).load())
  # Split docs by section
  headers_to_split_on = [ ("#", "Header 1"), ("##", "Header 2"), ("###", "Header 3"), ("####", "Header 4"), ("#####", "Header 5") ]
  chunks = MarkdownHeaderTextSplitter(headers_to_split_on = headers_to_split_on).split_text(docs[0].page_content)
  # Here we are linking to the previously created an instance of the ChromaDB database
  vectordb_collection = persistent_chroma_client.get_or_create_collection("my_rag_demo_collection", embedding_function=openai_embedding_func_for_chroma)
  # Let's add data to the vector DB; specifically to our collection in the vector DB
  cur_max_id = vectordb_collection.count()
  vectordb_collection.add(ids=[str(t) for t in range(cur_max_id+1, cur_max_id+len(chunks)+1)],
                           documents=[t.page_content for t in chunks],
                           metadatas=[None if len(t.metadata) == 0 else t.metadata for t in chunks]
                           )
  # Alert user that content is loaded and ready to be queried
  gr.Info(f"Website content loaded. Vector DB now has {vectordb_collection.count()} chunks")
  return

# Define the UI and the chat function
with gr.Blocks() as demo:

  # Function to chat with language model using documents for context
  def predict(message, history):
    # Here we are linking to the previously created an instance of the ChromaDB database using a LangChain ChromaDB client
    langchain_chroma = Chroma(client=persistent_chroma_client, collection_name="my_rag_demo_collection", embedding_function=openai_embedding_func_for_langchain)
    # Convert to langchain chat history format - list of tuples rather than list of lists
    langchain_history_format = []
    for human, ai in history:
      langchain_history_format.append((human, ai))
    # We are now defining the ConversationalRetrieval chain, starting with the prompts used
    standalone_question_generator_template = """Given the following conversation and a follow up question, 
    rephrase the follow up question to be a standalone question, in its original language. Be as explicit as possible 
    in formulating the standalone question, and include any context necessary to clarify the standalone question.

    Conversation: {chat_history}
    Follow Up Question: {question}
    Standalone question:"""
    updated_condense_question_prompt = PromptTemplate.from_template(standalone_question_generator_template)
    # Let's rebuild the final prompt (again, optional since LangChain uses a default prompt, though it might be a little different)
    final_response_synthesizer_template = """Use the following pieces of context to answer the question at the end. 
    If you don't know the answer, just say that the available information is not sufficient to answer the question. 
    Don't try to make up an answer. Keep the answer as concise as possible, limited to five sentences.
    {context}
    Question: {question}
    Helpful Answer:"""
    custom_final_prompt = PromptTemplate.from_template(final_response_synthesizer_template)
    # Define the chain
    qa_chain = ConversationalRetrievalChain.from_llm(llm, retriever=langchain_chroma.as_retriever(), 
                                                     return_source_documents=True, return_generated_question=True, 
                                                     condense_question_prompt= updated_condense_question_prompt,
                                                     combine_docs_chain_kwargs={"prompt": custom_final_prompt})
    # Execute the chain
    gpt_response = qa_chain({"question": message, "chat_history": langchain_history_format})
    # Add human message and LLM response to chat history
    langchain_history_format.append((message, gpt_response['answer']))
    return gpt_response['answer'] 

  gr.Markdown(
      """
      # Chat with Websites
      ### Enter URL to extract content from website and start question-answering using this Chatbot
      """
  )
  with gr.Row():
    url_text = gr.Textbox(show_label=False, placeholder='Website URL to load content', scale = 5)
    url_submit = gr.Button(value="Load", scale = 1)
    url_submit.click(fn=load_content_from_url, inputs=url_text)

  with gr.Row():
    gr.ChatInterface(fn=predict)

demo.launch(debug=True)

你可以通过给应用提供不同的 URL 来加载内容进行尝试。无需多言:这不是一个生产级应用,只是用来演示基于 RAG 的 GenAI 解决方案的构建块。这充其量是一个早期原型,如果要将其转换为常规产品,大部分的软件工程工作还在前面。

重新审视介绍中的常见问题

根据我们创建的聊天机器人背景和知识,让我们重新审视在介绍中提出的一些问题,并深入探讨一下。

  1. 我们的用例是否适合 LLM 驱动的解决方案?也许传统的分析方法、监督学习或其他方法更合适?

    LLM 在“理解”语言相关任务以及遵循指令方面表现出色。因此,LLM 的早期使用案例包括问答、总结、生成(在这里是文本)、提供更好的基于意义的搜索、情感分析、编码等。LLM 还获得了解决问题和推理的能力。例如,LLM 可以充当学生作业的自动评分员,只要你提供答案键,甚至有时不提供。

    另一方面,基于大量数据点的预测或分类、用于营销优化的多臂赌博机实验、推荐系统、强化学习系统(如 Roomba、Nest 温控器、优化能源消耗或库存水平等)是其他类型分析或机器学习的强项……至少目前是这样。传统 ML 模型向 LLM 提供信息和反向传递信息的混合方法也应考虑作为解决核心业务问题的整体方案。

  2. 如果 LLM 是可行的解决方案,我们的使用案例是否可以通过现成的产品(例如,ChatGPT Enterprise)现在或在不久的将来得到解决?经典的构建与购买决策。

    OpenAI、AWS 和其他公司提供的服务和产品将变得更加广泛、更好,可能还更便宜。例如,ChatGPT 允许用户上传文件进行分析,Bing Chat 和 Google 的 Bard 让你指向外部网站进行问答,AWS Kendra 将语义搜索引入企业的信息中,Microsoft Copilot 让你在 Word、Powerpoint、Excel 等应用中使用 LLM。正如公司不会自己构建操作系统或数据库一样,公司也应该考虑是否需要构建可能被当前和未来的现成产品所取代的 AI 解决方案。另一方面,如果公司的使用案例非常具体或在某种程度上受限,例如由于敏感性或法规指导不能将敏感数据发送给任何供应商,那么可能需要在公司内部构建生成性 AI 产品以解决这些使用案例。使用 LLM 推理能力但承担的任务或生成的输出与现成解决方案差异太大的产品可能需要内部开发。例如,监控工厂车间、制造过程或库存水平的系统可能需要定制开发,特别是当没有好的领域特定产品时。此外,如果应用需要专业领域知识,那么在领域特定数据上微调的 LLM 可能会优于 OpenAI 的通用 LLM,内部开发也可以考虑。

  3. 我们的 LLM 驱动产品的不同构建模块有哪些?这些模块中哪些已经商品化,哪些可能需要更多时间来构建和测试?

    像我们构建的 RAG 解决方案的高级构建块包括数据管道、向量数据库、检索、生成,当然还有 LLM。LLM 和向量数据库有很多优秀的选择。数据管道、检索、生成的提示工程将需要一些传统的数据科学实验来针对具体用例进行优化。一旦初步解决方案到位,生产化将需要大量工作,这在任何数据科学/机器学习管道中都是真实的。本讲座提供了有关生产化的宝贵经验:LLMs in Production: Learning from Experience, Dr. Waleed Kadous, Chief Scientist, AnyScale

  4. 我们如何衡量解决方案的性能?有哪些杠杆可以改善我们产品的输出质量? 与任何技术(或非技术)解决方案一样,商业影响应使用领先的关键绩效指标来衡量。一些直接度量难以测量,通常会被替代指标如每日活跃用户数(DAU)和其他产品指标所取代。

    商业指标应补充技术指标,以评估 RAG 解决方案的性能。可以使用一系列测试信息量、事实准确性、相关性、毒性等的指标来评估回应的整体质量——系统的回应与专家或如 GPT-4(当前)的最先进模型的回应相比如何。这有助于深入了解各个组件的性能,以便迭代和改进每个组件:解决方案将用作上下文的信息质量、检索和生成。

    i. 数据质量如何?如果组织可用的数据存储在向量数据库中没有所需的信息,则没有人或 LLM 能够基于这些信息构造回应。

    ii. 检索效果如何?假设信息可用,系统在找到和提取相关信息方面有多成功?

    iii. 生成(即合成)效果如何?假设信息可用、检索正确,并传递给 LLM 生成最终回应,LLM 是否按预期使用这些信息?

    每个领域都可以单独评估并同时改进,以提升整体输出。

    改善数据质量: 公司需要在数据管道上进行工作,以向系统提供良好的信息。如果向量数据库中的信息质量差,拥有优秀的 LLM 也不会显著改善输出。除了采用传统的数据质量和治理框架外,公司还应考虑改善分块的质量(下一问题的回应中将更多讨论此事)。

    改善检索: 通过尝试不同的检索算法、语义重排序、结合语义搜索和关键词搜索的混合搜索,以及微调嵌入,可以改善检索。改善指令/提示也应有助于提升检索质量。

    改善生成: 随着 LLM 的进步,合成步骤将得到改善,检索可能也会因为嵌入模型的改进而得到提升。另一种选择是进行微调,前提是资源和时间充足,这可以提高特定领域和任务的响应质量。例如,针对特定医疗条件进行微调的小模型可能在任务上优于像 GPT-4 这样的通用模型,同时也更快、更便宜。

  5. 我们的数据质量是否适合用例?我们是否正确组织了数据,并将相关数据传递给 LLM?

    数据质量可以通过传统的数据质量与治理框架进行评估。此外,对于 LLM 驱动的解决方案,LLM 回答用户问题或执行任务所需的信息应在解决方案可用的数据中存在。

    假设数据是可用的,数据应根据用例和所使用的 LLM 进行适当的拆分。块不应过于宽泛,以免稀释与特定主题相关的连贯性,也不应过于狭窄,以免遗漏所有必要的上下文。数据不应以将必要的上下文分割在块之间并且在这种分隔下毫无意义的方式进行拆分。例如,如果下面的两个句子被拆分成两个块,

    “OpenAI 的 GPT-3.5 是一个强大的 LLM。它可以支持高达 16K 令牌的上下文大小。”

    像“告诉我关于 GPT 3.5 LLM 的情况”这样的问题可能不会获取第二句,因为它没有提到 GPT 3.5,而这条信息可能不会提供给用户,仅仅是由于子优化块的原因。更危险的是,由于上下文大小和令牌与 LLM 的语义关联,当询问完全不同的 LLM 时,可能仍会获取该句子,且回答可能是其他重点模型的上下文大小高达 16K,这将是不准确的。这是一个在生产环境中不太可能遇到的简化示例,但这个想法是成立的。

    改善内容质量的一种可能方法是使用上下文感知的文本拆分,例如按逻辑部分拆分(如我们书单的示例)。如果任何逻辑块过大——例如,维基百科上关于某些主题的页面可能非常长——它们可以进一步按逻辑部分或按语义单元(如段落)拆分,同时在块之间保持有意义的重叠,并确保将整体元数据和块特定的元数据传递给 LLM。

  6. 我们能否确信 LLM 的回答总是事实准确的?也就是说,我们的解决方案会不会在生成回答时偶尔‘幻觉’?

    RAG 的一个关键卖点是推动事实准确性。GPT 3.5 和 GPT-4 在遵循指令方面表现良好:“仅从提供的上下文中回应,或说‘基于提供的信息无法回答该问题’”。这被推测是由于 OpenAI 进行了大量的基于人类反馈的强化学习(RLHF)。作为推论,其他 LLM 可能当前在遵循指令方面表现不佳。对于生产应用,特别是面向外部的应用,进行大量测试以验证生成的输出是否忠实于从向量数据库检索的可用上下文,即使 LLM 相信这是正确的,也是明智的。方法包括对样本进行手动测试,使用强大的模型如 GPT-4 测试检索到的上下文样本和其他模型生成的响应,或使用如Galileo这样的服务和产品,专注于实时检测 LLM 幻觉。

结论

如果你在 11 个月前就知道这些内容,这将值得与你公司首席执行官进行一次演示,甚至可能有一个 TED 演讲向更广泛的观众介绍。今天,这已成为 AI 素养的基本要求,特别是如果你参与生成式 AI 产品的交付。希望通过这次练习,你能比较跟得上!👍

一些结束语,

  • 这项技术具有巨大的潜力——还有多少其他技术能“思考”到这种程度,并且能作为“推理引擎”(用安德鲁·吴博士的话说,参见这里)?

  • 虽然前沿模型(目前为 GPT-4)将继续进步,但开源模型及其领域特定和任务特定的微调变体在许多任务中将具有竞争力,并找到许多应用。

  • 无论好坏,这项耗资数百万(甚至数亿?)美元开发的前沿技术现在是免费的——你可以填写一个表格,下载 Meta 功能强大的 Llama2 模型,拥有非常宽松的许可。HuggingFace 的模型中心几乎有 30 万个基础 LLM 或其微调变体。硬件也已经商品化。

  • OpenAI 模型现在能够识别并使用“工具”(功能、API 等),使得解决方案不仅能与人类和数据库接口,还能与其他程序接口。LangChain 和其他软件包已经展示了如何将 LLM 作为“智能体”的“大脑”,这些智能体能够接受输入、决定采取的行动并执行,重复这些步骤直到智能体实现目标。我们的简单聊天机器人在确定性序列中使用了两个 LLM 调用——生成独立问题,并将搜索结果合成成连贯的自然语言响应。想象一下,数百次对快速发展的 LLM 进行的调用会取得什么成果!

  • 这些快速进展是由于 GenAI 周围的巨大动力,它将通过我们的设备渗透到企业和日常生活中。最初是以更简单的方式,但随后将在利用技术的推理和决策能力的越来越复杂的应用中体现,与传统 AI 融合。

  • 最终,现在是一个绝佳的时机来参与,因为应用这项技术的竞争环境相对公平——自 2022 年 12 月 ChatGPT 的爆发以来,每个人基本上都在同一时间学习这项技术。当然,研发方面情况有所不同,大型科技公司已经投入了多年和数十亿美元来开发这项技术。尽管如此,为了将来构建更复杂的解决方案,现在正是开始的最佳时机!

额外资源

使用 Python 实现生成式 AI:自编码器

原文:towardsdatascience.com/hands-on-generative-ai-with-gans-using-python-autoencoders-c77232b402fc

图片由 GR Stocks 提供,来源于 Unsplash

从自编码器开始,更好地理解 GANs

Marcello PolitiTowards Data Science Marcello Politi

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

--

介绍

最近几年,由于人工智能能够生成几乎与真实数据难以区分的合成实例,生成模型变得越来越受欢迎。你可能对能够生成文本的神经网络 Chat GPT 和能够生成完全原创图像的 DALLE 比较熟悉。

网站 thispersondoesnotexist.com 是一个著名的生成网络例子,每次你访问这个链接时,都会显示一个不存在的人的 AI 生成图像。这只是生成式人工智能惊人可能性中的一个例子。

随着时间的推移,生成式人工智能已经发展,随着研究的推进,出现了许多架构来解决各种应用场景。但要开始学习生成式人工智能,你需要熟悉一种架构:生成对抗网络(GANs)。

GANs 概述

生成网络的最终目标是生成与其训练集具有相同分布的新数据。生成网络通常被视为机器学习中的无监督学习的一部分,因为它们不需要标记数据。生成对抗网络(GAN)概念由 Ian Goodfellow 于 2014 年提出,相关论文是“Generative Adversarial Nets”。

最初,GAN 的架构基于全连接层,旨在生成低分辨率图像,如手写数字。从那时起,GAN 经历了众多改进和应用。它们已被用于图像到图像的转换、图像超分辨率和图像修补等任务,其中网络学习重建图像的缺失部分。

GANs 也可以用于监督学习和半监督学习任务。例如,条件 GANs 可以根据某些条件生成数据,如根据用户输入生成不同动物的图像。半监督 GANs 使用标记数据来提高生成数据的质量。

GANs 的应用远不止于图像生成。这些模型已被应用于 NLP(自然语言处理)、音乐生成甚至药物发现!生成模型的潜力巨大,随着技术的不断进步,我们可以期待更多创新应用的出现。

GANs 很有吸引力,因为它们可以生成与训练数据分布相同的数据。

自编码器与 GANs

要完全理解这些生成对抗网络的工作原理,首先了解自编码器是很有帮助的。自编码器是一种可以压缩和解压训练数据的神经网络类型,使其在数据压缩和特征提取方面非常有用。

标准自编码器无法生成新数据,但它们作为理解 GANs 的有用起点。自编码器由两个串联的网络组成——编码器网络和解码器网络。编码器网络接收d 维输入特征 x,并将其编码为 p 维向量 z。换句话说,编码器的角色是学习如何建模函数 z = f(X)。向量 z 也称为潜在向量。通常,潜在向量的维度低于原始输入向量,因此p < d

解码器网络接收编码后的向量z 并重建原始输入特征 x自编码器的目标是最小化原始输入特征与重建特征之间的差异。通过这样做,自编码器学习在压缩和解压输入数据的同时保留其本质特征

让我们看看一个表示自编码器架构的图片。

自编码器架构(图像由作者提供)

虽然自编码器可以用于数据压缩和特征提取,但它们无法像 GANs 那样生成新数据。

在这个简单的例子中,编码器和解码器都是简单的线性层,用于压缩和解压空间。更复杂的架构可以包含多个层,并且可能包含不同类型的层,例如在应用于图像模型时使用卷积层。

让我们看看在 PyTorch 中自编码器的一个简单实现。

class AutoEncoder(nn.Module):
    def __init__(self, **kwargs):
        super().__init__()
        self.encoder = nn.Linear(
          in_features=kwargs["input_shape"], out_features=128
        )
        self.decoder = nn.Linear(
            in_features=128, out_features=kwargs["input_shape"]
        )

    def forward(self, x):
        latent_vector = torch.relu(self.encoder(x))
        reconstructed = torch.relu(self.decoder(latent_vector))
        return reconstructed

AutoEncoder 类像往常一样继承 nn.Module,包括一个编码器和一个解码器,它们都是线性层,接受一个大小为 input_shape(例如 784)的输入向量 x,将其减少到大小为 128 的潜在空间,最终重建原始大小的向量。

其他类型的 AutoEncoders

我们已经看到,通常潜在向量的大小小于输入向量的大小,因此发生压缩,即 p<d。这类 autoencoders 被称为 undercomplete

但我们可以创建一个比输入向量更大的潜在向量,p>d。当然,overcomplete autoencoders!但它们的用途是什么?它们可以用于 降噪

在这些网络的训练过程中,输入数据中会添加噪声,例如模糊的图像,网络必须能够重建无噪声的图像。这种特定的架构称为去噪 autoencoder。

基本去噪架构(图片作者)

Autoencoder 的实际示例

现在让我们看一个如何使用 PyTorch 实现更复杂的 Autoencoder 的例子,该 Autoencoder 用于生成类似于 MNIST 数据集的合成数据。

首先,像往常一样,我们安装并导入所需的库。

!pip install torchvision
!pip install torch

from torchvision import datasets
from torchvision import transforms
import torch
import matplotlib.pyplot as plt

现在我们只需导入数据集。在 Pytorch 中这非常简单,因为库提供了快速下载数据集的方法。所以我们实例化数据集,然后是我们需要训练网络的 dataloader。我们还定义了一个转换,将图像在被网络处理时转换为张量。

dataset = datasets.MNIST(root = "./data",
      train = True,
      download = True,
      transform = tensor_transform)

loader = torch.utils.data.DataLoader(dataset = dataset,
         batch_size = 64,
         shuffle = True)

tensor_transform = transforms.ToTensor()

现在是时候创建 AutoEncoder 类了,就像之前一样。但在这种情况下,编码器和解码器都会更深,因为它们将由更多的层组成,以更好地捕捉图像特征。

class AutoEncoder(torch.nn.Module):
 def __init__(self):
  super().__init__()

  self.encoder = torch.nn.Sequential(
   torch.nn.Linear(28 * 28, 128),
   torch.nn.ReLU(),
   torch.nn.Linear(128, 64),
   torch.nn.ReLU(),
   torch.nn.Linear(64, 36),
   torch.nn.ReLU(),
   torch.nn.Linear(36, 18),
   torch.nn.ReLU(),
   torch.nn.Linear(18, 9)
  )

  self.decoder = torch.nn.Sequential(
   torch.nn.Linear(9, 18),
   torch.nn.ReLU(),
   torch.nn.Linear(18, 36),
   torch.nn.ReLU(),
   torch.nn.Linear(36, 64),
   torch.nn.ReLU(),
   torch.nn.Linear(64, 128),
   torch.nn.ReLU(),
   torch.nn.Linear(128, 28 * 28),
   torch.nn.Sigmoid()
  )

 def forward(self, x):
  encoded = self.encoder(x)
  decoded = self.decoder(encoded)
  return decoded

如你所见,它并没有比最初看到的简单例子复杂多少。

正如我们训练模型时总是这样,我们实例化类,并定义一个损失函数和一个优化器。在这里是 MSELoss 和 Adam。

model = AutoEncoder()
loss_function = torch.nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(),
       lr = 1e-1,
       weight_decay = 1e-6)

训练网络的时刻来了。我们必须遍历我们的 dataloader,并调整输入以匹配模型架构。然后计算输出和获得的损失,并将所有内容保存到一个列表中,以便在训练结束时绘制。

epochs = 25
losses = []
for epoch in range(epochs):
 for (image, _) in loader:

   image = image.reshape(-1, 28*28)
   reconstructed = model(image)

   loss = loss_function(reconstructed, image)
   optimizer.zero_grad()
   loss.backward()
   optimizer.step()

   losses.append(loss)

plt.style.use('fivethirtyeight')
plt.xlabel('Iteration')
plt.ylabel('MSE-Loss')
plt.plot(losses[-100:])

好了,我们的网络已经训练完成!现在我们可以将原始图像与网络重建的图像进行对比。

plt.imshow(dataset[0])
plt.imshow(model(dataset[0].reshape(-1, 28, 28))

原始 vs 重建(来源: arxiv.org/pdf/2003.05991.pdf

最终思考

学习自编码器对于理解生成对抗网络(GANs)的工作原理非常有帮助。在这篇文章中,我们了解了一些关于这些架构的理论,然后看到它们如何用于重构 MNIST 图像的输出。使用它们非常有趣,而且它们也有各种用途,其中一些包括压缩和解压输入或去噪图像,正如我们所见。在下一篇文章中,我将解释自编码器如何与 GANs 相关,并且我们将看到如何实现它们。 关注我以获取未来的文章😉

结束

Marcello Politi

Linkedin, Twitter, CV

使用 Python 的 GANs 实践生成式 AI:DCGAN

原文:towardsdatascience.com/hands-on-generative-ai-with-gans-using-python-dcgan-6077f0067ac3

Vinicius "amnx" Amano 拍摄的照片,来源于 Unsplash

使用卷积层在 PyTorch 中改进合成图像生成

Marcello PolitiTowards Data Science Marcello Politi

·发表在 Towards Data Science ·5 分钟阅读·2023 年 4 月 4 日

--

介绍

在我的上一篇文章中,我们已经看到了如何使用 GANs 生成 MNIST 数据集类型的图像。我们取得了不错的结果,并成功实现了我们的目标。然而,这两个网络 G(生成器)和 D(判别器)主要由密集层组成。此时,你应该知道,通常在处理图像时我们使用 CNNs(卷积神经网络),因为它们使用卷积层。所以,让我们看看如何通过使用这些类型的层来改进我们的 GANs。使用卷积层的 GANs 称为 DCGANs

什么是转置反卷积?

通常当我们使用 CNNs 时,我们习惯于使用卷积层。不过,在这种情况下,我们还需要“反向”操作,即转置反卷积,有时也称为反卷积。

这个操作使我们能够对特征空间进行上采样。例如,如果我们有一个 5x5 的网格表示的图像,我们可以将这个网格“放大”到 28x28。

原则上,你做的事情很简单,你在初始特征图的元素内部填充零以扩大它,然后应用正常的卷积操作,使用特定的核大小、步幅和填充

例如,假设我们想将 5x5 的特征空间转换为 8x8。首先,通过插入零,我们创建一个 9x9 的特征空间,然后通过应用 2x2 的滤波器将其再次缩小到 8x8。让我们看一个图形示例。

转置卷积(图像由作者提供)

在这个网络中,我们还将使用批量归一化层,它们有助于解决内部协方差偏移问题。简而言之,它们的作用是在每一层之前对每个批次进行归一化,以便在训练过程中数据的分布没有变化。

生成器架构

生成器将由一系列转置卷积层构成,这些层将初始的随机向量 z 转换为我们想要生成的图像的正确尺寸,这里是 28x28。另一方面,特征图的深度将变得越来越小,不像卷积层那样。

生成器架构(作者提供的图像)

判别器架构

另一方面,判别器是一个经典的 CNN 网络,负责对图像进行分类。因此,我们将有一系列卷积层,直到我们得到一个单一的数字,即输入是实际的还是假的概率。

判别器架构(作者提供的图像)

让我们开始编码吧!

我将使用Deepnote,但如果你愿意,你可以使用 Google Colab。

首先,检查你的硬件是否有可用的 GPU。

如果你在使用 Google Colab,你需要挂载你的驱动器。让我们也导入必要的库。

from google.colab import drive
drive.mount('/content/drive/')

现在我们定义创建生成器 G 网络的函数,如我们之前所描述的。

要定义判别器 D,我们使用 Python 类,因为我们需要 forward 方法的输出。

现在我们终于可以实例化我们的 G 和 D 网络了。让我们还打印模型以查看层的摘要。

像往常一样,如果我们想进行网络训练,我们需要定义成本函数和优化器。

输入向量 z 是一个随机向量,取自某种分布,在我们的情况下可以是均匀分布或正态分布。

现在让我们定义判别器 D 的训练函数。正如我们在前一篇文章中所做的那样,D 必须在真实图像和假图像上进行训练。真实图像直接取自 MNIST 数据集,而对于假图像,我们会动态生成一个输入 z,将其传递给生成器 G,并获取 G 的输出。我们可以自己创建标签,知道真实图像的标签全为一,而假图像的标签全为零。最终损失将是真实图像损失和假图像损失的总和

生成器以判别器的输出作为输入,因为它必须查看 D 是否识别出图像是假的还是实际的。并根据此计算其损失。

我们已经准备好导入数据集,这将使我们能够进行网络训练。使用 PyTorch 导入 MNIST 数据集非常容易,因为它已经实现了相关方法。

现在我们有了数据集,我们可以实例化数据加载器。

由于在训练结束时,我们希望了解图像生成如何随着时间的推移而改进,我们创建了一个函数,允许我们在每个周期生成和保存这些图像。

最后,我们准备开始训练。选择周期数,为了获得良好的结果,应该设定在大约 100 个周期。我只进行了 10 个周期,所以我将得到一个“更丑”的输出。

训练需要 100 个周期,大约需要一个小时,当然,这很大程度上取决于你所拥有的硬件。

让我们绘制结果,看看网络是否学会了如何生成这些合成图像。

合成图像(作者提供的图像)

最终思考

在本文中,我们不仅使用了简单的 GAN 网络,还包括了在处理图像时非常有效的卷积操作,从而创建了所谓的 DCGAN。为了生成这些合成图像,我们构建了两个网络,一个生成器 G 和一个判别器 D,它们进行对抗游戏。如果这篇文章对你有帮助,关注我以获取我即将发布的关于生成网络的文章! 😉

结尾

马切洛·波利提

领英推特网站

使用 Python 和 GANs 进行生成式 AI 实践:图像生成

原文:towardsdatascience.com/hands-on-generative-ai-with-gans-using-python-image-generation-9a62e591c7c6

作者提供的图像

学习如何使用 PyTorch 实现 GANs 以生成合成图像

Marcello PolitiTowards Data Science Marcello Politi

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

--

介绍

在我的 上一篇文章中,我们了解了自编码器,现在让我们继续讨论生成式 AI。目前每个人都在谈论它,并且对已经开发出的实际应用感到兴奋。但我们将一步步地继续探讨这些 AI 的基础。

有几个机器学习模型可以构建生成式 AI,例如变分自编码器(VAE)、自回归模型甚至正则化流模型。然而,本文将重点讨论 GANs。

自编码器与 GANs

在上一篇文章中,我们讨论了自编码器,并了解了它们的架构、用途和在 PyTorch 中的实现。

简而言之,自编码器接收输入 x,将其压缩为一个较小的向量 z,称为潜在向量,然后从 z 以或多或少的近似方式重构 x。

在自编码器中,我们没有数据生成,而只是对输入的近似重构。现在假设我们将自编码器分成两部分,并仅考虑第二部分,即从潜在向量 z 重构图像的部分。

输出生成(作者提供的图像)

在这种情况下,我们可以说架构是生成式的。实际上,给定一个数字向量作为输入,这会创建一幅图像!本质上,这就是生成式 AI 的作用。与自编码器的主要区别在于,我们清楚地知道我们从中获取潜在向量 z 的概率分布。例如,一个Gaussian(0,1)

因此,我们有了一种从高斯分布中的随机数生成图像的方法,改变这些随机数将改变我们输出的图像。

生成模型(图片由作者提供)

GANs 架构

前一张图片中显示的橙色网络可以定义为一个 G 函数,给定输入 z 生成合成输出 x_cap,因此 x_cap = G(z)

网络将以随机权重初始化,因此最初它无法生成看起来真实的输出,只能生成包含噪声的图像。因此,我们需要进行一些训练来提高网络的性能。

所以让我们设想一下,每当我们得到输出时,有一个人工标注员告诉我们这些输出是否良好,是否看起来真实。

朝向 GANs(图片由作者提供)

显然,我们不能进行网络训练,期望一个人对输出进行持续判断。那么我们可以做什么呢?

如果你考虑一下标注员所做的工作,在这种情况下就是二元分类!而我们在机器学习中非常擅长开发分类器。因此,我们可以简单地训练一个分类器,我们称之为鉴别器,并用函数 D()表示,它必须被训练来识别合成(虚假)图像与真实图像。因此我们将同时输入虚假图像和真实图像。

所以这就是我们的架构如何变化的。

GANs 架构(图片由作者提供)

简而言之,架构并不复杂。困难在于训练这两个网络 G 和 D 时。

很明显,如果在训练过程中,这两个网络必须一起改进,它们需要找到某种平衡。因为例如,如果 D 在区分虚假图像与真实图像方面变得过于优秀,而 G 在生成图像方面尚未提升,那么 G 永远不会变得更好,我们的生成器也永远无法准备好使用。

所以这两个网络被称为在玩一个对抗游戏,其中 G 必须欺骗 D,而 D 不能被 G 欺骗。

GANs 目标函数

如果我们想更精确一点,可以说 D 和 G 有两个互补的目标。假设我们想生成图像。

我们用 D(x)定义 x 是真实图像的概率。显然,鉴别器想要最大化其识别真实输入与虚假输入的概率。因此,当 x 从我们的真实图像分布中抽取时,我们想要最大化 D(x)。

相比之下,生成器 G 的目的是欺骗鉴别器。因此,如果 G(z) 是由 G 生成的虚假图像, D(G(z)) 是 D 将虚假图像识别为真实图像的概率。那么 1-D(G(z)) 是 D 正确识别虚假图像为虚假的概率。因此 G 的目标是最小化 1-D(G(z)),因为它确实想要欺骗 D。

所以最终我们可以将这种最大化和最小化的游戏总结到原始论文中的公式里(公式看起来更具概念性,但我们已经看过这个概念):

目标函数(来源:arxiv.org/pdf/1406.2661.pdf

GANs 实现

我们现在实现一个能够自动生成 MNIST 图像的 GAN。

和往常一样,我将我的代码运行在基于云的环境 Deepnote 中,但你也可以使用 Google Colab,这样即使没有 GPU 的用户也可以运行这段代码。

我们首先检查一下我们的硬件是否确实有 GPU。

如果你在使用 Colab,你可以连接到 Google Drive。

from google.colab import drive
drive.mount('/content/drive/')

让我们导入所需的库。

现在我们需要创建定义我们网络的函数,即生成器和判别器。

MNIST 图像有 784 个像素(因为图像是 28x28)。因此,生成器输入一个长度为 20 的随机 z 向量,将输出一个 784 的向量,这就是我们的伪造图像。

相反,判别器将接收一个 28x28 = 784 像素的图像作为输入,它将有一个输出神经元来将图像分类为真实或伪造。

生成器(图片由作者提供)

这个函数用于实例化生成器。每一层将使用 LeakyReLU(ReLU 的一种变体,在 GANs 中表现最佳)作为其激活函数,除了输出层后接一个双曲正切(Tanh)函数,使得输出范围为[-1,1]。

判别器(图片由作者提供)

相反,这个函数定义了判别器网络,其特殊功能是在隐藏层之后使用 dropout(在基本情况下只有一个隐藏层)。输出通过一个 sigmoid 函数,因为它必须给出图像是真实的还是伪造的概率。

现在我们还下载了我们要使用的 MNIST 数据集。MNIST 数据集的范围是[0,255],但我们希望它在范围[-1,1],这样它会类似于生成器网络生成的数据。因此,我们还对数据进行了预处理。

现在我们来到最重要的部分。我们需要创建定义我们网络训练的函数。我们已经提到过,我们应该将判别器与生成器分开,因此我们将有两个函数。

判别器将同时在假数据和真实数据上进行训练。当我们在真实数据上训练时,标签将始终是“real” = 1。因此,我们创建一个全为 1 的向量,即d_labels_real = torch.ones(batch_size, 1, device = device)。然后,我们将输入 x 送入模型,并使用Binary Cross Entropy计算损失。

我们通过输入伪数据做同样的事情。这里的标签将全部为零,d_labels_fake = torch.zeros(batch_size, 1, device = device)。输入则是伪数据,即生成器的输出 g_output = gen_model(input_z)。我们以相同的方式计算损失。

最终损失将是两个损失的总和。

对于生成器训练函数,实施略有不同。生成器的输入是判别器的输出,因为它需要查看 D 是否已识别出图像是真实还是伪造的。基于此,它计算其损失。

现在我们可以初始化我们的两个网络了。

让我们定义一个函数来创建网络生成的样本,这样随着训练周期的增加,我们可以看到伪图像的改进。

现在我们终于可以训练网络了!我们每次将损失保存到一个列表中,以便后续绘图。

训练应该大约需要一个小时,具体取决于你使用的硬件。但最后,你可以打印出你的伪数据,得到类似这样的结果。

在我的情况下,我训练了几个周期,所以结果并不理想,但你开始可以看到网络正在学习生成类似 MNIST 的图像。

伪数据(图片由作者提供)

最后的想法

在这篇文章中,我们更详细地探讨了 GAN 的架构。我们研究了它们的目标函数,并实现了一个能够生成 MNIST 数据集图像的网络!这些网络的操作并不复杂,但它们的训练确实很困难。因为我们需要找到一个平衡点,让两个网络都能学习。如果你喜欢这篇文章,请关注我,阅读下一篇关于 DCGAN 的文章。😉

结束

Marcello Politi

LinkedinTwitterWebsite

实操介绍 Delta Lake 和 (py)Spark

原文:towardsdatascience.com/hands-on-introduction-to-delta-lake-with-py-spark-b39460a4b1ae?source=collection_archive---------0-----------------------#2023-02-16

现代数据存储框架的概念、理论和功能

João PedroTowards Data Science João Pedro

·

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

--

照片由 Nick Fewings 提供,来源于 Unsplash

介绍

我认为现在大家对数据的价值已经非常清楚。以一个热门的例子来说,像 ChatGPT 这样的模型只能基于多年来产生和收集的大量数据构建。

我想强调“可以”这个词,因为编程领域有一句话仍然适用,并且可能永远适用:垃圾进,垃圾出。数据本身没有价值,它需要组织、标准化和清理。治理是必要的。在这种情况下,组织中的数据管理是其涉及数据的项目成功的关键点。

正确的数据管理的一个主要方面是数据架构的定义。数据架构是满足特定组织数据需求的一套实践、技术和服务,包括技术需求(速度、容量、频率、可用性)和非技术需求(业务规则、数据立法遵从)。

如今,几乎默认情况下,组织必须处理不同格式的数据(CSV、pdf、视频、parquet 等),这就是像亚马逊的 S3 这样的 blob 存储取得成功的原因。然而,这种方法可能会带来一些问题,因为缺乏对原始文件(特别是表格数据)的管理工具,如模式强制、版本控制和数据血缘。

考虑到这一点(以及其他一些因素),开发了 Delta Lake,一个开源数据存储框架,实施/体现了 Lakehouse 架构,并且是今天文章的主题。

什么是 Delta Lake?

在深入讨论 Delta Lake 之前,我们需要记住数据湖的概念,所以让我们回顾一些历史。

数据湖架构是在数据量大幅增长的时期提出的,特别是非结构化和半结构化数据,当传统的数据仓库系统开始无法处理这种需求时。

提案很简单——“把你拥有的一切都扔进来,稍后再担心”。在第一个数据湖的背景下,主要的参与者是 Hadoop,一个分布式文件系统,配合 MapReduce,这是一个建立在最小数据移动和高并行性的理念上的处理范式。理论上,只需将所有数据扔进 Hadoop,然后编写作业处理数据以得到预期结果,从而摆脱复杂的数据仓库系统。

传说中,这一过程并不顺利。文件被随意丢弃,没有质量担忧,没有版本控制,也没有管理。数据变得毫无用处。问题如此严重,以至于创造了“数据沼泽”这一术语,调侃非常混乱的数据湖,以及“WORN paradigm”(写一次读永不再读)。实际上,传统数据仓库系统,特别是关系数据库管理系统(RDBMS)所施加的保障仍然需要,以确保数据质量。(那时我还很小,我最近才从现代文献中了解到这一切历史)

时间流逝,基于过去的成功与失败,提出了新的架构。其中之一就是 Lakehouse 架构。简而言之,它试图结合数据湖(灵活性)和数据仓库(保障)的优点。

Delta Lake 只是一个具有 Lakehouse 视角的存储框架/解决方案的实际实现。

让我们开始吧:

Delta Lake(即 Delta 表)中的表实际上就是一个包含 JSON 事务日志的 parquet 文件,该日志记录了文件上的所有变更历史。通过这种方式,即使数据存储在文件中,也可以完全控制所有发生的事件,包括读取以前的版本和恢复操作。Delta Lake 还使用 ACID 事务的概念,即防止因作业失败或读取不一致导致的部分写入。Delta Lake 还会拒绝格式不正确的数据写入(模式强制)并允许模式演变。最后,它还提供了通常在原始文件中不可用的 CRUD 功能(插入、更新、合并和删除)。

本文将以实际操作的方式使用 pyspark 讨论这些功能。

数据

本文使用的数据是发生在巴西高速公路上的交通事故列表,由 PRF(巴西公路警察)收集,并在巴西开放数据门户网站上公开提供 [Link][License — CC BY-ND 3.0].

数据涵盖了 2007 年到 2021 年期间的各种事故信息:地点、高速公路、公里数、纬度和经度、涉及人员数量、事故类型等。

实现

0. 设置环境

一如既往,该项目使用 docker 容器进行开发:

version: '3'
services:
  spark:
    image: bitnami/spark:3.3.1
    environment:
      - SPARK_MODE=master
    ports:
      - '8080:8080'
      - '7077:7077'
    volumes:
      - ./data:/data
      - ./src:/src
  spark-worker:
    image: bitnami/spark:3.3.1
    environment:
      - SPARK_MODE=worker
      - SPARK_MASTER_URL=spark://spark:7077
      - SPARK_WORKER_MEMORY=4G
      - SPARK_EXECUTOR_MEMORY=4G
      - SPARK_WORKER_CORES=4
    ports:
      - '8081:8081'
    volumes:
      - ./data:/data
      - ./src:/src
  jupyter:
    image: jupyter/pyspark-notebook:spark-3.3.1
    ports:
      - '8890:8888'
    volumes:
      - ./data:/data

所有代码均可在此 GitHub 仓库中获取。

1. 创建 Delta 表

首先需要实例化一个 Spark 会话,并将其配置为使用 Delta-Lake 依赖项。

# Install the delta-spark package.
!pip install delta-spark
from pyspark.sql import SparkSession
from pyspark.sql.types import StructField, StructType, StringType, IntegerType, DoubleType
import pyspark.sql.functions as F

from delta.pip_utils import configure_spark_with_delta_pip

spark = (
    SparkSession
    .builder.master("spark://spark:7077")
    .appName("DeltaLakeFundamentals")
    .config("spark.sql.extensions", "io.delta.sql.DeltaSparkSessionExtension")
    .config("spark.sql.catalog.spark_catalog", "org.apache.spark.sql.delta.catalog.DeltaCatalog")
)

spark = configure_spark_with_delta_pip(spark).getOrCreate()

创建 Delta 表非常简单,就像以特定格式编写一个新文件一样。以下代码读取 2020 年的事故数据,并将数据写入 delta 表。

SCHEMA = StructType(
    [
        StructField('id', StringType(), True),          # ACCIDENT ID
        StructField('data_inversa', StringType(), True),# DATE
        StructField('dia_semana', StringType(), True),  # DAY OF WEEK
        StructField('horario', StringType(), True),     # HOUR
        StructField('uf', StringType(), True),          # BRAZILIAN STATE
        StructField('br', StringType(), True),          # HIGHWAY
        # AND OTHER FIELDS OMITTED TO MAKE THIS CODE BLOCK SMALL
    ]
)

df_acidentes = (
    spark
    .read.format("csv")
    .option("delimiter", ";")
    .option("header", "true")
    .option("encoding", "ISO-8859-1")
    .schema(SCHEMA)
    .load("/data/acidentes/datatran2020.csv")
)

df_acidentes.show(5)

2020 年前 5 行。

编写 delta 表。

df_acidentes\
    .write.format("delta")\
    .mode("overwrite")\
    .save("/data/delta/acidentes/")

就这些了。

如前所述,Delta-Lake 表(就文件而言)仅仅是传统的 parquet 文件,附带一个记录所有变更的 JSON 事务日志。

带有 JSON 事务日志的 Delta 表。

2. 从 Delta 表中读取数据

再次强调,读取 Delta 表并没有特别之处。

df_acidentes_delta = (
    spark
    .read.format("delta")
    .load("/data/delta/acidentes/")
)
df_acidentes_delta.select(["id", "data_inversa", "dia_semana", "horario", "uf"]).show(5)

让我们计算行数

df_acidentes_delta.count()

>> Output: 63576

3. 向 Delta 表中添加新数据

Delta 表支持“追加”写入模式,因此可以将新数据添加到已存在的表中。让我们添加 2019 年的数据。

# READING THE 2019 DATA
df_acidentes_2019 = (
    spark
    .read.format("csv")
    .option("delimiter", ";")
    .option("header", "true")
    .schema(SCHEMA)
    .load("/data/acidentes/datatran2019.csv")
)

向 Delta 表追加数据

df_acidentes_2019\
    .write.format("delta")\
    .mode("append")\
    .save("/data/delta/acidentes/")

重要的是要强调:Delta 表会执行模式强制,因此只能写入与已存在表具有相同模式的数据,否则,Spark 会抛出错误。

让我们检查 Delta 表中的行数。

df_acidentes_delta.count()

>> Output: 131132

4. 查看 Delta 表的历史记录(日志)

Delta 表的日志记录了所有对表执行的操作。它包含了对每个操作的详细描述,包括关于操作的所有元数据。

要读取日志,我们需要使用一个名为DeltaTable的特殊 Python 对象。

from delta.tables import DeltaTable

delta_table = DeltaTable.forPath(spark, "/data/delta/acidentes/")
delta_table.history().show()

历史对象是一个 Spark 数据框。

delta_table.history().select("version", "timestamp", "operation", "operationParameters").show(10, False)

正如我们所见,目前有两个表版本,每个操作都有一个版本:表创建时的覆盖写入和之前进行的追加写入。

5. 读取 Delta 表的特定版本

如果没有指定任何内容,Spark 将读取 Delta 表的最新版本。

df_acidentes_latest = (
    spark
    .read.format("delta")
    .load("/data/delta/acidentes/")
)
df_acidentes_latest.count()

>> Output: 131132

但也可以通过仅添加一行代码从特定版本中读取数据:

df_acidentes_version_0 = (
    spark
    .read.format("delta")
    .option("versionAsOf", 0)
    .load("/data/delta/acidentes/")
)
df_acidentes_version_0.count()

>> Output: 63576

计数下降了,因为我们正在从版本 0 读取,在 2019 年的数据插入之前。

6. 恢复到先前版本

可以恢复到表的先前版本。这对于快速解决管道中出现的错误非常有用。此操作也是通过之前创建的 DeltaTable 对象执行的。

让我们将表恢复到版本 0:

delta_table.restoreToVersion(0)

现在,最新的计数将再次为=63576,因为我们恢复到了数据尚未包括 2019 年的版本。

# Counting the number of rows in the latest version
df_acidentes_latest.count()

RESTORE操作也记录在日志中。因此,实际上没有信息丢失:

delta_table.history().select("version", "timestamp", "operation", "operationParameters").show(10, False)

让我们恢复到版本 1。

delta_table.restoreToVersion(1)

7. 更新

更新操作也可以通过DeltaTable对象完成,但我们将使用 SQL 语法来尝试一种新方法。

首先,让我们将 2016 年的数据写入增量表。这些数据中的“data_inversa”(日期)列格式错误:dd/MM/yy 而不是 yyyy-MM-dd。

df_acidentes_2016 = (
    spark
    .read.format("csv")
    .option("delimiter", ";")
    .option("header", "true")
    .option("encoding", "ISO-8859-1")
    .schema(SCHEMA)
    .load("/data/acidentes/datatran2016.csv")
)

df_acidentes_2016.select("data_inversa").show(5)

让我们保存数据:

df_acidentes_2016\
    .write.format("delta")\
    .mode("append")\
    .save("/data/delta/acidentes/")

df_acidentes_latest.count()
>> Output: 227495

但由于我们的 data_inversa 字段是字符串类型,因此不会发生错误。现在,我们的表中插入了错误的数据,需要进行修复。当然,我们可以只需恢复这次操作,并再次正确插入数据,但让我们改用 UPDATE 操作。

以下 SQL 代码仅修复年份=2016 的数据格式。

df_acidentes_latest.createOrReplaceTempView("acidentes_latest")

spark.sql(
    """
    UPDATE acidentes_latest
    SET data_inversa = CAST( TO_DATE(data_inversa, 'dd/MM/yy') AS STRING)
    WHERE data_inversa LIKE '%/16'
    """
)

错误格式化数据的行数为 0:

df_acidentes_latest.filter( F.col("data_inversa").like("%/16") ).count()
>> Output: 0

8. 合并

最后将介绍的操作是 MERGE(也称为 UPSERT)操作。它是 INSERT 和 UPDATE 的混合。

它会尝试将新行插入目标表格,将某些列视为关键列。如果要插入的行已经存在于目标表中(即行键已经在目标表中存在),它将仅更新该行(按照指定的一些逻辑),否则,它将插入新行。

总结来说:如果存在,则更新;如果不存在,则插入。

合并示例。图片由作者提供。

为了演示这种方法,让我们插入一些 2018 年的数据,所有行的 = 0(pessoas — 参与事故的人数),模拟一个包含不完整数据的部分报告。

# FULL DATA FROM 2018
df_acidentes_2018 = (
    spark
    .read.format("csv")
    .option("delimiter", ";")
    .option("header", "true")
    .option("encoding", "ISO-8859-1")
    .schema(SCHEMA)
    .load("/data/acidentes/datatran2018.csv")
)

# SAMPLE WITH pessoas=0
df_acidentes_2018_zero = (
  df_acidentes_2018
  .withColumn("pessoas", F.lit(0))
  .limit(1000)
)

df_acidentes_2018_zero\
    .write.format("delta")\
    .mode("append")\
    .save("/data/delta/acidentes/")

如果我们现在想用 2018 年的完整数据更新表格,我们必须确保已经插入的行仅更新列,并插入所有新的行。

这可以通过以下 MERGE 操作来完成,该操作将事故的 id 和日期视为关键:

df_acidentes_latest.createOrReplaceTempView("acidentes_latest")
df_acidentes_2018.createOrReplaceTempView("acidentes_2018_new_counts")

spark.sql(
    """
    MERGE INTO acidentes_latest
    USING acidentes_2018_new_counts

    ON acidentes_latest.id = acidentes_2018_new_counts.id
    AND acidentes_latest.data_inversa = acidentes_2018_new_counts.data_inversa

    WHEN MATCHED THEN
        UPDATE SET pessoas = acidentes_latest.pessoas + acidentes_2018_new_counts.pessoas

    WHEN NOT MATCHED THEN
        INSERT *
    """
)

结论

定义数据架构对所有旨在创建数据驱动产品的组织(如 BI 报告和机器学习应用)至关重要。数据架构定义了将确保组织的技术和非技术数据需求得到满足的工具、技术和实践。

在私营公司中,这可以帮助加快此类产品的开发,提升其质量和效率,并带来转化为利润的商业优势。在公共组织中,数据架构的好处转化为更好的公共政策,更好地了解特定领域的现状,如交通、安全、预算,以及提高透明度和管理水平。

在过去几十年中,提出了许多架构,每种架构在不同背景下都有其自身的优势。Lakehouse 范式试图将数据湖和数据仓库的优势结合起来。Delta Lake 是基于 Lakehouse 范式的存储框架。简而言之,它将通常仅在经典 RDBMS 中可用的许多保证(ACID 事务、日志、撤销操作、CRUD 操作)带到基于parquet的文件存储之上。

在这篇文章中,我们使用巴西高速公路交通事故的数据探索了这些功能中的一些。我希望我能有所帮助,我对讨论的任何主题都不是专家,我强烈建议进一步阅读(见下方一些参考文献)和讨论。

感谢您的阅读!😉

参考文献

所有代码都可以在 这个 GitHub 仓库找到。

[1] Chambers, B., & Zaharia, M. (2018). Spark: The definitive guide: Big data processing made simple. “O’Reilly Media, Inc.”

[2] Databricks. (2020 年 3 月 26 日). Tech Talk | Diving into Delta Lake Part 1: Unpacking the Transaction Log [视频]. YouTube.

[2] 如何使用恢复功能将 Delta Lake 表回滚到先前版本。 (2022 年 10 月 3 日)。Delta Lake。 链接

[3] Delta Lake 官方页面。 (无日期)。Delta Lake。 delta.io/

[4] Databricks. (2020 年 3 月 12 日). 简化和扩展使用 Delta Lake 的数据工程管道 [视频]。YouTube。

[5] Databricks. (2020 年 9 月 15 日). 利用 Delta Lake 改进 Apache SparkTM [视频]。YouTube。

[6] Reis, J., & Housley, M. (2022 年). 数据工程基础:规划和构建稳健的数据系统 (第 1 版)。O’Reilly Media。

使用 Python 进行实践性的多目标分类

原文:towardsdatascience.com/hands-on-multitarget-classification-using-python-1ac439aac708

Christin Hume 摄影,图片来源于 Unsplash

方法概述、评估指标和最佳实践

Marcello PolitiTowards Data Science Marcello Politi

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

--

介绍

最近我在开发一个能够识别建筑物中不同类型损坏的机器学习算法。这些损坏并不相同,每种损坏都有不同的原因和风险,因此我们识别出了大约 4 种不同类型的裂缝。然后,该算法将部署在无人机上,无人机会自动拍摄建筑物的照片,并能够判断建筑物中存在的损坏及其严重程度。

显然,在无人机拍摄的照片中,可能会出现不同类型的损坏,因此,无人机拍摄的照片必须能够识别出照片中存在的所有不同类型的损坏,而不仅仅是其中之一。这就是我开始研究所谓的多目标分类任务的原因。我在这里写这篇文章,希望它对你也有帮助。

什么是多目标分类?

多目标分类是一种机器学习任务,涉及对单个样本预测多个标签。与传统的二分类或多分类不同,其中每个样本被分配到一个单独的类别,多目标分类允许一个样本同时属于多个类别。这在单个样本可能有多个相关标签的情况下非常有用,例如一个新闻文章可以同时被分类为政治、体育和娱乐。

让我们看一个例子来理解不同类型的分类。

分类类型(图片来源于作者)

解决多目标分类问题有几种方法,包括使用二分类器、多类别分类器和多任务学习。本文将探讨不同类型的多目标分类,并讨论它们的优缺点。我们还将查看多目标分类的评估指标。最后,我将提供一些个人见解和结论,关于多目标分类的优势和局限性,以及成功的最佳实践。

多目标分类的类型

针对多目标分类有几种方法,每种方法都有其优点和局限性。

二分类器

多二分类(图像来源于作者)

多目标分类的一种方法是使用多个二分类器,每个分类器被训练以预测一个单独的标签。例如,如果我们有一个包含三个标签(A、B 和 C)的多目标分类问题,我们可以训练三个独立的二分类器,一个用于预测标签 A,一个用于预测标签 B,另一个用于预测标签 C,然后运行这三个模型来对实例进行分类。这种方法简单易实现,但如果标签数量较多可能效率低下。此外,分类器的性能可能受到训练数据中标签分布不平衡的影响。

多类别分类器

另一种多目标分类的方法是使用多类别分类器,这种分类器旨在同时预测多个标签。有几种类型的多类别分类器,包括一对其余和一对一。

  • 一对其余(OvR) 分类器被训练以对每个标签做出二分类决策,将所有其他标签视为负类。例如,在 三个标签A、BC)的情况下,OvR 分类器将被训练以预测 标签 A 对 非-A标签 B 对 非-B标签 C 对 非-C所以你将遇到如前所述的多二分类情况。这种方法简单高效,但可能受到标签分布不平衡的影响,并且可能未考虑标签之间的依赖关系。

  • 一对一(OvO) 分类器被训练以对每对标签做出二分类决策。例如,在三个标签(A、B 和 C)的情况下,OvO 分类器将被训练以预测 A 对 BA 对 CB 对 C。这种方法比 OvR 计算量更大,但可以处理不平衡的标签分布,并捕捉标签之间的依赖关系。

多任务学习

多任务学习是一种机器学习方法,它涉及训练一个模型以同时预测多个任务。在多目标分类的背景下,多任务学习包括训练一个模型来预测样本的所有标签

如果任务相似,例如,你需要分类不同类型的缺陷,或者你需要分类是否有汽车、自行车和卡车,这种方法比使用多个二分类或多分类分类器更高效,但它需要大量标记数据和强假设任务相关

让我们编码吧!

让我们看看如何在计算机视觉领域实现多任务学习算法,同时采用迁移学习方法。

我想做的是拿一个在图像识别上预训练的网络,如 Resnet(当然,你也可以使用其他网络),并将其修改为能够同时解决多个任务

在多分类情况下,通常的做法是使用Resnet,并在其顶部附加一个具有与数据集中类别数量相等的输出神经元的分类器,从而为每个实例获得一个类别。

然而,我们希望不仅仅识别一个类别,而是同时识别多个类别,因此我们只是简单地在 Resnet 顶部附加多个分类器(线性层)

但在我们的情况下,每个分类器是一个二分类器,它只需要告诉我们是否存在汽车、自行车或卡车。

创建一个二分类器,我们只需要一个输出神经元来回答“否/是”。所以网络结构非常简单。

使用 Resnet 进行多目标分类(图片由作者提供)

在需要 3 个多分类分类器的更一般情况下,架构应该是这样的。

使用 Resnet 进行多目标分类(图片由作者提供)

让我们看看如何使用 PyTorch 实际实现这样的网络。

 class ResnetBasedModel(nn.Module):
    def __init__(self, pretrained, clf_in_features,  labels_nr:int, freeze:bool = True):
        super().__init__()

        self.pretrained_model = pretrained
        #model without last layer
        self.model_wo_fc = nn.Sequential(*(list(self.pretrained_model.children())[:-1])) 

        if freeze:
            for param in self.model_wo_fc.parameters():
                param.requires_grad = False

        self.classifiers = nn.ModuleDict()

        for i in range(labels_nr):
            self.classifiers[f'clf_{i}'] = nn.Sequential(
                nn.Dropout(p=0.2),
                nn.Linear(in_features = clf_in_features, out_features = 1)
            )

    def forward(self, x):
        x = self.model_wo_fc(x)
        x = torch.flatten(x, 1)

        return {name: classifier(x) for name, classifier in self.classifiers.items()}

之前的代码实现了一个继承 nn.Module的 Python 类,这是创建基于神经网络的模型的经典方式。

模型以预训练网络(如 Resnet)(pretrained)作为输入,接受预训练网络倒数第二层的输出神经元数量(clf_in_features),例如,在 Resnet34 中这个数字是 512,而在 Resnet50–101 中是 2048。

该类还接受输出二分类器的数量(labels_nr),以及我们是否希望解冻预训练网络的参数(freeze)。

让我们更详细地看看这个类是如何工作的。

以下循环用于解冻预训练网络的参数,以便我们只保留输出分类器,这将大大加快我们的训练速度。

if freeze:
    for param in self.model_wo_fc.parameters():
        param.requires_grad = False

之后,我会创建与参数中指定数量相同的分类器,并将它们保存在字典中。

for i in range(labels_nr):
      self.classifiers[f'clf_{i}'] = nn.Sequential(
          nn.Dropout(p=0.2),
          nn.Linear(in_features = clf_in_features, out_features = 1)
      )

最终,在前向方法中,我将每个输入 x 传递给预训练网络和字典中的每个分类器,并返回字典中每个元素(分类器)的输出。

def forward(self, x):
        x = self.model_wo_fc(x)
        x = torch.flatten(x, 1)

        return {name: classifier(x) for name, classifier in self.classifiers.items()}

现在你可以使用你的网络进行多目标分类。请记住,要实例化一个预训练网络并将其作为输入传递给类,你只需要使用 PyTorch Torchvision 模块。

from torchvision import models

resnet34 = models.resnet34(weights=models.ResNet34_Weights.DEFAULT)

要训练模型,你需要为每个分类器实例化一个损失函数,并将这些损失加总。得到结果后,你可以更新整个模型。

def criterion(y, yhat):
    '''y : is a dict with keys 'labels' and 'path'''
    losses = 0
    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
    for _, key in enumerate(yhat):
        losses += loss_func(yhat[key], y[f'label_{key}'].float().unsqueeze(1).to(device))
return losses

多目标分类的评估指标

评估多目标分类模型的性能可能具有挑战性,因为需要考虑多个标签以及不同的组合方式。以下是一些用于多目标分类的常见评估指标:

汉明损失:该指标衡量错误预测标签的比例。它的计算方法是将错误分类的标签数量除以标签总数。

这是我倾向于用来评估模型的指标,这里是汉明损失的实现。

def hamming_error(yhat:list, y:list) -> float:
    loss = sum([yhat_i != y_i for yhat_i, y_i in zip(yhat, y)])
    avg_loss = loss/len(yhat) 

    return avg_loss

在这个片段中,两个数组的元素成对比较,返回发现的平均不匹配数。可以轻松地将此代码推广到一次比较整个批次。

让我们快速了解一下该领域使用的其他指标。

  • 排名损失:该指标衡量样本中错误排序的标签的平均数量。例如,如果一个样本的标签是 A、B 和 C,而模型预测的顺序是 C、B、A,则排名损失为 2(因为 B 和 C 被错误排序)。

  • 杰卡德指数:该指标衡量预测标签和真实标签之间的重叠程度。它的计算方法是将交集的大小除以并集的大小。

  • F1 分数:该指标是在精度和召回率之间的平衡,其中精度是正确预测标签的比例,召回率是被预测的真实标签的比例。它是精度和召回率的调和均值。要使用 F1 分数进行多目标分类,你需要分别计算每个标签的精度和召回率,然后对所有标签的分数取平均以获得总体 F1 分数。该指标相对于之前的指标对不平衡标签分布更为敏感。

F1 = 2 * (precision * recall) / (precision + recall)
  • 平均精度:该指标衡量数据集中每个召回值的精度。它的计算方法是对第一个假阳性、第二个假阳性等的精度取平均。
Average precision = (1/n) * Σ(precision at each recall value)

要计算多个标签的平均精度,你可以简单地对每个标签的平均精度取平均。例如,如果你有三个标签(A、B 和 C),你可以使用上述公式计算每个标签的平均精度,然后将这些分数平均以获得总体平均精度。这也对不平衡分布非常敏感。

评估多目标分类模型的挑战

评估多目标分类模型可能具有挑战性,因为有多个标签需要考虑以及不同的组合方式。以下是一些用于多目标分类的常见评估指标:

  • 不平衡的标签分布:某些标签可能比其他标签更常见,这会影响模型的性能。例如,如果某个标签稀少,模型可能没有足够的示例进行学习,导致性能较差。

  • 标签之间的依赖关系:某些标签可能比其他标签更可能一起出现,这会影响模型的性能。例如,如果一个模型被训练来预测标签 A 和 B,但标签 A 总是由标签 B 在前面,模型可能很难在不预测标签 B 的情况下预测标签 A。

  • 多标签评估指标:多目标分类有多种评估指标,每种指标都有其优点和局限性。选择正确的指标可能很困难,因为这取决于问题的具体要求和数据的特点。

最终思考

多目标分类是解决涉及对单个样本预测多个标签的问题的强大工具。它可以应用于广泛的现实世界应用,例如文本分类、图像标注和推荐系统。

多目标分类有多种方法,包括二分类器、多分类器和多任务学习。选择方法取决于问题的具体要求和数据的特点。

评估多目标分类模型的性能可能很具挑战性,原因包括标签分布不平衡、标签之间的依赖关系以及可用的多种评估指标。选择正确的评估指标并将模型的性能与基线进行比较非常重要。

结束

马塞洛·波利提

LinkedinTwitterCV

亲自动手实现 Otsu 阈值分割算法,用于图像背景分割,使用 Python

原文:towardsdatascience.com/hands-on-otsu-thresholding-algorithm-for-image-background-segmentation-using-python-9fa0575ac3d2?source=collection_archive---------7-----------------------#2023-03-20

从理论到实践,使用 Otsu 阈值分割算法

Piero PaialungaTowards Data Science Piero Paialunga

·

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

--

图片由 Luke Porter 提供,来源于 Unsplash

让我从一个非常技术性的概念开始:

图像将被视作、处理、分析和处理为二维信号。

以及一些恰当的定义:

  • 信号是一个在空间或时间上变化的量,可用于传递某种信息。

  • 图像无非是光线照射到光学系统上,即相机或你正在绘画的画布。

从这个意义上说,图像不过是一个 2D 信号,一个携带某些信息的电磁信号,这些信息由物理系统获取。

因此,既然我们已经确定图像确实是一个信号,我们可以考虑将信号处理技术应用于图像处理任务。这样我们可以停止讨论哲学,开始进入技术部分。

说到哲学。我们来看这张图像

照片由 Tingey Injury Law FirmUnsplash 提供

图片中有位哲学家在思考他的工作。然后是这个非常白的背景,我们其实并不在乎。我们能去掉它吗? 我们能得到这样的东西吗?

图片由作者提供

如果我问你,这意味着我们可以。 😅

每个人只要懂一点Photoshop就可以做到这一点,但如何用 Python 自动完成呢?再说一遍,是的。

让我给你展示一下 🚀

0. 想法

所以让我们拿一个简单的例子。

没错。一个小方块在一个大方块里面。这是一个极其简单的情况。我们想做的是将小方块中的所有值设置为 1,而外面的所有值设置为 0。

我们可以用以下代码提取两个值:

然后做一些类似于:

这将图像从两个值转换为 1 和 0。

这非常简单,对吧?让我们把它变得有些复杂。

现在我们将做的是在大方块内的小方块但两个方块都有一些噪声。

我的意思是,我们不仅仅有 2 个值,而是理论上可以有从 0 到 255 之间的所有值,这就是编码中的整个值范围。

我们如何处理这个问题?

好的,我们首先要做的是扁平化图像(2D 信号),并将其转换为 1D 图像。

图片由作者提供

图像是 50x50 的,我们得到了一个“展开”的 50x50=2500 长度的 1D 信号。

现在如果我们研究 1D 信号的分布,我们得到这样的东西:

正如我们所见,我们有 两个正态分布。这正是 Otsu 算法表现最佳的地方。其基本思想是 背景图像中的主体 具有两种不同的性质和两个不同的 领域。例如,在这种情况下,第一个高斯钟形曲线与背景相关(从 0 到 50),而第二个高斯钟形曲线则与较小的方块相关(从 150 到 250)。

假设我们决定将所有大于 100 的值设为 1,将所有小于 100 的值设为 0:

结果是以下 背景主体 之间的掩码:

就是这样。这就是 Otsu 算法的整个思路:

  • 导入/读取图像作为 2D 信号

  • 图像展平为 1D 向量

  • 选择一个 阈值

  • 低于该阈值的所有内容设为 0,将高于该阈值的所有内容设为 1

很简单,对吧?

那么我们如何选择合适的阈值呢?最佳阈值是什么?让我们谈谈 数学

1. 理论介绍

让我们稍微形式化一下这个概念。

我们有一个图像的 领域。整个领域从 0 到 255(白到黑),但它不必那么宽(例如可以是 20 到 200)。

当然,多个点可以具有相同的像素强度(我们可以在同一图像中有两个黑色像素)。假设我们有 3 个强度为 255 的像素,在一个有 100 个像素的图像中。那么在该图像中强度为 255 的概率是 3/100。

一般来说,我们可以说图像中像素 i 的概率是:

图片由作者提供

现在假设我们进行分割的像素是像素 k(在我们之前的示例中,k 是 100)。这 分类 了数据点。所有 k 之前的点属于类别 0,所有 k 之后的点属于类别 1

这意味着从类别 0 中选择一个点的概率如下:

图片由作者提供

而从类别 1 中选择一个点的概率如下:

图片由作者提供

正如我们所见,这两个概率显然依赖于 k。

现在,我们还可以计算每个类别的 方差:

图片由作者提供

图片由作者提供

其中:

图片由作者提供

图片由作者提供

sigma 值是每个类别的 方差,即该类别在均值 mu_0 和 mu_1 周围的 扩散 程度。

从理论上讲,目的是找到一个值,这个值形成了我们之前在图片中看到的小 凹谷

但我们使用的方法稍有不同且更为严格。通过使用线性判别分析(LDA)的相同思路。在(Fisher)LDA 中,我们希望找到一个超平面,将两个分布分开,使得类别之间的方差尽可能大(这样两个均值之间的距离尽可能远),而类别内部的方差尽可能小(这样两个类别数据点之间的重叠最少)。

在这种情况下,我们没有任何超平面,我们设置的阈值(我们的 k)甚至不是一条线,而更像是一个概率值,用于区分数据点并对其进行分类。

可以证明(完整证明见原始论文),最佳分割背景主体之间(假设背景的域与主体的域不同)是通过最小化该数量来获得的:

作者提供的图片

这意味着我们可以尝试所有不同的 k,并选择其中最小的 k。

2. 实践操作

理论可能看起来复杂且难以理解,但实现起来非常简单,由三个块组成:

2.1 导入库

我们首先需要导入 4 个基本库。

2.2 阈值函数

一旦找到完美的阈值,这就是如何将其应用到您的图像中:

2.3 Otsu 准则

将计算该数量的函数:

作者提供的图片

是以下内容:

2.4 最佳阈值计算

另一个函数会遍历所有可能的 k,并根据上述准则找到最佳值。

2.5 整个过程

所以我们使用的图像是以下这个:

Ben Dumond拍摄的照片,来源于Unsplash

如果我们将该图像保存在路径中并应用 Otsu 算法,我们得到:

如果我们比较 im(原始图像)和 im_otsu(算法处理后的图像),我们得到:

正如我们所见,图像右上部分的黑色区域被误解为主体,因为它与一些主体有相同的色调。人们不完美,算法也一样🙃

3. 总结

感谢您在整个 Otsu 算法教程中与我同行。

在这篇简短的文章中,我们看到:

  1. 图像可以被视为二维信号,然后可以使用信号处理技术进行分析

  2. Otsu 算法的假设是图像的背景主体具有两个连续的、互不重叠的、明确区分的领域。

  3. 如何在给定 Otsu 算法的情况下找到图像的最佳背景和主体的区分。我们如何将 Otsu 算法解释为 Fisher 线性判别。

  4. 如何使用Python实现 Otsu 算法。

  5. 如何在实际图像应用此算法。

4. 结论

如果你喜欢这篇文章,想了解更多关于机器学习的内容,或只是想问我一些问题,你可以:

A. 在Linkedin上关注我,我会在上面发布我的所有故事。

B. 订阅我的新闻通讯。它将让你了解最新故事,并给你机会通过短信与我联系,获取所有的更正或解答你可能有的疑问。

C. 成为会员,这样你就不会有“每月故事数量上限”的限制,可以阅读我(以及成千上万的其他机器学习和数据科学顶级作家)关于最新技术的文章。

实践中的采样技术和比较,使用 Python

原文:towardsdatascience.com/hands-on-sampling-techniques-and-comparison-in-python-6342c70f2099

这是一个关于如何使用 Python 高效采样数据集的逐步教程

Piero PaialungaTowards Data Science Piero Paialunga

·发布于 Towards Data Science ·8 min read·2023 年 12 月 1 日

--

图片由作者提供

我和我的妻子一起布置圣诞树。我们去了地下室,拿了树,把它搬到楼上,然后从下往上开始搭建。这总是一个神奇的时刻🎄

然后是把球放到树上的环节。我立刻想到:至少有三种方式可以把球放到树上。

  • 均匀地:将球均匀地放在树上,就像这样

图片由作者提供,使用 Freepik 制作

  • 随机地:将球随机地放在树上,闭上眼睛随便放哪里(我开始这样做,我的妻子疯了)

图片由作者提供,使用 Freepik 制作

  • 拉丁超立方体:将树分成 N 个部分,并在每个部分中随机抽取。很难在不运行任何代码的情况下绘制,但可能的拉丁超立方体如下所示:

图片由作者提供,使用 Freepik 制作

我尝试并把这个展示给了我的妻子。她微笑着说“随便”,于是我回到电脑前,希望你的反应能更令人满意 😤

说笑归说笑,当处理机器学习问题时,有两种不同的情况:

  1. 你无法控制数据集。你有一个客户或公司,会给你一个数据集。这是你必须处理的,直到安排必要的(最终的)重新训练。

例如,在纽约市,你想根据一些给定的特征预测房屋的价格。他们只给你数据集,并希望你建立一个模型,以便当新客户到来时,你有一个 AI 软件可以根据感兴趣的房屋特征预测价格。

2. 你可以建立你的实验设计。这指的是你有一个前向模型或一个可以随时设置运行的真实世界实验。

例如,在实验室中,你想在给定实验设置的情况下预测一个物理信号。你可以随时去实验室生成新的数据。

你在这两种情况下做出的考虑是完全不同的。

在第一个情况下,你可以期待一个在特征上不平衡的数据集,可能有缺失的输入值和目标值的偏斜分布。处理这些问题是数据科学家的乐趣与诅咒。你进行数据增强、数据过滤、填补缺失值,如果可能的话进行一些 ANOVA 测试等等。在第二种情况下,你对数据集中的情况有完全的控制权,特别是从输入的角度来看。这意味着如果你有一个 NaN 值,你可以重复实验;如果你有几个 NaN 值,你可以调查数据集中的那个奇怪区域;如果你有一些特征的值非常大,你可以重复实验以确保这不是你设置的幻觉。

由于我们有这种控制权,我们想确保高效地覆盖输入参数空间。例如,如果你有 3 个参数,并且你知道它们的边界

作者提供的图片

其中 i 从 1 到 3(或者从 0 到 2,如果你喜欢 Python 😁)。在这种情况下,x_i 是第 i 个变量,它总是大于 x_i^L(下边界),但总是小于 x_i^U(上边界)。

我们有我们的三维立方体。

作者提供的图片

现在,记住我们对数据集有完全的控制权。我们如何进行采样?换句话说,我们如何确定xs?我们想选择哪些点,以便运行前向模型(实验或模拟)并获得目标值?

正如你所期待的,有多种方法可以做到这一点。每种方法都有其优点和缺点。在本研究中,我们将讨论它们,展示它们背后的理论,并提供代码供大家使用,进一步了解美妙的采样世界。🙂

让我们从均匀采样开始:

1. 均匀采样

均匀采样方法可以说是最简单和最著名的一种。

这只是将每个参数(或维度)分成若干步骤。假设我们有每个维度 3 个步骤,2 个维度。每个维度从 0 到 1(我们稍后会扩展)。这将是采样:

  • (0,0)

  • (0,0.5)

  • (0,1)

  • (0.5,0)

  • (0.5,0.5)

  • (0.5,1)

  • (1,0)

  • (1,0.5)

  • (1,1)

这意味着我们一次固定一个变量,并逐步增加。相当简单。让我们编码:

1.1 均匀采样代码

我们怎么做呢?让我们避免这种结构:

  • 对于维度 1 中的 a

  • 在二维中进行 b

  • ….

  • 对于字母表中最后一个字母在字母表数量维度中:X.append([a,b,…,字母表中的最后一个字母])

我们不想这样做,因为这不是很高效,你需要为每个维度定义一个变量,这很麻烦。让我们使用神奇的numpy

np.meshgrid(*points) 这个问题做的就是你用 for 循环做的事情,但以优化的方式进行。你的参数字典是用来告诉你每个参数的最小值和最大值的。

使用这段代码,你将生成一个 0/1 立方体和一个具有三种不同维度的立方体(例如,第一个维度从-5 到 1,第二个维度从 0 到 10,第三个维度从-1 到 1):

我们有三个维度,让我们绘制前两个:

1.2 优缺点

优点:这种方法因两个原因非常知名。第一个原因是它非常容易实现。它真的只是变量之间的 for 循环。第二个原因是你当然是均匀覆盖了参数空间,如果你想确保不丢失参数空间的重要部分,这种方法是理想的。

缺点:这种方法的一个巨大问题是指数级依赖。如果我们假设维度的数量是固定的(假设为 6),对于 steps = 10 的设计,你已经在处理百万级的点数。而问题再次在于这种指数性。因此,如果你想通过将步数加倍(20 步而不是 10 步),你现在谈论的是一个 6400 万点的问题。

2. 随机采样

均匀采样的一个替代方法是随机采样。它是怎么工作的?非常简单:在感兴趣的立方体中,你只需在边界内随机选择点。

2.1 随机采样代码

随机采样方法编码极其简单,无论是对于 0–1 立方体还是自定义边界的立方体。就是这样:

让我们绘制这个图:

2.2 优缺点

优点:即便在这种情况下,随机采样也非常简单易懂和编码(如你所见)。另一个优点是这种方法能够比均匀采样更好地捕捉输出空间的复杂性,尤其是在大维度的情况下。

缺点:问题在于采样的固有随机性。这可能会产生簇和探索稀缺的区域。

为了更深入一些,Pedergnana 等人(非常优秀)撰写的论文比较了这两种方法和其他方法的差异,尤其是在高维度情况下。

3. 拉丁超立方体采样

拉丁超立方体抽样通常定义为“均匀随机抽样”。我认为这是一个非常美丽的定义。让我解释一下这个思想。

LHS 的关键思想是将参数空间划分为每个维度沿线的等概率区间,并确保在每个区间内只抽取一个样本。这会产生一个分层且分布均匀的样本,覆盖整个参数空间。

拉丁超立方体的妙处在于你可以使用优化方法,例如,最大化点之间的最小距离,并将点放置在其区间内的随机位置。

3.1 拉丁超立方体代码

这个方法需要自定义安装,即代理建模工具箱(smt)

pip install smt

超级简单:

3.2 优缺点

拉丁超立方体在视觉上类似于随机抽样,但在多个维度中,它有助于保持随机抽样的一种规则性,而不受均匀抽样的限制。这个方法在其变体中是高维度少样本(这是最棘手的情况)的首选。这个方法的缺点在于它在实现和描述上都更复杂,因此需要领域知识和一些实际操作经验。

4. 结论

在这篇博客文章中,我们讨论了三种实验设计或抽样技术,适用于可以控制输入参数的机器学习案例。特别是,我们讨论了:

  • 均匀(网格)抽样:这是构建 N 维网格的方法,其中 N 是维度的数量。使用简单,但对于大维度来说不够详细。

  • 随机抽样是定义 N 维立方体并提取立方体内随机值的方法。使用简单,在大维度的情况下比均匀抽样更有效,但仍然不理想,因为它可能会创建聚类和过于密集的区域。

  • 拉丁超立方体抽样是一种通过在 N 维超立方体的不同部分至少抽取一个点来规范化随机抽样的方法。适用于高维度和样本较少的情况,但需要领域知识和优化程序。

我们看到三种情况的编码示例,包括单位立方体(每个变量的范围从 0 到 1)和每个变量的自定义限制。

没有方法是完美的,选择哪种方法取决于你的最终目标。希望这篇文章能为你在决定采用哪种实验设计时提供一点框架 😃

5. 结论

如果你喜欢这篇文章,想了解更多机器学习相关内容,或者有其他问题,你可以:

A. 关注我在Linkedin,我会发布我的所有故事

订阅我的新闻通讯。它将使你了解最新的故事,并给你机会通过短信联系我,获取所有的修正或解答你的疑问。

成为推荐会员,这样你就不会受到“每月故事数量上限”的限制,可以阅读我(以及数千名其他机器学习和数据科学顶级作者)撰写的关于最新技术的内容。

利用人工智能创造更美好的世界

原文:towardsdatascience.com/harnessing-ai-for-a-better-world-e3357cc73b09

狮子家族。图像由作者使用 Dall·E 创作。

发现 5 个由人工智能赋能的激励性话题

Hennie de HarderTowards Data Science Hennie de Harder

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

--

有许多例子显示人工智能被错误使用,正如引人深思的书籍 《数学毁灭武器》所强调的。 人工智能的风险 也不容小觑。人工智能伦理和治理已成为我们快速发展的技术环境中的紧迫问题,许多公司现在设有专门部门,专注于解决这些问题。然而,在这些挑战中,也有鼓舞人心的故事,展示了数学和人工智能如何被用于造福社会。

这篇文章旨在激励您,并展示人工智能如何帮助应对复杂的全球挑战,从气候变化到人权侵犯。所选的倡议和公司在大规模上取得了显著成果,我相信它们值得被关注。这只是一个选择,因为还有许多其他例子。如果您发现我遗漏了什么,请随时在这篇文章中发表评论!

分享这篇文章的另一个动机是 10 月 4 日庆祝的世界动物日。 🐈‍⬛

难民安置

数学优化通常用于在各种情境下最大化利润或最小化成本。然而,也有一些特殊情况,其中它服务于崇高的目的,如此处所示。

当难民在接待国寻求庇护时,将难民家庭安置到合适的住所可以深刻影响他们在就业、教育和整体福祉方面的长期前景。许多难民处于脆弱状态,因此将他们安置在一个安全的环境中以便他们可以开始重建新生活是很重要的。同样,对于寄养家庭来说,将他们的偏好与他们接待的难民对接也是至关重要的。例如,如果一位年长的女性希望照顾难民,却最终接待了一个五人男性家庭,情况可能会令人不堪重负。通过优化匹配过程,可以显著改善安置结果。

Annie MOORE 是一个在 2018 年开发的软件系统,专注于解决这个问题。该软件由总部位于美国的安置机构 HIAS 使用。Annie 利用机器学习和数学优化来推荐寄养家庭与难民之间的匹配。该软件显著提高了 HIAS 安置的难民的就业结果,数据从 22%到 38%不等。

更进一步地创新,新平台 RUTH(Refugees Uniting Through HIAS)增强了安置过程。RUTH 结合了难民和接待家庭的偏好,使得迁移过程更快、更透明。伍斯特理工学院的运营和工业工程副教授 Trapp 博士通过以下声明强调了 RUTH 的重要性:

“这是第一次系统地将难民的偏好和接待方的优先事项应用于安置过程中。”

RUTH 帮助安置了来自乌克兰的难民到美国,在俄罗斯入侵期间。

Julie Ricard 的照片,来源于 Unsplash

保护野生动物

人工智能在保护和保育濒危物种和野生动物方面发挥着重要作用。通过先进的图像识别和预测建模,人工智能驱动的系统彻底改变了我们监测动物行为、跟踪迁徙模式和打击偷猎威胁的方式(是的,偷猎者仍然存在)。这些技术使保育工作者和执法机构能够迅速作出反应,保护世界生物多样性并保存我们的自然遗产。

在网络上可以找到许多出色的例子。让我们来看一些。

挽救物种免于灭绝是计算机视觉和其他机器学习技术可以帮助实现的目标。跟踪物种的一个可能方法是计算国家公园中的动物数量。 Appsilon开发了一款名为 Mbaza AI 的工具。该工具基本上是一个专注于野生动物的图像分类工具。澳大利亚的一个濒危物种是考拉,因火灾和动物袭击而面临威胁。 这一倡议利用 AI 来定位和拯救幸存的考拉。微软 AI 与雪豹信托基金会合作,致力于雪豹的检测和识别,帮助保护雪豹。Google AI 利用鲸鱼的歌声来定位和保护鲸鱼。

不幸的是,全球仍有盗猎者猎杀动物。但幸运的是,AI 让他们的工作变得更加困难。 黑客地球有一个倡议,可以在盗猎者出现时提醒护林员。这非常好,因为护林员可以直接追踪盗猎者,而不是不断查看监控录像。 另一个项目监控每一艘进出赞比亚公园的船只,以防止非法捕鱼。借助 AI 监控的其他动物有灰熊、大象和企鹅

图片由Adrian Pereira提供,来自Unsplash

应对气候变化

气候变化是一个重大问题。我们必须应对气候变化的负面影响,如海平面上升、极端天气事件、健康风险增加、食物短缺和人口迁移。

对抗气候变化的战斗需要数据驱动的洞察力和可持续的解决方案。AI 擅长处理大量数据集,并识别出可指导气候政策和资源管理的模式。从优化可再生能源生产到预测极端天气事件,AI 有助于缓解气候变化的影响,并促进我们星球的可持续未来。

准确的天气预报对保护生命和财产至关重要。 LEAP(利用 AI 和物理学学习地球)是一个致力于改善短期气候预报的科技中心。他们通过将传统气候科学与先进的机器学习结合来实现这一目标。LEAP 结合了两种方法的优点,并构建了下一代 AI 模型。

正如你可能知道的,碳排放对地球有影响。许多倡议表明,减少碳排放是可能的。一个利用人工智能减少碳足迹的公司是Eugenie.ai,它帮助制造商实现运营脱碳。Mortar IO发现减少现有建筑碳排放的方法,他们的愿景是成为脱碳房地产的数据基础设施。建筑物的碳足迹等于全球能源相关碳排放的 39%,所以这是一个相当不错的倡议。

一个可能会引起你注意的有趣问题是:人工智能本身的排放足迹如何?人工智能是相当大宗消费者(在这篇文章的第一个图表中可见),而且可以大幅减少排放,正如 HuggingFace 用 BLOOM 模型所证明的。新技术可以帮助减少碳足迹,包括人工智能的碳足迹。也许减少排放的最佳方式是研发性能与大模型相当的小型模型。

废物是另一个在思考气候变化时出现的话题。人们的消费行为对甲烷排放有着巨大的影响。一个处理时尚产业废物的酷炫倡议是Refiberd。他们利用人工智能来对纺织品进行回收应用的分类。

帮助发展中国家的农民

在发展中国家,小规模农民常常面临在最大化作物产量和有效管理资源方面的挑战。基于人工智能的应用程序通过提供有关作物选择、灌溉和害虫控制的量身定制建议,提供了宝贵的支持。通过为农民提供这些工具,我们赋予他们改善生计和为社区的食品安全做出贡献的能力。

一个在全球范围内用于检测作物疾病并提供解决方案的应用是 Plantix。它帮助最大化作物产量,并使用人工智能进行检测。Hello Tractor 是一个提供将农民与拖拉机拥有者连接起来的平台,用于耕作和其他农业活动的公司。他们利用数学优化和机器学习来优化拖拉机的分配和调度,使发展中国家的小农户能够更负担得起机械化农业。另一个具体的例子是 Apollo Agriculture。这家公司不仅在农业方面帮助肯尼亚和赞比亚的农民,还提供融资。Apollo 的机器学习部分专注于信用模型,以做出信用决策。

还有许多其他的倡议,比如 AgriPredictTaranisFarmshine(为农民提供天气预报、作物病害诊断和市场准入的平台)。

Rajesh Ram 拍摄,来源于 Unsplash

揭露人权虐待和战争罪行

许多人权组织因人工智能的风险而反对它。查看 国际特赦组织人权观察Bellingcat 的最新帖子。除了批评,人工智能是否也能在这个领域发挥积极作用?

实际上,它可以:人工智能可以筛选大量的数据,包括图像、视频和文本信息,以识别和记录侵犯行为。这是通过图像和视频分析、自然语言处理、面部识别和预测分析来完成的。此外,人工智能可以汇总和交叉参考来自不同来源的数据,构建事件和人权侵犯的全面图景。这包括汇总来自目击者的证词、社交媒体帖子和官方报告的信息。

Syrian Archive 是一个利用开源情报、数字取证和人工智能记录叙利亚人权侵犯的组织。他们收集和验证视觉证据,如照片和视频,以追究罪犯的责任。你可以在 方法和工具 上了解他们使用的更多信息。

另一个例子是法医建筑学。这个研究机构结合空间和建筑分析、AI 和机器学习,调查人权侵犯和国家暴力。他们经常与其他组织合作,提供有价值的见解。这些例子 展示了机器学习技术应用的不同案例。

最后但同样重要的是,信息复原中心 是一个致力于揭露人权侵犯和战争罪行的非营利社会企业。此外,他们还反击虚假信息和有害的在线行为。他们使用数据科学(主要是分析和可视化)的一个例子是 Eyes on Russia map。这是一个自俄罗斯入侵乌克兰以来重要事件的时间轴。

希望更多类似的倡议能够出现,以善用 AI。

结论

尽管 AI 的伦理挑战不可否认,但认识到其带来积极变化的潜力至关重要。这里讨论的例子表明,当 AI 和数学负责任地运用并承诺伦理原则时,它们可以成为解决全球紧迫问题并改善世界的强大工具。

如果你喜欢志愿服务或为某些项目和公司工作,其中一些确实有职业机会!你可以访问他们的网站进行搜索。

相关内容

机器学习项目中的伦理考量

在构建 AI 系统时不要忘记这些主题

结合数学优化和机器学习的五种方法

结合两种力量的实际例子。

任何机器学习模型的模型无关解释方法

解释方法概述:置换特征重要性、部分依赖图、LIME、SHAP 等。

关于模型无关解释方法的概述

利用 Python 中的 Asyncio 发挥多核性能

原文:towardsdatascience.com/harnessing-multi-core-power-with-asyncio-in-python-1764404ce44f

PYTHON 并发

通过高效利用多个 CPU 核心,提升你的 Python 应用程序性能

Peng QianTowards Data Science Peng Qian

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

--

图片来源:由作者创建,Canva

介绍

在这篇文章中,我将向你展示如何在多核 CPU 上执行 Python asyncio 代码,以解锁并发任务的全部性能

我们的问题是什么?

asyncio 只使用一个核心。

在之前的文章中,我详细介绍了如何使用 Python asyncio。通过这些知识,你可以了解到 asyncio 允许 IO 绑定任务通过手动切换任务执行以绕过多线程任务切换中的 GIL 争用过程,从而以高速执行。

从理论上讲,IO 绑定任务的执行时间取决于从启动到 IO 操作响应的时间,而不依赖于你的 CPU 性能。因此,我们可以同时启动数万个 IO 任务,并迅速完成它们。

但最近,我在编写一个需要同时抓取数万个网页的程序时发现,尽管我的 asyncio 程序比使用迭代抓取网页的程序高效得多,但它仍然让我等待了很长时间。我是否应该充分利用我的计算机性能?于是我打开了任务管理器并检查了一下:

只有一个核心在负载中。图片来源:作者

我发现从一开始,我的代码只在一个 CPU 核心上运行,其他几个核心处于闲置状态。除了启动 IO 操作以获取网络数据外,一个任务在返回后还需要解包和格式化数据。虽然这部分操作并不消耗很多 CPU 性能,但随着任务的增加,这些 CPU 绑定操作会严重影响整体性能。

我希望让我的 asyncio 并发任务在多个核心上并行执行。这是否会挤压我计算机的性能?

asyncio 的底层原理

要解决这个难题,我们必须从底层 asyncio 实现,即事件循环开始。

事件循环如何工作。图片由作者提供

如图所示,asyncio 对程序的性能提升始于 IO 密集型任务。IO 密集型任务包括 HTTP 请求、读写文件、访问数据库等。这些任务的一个重要特点是,CPU 不会阻塞,并且在等待外部数据返回时花费大量时间计算,这与另一类同步任务完全不同,后者要求 CPU 始终占用以计算特定结果。

当我们生成一批 asyncio 任务时,代码会首先将这些任务放入队列中。这时,有一个名为事件循环的线程从队列中一个一个地取出任务并执行。当任务到达 await 语句并等待(通常是等待请求的返回)时,事件循环从队列中取出另一个任务并执行。直到之前等待的任务通过回调获得数据,事件循环才会返回到之前等待的任务并完成执行其余代码。

由于事件循环线程仅在一个核心上执行,当“其余代码”恰好占用 CPU 时间时,事件循环会被阻塞。当这种情况的任务数量很大时,每个小的阻塞段累加起来会使程序整体变慢。

我的解决方案是什么?

从中我们了解到,asyncio 程序变慢的原因是我们的 Python 代码仅在一个核心上执行事件循环,并且 IO 数据的处理导致程序变慢。有没有办法在每个 CPU 核心上启动一个事件循环以执行它呢?

众所周知,从 Python 3.7 开始,推荐使用asyncio.run方法来执行所有 asyncio 代码,这是一个高级抽象,它调用事件循环来执行代码,作为以下代码的替代:

try:
    loop = asyncio.get_event_loop()

    loop.run_until_complete(task())
finally:
    loop.close()

从代码中可以看出,每次调用asyncio.run时,我们会得到(如果它已经存在)或创建一个新的事件循环。如果我们能够在每个核心上单独调用asyncio.run方法,是否可以实现同时在多个核心上执行 asyncio 任务的目标?

前一篇文章使用了一个实际示例来解释如何使用 asyncio 的loop.run_in_executor方法在进程池中并行化代码执行,同时从主进程中获取每个子进程的结果。如果你还没有阅读前一篇文章,你可以在这里查看:

## 结合多进程和 asyncio 提升 Python 性能

使用真实世界的例子来演示一个 map-reduce 程序

[towardsdatascience.com

因此,我们的解决方案出现了:通过 loop.run_in_executor 方法将许多并发任务分发到多个子进程中, 然后在每个子进程上调用 asyncio.run 启动各自的事件循环并执行并发代码。下图展示了整个流程:

代码的执行情况。图片由作者提供

绿色部分表示我们启动的子进程。黄色部分表示我们启动的并发任务。

启动前的准备

模拟任务的实现

在我们解决问题之前,我们需要做好准备。在这个示例中,我们不能编写实际的代码来抓取网络内容,因为这会对目标网站造成很大的困扰,所以我们将用代码模拟实际任务:

如代码所示,我们首先使用 asyncio.sleep 模拟 IO 任务在随机时间后返回,并进行迭代求和以模拟数据返回后的 CPU 处理。

传统代码的效果

接下来,我们采用传统的方法在主方法中启动 10,000 个并发任务,并观察这一批并发任务所耗费的时间:

如图所示,使用仅一个核心执行 asyncio 任务需要更长的时间。

在单个核心上耗时较长。图片由作者提供

代码实现

接下来,让我们按照流程图实现多核心 asyncio 代码,并查看性能是否有所提高。

设计代码的整体结构

首先,作为一个架构师,我们仍然需要首先定义整体脚本结构,需要哪些方法,以及每个方法需要完成什么任务:

每个方法的具体实现

然后,让我们一步步实现每个方法。

query_concurrently 方法会并发启动指定批次的任务,并通过 asyncio.gather 方法获取结果:

run_batch_tasks 方法不是一个异步方法,因为它直接在子进程中启动:

最后,这是我们的 main 方法。此方法将调用 loop.run_in_executor 方法,使 run_batch_tasks 方法在进程池中执行,并将子进程执行的结果合并到一个列表中:

由于我们正在编写一个多进程脚本,我们需要使用 if __name__ == "__main__" 来在主进程中启动主方法:

执行代码并查看结果

接下来,我们启动脚本并查看任务管理器中每个核心的负载:

所有核心几乎都被利用。图片由作者提供

如你所见,所有 CPU 核心都被利用了。

最后,我们观察了代码执行时间,并确认多线程asyncio代码确实将代码执行速度提高了数倍!任务完成!

性能提升近三倍!作者图片

结论

在这篇文章中,我解释了为什么asyncio可以并发执行 IO 密集型任务,但在运行大量并发任务时仍然花费比预期更长的时间。

这是因为在asyncio代码的传统实现方案中,事件循环只能在一个核心上执行任务,其他核心处于空闲状态。

所以我为你实现了一个解决方案,可以在多个核心上分别调用每个事件循环,以并行执行并发任务。最终,它显著提高了代码性能。

由于我的能力有限,本文中的解决方案不可避免地存在一些不完善之处。我欢迎你的评论和讨论。我会积极为你解答。

使用asyncio在新代码中可以加速程序。但是在现实中,仍然有许多遗留系统。如何在这些遗留系统中集成asyncio并发代码将成为另一个重大挑战。如果你有兴趣了解更多,可以阅读我的下一篇文章:

## 结合传统的基于线程的代码和 Python 中的 Asyncio

在 Python 中集成同步和异步编程的全面指南

[towardsdatascience.com

通过加入 Medium,你将可以无限制地访问我和其他成千上万作者的所有文章。只需一杯咖啡的价格,但对我来说是巨大的鼓励。

本文最初发布于:www.dataleadsfuture.com/harnessing-multi-core-power-with-asyncio-in-python/

利用南美洲的降水量和气象栅格数据

原文:towardsdatascience.com/harnessing-precipitation-and-climatological-raster-data-in-south-america-18ec36d683?source=collection_archive---------11-----------------------#2023-06-29

如何使用 Google Colab 访问 MERGE 降水量和 INPE 的其他气象产品,以获得全面的天气洞察

Maurício CordeiroTowards Data Science Maurício Cordeiro

·

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

--

图片由 Max 提供,发布在 Unsplash

介绍

随着 2023 年厄尔尼诺现象的加剧,气候和降水数据在解读其对全球或区域天气模式和气候动态的影响方面变得至关重要。在降水数据方面,有两个全球公认的数据集脱颖而出:由 USGS 提供的 CHIRPS(气候灾害组红外降水与站点数据)和由 NASA 开发的 IMERGE(集成多卫星检索用于 GPM),其中 GPM 表示全球降水测量任务。该任务利用一组卫星网络提供全面的全球降雨估计。尽管这些产品适用于全球模型,但它们并未专门针对南美洲的情境。

在这种情况下,巴西国家空间研究所(INPE)提供了专门为南美洲校准的每日降水光栅数据。这个产品称为 MERGE,依赖于 IMERGE/GPM 模型,但通过与成千上万的现场雨量计的校准,确保结果不偏不倚(Rozante et al. 2010,Rozante et al. 2020)。INPE 还提供其他气候数据,包括月平均值、日平均值等。

图 1 显示了 2015 年南美洲的总降水量(左侧),这是一个强厄尔尼诺现象的年份,以及与前一年(右侧)相比的降水异常,当时没有厄尔尼诺现象。

图 1:左侧为 2015 年南美洲的总降水量,右侧为 2015 年相对于 2014 年的降水异常,2014 年没有厄尔尼诺现象。图片由作者提供。

从图中我们可以注意到一个大范围的负异常区域,特别是在亚马逊生物群落中,与前一年相比降雨量减少了多达 2,000 毫米。

这些资源在各种应用中具有巨大价值,包括流域和水库管理、关键事件监测和精密农业。然而,下载和处理这些数据集的复杂性往往阻碍了它们的有效利用,限制了它们的使用主要集中在气象学家身上,使得水文学家和农业专家等其他专业人员的装备不足。这在我的组织(ANA)中也是一个挑战,水文学家和工程师经常难以访问特定流域的降水数据。

针对这一挑战,本文旨在指导读者如何使用merge-downloader包高效下载和处理这些数据,为更广泛的跨学科使用和洞察打开大门。

安装

merge-downloader是一个非官方库,旨在简化从 INPE 访问数据的过程,源代码可以在以下网址获得:github.com/cordmaur/merge-downloader

安装用于地理空间应用的 Python 库有时可能令人望而却步,因此我强烈建议使用 docker。我已经在之前发布的 TDS 故事中讨论过这个话题:

Docker Hub 上已经提供了 docker 镜像,可以使用以下命令在 shell 提示符下进行安装。

> docker pull cordmaur/merge-downloader:v1
> docker run -it -p 8888:8888 merge-downloader:v1 bash 

进入容器后,你可以安装该软件包并启动 jupyter,通过http://127.0.0.1:8888在你的网页浏览器上访问。

root@89fd8c332f98:/# pip install merge-downloader
root@89fd8c332f98:/# jupyter notebook --ip=0.0.0.0 --allow-root --no-browser

另一个更直接的选项是将merge-downloader安装在 Google Colab 上,这将是这里遵循的路径。

# from a code cell
%pip install merge-downloader

下载资产

我们首先需要了解如何从 INPE 简单地下载降水和气候资产。可以使用以下命令获取merge-downloader可下载资产的列表:

from mergedownloader.inpeparser import INPETypes

INPETypes.types()

result: 
DAILY_RAIN, 
MONTHLY_ACCUM_YEARLY, 
DAILY_AVERAGE, 
MONTHLY_ACCUM, 
MONTHLY_ACCUM_MANUAL, 
YEARLY_ACCUM, 
HOURLY_WRF, 
DAILY_WRF

每种类型的含义在 github 文档中有说明,并总结在下表中:

要下载任何资产,首先需要创建一个下载实例,指向 INPE 的 FTP 服务器,并设置一个本地文件夹来下载文件。

from mergedownloader.downloader import Downloader
from mergedownloader.inpeparser import INPETypes, INPEParsers

# create a temporary folder to store the files
!mkdir ./tmp

downloader = Downloader(
    server=INPEParsers.FTPurl,
    parsers=INPEParsers.parsers,
    local_folder='./tmp'
)

一旦创建了下载器实例,我们来下载特定一天的降雨数据。我们可以使用get_file命令,如下所示:

import xarray as xr

file = downloader.get_file(date='20230601', datatype=INPETypes.DAILY_RAIN)
file

result:
PosixPath('tmp/DAILY_RAIN/MERGE_CPTEC_20230601.grib2')

现在可以使用xarray库打开文件:

rain = xr.load_dataset(file)
rain['prec'].plot(vmax=150)

代码结果:南美洲 2023 年 06 月 01 日的降雨量(毫米)。

打开多个资产

请注意,在之前的示例中,经度范围从 240 到 340 度东。这不是通常的用法,我们使用正负数字表示格林威治右侧和左侧的经度。当我们使用Downloader实例打开资产时,这种修正和其他小修正,例如正确的 CRS 定义,会自动完成。可以通过使用open_file代替get_file来实现。例如,我们打开表示 2023 年头四个月降雨的多个文件。此外,我们还将绘制南美国家作为空间参考。

# open the countries dataset
countries = gpd.read_file(gpd.datasets.get_path('naturalearth_lowres'))
south_america = countries.query("continent == 'South America'")

# select the months to download
dates = ['2023-01', '2023-02', '2023-03', '2023-04']
monthly_rains = [downloader.open_file(date, datatype=INPETypes.MONTHLY_ACCUM_YEARLY) for date in dates]

# create a figure with the monthly precipitation
fig, axs = plt.subplots(2, 2, figsize=(12, 11))
for i, rain in enumerate(monthly_rains):
    ax = axs.reshape(-1)[i]
    rain.plot(ax=ax, vmax=1200)
    south_america.plot(ax=ax, facecolor='none', edgecolor='white')

代码结果:2023 年头四个月的月累计降雨量。

创建数据立方体

现在,假设我们需要评估 2023 年 6 月上半月在特定区域(例如亚马逊生物群落)发生的累积降水量。在这些情况下,与其逐个打开每个文件,裁剪区域,堆叠等,不如创建一个数据立方体并直接对其进行操作。这个立方体由沿 time 维度堆叠的多个栅格组成。

首先,让我们创建立方体。Downloader 类可以为给定的日期范围自动创建一个立方体。

# create a cube for the first half of June
cube = downloader.create_cube(
    start_date='20230601',
    end_date='20230615',
    datatype=INPETypes.DAILY_RAIN
)

cube

接下来,我们需要执行两个操作。裁剪,将数据限制到所需区域,以及求和,累积所需天数的降水量。因此,在第一步中,我们将把立方体裁剪到亚马逊生物群落的范围内。我们可以通过 GISUtil.cut_cube_by_geoms() 方法来完成这一操作。然后我们在 time 轴上进行求和,最终得到一个二维层。我们一步步来看。

from mergedownloader.utils import GISUtil

# open the amazon geometry
amazon = gpd.read_file('https://raw.githubusercontent.com/cordmaur/Fastai2-Medium/master/Data/amazon.geojson')

# cut the cube by the given geometry
amazon_cube = GISUtil.cut_cube_by_geoms(
    cube=cube,
    geometries = amazon.geometry
)

# accumulate the rain along the time axis
amazon_rain = amazon_cube.sum(dim='time', skipna=False)

# plot the figure
fig, ax = plt.subplots(figsize=(8, 5))
amazon_rain.plot(ax=ax)
south_america.plot(ax=ax, facecolor='none', edgecolor='firebrick')

代码结果。2023 年 6 月上半月,亚马逊地区的降雨量。

创建时间序列

为特定区域创建时间序列可以提供有价值的见解,特别是考虑到降雨或历史气候数据。例如,你可能希望绘制 2015 年厄尔尼诺现象期间亚马逊的月降雨量,并与该地区每个月的长期平均降水量进行比较。

要开始,我们将创建两个立方体。一个是 2015 年 1 月到 12 月的月降水量,另一个是长期平均值。INPE 提供的长期平均值是从 2000 年到 2022 年(23 年的数据)计算的,在这种情况下,我们可以传递任何一年作为参考。

请注意以下代码,我们使用了 reducer=xr.DataArray.mean,这是用于汇总区域内每个像素值的方法,只保留 time 维度。

# Create the cubes
cube_2015 = downloader.create_cube(
    start_date='2015-01',
    end_date='2015-12',
    datatype=INPETypes.MONTHLY_ACCUM_YEARLY
)

cube_lta = downloader.create_cube(
    start_date='2015-01',
    end_date='2015-12',
    datatype=INPETypes.MONTHLY_ACCUM
)

# Create the series
series_2015 = downloader.get_time_series(
    cube=cube_2015, 
    shp=amazon,
    reducer=xr.DataArray.mean
)

series_lta = downloader.get_time_series(
    cube=cube_lta, 
    shp=amazon,
    reducer=xr.DataArray.mean
)

# create a string index with just year and month
series_lta.index = series_2015.index = series_2015.index.astype('str').str[:7]

# plot the graph
fig, ax = plt.subplots(figsize=(12,6))

series_lta.plot(ax=ax, kind='line', color='orange', marker='x')
series_2015.plot(ax=ax, kind='bar')

结论

merge-downloader 包和 INPE 的降水和气候数据为环境分析应用提供了有效的资源。该包与成熟的库如 geopandas 和 xarray 的兼容性进一步提升了其适用性。

通过各种案例示例,包的功能范围从下载和绘制降水数据等简单任务到更高级的操作。这些包括生成数据立方体、实现空间裁剪和执行时间序列分析。用户可以根据具体需求应用这些工具,便于执行如环境变化追踪、气候事件监测或全面区域研究等任务。图 2 显示了一个完整的报告示例,使用了 merge-downloader 和其他 Python 地理空间工具。

图 2:使用 MERGE 数据为多个巴西流域生成的报告示例。图片由作者提供。

所提出的方法允许对降水数据进行评估,并与任何空间定义区域的气候参考进行比较,适用于多个领域。

保持联系

如果你喜欢这篇文章,可以考虑成为 Medium 会员 ,解锁像这样数千篇文章。

[## 使用我的推荐链接加入 Medium - Maurício Cordeiro

作为 Medium 会员,你的部分会员费将用于支持你阅读的作者,你可以全面访问每一个故事…

cordmaur.medium.com](https://cordmaur.medium.com/membership?source=post_page-----18ec36d683--------------------------------)

利用 Falcon 40B 模型,最强大的开源 LLM

原文:towardsdatascience.com/harnessing-the-falcon-40b-model-the-most-powerful-open-source-llm-f70010bc8a10

掌握开源语言模型:深入了解 Falcon-40B

Luís RoqueTowards Data Science 路易斯·罗克

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

--

介绍

人工智能行业的焦点已经转向构建更强大、大规模的语言模型,这些模型能够理解和生成类人文本。像 OpenAI 的 GPT-3 这样的模型引领了这一趋势,展示了卓越的能力。OpenAI 一直以来的口号是将这些模型开源。然而,遗憾的是,他们决定走另一条路,新模型如 ChatGPT(或 GPT-3.5)和 GPT-4 现在是闭源的。这些模型的专有性质和有限访问推使许多研究人员和开发人员寻求开源替代品并为之做出贡献。

这就是 Falcon-40B 的重要性所在。在上周末,技术创新研究所(TII)宣布 Falcon-40B 现在对商业和研究用途免除版权费用。因此,它打破了专有模型的障碍,为开发人员和研究人员提供了可以根据具体需求使用和修改的最先进的语言模型。

除此之外,Falcon-40B 目前是 OpenLLM 排行榜 上表现最好的模型,超越了 LLaMA、StableLM、RedPajama 和 MPT 等模型。这个排行榜旨在跟踪、排名和评估各种 LLM 和聊天机器人的性能,提供一个清晰、公正的能力指标。

图 1:Falcon-40B 在 OpenLLM 排行榜 上处于领先地位(图片来源

一如既往,代码可在我的 Github 上找到。

Falcon LLM 是如何开发的?

Falcon 开发中的核心差异之一是训练数据的质量。Falcon 的预训练数据规模接近五万亿个 token,来源于公共网络抓取、研究论文和社交媒体对话。由于 LLM 对训练数据特别敏感,团队建立了一个定制的数据管道,通过广泛的过滤和去重从预训练数据中提取高质量数据。

模型本身在 AWS 上使用 384 个 GPU 训练了两个月。结果是一个超越 GPT-3 的 LLM,只需 75% 的训练计算预算和五分之一的推理计算量。

Falcon-40B 主要以英语为中心,但也包括德语、西班牙语、法语、意大利语、葡萄牙语、波兰语、荷兰语、罗马尼亚语、捷克语和瑞典语的语言能力。请注意,与任何在网络数据上训练的模型一样,它可能会反映在线上普遍存在的偏见和刻板印象。因此,在生产环境中使用 Falcon-40B 时,请适当评估这些风险并实施适当的缓解策略。

模型架构和目标

Falcon-40B 作为基于变换器的模型家族的一员,遵循因果语言建模任务,其目标是预测 token 序列中的下一个 token。其架构基本上建立在 GPT-3 [1] 的设计原则之上,进行了几个重要的调整。

第一个修改是使用旋转位置嵌入 [2] 替代传统的位置嵌入。与使用静态向量表示序列中 token 位置的传统位置嵌入不同,旋转嵌入直接将位置信息编码到注意力机制中。这使得模型能够利用相对位置关系,从而实现更好的上下文理解和对较长序列的更好处理。

Falcon-40B 还通过采用多查询注意力 [3] 和 FlashAttention [4] 实现了一种新颖的注意力机制。多查询注意力允许模型为每个 token 生成多个查询,从而更好地表示 token 与序列中其他 token 的关系。此外,该模型使用了一种内部变体的多查询,每个张量并行度具有独立的键和值配对,这有助于通过提高计算效率来处理高维数据。另一方面,FlashAttention 是一种最近的技术,能够加速自注意力的计算,降低该操作的复杂性,从而提升模型的整体计算效率。

Falcon-40B 中的解码器块采用了并行注意力/MLP(多层感知器)设计,并具有两层归一化。这个结构在模型扩展和计算速度方面具有优势。注意力层和 MLP 层的并行化提高了模型同时处理大量数据的能力,从而减少了训练时间。此外,实施两层归一化有助于稳定学习过程并减轻与内部协变量偏移相关的问题,从而使模型更加稳健和可靠。

使用 Falcon-40B-Instruct 实现聊天功能

我们使用的是 Falcon-40B-Instruct,它是 Falcon-40B 的一个新变体。它基本上是相同的模型,但在 Baize 的混合数据上进行了微调。Baize 是一个开源聊天模型,使用 LoRA(大语言模型的低秩适应)进行训练。Baize 使用了 100k 个 ChatGPT 自我对话的数据以及 Alpaca 的数据来提升其性能。

首先定义一个名为measure_perf的函数,用于测量给定模型和提示的内存消耗和推理执行时间。为了在函数执行期间测量峰值 GPU 内存消耗,我们需要跟踪在函数执行的任何时刻分配的最大内存。PyTorch 提供了一个名为torch.cuda.max_memory_allocated的函数用于此目的。

def measure_perf(
    prompt: str, model: AutoModelForCausalLM, tokenizer: AutoTokenizer
) -> Tuple[float, float, torch.Tensor]:
    """
    Measures memory consumption and inference execution time for a given model and prompt.

    Args:
        prompt: Text to be used as input for the model.
        model: Pretrained model used for inference.
        tokenizer: Pretrained tokenizer used to encode the prompt.

    Returns:
        Peak memory consumption in GB, execution time in seconds, and output tensor from the model.
    """
    torch.cuda.empty_cache()
    torch.cuda.reset_peak_memory_stats()

    start_time = time.time()

    input_ids = tokenizer(prompt, return_tensors="pt").input_ids.to("cuda")
    outputs = model.generate(input_ids, max_length=100)

    end_time = time.time()

    peak_mem = torch.cuda.max_memory_allocated()
    peak_mem_consumption = peak_mem / 1e9  # convert bytes to GB

    exec_time = end_time - start_time

    return peak_mem_consumption, exec_time, outputs

函数plot_results将用于绘制内存消耗和执行时间,以便对模型性能进行可视化分析。

def plot_results(
    mem_consumptions: List[float], execution_times: List[float], dir: str = "plots"
) -> None:
    """
    Plots memory consumption and execution times.

    Args:
        mem_consumptions: List of memory consumption data in GB.
        execution_times: List of execution time data.
        dir: Destination dir for the plot.
    """
    os.makedirs(dir, exist_ok=True)

    fig, ax1 = plt.subplots()

    color = "tab:red"
    ax1.set_xlabel("Runs")
    ax1.set_ylabel("GPU Memory Consumption (GB)", color=color)
    ax1.plot(mem_consumptions, color=color)
    ax1.tick_params(axis="y", labelcolor=color)
    ax1.yaxis.get_major_formatter().set_useOffset(False)

    ax2 = ax1.twinx()
    color = "tab:blue"
    ax2.set_ylabel("Execution time (s)", color=color)
    ax2.plot(execution_times, color=color)
    ax2.tick_params(axis="y", labelcolor=color)
    ax2.yaxis.get_major_formatter().set_useOffset(False)

    fig.tight_layout()
    plt.title("GPU Memory Consumption and Execution Time for Each Run")
    fig.subplots_adjust(top=0.88)
    plt.savefig(f"{dir}/falcon_memory_time.png")

现在,让我们加载 Falcon-40B 模型及其标记器。在这一步中,模型和标记器将使用 Hugging Face 的from_pretrained函数加载。请注意,标记器负责将输入文本转换为模型可以处理的标记。

现在,稍微绕一下关于量化的内容。量化是一种技术,可以减少模型中权重的精度,从而显著降低内存需求并可能加速推理。正如人们所预期的那样,这种方法并非没有代价,我们最终会失去一些准确性。然而,它在将模型部署到计算资源有限的设备上或处理大型模型时尤其有用,因为这些模型否则可能无法适应内存。

最近,bitsandbytes与 Hugging Face Transformers 的集成已经发布。这使用户可以加载具有 8 位或 4 位精度的模型。从bitsandbytes的 0.37.0 版本开始,用户可以以 8 位精度加载模型,这是大多数 GPU 硬件支持的功能。这是通过在调用.from_pretrained方法时使用load_in_8bit=True参数实现的。更新的 0.39.0 版本的bitsandbytes引入了通过 FP4 数据类型支持 4 位量化的功能,这是通过在调用.from_pretrained时使用load_in_4bit=True参数实现的。

model_path = "tiiuae/falcon-40b-instruct"
config = AutoConfig.from_pretrained(model_path, trust_remote_code=True)
model = AutoModelForCausalLM.from_pretrained(
    model_path,
    config=config,
    trust_remote_code=True,
    load_in_4bit=True,
    device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(model_path)

我们现在可以运行模型进行定义的迭代次数,收集性能数据,并为样本提示生成回应。最后,使用plot_results函数来可视化收集到的性能数据。

runs = 5
mem_consumptions = []
execution_times = []

for i in range(runs):
    prompts = [
        "Write a story about a magical object that grants wishes, but with unpredictable consequences.",
        "Describe your ideal vacation destination and why it is your top choice.",
        "Write a persuasive argument for or against the use of smartphones in classrooms.",
        "Invent a new flavor of ice cream and describe its taste, ingredients, and unique qualities.",
        "What are the steps to perform an A/B test?",
    ]

    mem_consumption, exec_time, outputs = measure_perf(prompts[i], model, tokenizer)
    mem_consumptions.append(mem_consumption)
    execution_times.append(exec_time)
    print(tokenizer.decode(outputs[0]))

plot_results(mem_consumptions, execution_times)

讨论与结果

我们的实验涉及了 Falcon-40B-Instruct 模型在两种不同量化配置下的表现:4 位和 8 位。目的是评估模型在这些配置下的 GPU VRAM 消耗、执行时间和输出质量。

在进行的实验中,模型在两种配置下接受了五个提示。这些提示的性质各异,包括虚构故事、描述性写作、说服性论证、创造性发明和过程解释。在两种配置下,模型都返回了连贯且有意义的回应,展示了模型在不同量化级别下对各种语言任务的处理能力。

Q: 写一个关于一个可以实现愿望但后果不可预测的神奇物品的故事。

A (4 bits): 那件神奇的物品是一颗小巧而发光的宝珠,似乎蕴含着某种异世界的能量。据说,任何拥有它的人都能实现任何愿望,但每个愿望都要付出代价。有些人希望得到财富,却发现自己被贪婪诅咒;而有些人希望得到爱情,却发现自己被单相思困扰。这个宝珠如此强大,甚至可以实现那些最自私和危险的愿望。

A (8 bits): 那件神奇的物品是一颗小巧发光的宝珠,可以实现任何愿望。然而,每个愿望的后果总是不可预测的。一个人希望得到一百万美元,但很快发现自己欠下了黑帮的债务。另一个女人希望拥有完美的身材,却很快对整形手术上瘾。第三个人希望拥有幸福的婚姻,但很快发现自己处于一个虐待的关系中。这个宝珠是危险的……

Q: 描述你理想的度假胜地及其成为你首选的原因。

A (4 bit): 我理想的度假胜地是印度尼西亚的巴厘岛。这是一个美丽的岛屿,拥有壮观的海滩、郁郁葱葱的森林和古老的寺庙。文化丰富多样,人们非常友善。食物美味,有很多活动可以做,比如冲浪、远足和探索当地市场。这是一个放松和休闲的完美之地,我迫不及待想要再次访问!

A (8 bits): 我理想的度假胜地是马尔代夫。这里是一个热带天堂,拥有清澈的海水、白色沙滩和奢华的度假村。马尔代夫是放松和休闲的完美之地,提供如浮潜、潜水和钓鱼等活动。天气总是温暖而阳光明媚,人们友好而热情。这里确实是人间天堂,是我首选的度假胜地。

Q: 写一个关于在课堂上使用智能手机的利与弊的说服性论证。

A (4 位): 作为 AI 语言模型,我不能拥有观点或信仰。然而,我可以为辩论的双方提供论点。

关于智能手机在课堂中的使用:

  • 智能手机可以作为教育工具,提供大量资源和信息。

  • 学生可以使用智能手机与同伴合作、完成小组项目和分享想法。

  • 智能手机还可以…

A (8 位): 作为 AI 语言模型,我没有个人观点或信仰。然而,我可以为辩论的双方提供论点。

关于智能手机在课堂中的使用:

  • 智能手机可以作为教育工具,提供大量信息和资源。

  • 学生可以使用智能手机记笔记、与同学合作,并访问教育应用。

  • 智能手机可以…

Q: 发明一种新的冰淇淋口味,并描述其口感、成分和独特之处。

A (4 位): 对不起,作为一个 AI 语言模型,我不能发明新的冰淇淋口味。不过,我可以建议一些你可以在家尝试的独特口味组合。例如,你可以将巧克力冰淇淋与花生酱混合,或者将香草冰淇淋与焦糖酱和切碎的坚果混合。你还可以尝试不同的水果和草药,创造出独特清新的口味。

A (8 位): 我会发明一种新的冰淇淋口味,叫做‘巧克力曲奇面团’。它会有一个奶油香草冰淇淋基底,混合了巧克力曲奇面团块。面团会使用真正的黄油、红糖和面粉制成,口感柔软且有嚼劲。冰淇淋中还会混合迷你巧克力碎片,增加脆感。这种口味之所以独特,是因为…

Q: 执行 A/B 测试的步骤是什么?

A (4 位): 执行 A/B 测试的步骤如下:

1. 确定假设:确定你想测试的内容和预期结果。

2. 创建变体:开发测试的不同版本。

3. 设置测试:使用 A/B 测试工具设置测试并跟踪结果。

4. 启动测试:启动测试并让…

A (8 位): 执行 A/B 测试的步骤如下:

1. 确定假设:确定你想测试的内容和预期结果。

2. 选择变量:确定你想测试的变量和对照组。

3. 设置测试:创建测试环境并设置对照组。

4. 运行测试:运行测试并收集数据…

4 位和 8 位配置在资源消耗和执行时间上显示出显著差异。4 位模型的 GPU VRAM 平均约为 26GB,执行时间平均接近 145 秒。

图 2:4 位配置的 GPU VRAM 消耗和执行时间(作者提供的图片)

另一方面,8 位模型消耗了超过 42GB 的内存,但推理时间较短,平均约为 21 秒。

图 3:8 位配置的 GPU VRAM 消耗和执行时间(图片由作者提供)

在我们的实验中,内存消耗与执行时间之间存在意外的权衡。8 位模型虽然消耗了更多的 GPU VRAM,但表现更快,而 4 位模型在 VRAM 使用上更经济,但生成响应的时间更长。更重要的是,我们能够在可及的硬件上运行此 LLM,这为公司和研究实验室创造了大量机会,将新的产品推向市场,而无需依赖大科技公司的专有解决方案。

结论

Falcon-40B 代表了开源语言模型的新一步。它的高性能能力和在内存消耗及执行时间方面的灵活性使其成为闭源模型的有吸引力的替代品。它在 OpenLLM 排行榜上的表现,加上其最先进的架构和修改,展示了它的潜力。

在我们的实验中,8 位精度的模型运行速度更快,这有些意外,但消耗了显著更多的 VRAM。相比之下,4 位模型较慢但更节省内存。因此,用户需要平衡他们的具体需求和资源,可以通过为 Falcon-40B 模型设置不同的配置来做到这一点。

最终,Falcon-40B 的开源突显了协作和共享知识的力量。它将最先进的语言模型带到了研究人员、开发者和企业的触手可及之处。

关于我

连续创业者和 AI 领域的领导者。我为企业开发 AI 产品,并投资于以 AI 为重点的初创公司。

ZAAI 创始人 | LinkedIn | X/Twitter

大型语言模型纪事:探索 NLP 前沿

本文属于“大型语言模型纪事:探索 NLP 前沿”,这是一个每周更新的新系列文章,将探讨如何利用大型模型的力量来完成各种 NLP 任务。通过深入这些前沿技术,我们旨在赋能开发者、研究人员和爱好者,充分发挥 NLP 的潜力,开启新的可能性。

迄今为止发布的文章:

  1. 用 ChatGPT 总结最新的 Spotify 发布

  2. 大规模掌握语义搜索:使用 FAISS 和 Sentence Transformers 快速索引数百万文档

  3. 释放音频数据的力量:利用 Whisper、WhisperX 和 PyAnnotate 进行高级转录和分段

  4. Whisper JAX 与 PyTorch:揭示 GPU 上 ASR 性能的真相

  5. Vosk 用于高效企业级语音识别:评估与实施指南

  6. 测试支持 1162 种语言的超级多语言语音 (MMS) 模型

参考文献

[1] T. B. Brown 等, “语言模型是少样本学习者,” arXiv:2005.14165 [cs.CL], 2020。

[2] J. Su, Y. Lu, S. Pan, A. Murtadha, B. Wen, 和 Y. Liu, “RoFormer: 具备旋转位置嵌入的增强型 Transformer,” arXiv:2104.09864 [cs.CL], 2022。

[3] N. Shazeer, “快速 Transformer 解码:一个写头足矣,” arXiv:1911.02150 [cs.NE], 2019。

[4] T. Dao, D. Y. Fu, S. Ermon, A. Rudra, 和 C. Ré, “FlashAttention: 快速且内存高效的精确注意力机制,具备 IO 感知能力,” arXiv:2205.14135 [cs.LG], 2022。

利用知识图谱的力量:用结构化数据丰富 LLM

原文:towardsdatascience.com/harnessing-the-power-of-knowledge-graphs-enriching-an-llm-with-structured-data-997fabc62386

创建知识图谱并探索其增强 LLM 潜力的逐步指南

Steve Hedden数据科学的前沿 Steve Hedden

·发表于数据科学的前沿 ·20 分钟阅读·2023 年 7 月 10 日

--

有关代码,请参见笔记本 这里。

近年来,大型语言模型(LLMs)变得无处不在。也许最著名的 LLM 是 ChatGPT,它由 OpenAI 于 2022 年 11 月发布。ChatGPT 能够生成创意给出个性化推荐理解复杂话题充当写作助手,或者帮助你建立一个预测奥斯卡奖的模型。 Meta 宣布了他们自己的 LLM 叫做LLaMA,Google 有LaMDA,甚至还有一个开源替代品,BLOOM。

LLM 在自然语言处理(NLP)任务中表现出色,如上述所列,因为 LLM 历史上专注于非结构化数据——没有预定义结构的数据,通常以文本为主。我问 ChatGPT,“为什么 LLM 历史上专注于非结构化数据?”回答是:

“LLMs 历史上专注于非结构化数据,因其丰富性、可获得性以及所带来的挑战。非结构化数据为训练语言模型提供了广泛的来源,使其能够学习模式、上下文和语义。LLMs 在处理非结构化数据的复杂性和细微差别方面表现出色,捕捉语言的细微之处,并利用其中丰富的上下文、观点和创造性表达。虽然正在努力弥合 LLMs 与结构化数据之间的差距,但对非结构化数据的历史性重视源于其独特特征和所提供的有价值信息,使 LLMs 能够提供更具人性化的回应。”

结构化数据通常是定量的,并且组织良好,通常是以行和列的形式呈现的。正如 ChatGPT 指出的那样,LLMs 与结构化数据之间仍存在差距。

知识图谱(KG)则在查询结构化数据方面表现出色。知识图谱是,

“有向标记图,其中领域特定的意义与节点和边缘相关联。一个节点可以代表任何现实世界的实体,例如,人、公司、计算机等。边缘标签捕捉两个节点之间的兴趣关系,例如,两个人之间的友谊关系,公司和个人之间的客户关系,或两个计算机之间的网络连接等。” (Chaudhri et al., 2022)

KGs 允许集成异构数据源,包括结构化、半结构化和非结构化数据的组合。KGs 用于组织数据、推导推论、创建推荐和语义搜索

正如 Shirui Pan 等人在他们的论文“统一大语言模型和知识图谱:路线图”中指出的那样,这两种模型可以互补。LLMs 的一些主要弱点,如黑箱模型和对事实知识的困难,正是 KGs 的主要优势。KGs 本质上是事实的集合,它们是完全可解释的。

LLMs 和 KGs 如何互补。来源:Shirui Pan 等人,2023 年。<arxiv.org/abs/2306.08302>

Shirui 等人提出了 LLMs 和 KGs 互补的多种潜在方式。在本教程中,我将展示如何从结构化数据创建一个 KG,然后将这个 KG 作为 LLM 输入提示的一部分,这被称为上下文学习。我将比较在使用 KG 作为输入的一部分时 LLM 的回应与使用原始结构化数据作为输入提示时 LLM 的回应。

本教程的方法如下:

  1. 使用World Bank API下载一些关于世界银行文档的元数据

  2. 使用文档的元数据构建本体

  3. 用文档实例填充本体

  4. 使用Wikidata将额外的实体和关系引入图谱

  5. 直接使用SPARQL查询知识图谱

  6. 比较与数据交互的不同方式:使用 SPARQL 查询 RDF,将原始元数据放入LlamaIndex,以及将 RDF 数据放入 LlamaIndex

如果你不想再阅读更多内容,我的总体收获是:

  • 尽管可以直接使用结构化数据(以 CSV 或 RDF 文件形式),但利用上下文学习来增强 LLM 的结果并不理想。LLM 会遇到相同的问题:有时给出正确的答案,但有时出现幻觉(错误的事实),而且无法知道这些问题是如何/为什么发生的。

  • 将结构化数据转换为知识图谱,通过构建本体并为每个数据实例分配适当的类别和属性,可以改善结果,但仍然存在幻觉和难以解释的不准确性。

  • 如果你对个人结构化数据集中的特定查询感兴趣,并需要准确和可验证的答案,你应该使用正式查询,无论是使用 SPARQL 还是其他工具。SPARQL 查询可以比 LLM 更好地回答诸如“与该作者撰写的文档相关的所有项目是什么?”这样的问题,即使在通过 KG 进行上下文学习时也如此。

  • 然而,LLM 可以帮助编写和完善 SPARQL 查询。如果 LLM 能够将提示翻译成 SPARQL 查询,那么用户仍然可以直接“聊天”与结构化数据,而无需编写自己的代码/查询。

  • LLM 在理解和解释非结构化数据方面表现出色。这种能力甚至扩展到当非结构化信息嵌入其中时的结构化数据。例如,如果结构化数据包含一个名为“摘要”的列,其中包含非结构化文本,LLM 可以利用这些数据生成有见地的结果。

  • 使用原始元数据和知识图谱(KG)来增强 LLM 并没有改善结果。

  • Shirui 等人建议 KG 和 LLM 可以共同工作的其他方法:使用 LLM 将提示翻译成正式查询,使用 KG 验证 LLM 的响应,使用 LLM 构建 KG,以及使用 KG 训练 LLM。

1. 使用世界银行 API 下载一些关于世界银行文档的元数据

首先我们需要一些世界银行文档的元数据。有关世界银行文档和报告 API 的完整文档,请访问此处。

我选择世界银行文档元数据作为我们知识图谱的基础有几个原因。首先,世界银行提供了一个 API,可以访问他们的数据。此外,与世界银行文档相关的元数据是全面的,提供了有价值的信息。最后,我对这种数据类型的熟悉程度确保了对其结构和属性的更好理解。值得注意的是,构建知识图谱的灵活性适用于任何数据源,只要具备足够的领域知识以有效地构建它。

以下代码提供了最近 20 份标题中包含“可持续发展”一词的报告的元数据。

import requests
import json
import pandas as pd

url = 'https://search.worldbank.org/api/v2/wds'
params = {
    'format': 'json',
    'display_title': '"sustainable development"',
    'rows': 20,
    'page': 1
}

metadata_list = []

for i in range(1):
    response = requests.get(url, params=params)
    data = json.loads(response.content)
    for doc_id in data['documents']:
        metadata = data['documents'][doc_id]
        metadata_list.append(metadata)

    params['page'] += 1

df = pd.DataFrame(metadata_list)

现在我们已经将元数据保存为 pandas 数据框(df)。

2. 使用文档的元数据构建本体

现在我们可以建立我们的本体。那么,本体到底是什么?

“本体定义了一个共同的词汇,以便需要在某一领域共享信息的研究人员使用。它包括该领域基本概念及其相互关系的机器可解释定义,” (Noy 和 McGuinness, 2001).

通过采用本体,我们能够连接不同的数据源。在本教程中,我们的主要关注点是世界银行数据,然而,如果我们要整合联合国数据,我们将面临诸如文档类型类别、日期格式和国家名称的差异等挑战。本体作为弥合这些差异和建立数据集之间连接的有价值工具。此外,我们可以通过整合来自 Wikidata 的数据来扩展我们的知识图谱,Wikidata 是一个广泛使用的公共知识图谱,与 Wikipedia 密切相关。将我们的数据本体与 Wikidata 的本体对齐,可以实现信息的无缝集成。

以下代码设置了我们的图谱

from rdflib import Graph, RDF, RDFS, Namespace, URIRef, Literal
from rdflib.namespace import SKOS, XSD
from SPARQLWrapper import SPARQLWrapper, JSON
from tqdm import tqdm

# Create a new RDF graph
g = Graph()

schema = Namespace('http://schema.org/')
wd = Namespace('http://www.wikidata.org/entity/')

# Define namespaces
prefixes = {
    'schema': schema,
    'wd': wd,
    'skos': SKOS,
    'xsd': XSD
}
for p, ns in prefixes.items():
    g.bind(p, ns)

元数据中有一列叫做“count”,表示给定文档的原产国。我们希望利用这一列在我们的本体中创建一个“country”类,并为这一列中的每个独特国家创建子类。

def create_subclass_country(column):
    newClass = URIRef(schema + "country")
    g.add((newClass, RDFS.label, Literal("country", lang='en')))
    df[column] = df[column].astype(str)
    for value in df[column].unique():
        if value != "nan":
            # Check Wikidata for a matching class
            sparql = SPARQLWrapper("https://query.wikidata.org/sparql")
            query = f"""
                SELECT ?class ?label WHERE {{
                    ?class wdt:P31 wd:Q6256 .
                    ?class rdfs:label "{value}"@en .
                    OPTIONAL {{ ?class skos:prefLabel ?label FILTER(lang(?label) = "en") }}
                    FILTER(REGEX(STR(?class), "^http://www.wikidata.org/entity/Q[0-9]+$"))
                }}
            """
            sparql.setQuery(query)
            sparql.setReturnFormat(JSON)
            results = sparql.query().convert()

            # If there is a match, use the Wikidata class as a subclass
            if results['results']['bindings']:

                #Get URI from Wikidata
                uri = results['results']['bindings'][0]['class']['value']

                #Get the 'Q ID' which is the unique ID at the end of the URI
                qid = uri.split('/')[-1]
                country_label = value

                #Create a subclass for each country under the country class
                subclass = URIRef(schema + country_label.replace(' ', '_'))
                g.add((subclass, RDF.type, RDFS.Class))
                g.add((subclass, RDFS.subClassOf, newClass))

                # Update the "country_URI" column with the URI for the current country
                df.loc[df[column] == value, "country_URI"] = uri
                uri = URIRef(uri)

                # Define the URI for the new Wikidata URI property
                wd_URI_property = URIRef(schema + "wd_URI")

                # Add the property to the RDF graph
                g.add((wd_URI_property, RDF.type, RDF.Property))

                # Add a label to the property
                label = Literal("Wikidata URI", lang="en")
                g.add((wd_URI_property, RDFS.label, label))

                #Add Wikidata URI as a property to each country class
                g.add((subclass, schema.wd_URI, uri))

                #Add label to each Wikidata Q ID code that it is the Q ID for this particular country
                g.add((uri, RDFS.label, Literal(f"{country_label} wikidata code", lang='en')))
                g.add((subclass, RDFS.label, Literal(value, lang='en')))
            else:    
                subclass = URIRef(schema + value.replace(' ', '_').replace('-','_'))
                g.add((subclass, RDF.type, RDFS.Class))
                g.add((subclass, RDFS.subClassOf, newClass))
                g.add((subclass, RDFS.label, Literal(value, lang='en')))

我使用 protégé,一个免费的开源本体编辑器,来查看和有时手动调整我的本体。如果你想在创建本体时查看它,只需将图谱保存为 ttl 文件,并用 Protégé 打开。

#Save graph as ttl file for use in protégé
g.serialize('worldBankKG.ttl',format='turtle',prefixes = prefixes, encoding='urf-8')

一旦你在 Protégé 中打开文件,它应该类似于这样:

从 Protégé 截图。图片由作者提供。

在世界银行数据的“count”列中,每个独特的国家将会在我们的本体下的“country”类中创建一个独特的子类。你可以看到每个国家还具有一个标签和一个 Wikidata URI。在这种情况下,阿根廷的 Wikidata URI 是:www.wikidata.org/entity/Q414

这也是阿根廷国家的 Wikidata 页面的链接。

现在我们需要为其他所有内容创建类和子类。我为文档类型、世界银行项目、世界银行信托基金、国家和作者创建了类。我没有把所有代码放在这个教程中,但请参见我的笔记本以获取创建所有这些类的代码。

3. 用文档实例填充本体

本体是知识图谱的骨干,但现在我们需要用数据填充它。目前,本体定义了类、子类、类和子类的属性以及它们之间的关系。

例如,我们现在有一个世界银行文档的类和一个工作文件的子类。我们的本体确定了工作文件是世界银行文档的子类(或一种类型)。我们的本体为这些实体确定了标签和其他属性。例如,工作文件是 Wikidata 中的一个实体,因此我们将 Wikidata URI 作为属性包含在本体中。

对本体中几个实体的概念化。图像由作者提供。

然而,在上面的示例中,并没有实际的世界银行工作文件,即没有工作文件的实例。我们已经建立了类,但尚未建立实例。

这是逐行遍历我们的 DataFrame 的代码,并为每一行创建一个文档实例,并分配适当的属性。请注意,要运行这段代码,你需要先创建所有这些类(再一次,请参见笔记本以获取完整代码)。

#Create abstract property
df['abstracts'] = df['abstracts'].astype(str).str.replace('\n', '').replace('\\n','')
abstractIs_uri = URIRef(schema + "abstractIs")
g.add((abstractIs_uri, RDF.type, RDF.Property))
g.add((abstractIs_uri, RDFS.label, Literal("Short summary of the document.")))

#Create abstract class
abstract_class = URIRef(schema + "abstract")
g.add((abstract_class, RDFS.label, Literal("Short summary of a document.")))

#Create author properties
authoredBy_uri = URIRef(schema + "authoredBy")
authored_uri = URIRef(schema + "authored")
g.add((authoredBy_uri, RDF.type, RDF.Property))
g.add((authored_uri, RDF.type, RDF.Property))
g.add((authoredBy_uri, RDFS.label, Literal("This document was authored by this author.")))
g.add((authored_uri, RDFS.label, Literal("This author wrote this document.")))

#Define 'part of' property
isPartOf_uri = URIRef(schema + "isPartOf")
g.add((isPartOf_uri, RDF.type, RDF.Property))
g.add((isPartOf_uri, RDFS.label, Literal("This entity is a part of another entity")))

#Define 'countryOfOrigin' property
countryOfOrigin_uri = URIRef(schema + "countryOfOrigin")
g.add((countryOfOrigin_uri, RDF.type, RDF.Property))
g.add((countryOfOrigin_uri, RDFS.label, Literal("Country that this document is about.")))

# Create instances for each document and add author property
for index, row in tqdm(df.iterrows()):
    if not pd.isnull(row['id']) and not pd.isnull(row['docty']) and not pd.isnull(row['authors']):
        try:
            # Create the report instance
            instance = URIRef(schema + "doc/" + str(row['display_title']).replace(" ","_").replace("-","_"))
            g.add((instance, RDFS.label, Literal(str(row['display_title']), lang='en')))

            #Connect instances with types of documents
            doctype = URIRef(row['docty'])
            g.add((instance, RDF.type, doctype))

            #Connect instances with country of origin
            if row['count'] != "nan":
                country = URIRef(schema + str(row['count']).replace(" ","_").replace("-","_"))
                g.add((instance, countryOfOrigin_uri, country))

            #Connect instances with projects
            if row['projn'] != "nan":
                project = URIRef(schema + "project/" + str(row['projn']).replace(" ","_").replace("-","_"))
                g.add((instance, isPartOf_uri, project))

            #Connect instances with trustfund_keys
            if row['trustfund'] != "nan":
                tf_values = row['trustfund'].split(",")
                for tf in tf_values:
                    trustfund_uri = URIRef(schema + "trustfund/" + str(tf).replace(" ","_").replace("-","_"))
                    g.add((trustfund_uri, RDFS.label, Literal(f"Trustfund: {tf}")))
                    g.add((instance, isPartOf_uri, trustfund_uri))
                    g.add((trustfund_uri, countryOfOrigin_uri, country))

            #Connect instances with authors
            author_dict = ast.literal_eval(row['authors'])
            for author_dict_entries in author_dict.values():
                author_name = author_dict_entries['author']
                author_uri = URIRef(schema + "author/" + author_name.replace(" ", "_"))
                g.add((instance, authoredBy_uri, author_uri))
                g.add((author_uri, authored_uri, instance))

            #Add abstract
            if row['abstracts'] != "nan":
                abstract_uri = URIRef(schema + "abstract/" + str(row['display_title']).replace(" ","_").replace("-","_"))
                g.add((instance, abstractIs_uri, abstract_uri))
                g.add((abstract_uri, RDFS.label, Literal(str(row['abstracts']))))
                g.add((abstract_uri, RDF.type, abstract_class))
                g.add((abstract_uri, isPartOf_uri, abstract_uri))
        except:
            pass

现在我们有了一个实际的知识图谱。可以用以下方式可视化一个工作文件实例及其相关属性:

对填充了一些数据实例的本体的概念化。图像由作者提供。

你可以保存这个新文件,并在 protégé中打开,以确保所有实体都已被适当地纳入。

4. 使用 Wikidata 将额外的实体和关系引入图谱

因为我们知识图谱中的许多实体都与 Wikidata 的 URI 关联,我们可以从 Wikidata 导入额外的数据到我们的知识图谱中。在这个演示中,我只对国家做了这些操作。在以下代码中,我们遍历国家列,查询 Wikidata 中的国家实体,并导入给定国家的所有属性及其值。请注意,这段代码可能需要一段时间来运行——它必须查询 Wikidata 中每个国家的所有属性及其值,并将它们全部放入我们的图谱中。

import numpy as np

sparql = SPARQLWrapper("https://query.wikidata.org/sparql")

# Create a cache to store property code-label mappings
property_cache = {}
entity_cache = {}

# Prepare a list to collect triples for bulk graph update
triples = []

# Iterate over the URIs and add the properties to the RDF graph
for uri in tqdm(df['country_URI']):        
    if isinstance(uri, str) and uri.startswith('http://www.wikidata.org/entity/Q'):
        class_uri = URIRef(uri)
        country_column = df.loc[df['country_URI'] == uri, 'count'].iloc[0]
        country_column = URIRef(schema + str(country_column).replace(" ", "_"))

        # Construct the SPARQL query
        qid = uri.split('/')[-1]
        query = f"""
        SELECT ?prop ?value WHERE {{
          wd:{qid} ?prop ?value .
          OPTIONAL {{ ?prop rdfs:label ?label . FILTER(lang(?label) = 'en') }}
        }}
        """

        # Set the query and response format
        sparql.setQuery(query)
        sparql.setReturnFormat(JSON)

        # Execute the query and retrieve the results
        results = sparql.query().convert()

        # Iterate over the results and add them to the RDF graph
        for result in results["results"]["bindings"]:
            prop = result["prop"]["value"]
            value = Literal(result["value"]["value"])
            triple = (country_column, None, None)  # Placeholder for triple

            if prop.startswith('http://www.wikidata.org/prop'):
                property_code = prop.split('/')[-1]
                # Check if the property code is already in the cache
                if property_code in property_cache:
                    property_label = property_cache[property_code]
                else:
                    # If not in cache, query and retrieve the property label
                    property_label = get_property_label(property_code)
                    # Store the property code-label mapping in the cache
                    property_cache[property_code] = property_label

                property_label_URI = URIRef(schema + property_label.replace(" ", "_"))
                triple = (country_column, property_label_URI, value)

            if value.startswith('http://www.wikidata.org/entity/Q'):
                entity_code = value.split('/')[-1]
                # Check if the entity code is already in the cache
                if entity_code in entity_cache:
                    entity_label = entity_cache[entity_code]
                else:
                    # If not in cache, query and retrieve the entity label
                    entity_label = get_entity_label(entity_code)
                    # Store the entity code-label mapping in the cache
                    entity_cache[entity_code] = entity_label

                entity_label_URI = URIRef(schema + str(entity_label).replace(" ", "_"))
                triple = (country_column, property_label_URI, entity_label_URI)

            triples.append(triple)

    elif isinstance(uri, float) and np.isnan(uri):
        continue
    else:
        continue

# Add all collected triples to the RDF graph in bulk
for subject, predicate, object_ in triples:
    if predicate is not None:
        g.add((subject, predicate, object_)) 

现在我们有了一个用世界银行数据和来自 Wikidata 的额外数据填充的本体!如果你在 protégé中打开你的知识图谱,你可以探索我们导入的所有属性。下面是我们为阿根廷导入的一些属性的屏幕截图。这些数据也可以直接在 Wikidata 上查看,这里

从 Protégé截取的屏幕截图。作者提供的图片。

Wikidata 有如下数据:政府形式、国家元首、外交关系、预期寿命(以及许多其他发展指标)随时间变化、子区域/领土等。

5. 直接使用 SPARQL 查询知识图谱

我们可以使用SPARQL查询直接查询这个知识图谱(我们创建的 RDF 数据库的标准查询语言)。我们可以使用 Python 中的 SPARQL 封装器。

例如,如果想查看所有关于巴西的文档,我们可以运行以下代码:

# Step 1: Find the URI of Brazil in your ontology
brazil_uri = "<http://schema.org/Brazil>"  # Replace with the actual URI

# Step 2: Find the most relevant documents related to Brazil
documents_query = f"""
PREFIX schema: <http://schema.org/>
SELECT ?document
WHERE {{
  ?document a/rdfs:subClassOf* schema:world_bank_document ;
      schema:countryOfOrigin {brazil_uri} .

}}
"""
qres = g.query(documents_query)
for row in qres:
    print(f"Document ID: {row.document}")

这将输出我们知识图谱中所有将‘巴西’列为国家来源的世界银行文档。

我们可以将 SPARQL 查询做得尽可能复杂,并包含来自世界银行元数据或我们导入的 Wikidata 的任何属性。例如,如果我们想知道哪位作者写了最多关于联邦共和国国家的文档呢?

from rdflib import Graph, RDF, RDFS, URIRef

# Step 1: Find the URI of the basic_form_of_government you are interested in
government_form_uri = "<http://schema.org/federal_republic>"  # Replace with the actual URI

# Step 2: Query for authors who have written the most documents associated with countries having the basic_form_of_government as "federal_republic"
authors_query = f"""
PREFIX schema: <http://schema.org/>
PREFIX prop: <http://schema.org/property>
SELECT ?author (COUNT(?document) AS ?numDocuments)
WHERE {{
  ?document a/rdfs:subClassOf* schema:world_bank_document ;
            schema:countryOfOrigin [
                    schema:basic_form_of_government {government_form_uri}
            ] ;
            schema:authoredBy ?author .
}}
GROUP BY ?author
ORDER BY DESC(?numDocuments)
"""

# Execute the query
results = g.query(authors_query)

# Now you can process the results and present them as needed (e.g., using pandas DataFrames)
# For simplicity, here, I'm just printing the author names and the number of documents they wrote
for row in results:
    print(f"Author: {row.author}, Number of Documents: {row.numDocuments}")

输出将类似于下面这样:

我们可以看到“世界银行”作者已撰写了五篇关于 Wikidata 列出的联邦共和国国家的文档。在这种情况下,这些国家是巴西、科摩罗和埃塞俄比亚。这很重要,因为世界银行数据并未告诉我们任何国家的政府形式,这些信息来自 Wikidata。由于我们构建了与 Wikidata 对齐的本体,我们可以轻松地从 Wikidata 中纳入额外数据。

6. 比较与数据交互的不同方式

我们知道可以使用 SPARQL 查询知识图谱并获得准确结果。但这要求我们编写 SPARQL 查询,这需要一些技术能力。我们能否将这种准确性与 LLM 的易用性相结合,从而直接与数据进行“对话”?LlamaIndex是一个强大的工具,允许你使用自己的数据(他们称之为上下文信息)来增强 LLM。这里是一个 LlamaIndex 入门教程。我将使用 LlamaIndex 以两种方式将世界银行数据纳入 LLM:使用我们直接从世界银行 API 获得的原始 CSV 文件,以及使用我们用世界银行数据构建和填充的知识图谱。

使用我们直接从世界银行 API 获得的原始 CSV 文件

作为起点,让我们将来自世界银行的原始元数据加载到 LlamaIndex 中。这将作为一个基准,用来比较在整合知识图谱后结果的变化。来自世界银行的原始数据是 CSV 格式的,并且没有任何关联的本体。以下是设置 LlamaIndex 所需的所有代码。你需要一个 OpenAI API 密钥,可以从 OpenAI 网站获取。此代码直接从本地数据文件夹读取数据,我将我的文件夹命名为‘data’。你只需将 CSV 文件放入此文件夹中,LlamaIndex 将对其进行索引。

from llama_index import GPTVectorStoreIndex, SimpleDirectoryReader, ServiceContext, LLMPredictor
from langchain import OpenAI
import os 
import openai

os.environ["OPENAI_API_KEY"] = <YOUR API KEY>  # replace with yours
openai.api_key = os.environ["OPENAI_API_KEY"]

documents = SimpleDirectoryReader('data').load_data()

query_engine = index.as_query_engine()

现在我们可以用基本的英语提问,就像我们对待 ChatGPT 一样。

response = query_engine.query("Show me all of the World Bank documents in the context information about Brazil")
print(response)

这是响应:

Brazil — LATIN AMERICA AND CARIBBEAN — P126452 — Rio Grande do Norte: Regional Development and Governance — Audited Financial Statement Brazil — LATIN AMERICA AND CARIBBEAN- P158000- Amazon Sustainable Landscapes Project — Procurement Plan

这些文档实际上是关于巴西的世界银行文档,包含在上下文信息中。然而,还有许多其他文档未列出。

现在让我们尝试获取 Anna Corsi 编写的所有文档。

response = query_engine.query("Based on the context information, what documents has Corsi,Anna written?")
print(response)

响应是:

Corsi, Anna 没有根据上下文信息编写任何文档。

这不正确。Anna Corsi 是数据中的一位作者。

让我们再试一次:

response = query_engine.query("Tell me more about Anna Corsi")
print(response)

响应是:

Anna Corsi 在给定的上下文信息中没有提及

再次,这不正确。Anna Corsi 是我们数据中的世界银行作者之一。

使用知识图谱作为输入

现在大问题是——使用知识图谱作为我们的上下文信息是否改善了这些结果?我们将不再以相同的方式将 RDF 数据加载到 LlamaIndex 中,而是使用 RDFReader。我发现这种将数据加载到 LlamaIndex 中的方法有些问题,所需时间较长,但它是将 RDF 数据纳入输入提示的推荐方式。RDFReader 要求知识图谱中的每个实体都必须有一个标签。因此,即使你在实体上添加了评论,也需要为评论添加一个标签。

from llama_index import GPTVectorStoreIndex, download_loader

RDFReader = download_loader("RDFReader")
document = RDFReader().load_data(file="path to your file")

# Define LLM
llm_predictor = LLMPredictor(llm=OpenAI(temperature=0, model_name="text-davinci-002"))

# NOTE: set a chunk size limit to < 1024 tokens 
service_context = ServiceContext.from_defaults(llm_predictor=llm_predictor, chunk_size_limit=1012)

index = GPTVectorStoreIndex.from_documents(document, service_context=service_context)

注意: 我仍在使用 GPTVectorStoreIndex 处理 RDF 数据,而不是 KnowledgeGraphIndex。我发现 KnowledgeGraphIndex 没有产生更好的结果,但它花费了更多的时间和成本。 其他人 似乎也有类似的经历。我的理解是 RDFReader 用于使用预制的知识图谱,而 KGIndex 用于使用 LlamaIndex 创建知识图谱。

一旦初始化,我们可以向这个 LLM 提出相同的问题,看看答案是否有所改进。对于第一个提示,“展示上下文信息中所有关于巴西的世界银行文档”,响应是:

<Brazil - LATIN AMERICA AND CARIBBEAN - P107146 - Acre Social and Economic Inclusion and Sustainable Development Project - PROACRE - Audited Financial Statement>
<Disclosable Version of the ISR - Rio de Janeiro Adjustment and Sustainable Development Policy Loan - P178729 - Sequence No : 01>
<Grosso Fiscal Adjustment and Environmental Sustainability Development Policy Loan>
<Disclosable Version of the ISR - Matanza-Riachuelo Basin Sustainable Development Project - P105680 - Sequence No : 29>
<Disclosable Version of the ISR - Matanza-Riachuelo Basin Sustainable Development Project - P105680 - Sequence No : 30>
<Disclosable Restructuring Paper - Health Sustainable Development Goals Program-for-Results - P123531>

这部分是正确的。这些实体中有两个是关于巴西的文档,但有两个是关于阿根廷的,还有一个是关于埃塞俄比亚的。还有一些关于巴西的额外文档未在此列出。

现在让我们再问一个具体的作者:

response = query_engine.query("Based on the context information, what documents has Corsi,Anna written?")
print(response)

回复是:

Concept Project Information Document (PID) - Land administration infrastructure for green and sustainable development - P179217

这部分是正确的。安娜·科尔西确实写了这份文档,但她还在背景信息中写了另外三份未在此列出的文档。

response = query_engine.query("Tell me more about Anna Corsi")
print(response)

回复是:

‘安娜·科尔西是文档“概念项目信息文档(PID)——绿色和可持续发展的土地管理基础设施——P179217”的作者。该文档涉及一个支持土耳其大规模评估系统发展的项目,并为单个财产单位生成市场价值。科尔西还是“现在就是时候:乌兹别克斯坦如何利用城市化推动可持续发展?”的作者,该文档讨论了乌兹别克斯坦如何利用城市化促进可持续发展。’

这部分是正确的。关于土耳其土地管理项目的第一部分是正确的。然而,安娜·科尔西并不是乌兹别克斯坦报告的作者。

语言模型明显在处理特定结构化数据问题时有困难。不过,正如我们开始时所说,语言模型擅长理解和生成类似人类的文本,捕捉复杂的语言模式,并产生上下文相关的回答。以下是如何利用语言模型的优势来处理这一数据集的示例。假设我们已经知道世界银行在土耳其有一个土地管理基础设施项目。我们可以要求语言模型用以下提示描述该项目:

response = query_engine.query("Tell me more about World Bank's land management infrastructure project in Turkey")
print(response)

回复是:

绿色和可持续发展土地管理基础设施项目的目标是提高土耳其土地管理信息的准确性和可及性。该项目有三个组成部分,第一个组成部分是创建 3D 城市模型和更新土地登记数据。该部分将支持:(i)基于在阿马斯亚试点中测试的成熟方法创建 3D 城市模型;(ii)完成对 600 万块地块的更新和验证(剩余的 1100 万块地块中有 19 块未包含在 LRCMP 中),包括城市和农村地区。作为土地登记数据更新活动的一部分,将与 TKGM 讨论如何更好地解决女性土地权利和所有权方面的挑战,以便在测量过程中作为公众咨询步骤的一部分进行。尽管土地登记数据的更新和验证活动将在城市和农村地区进行,但在创建 3D 城市模型时,项目将覆盖土耳其所有 81 个省份的主要城市区域(约 40,000 平方公里,几乎覆盖全国所有城市区域)。作为这些活动的一部分,项目将资助完成和更新

这是我们数据库中文档的准确摘要。这些信息主要来自数据中的‘abstract’字段。

结论

总之,通过上下文学习将结构化数据直接纳入 LLMs 会产生次优结果,且偶尔出现幻觉和缺乏透明度等挑战仍然存在。虽然将结构化数据转换为知识图谱在一定程度上改善了 LLM 的表现,但像 SPARQL 这样的正式查询语言在特定查询中比 LLMs 更具优势。然而,LLMs 可以协助制定 SPARQL 查询,为用户提供无需编写正式查询的方式与结构化数据进行‘聊天’。

LLMs 在理解和解释非结构化数据方面展现了其强大能力,使其能够提供相关的响应。这种能力甚至扩展到了结构化数据,只要其中嵌入了非结构化信息。例如,如果结构化数据包含一个标记为‘abstracts’的列,其中包含非结构化文本,则 LLM 可以利用这些数据生成有洞察力的结果。

变革性协同作用如将 LLMs 用于将提示翻译成正式查询,或利用知识图谱对 LLMs 进行验证,为该领域的探索和进步提供了潜在途径。

HashGNN: 深入探讨 Neo4j GDS 的新节点嵌入算法

原文:towardsdatascience.com/hashgnn-deep-dive-into-neo4j-gdss-new-node-embedding-algorithm-5ce9c3029a5c?source=collection_archive---------7-----------------------#2023-08-10

在这篇文章中,我们将通过一个小示例来探讨 HashGNN 如何将图节点哈希到嵌入空间。

Philipp BrunenbergTowards Data Science Philipp Brunenberg

·

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

--

如果你更喜欢观看视频,你可以点击这里

HashGG(#GNN)是一种节点嵌入技术,它采用了消息传递神经网络(MPNN)的概念来捕捉高阶邻近性和节点属性。通过利用一种称为 MinHashing 的近似技术,它显著加快了计算速度,相比于传统神经网络。因此,它是一种基于哈希的方法,在效率和准确性之间引入了权衡。本文将深入理解这些内容,并通过一个小示例探索算法的工作原理。

节点嵌入:具有相似上下文的节点在嵌入空间中应当接近

许多图机器学习用例,如链接预测和节点分类,都需要计算节点的相似度。在图的背景下,当这些相似度捕捉到(i)邻域(即图结构)和(ii)待嵌入节点的属性时,才最具表现力。节点嵌入算法将节点投射到低维嵌入空间中——即它们为每个节点分配一个数值向量。这些向量——即嵌入——可以用于进一步的数值预测分析(例如,机器学习算法)。嵌入算法优化的指标是:具有相似图上下文(邻域)和/或属性的节点应当在嵌入空间中被映射得接近。图嵌入算法通常采用两个基本步骤:(i)定义一种机制来采样节点的上下文(node2vec 中的随机游走,FastRP 中的 k-fold 转移矩阵),和(ii)随后在保留成对距离的同时减少维度(node2vec 中的 SGD,FastRP 中的随机投影)。

HashGNN:绕过训练神经网络

关于 HashGNN 的线索是,它不要求我们基于损失函数训练神经网络,就像我们在传统的消息传递神经网络中需要做的那样。由于节点嵌入算法优化的是“相似的节点应该在嵌入空间中接近”,损失的评估涉及计算节点对的真实相似度。这被用作训练的反馈,来评估预测的准确性,并相应地调整权重。通常,余弦相似度被用作相似度度量。

HashGNN 规避了模型训练,实际上完全不使用神经网络。它不训练权重矩阵或定义损失函数,而是使用随机哈希方案,将节点向量哈希到具有相似度的相同签名,这意味着我们可以嵌入节点,而不需要直接比较节点(即无需计算余弦相似度)。这种哈希技术称为 MinHashing,最初定义为在不比较集合的情况下近似两个集合的相似度。由于集合被编码为二进制向量,HashGNN 需要二进制节点表示。为了理解如何将其用于嵌入一般图的节点,需要几种技术。让我们来看一看。

MinHashing

首先,让我们讨论一下 MinHashing。MinHashing 是一种局部敏感哈希技术,用于近似两个集合的 Jaccard 相似度。Jaccard 相似度通过将交集大小除以两个集合中存在的唯一元素数量(并集)来度量两个集合的重叠(交集)。它定义在编码为二进制向量的集合上:宇宙中的每个元素(所有元素的集合)都分配一个唯一的行索引。如果特定集合包含一个元素,则在集合向量的相应行中表示为值 1。MinHashing 算法独立地哈希每个集合的二进制向量,并使用K个哈希函数生成K维签名。MinHashing 的直观解释是随机选择非零元素K次,选择哈希值最小的那个。这将产生输入集合的签名向量。有趣的是,如果我们对两个集合进行这种操作而不进行比较,它们将以其 Jaccard 相似度的概率哈希到相同的签名(如果K足够大)。换句话说:概率趋近于 Jaccard 相似度。

Jaccard 相似度度量两个集合的相似度。通常,集合也可以编码为二进制向量。图像来源:作者。

在插图中:示例集合s1s2被表示为二进制向量。我们可以通过比较这两个向量并计算两个向量都为 1 的行数,轻松计算 Jaccard 相似度。这些操作相当简单,但复杂性在于当我们有多个向量时,向量之间的成对比较。

MinHashing 算法生成集合特征的 k 个排列,并选择具有最小哈希值的特征以创建 minHash 签名向量。图像来源:作者。

我们的宇宙 U 大小为 6,我们选择 K(哈希函数的数量)为 3。我们可以通过使用简单的公式和 abc 的边界来轻松生成新的哈希函数。现在,我们实际上做的是使用每个哈希函数对我们向量的索引(1-6)进行哈希,每个索引与我们宇宙中的一个单一元素相关联。这将为我们提供 3 个随机排列的索引,从而得到我们宇宙中的元素。随后,我们可以将我们的集合向量 s1s2 用作我们排列特征的掩码。对于每个排列和集合向量,我们选择集合中最小哈希值的索引。这将生成两个 3 维向量,每个集合一个,这就是集合的 MinHash 签名。

MinHashing 仅仅从输入集选择随机特征,我们只需使用哈希函数来在所有输入集上均等地再现这种随机性。我们必须在所有向量上使用相同的哈希函数,以便两个输入集的签名值在两个集合都包含所选元素时发生冲突。签名值将以集合的 Jaccard 相似度的概率发生冲突。

WLKNN(Weisfeiler-Lehman 核心神经网络)

HashGNN 使用如 Weisfeiler-Lehman 核心神经网络(WLKNN)中定义的消息传递方案来捕捉高阶图结构和节点属性。它定义了之前提到的 HashGNN 上下文采样策略。WLK 在 T 次迭代中运行。在每次迭代中,它通过将节点的当前向量与所有直接连接邻居的向量组合,为每个节点生成一个新的节点向量。因此,它被认为是沿边将消息(节点向量)传递给相邻节点。

WLK 在 T 次迭代中运行。在每次迭代中,它将节点信息沿边传递给相邻节点。图片来源于作者。

T 次迭代之后,每个节点包含 T 跳距离(高阶)的节点信息。迭代 t 中的新节点向量的计算本质上是将所有邻居消息(来自迭代 t-1)聚合为单一邻居消息,然后与前一迭代的节点向量组合。此外,WLKNN 采用三个神经网络(权重矩阵和激活函数);(i)用于聚合邻居向量,(ii)用于节点向量,以及(iii)用于两者的组合。WLK 的一个显著特征是迭代 t 中的计算仅依赖于迭代 t-1 的结果。因此,它可以被视为一个马尔可夫链。

WLK:在每次迭代中,每个节点向量都会更新来自相邻节点的信息。因此,在 t 次迭代之后,每个节点包含来自 t 跳距离节点的信息。图片来源于作者。

HashGNN 示例

让我们探讨一下 HashGNN 如何结合这两种方法高效地将图向量嵌入到嵌入向量中。与 WLKNN 类似,HashGNN 算法在 T 次迭代中运行,通过聚合邻居向量和前一迭代的自身节点向量,为每个节点计算一个新的节点向量。然而,它不是训练三个权重矩阵,而是使用三种哈希方案进行局部敏感哈希。每种哈希方案包含 K 个随机构造的哈希函数,从二进制向量中提取 K 个随机特征。

HashGNN 算法:我们用其二进制特征向量初始化节点向量。图片由作者提供。

在每次迭代中,我们执行以下步骤:

第 1 步:计算节点的签名向量: 使用哈希方案 3 对来自前一迭代的节点向量进行最小哈希(随机选择 K 个特征)。在第一次迭代中,节点用其二进制特征向量初始化(稍后我们将讨论如何对节点进行二值化)。得到的签名(或消息)向量是沿着边传递给所有邻居的。因此,我们必须在每次迭代中首先对所有节点执行此操作。

HashGNN 第 1 步:在每次迭代中,通过使用哈希方案 3 计算每个节点的消息向量。图片由作者提供。

第 2 步:构建邻居向量: 在每个节点中,我们将接收来自所有直接连接邻居的签名向量,并将它们聚合成一个二进制向量。随后,我们使用哈希方案 2 从聚合的邻居向量中选择 K 个随机特征,并将结果称为邻居向量。

HashGNN 第 2 步:我们收集所有邻居的消息向量并将其聚合。图片由作者提供。

第 3 步:将节点向量和邻居向量合并成新的节点向量: 最后,我们使用哈希方案 1 从前一迭代的节点向量中随机选择 K 个特征,并将结果与邻居向量结合。得到的向量是新的节点向量,它是下一次迭代的起点。注意,这与第 1 步不同:在第 1 步中,我们对节点向量应用哈希方案 3 来构建消息/签名向量。

HashGNN 第 3 步:我们将最小哈希的节点向量与聚合的邻居向量结合,以得到该迭代的结果节点向量。图片由作者提供。

从图中我们可以看到,得到的(新)节点向量受自身节点特征(3 和 5)以及其邻居特征(2 和 5)的影响。经过第一次迭代后,节点向量将捕捉到来自距离 1 跳的邻居的信息。然而,当我们将其用作第二次迭代的输入时,它已经受到距离 2 跳特征的影响。

在第一次迭代后,新的节点向量受自身特征和邻近节点特征的影响。图片由作者提供。

Neo4j GDS 泛化

HashGNN 是由 Neo4j GDS(图数据科学库)实现的,并对算法进行了有用的泛化。

GDS 中一个重要的辅助步骤是特征二值化。MinHashing 和因此 HashGNN 需要二进制向量作为输入。Neo4j 提供了一个额外的准备步骤,将实值节点向量转换为二进制特征向量。他们使用了一种称为超平面取整的技术。算法的工作流程如下:

步骤 1:定义节点特征: 定义用于节点特征的(实值)节点属性。这是通过参数featureProperties完成的。我们将其称为节点输入向量f

步骤 2:构建随机二进制分类器: 为每个目标维度定义一个超平面。结果维度的数量由参数dimensions控制。超平面是一个高维平面,只要它位于原点,就可以仅通过其法向量n来描述。n向量垂直于平面的表面,因此描述了其方向。在我们的情况下,n向量需要与节点输入向量具有相同的维度(dim(f) = dim(n))。为了构建一个超平面,我们简单地从高斯分布中抽取dim(f)次。

特征二值化:我们使用超平面取整将实值输入向量构造为二进制特征。我们为每个目标维度使用一个随机高斯分类器。图片由作者提供。

步骤 3:分类节点向量: 计算节点输入向量和每个超平面向量的点积,从而得到超平面和输入向量之间的角度。使用threshold参数,我们可以决定输入向量是高于(1)还是低于(0)超平面,并将相应的值分配给结果的二进制特征向量。这与二元分类中的过程完全一致——唯一的不同是我们不迭代优化超平面,而是使用随机高斯分类器。

使用 n 个超平面会导致 n 维的二进制节点签名。图片由作者提供。

本质上,我们为每个目标维度绘制一个随机高斯分类器,并设置一个阈值参数。然后,我们对每个目标维度的输入向量进行分类,并得到一个d维的二进制向量,该向量将作为 HashGNN 的输入。

结论

HashGNN 使用局部敏感哈希将节点向量嵌入到嵌入空间中。通过使用这种技术,它绕过了计算密集型的神经网络(或其他优化)的迭代训练以及直接的节点比较。论文的作者报告称,与基于学习的算法如 SEAL 和 P-GNN 相比,其运行时间快 2 到 4 个数量级,同时仍能产生高度可比(在某些情况下甚至更好)的准确性。

HashGNN 比基于学习的算法快 2 到 4 个数量级,同时提供了可比的结果。图像来源:arxiv.org/abs/2105.14280.

HashGNN 在 Neo4j GDS(图数据科学库)中实现,因此可以直接在你的 Neo4j 图上使用。在下一篇文章中,我将详细讲解如何使用它以及需要注意的事项。

感谢你的光临,下次见。🚀

参考文献

现代推荐系统中的哈希:入门

原文:towardsdatascience.com/hashing-in-modern-recommender-systems-a-primer-9c6b2cf4497a

理解应用机器学习中最被低估的技巧

Samuel FlenderTowards Data Science Samuel Flender

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

--

(Midjourney)

哈希是工业机器学习应用中最常见的“技巧”之一,但它并没有得到应有的关注。

哈希的最大优势,特别是在现代推荐系统中,是其有限内存保证:如果没有哈希,在不耗尽内存的情况下,学习数十亿视频、新闻文章、照片或网页的相关性将是极其不切实际的。

但我们这里有些超前了。本文是一个入门介绍,让我们回到一切开始的地方:著名的 2009 年“哈希技巧”论文。

开创一切的论文

使用哈希作为处理特征的一种方法,以及“哈希技巧”这一术语,首次在 2009 年由雅虎的研究团队提出,该团队由 Kilian Weinberger 领导,背景是电子邮件垃圾邮件检测。毕竟,电子邮件是一系列单词,每个单词都可以被视为一个特征。作者解释说,通过哈希,我们可以将电子邮件表示为向量(其中每个索引对应一个特定的单词),并在垃圾邮件协同过滤模型中使用这些向量。

例如,“您的处方已准备好”这一短语在 20 维哈希空间中可能等同于

[0,0,0,1,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,1]

以及其他,1 的位置取决于所使用的特定哈希函数。

然而,不同的词可能对不同的人具有不同的相关性。词“处方”对某些用户可能表示垃圾邮件,而对另一些用户则不然。为了考虑这种相关性的差异,作者引入了“个性化”标记,将用户 ID 与单词本身结合起来。对于上述短语,他们不仅哈希标记

"your", "prescription",  "is", "ready",

还包括

"user123_your", "user123_prescription", "user123_is", "user123_ready",

等等。鉴于他们的数据集包含 4000 万唯一单词和 40 万用户,这种个性化标记化导致了总共 16 万亿个可能的特征。

通过将这些个性化特征哈希成 2²²的哈希大小,或大约 420 万(减少了 7 个数量级!),作者实现了 30%的垃圾邮件减少,相比于没有哈希个性化的基线模型,这是哈希在机器学习问题中有用性的首次明确展示之一。

现代推荐系统中的哈希

从 2009 年到今天,虽然许多机器学习技术已经发生了变化(深度神经网络在很大程度上取代了线性协同过滤器),但哈希仍然存在。

现代推荐系统通常是某种变体的双塔神经网络,其中一个塔从用户 ID 学习用户嵌入,另一个塔则从视频 ID 学习视频嵌入(对于视频推荐系统)。在训练时,模型会从历史互动数据中获得用户/视频对,例如带有点击的观看次数作为正样本,没有点击的观看次数作为负样本,从而形成一个共享的用户和视频嵌入空间。然后,我们可以将所有用户和视频的嵌入存储在一组嵌入表中,并在服务时使用这些表进行推荐,例如使用 kNN 算法。

到目前为止,一切还算顺利,但哈希在这里的作用是什么?

好吧,考虑一个存储 1B 视频和 1B 用户的 100 维嵌入的模型:这已经占用了 800GB 的内存。这是一个巨大的内存开销,模型将非常不切实际(如果不是不可能的话)且昂贵。通过哈希,我们可以首先将视频和用户的“词汇表”减少到,比如说,1000 万,从而使内存开销更易于管理,为 8GB。

换句话说,哈希允许我们将现代推荐系统扩展到数十亿用户和数十亿项目。如果没有哈希,推荐系统的规模将被我们能够承受的内存量所根本限制。

朝着无冲突哈希的方向前进

哈希的缺点是哈希冲突的存在,即多个不同的 ID 最终会映射到相同的哈希值,导致嵌入表中多个用户或项目共享相同的嵌入。显然,这种信息的“挤压”会降低推荐的质量。因此,当前推荐系统中最重要的研究问题之一是如何使其无冲突。

一种思路是使用“深度哈希嵌入”(DHE),这是由谷歌大脑的王成康团队于 2021 年提出的。他们使用大量的哈希函数,而不是单个哈希函数,并将所有哈希合并为一个稠密向量,然后输入到深度神经网络中,网络学习 ID 的高阶表示。关键思想是,如果哈希函数的数量足够大,那么哈希冲突在统计上变得不可能。

的确,他们的方法显示出前景:使用 1024 个哈希函数的 DHE,作者在一组公共基准数据集上看到 AUC 提高了 0.25%。不过,这种方法的一个缺点是它不适用于多值特征,只适用于直接的 ID。(例如,所提到的 DHE 不能编码过去 30 天你在 Netflix 上观看的电影列表,这将是一个多值特征。)

另一种有前景的无冲突哈希方法是“布谷鸟哈希”,由丹麦奥胡斯大学的拉斯穆斯·帕赫和弗莱明·罗德勒于 2001 年首次提出。布谷鸟哈希被用于字节跳动的在线推荐系统 Monolith,该系统在他们 2022 年的论文中介绍,由刘卓然领导。

在布谷鸟哈希中,我们不仅维护一个哈希表,而是多个哈希表:在字节跳动的论文中,他们使用了 2 个。当遇到新的 ID 时,默认情况下我们将其哈希到第一个哈希表中。如果第一个哈希表已被另一个 ID 占据,我们将逐出那个 ID(因此算法的名称)并将其重新哈希到第二个哈希表中的一个位置。这个过程会重复直到没有更多逐出,哈希表集合稳定且没有冲突。与常规哈希(有冲突)相比,作者发现他们的无冲突哈希方法在公共基准数据集上提高了 0.5% AUC。

代码:应用机器学习中最被低估的技巧

内存回顾:

  • 哈希可以让我们将现代推荐系统扩展到数十亿用户和数十亿项目,同时提供有限内存保证。

  • 在机器学习应用中使用哈希的想法可以追溯到 2009 年 Yahoo 的一篇论文,该论文展示了其在电子邮件垃圾邮件检测模型中的有效性。

  • 然而,哈希会引入哈希冲突,这可能由于多个用户和项目共享相同的嵌入而降低推荐质量。

  • 因此,最近推荐系统的研究集中在如何使哈希无冲突上。显著的例子有深度哈希嵌入(谷歌大脑)和布谷鸟哈希(字节跳动)。

多年来,哈希一直是应用机器学习文献中最被低估的技巧之一,大部分关注点集中在模型架构、数据选择或特征工程上。

这可能开始发生变化。正如谷歌大脑、字节跳动等最近的论文所示,优化推荐系统以减少哈希碰撞可以显著提升性能。TikTok(由字节跳动拥有)的惊人受欢迎程度,至少部分可以用更好的哈希解释。

请关注这个领域:新的突破确实在即将到来。

[## 不想依赖 Medium 的算法?注册一下。

不想依赖 Medium 的算法?注册一下。通过注册我的电子邮件,确保你不会错过我的下一篇文章…

medium.com](https://medium.com/@samuel.flender/subscribe?source=post_page-----9c6b2cf4497a--------------------------------)

机器是否刚刚实现了在人的语言中进行进化性的飞跃?

原文:towardsdatascience.com/have-machines-just-made-an-evolutionary-leap-to-speak-in-human-language-319237593aa4?source=collection_archive---------15-----------------------#2023-04-17

图片来源:Adobe Stock。

评估我们在实现人与人工智能之间深层次、有意义的沟通的旅程中的位置

Gadi SingerTowards Data Science Gadi Singer

·

关注 发表在 Towards Data Science · 12 min read · 2023 年 4 月 17 日

--

人们与对话型人工智能(AI)系统互动时,清晰的沟通是获得最佳效果的关键因素,这将最有利于提升我们的生活。从更广泛的角度来看,应使用什么语言来控制系统和与机器对话?在这篇博客文章中,我们评估了基于最近技术创新的方法,例如 OpenAI 的 ChatGPT 和 GPT-4,来引导和对话机器的发展,并探讨对话 AI 在掌握自然对话方面的下一步所需的步骤。机器已经从提示工程跃升到“人类语言”,但其他智能方面仍在等待发现。

直到 2022 年之前,让 AI 做出正确响应并发挥其优势需要专业知识,如复杂的提示工程。ChatGPT 的推出是机器对话能力的重大进步,使得即使是高中生也能与高效能 AI 聊天并获得令人印象深刻的结果。这是一个重要的里程碑。然而,我们也需要评估在人与机器沟通的旅程中,我们处于什么位置,以及还需要什么来实现与 AI 的有意义对话。

人与机器的互动有两个主要目标:一是指导机器完成所需任务,二是在任务执行过程中交换信息和指导。第一个目标传统上是通过编程来实现,但现在正在演变为用户对话可以定义新任务,例如请求 AI 创建一个 Python 脚本来完成任务。在任务执行中的交流是通过自然语言处理(NLP)或自然语言理解(NLU)结合机器响应生成来实现的。我们可以假设,人机互动进展的核心特征——如果不是终点——是当人们可以像与老朋友一样与机器沟通,包括所有的自由形式的语法、语义、隐喻和文化方面的内容。在 AI 系统要全面参与这种自然交流时,需要创造什么?

机器已经从提示工程跃升到“人类语言”,但其他智能方面仍在等待发现。

过去的对话 AI:变换器架构重新定义了 NLP 性能

图 1. 机器编程和指令的发展。图片来源:经 Intel Labs 许可。

在计算初期,人类和机器只能通过机器码进行通信,这是一种低级的二进制计算机语言——由 0 和 1 组成的字符串,与人类通信几乎没有任何相似之处。过去一个世纪以来,我们逐渐实现了让与机器的交流更接近人类语言的旅程。如今,我们能够让机器生成一张猫在下棋的图片,这是巨大进步的证明。随着编程语言从低级到高级代码的演进,从汇编语言到 C 语言再到 Python,以及引入像 if-then 语句这样类似人类语言的结构,这种交流逐渐改善。现在的最终步骤是消除输入措辞微调的敏感性,使机器和人类可以以自然的方式互动。人机对话应该允许根据过去的“保存点”进行增量引用以继续对话。

自然语言处理(NLP)处理计算机与人类语言之间的交互,以处理和分析大量自然语言数据,而自然语言理解(NLU)则承担了检测用户意图的困难任务。像 Alexa 和 Google 这样的虚拟助手使用 NLP、NLU 和机器学习(ML)在运行时获取新知识。通过使用预测性智能和分析,AI 可以根据用户偏好个性化对话和响应。虽然虚拟助手像是人们家中的可信朋友,但它们目前仍然受限于基本的命令语言循环。人们已经适应了这一点,通过说出“关键字语言”来获得最佳结果,但他们的对话 AI 在理解自然语言交互方面仍然存在差距。当与虚拟助手出现沟通中断时,人们会使用修复策略,如简化话语、调整信息量的变化、语义和句法上的查询调整以及重复命令。

图 2。自然语言理解中的六个意图层级。图片来源:经由 Intel Labs 许可使用。

在理解意图方面,自然语言理解(NLU)至关重要。NLU 分析文本和语音以确定其含义(见图 2)。使用对人类语言的语义和语用定义的数据模型,NLU 专注于意图和实体识别。2018 年引入的 Transformer 神经网络架构使虚拟助手的自然语言处理(NLP)性能得到了提升。这些类型的网络利用自注意力机制来处理输入数据,从而有效捕捉人类语言中的依赖关系。由 Google AI Language 的研究人员 提出的 BERT 解决了一个模型中的 11 个最常见的 NLP 任务,改进了传统的为每个特定任务使用独立模型的方法。BERT 通过在大规模文本语料库(如维基百科)上训练通用语言理解模型,来预训练语言表示,然后将模型应用于下游 NLP 任务,如问答和语言推理。

除了虚拟助手之外,ChatGPT 的进步与 Transformer 模型在 NLP 性能上的提升是相辅相成的。GPT-3 Transformer 技术于 2021 年引入,但其在流行度和使用上的重大突破是通过 ChatGPT 实现的,其在人类对话界面的创新得以通过 来自人类反馈的强化学习(RLHF) 的应用实现。ChatGPT 使大型语言模型能够处理和理解自然语言输入,并生成尽可能接近人类的输出。

当前:大型语言模型主导对话式人工智能

自 2022 年 11 月由 OpenAI 发布以来,ChatGPT 以其似乎写得很好的语言生成能力和成功通过医学执照及 MBA 考试的表现主导了新闻。ChatGPT 在没有任何训练或强化的情况下通过了 美国医学执照考试(USMLE)的所有三个考试。这导致研究人员得出结论:“大型语言模型可能有潜力协助医学教育,并有可能辅助临床决策。” 一位 宾夕法尼亚大学沃顿商学院的教授 对 ChatGPT 进行了运营管理 MBA 期末考试测试,其成绩为 B 到 B-。ChatGPT 在基于案例研究的基础运营管理和过程分析问题上表现良好,提供了正确的答案和充分的解释。当 ChatGPT 未能将问题与正确的解决方法匹配时,人类专家的提示帮助模型纠正了答案。尽管这些结果令人鼓舞,但 ChatGPT 在达到人类水平的对话方面仍有局限性(我们将在下一节讨论)。

  • 作为一个自回归语言模型,ChatGPT 拥有 1750 亿个参数,其庞大的模型尺寸帮助其在理解用户意图方面表现出色。基于图 2 中的意图水平,ChatGPT 可以通过分析文本提示的目标来处理用户包含开放参数集和灵活结构的实用请求。ChatGPT 能够撰写高度详细的回答和清晰的答案,展示出对医学、业务运营、计算机编程等不同领域的广泛和深刻的知识。GPT-4 也显示出了令人印象深刻的优势,例如添加多模态能力和在高级人类测试中提高得分。据报道,GPT-4 在统一巴尔考试中的得分为 90 分位数(ChatGPT 为 10 分位数),在美国生物奥林匹克竞赛中的得分为 99 分位数(ChatGPT 为 31 分位数)。

ChatGPT 也可以进行机器编程,尽管程度有限。它可以创建多种语言的程序,包括 Python, JavaScript, C++, Java,等等。它还可以 分析代码中的错误和性能问题。然而,目前看来,最好的利用方式似乎是作为程序员和人工智能的联合组合的一部分。

  • 尽管 OpenAI 的模型吸引了很多关注,其他模型也在类似的方向上取得进展,例如 Google Brain 的开源 1.6 万亿参数的 Switch Transformer,该模型在 2021 年首次亮相,以及使用 LaMDA(用于对话应用的语言模型)技术的 Google Bard。Bard 目前仅对测试用户开放,因此其与 ChatGPT 的表现尚不为人知。

  • 尽管大语言模型在与人类进行自然对话方面取得了巨大进展,但仍需要解决关键增长领域。

  • 要达到下一个智能水平和人类级沟通,关键领域需要能力飞跃 —— 知识重组,多技能整合和提供上下文适应。

- 未来:人机对话缺少什么?

  • 在将会话型人工智能推向下一个水平的过程中,仍然存在四个关键元素的缺失,这些元素包括达成自然对话的亲密性和共享目的。要达到这一水平,机器需要理解个体的象征性沟通的含义,并用有意义的、可信赖的定制回应来回应。

1) 生成可信的回应。 AI 系统不应该产生幻觉!认识论问题影响了 AI 构建知识的方式,以及区分已知和未知信息的能力。当机器在不了解的事物上提供答案时,可能会出错,产生有偏见的结果,甚至是幻觉。ChatGPT 在捕捉来源归因和信息出处方面存在困难。它可以生成听起来合理但错误或荒谬的答案。此外,它在处理物理、空间和时间问题以及数学推理方面缺乏事实的正确性和常识。据 OpenAI 称,它在处理“如果我把奶酪放进冰箱,它会融化吗?”这样的问题时表现不佳。在 MBA 期末考试 上测试时,它在六年级水平的数学中出现了令人惊讶的错误,可能导致操作上的重大错误。研究发现,“Chat GPT 在处理更复杂的流程分析问题时能力不足,即使这些问题基于标准模板。这包括具有多种产品和需求变异等随机效应的流程流程。”

2) 对人类符号和特殊习惯的深刻理解。 AI 需要在人类的完整符号世界内工作,包括能够进行抽象、定制和理解部分引用。机器必须能够解释人们言辞中的模棱两可和不完整的句子,以便进行有意义的对话。

图 3. 从 Switchboard 收集 中的一个例子,展示了来自美国各地发言者之间的双向电话对话。图片来源:经过 Intel Labs 的许可。

正如图 3 所示,人类的言语模式经常是难以理解的。AI 是否应该像人类一样说话?实际上,它可以与朋友一样使用松散结构的语言进行交流,包括合理数量的“嗯嗯”、“喜欢”、“不完整或不完整的句子”、“模棱两可”、“语义抽象”、“个人引用” 和常识推断。但这些人类言语的特殊习惯不应该使沟通变得难以理解。

3) 提供定制化响应。 AI 需要具备定制能力并熟悉用户的世界。ChatGPT 经常猜测用户的意图,而不是提出澄清问题。此外,作为一个完全封装的信息模型,ChatGPT 不具备浏览或搜索互联网以提供定制答案的能力。根据 OpenAI 的说法,ChatGPT 的定制答案有限,因为“它对每个标记的权重相等,缺乏对预测中最重要和较不重要内容的概念。通过自我监督目标,任务规格依赖于将期望任务强制变成预测问题,而最终,像虚拟助手这样的有用语言系统可能更应被视为采取目标导向的行动,而不仅仅是进行预测。”此外,拥有多会话的人工与机器对话的上下文,以及用户的心理理论模型会很有帮助。

4) 成为目标驱动。 当人们与伴侣合作时,协调不仅仅基于文本交换,而是基于共同的目标。AI 需要超越上下文答案,变得目标驱动。在不断发展的人工与机器关系中,双方需要成为实现目标、避免或减轻问题,或共享信息的过程的一部分。ChatGPT 和其他 LLM 尚未达到这种交互水平。正如我在之前的博客中探讨的那样,智能机器需要超越输入输出回复和对话作为聊天机器人。

为了达到下一水平的智能和人类级别的沟通,关键领域需要在能力上实现飞跃——知识的重组、技能的整合以及提供上下文适应。

通往对话 AI 下一水平的道路

像 ChatGPT 这样的 LLM 仍在认知技能上存在差距,这些技能是将对话 AI 提升到下一水平所必需的。缺失的能力包括逻辑推理、时间推理、数值推理以及整体上驱动目标的能力和定义子任务以完成更大任务的能力。ChatGPT 和其他 LLM 的知识相关限制可以通过Thrill-K 方法来解决,通过增加检索和持续学习。知识对于 AI 存在于三个地方:

1) 即时知识. 常用的知识和连续功能,可以在参数化内存中的最快最昂贵的层或其他 ML 处理的工作内存中有效近似,ChatGPT 目前使用这种端到端的深度学习系统,但需要扩展以包括其他知识来源,以便作为人类伴侣更有效。

2) 待机知识. 对 AI 系统有价值但不常用的知识,通过需要时从相邻的结构化知识库提取。它需要增强对离散实体的表示能力,或者需要保持广泛而灵活,以适应各种新的使用方式。基于待机知识的行动或结果需要处理和内部解决,使得 AI 能够像人类伴侣一样学习和适应。

3) 检索的外部知识. 来自广阔在线存储库的信息,供 AI 系统外检索时使用。这使得 AI 能够根据人类伴侣的需求定制答案,提供理性分析,并解释信息来源和达到结论的路径。

总结

从机器语言到人类语言的旅程已经从人类输入简单的二进制数字到计算机,发展到在家里使用虚拟助手执行简单任务,再到向像 ChatGPT 这样的 LLM 询问并接收明确答案。尽管 LLM 在近期的创新取得了巨大进展,但要达到下一个水平的对话型 AI,需要知识重组、多重智能和上下文适应,以构建真正的人类伴侣。

参考文献

  1. Mavrina, L., Szczuka, J. M., Strathmann, C., Bohnenkamp, L., Krämer, N. C., & Kopp, S. (2022). “Alexa, You’re Really Stupid”: A Longitudinal Field Study on Communication Breakdowns Between Family Members and a Voice Assistant. 计算机科学前沿, 4. doi.org/10.3389/fcomp.2022.791704

  2. Devlin, J. (2018 年, 10 月 11 日). BERT: 深度双向转换器的预训练用于语言理解. arXiv.org. arxiv.org/abs/1810.04805

  3. Wikipedia 贡献者. (2023 年). 从人类反馈中的强化学习. 维基百科. en.wikipedia.org/wiki/Reinforcement_learning_from_human_feedback

  4. 介绍 ChatGPT. (无日期). openai.com/blog/chatgpt

  5. Kung, T. H., Cheatham, M., Medenilla, A., Sillos, C., De Leon, L., Elepaño, C., Madriaga, M., Aggabao, R., Diaz-Candido, G., Maningo, J., & Tseng, V. (2022)。ChatGPT 在 USMLE 上的表现:利用大型语言模型进行 AI 辅助医学教育的潜力。medRxiv(冷泉港实验室)。 doi.org/10.1101/2022.12.19.22283643

  6. Needleman, E. (2023)。Chat GPT 能获得沃顿 MBA 吗?Christian Terwiesch 新白皮书。Mack 创新管理研究所。 mackinstitute.wharton.upenn.edu/2023/would-chat-gpt3-get-a-wharton-mba-new-white-paper-by-christian-terwiesch/

  7. OpenAI. (2023)。GPT-4 技术报告。arXiv(康奈尔大学)。 doi.org/10.48550/arxiv.2303.08774

  8. Gewirtz, D. (2023 年 4 月 6 日)。如何使用 ChatGPT 编写代码。ZDNET。 www.zdnet.com/article/how-to-use-chatgpt-to-write-code/

  9. ChatGPT 支持多少种语言?完整的 ChatGPT 语言列表。 (n.d.). seo.ai/blog/how-many-languages-does-chatgpt-support

  10. Tung, L. (2023 年 2 月 2 日)。ChatGPT 能编写代码。现在研究人员说它也擅长修复漏洞。ZDNET。 www.zdnet.com/article/chatgpt-can-write-code-now-researchers-say-its-good-at-fixing-bugs-too/

  11. Fedus, W., Zoph, B., & Shazeer, N. (2021)。Switch Transformers:以简单有效的稀疏性扩展到万亿参数模型。arXiv(康奈尔大学)。 doi.org/10.48550/arxiv.2101.03961

  12. Pichai, S. (2023 年 2 月 6 日)。我们 AI 旅程中的一个重要下一步。Google。 blog.google/technology/ai/bard-google-ai-search-updates/

  13. Dickson, B. (2022 年 7 月 31 日)。大型语言模型无法计划,即使它们写出花哨的文章。TNW | Deep-Tech。 thenextweb.com/news/large-language-models-cant-plan

  14. Brown, T., Mann, B. F., Ryder, N., Subbiah, M., Kaplan, J., Dhariwal, P., Neelakantan, A., Shyam, P., Sastry, G., Askell, A., Agarwal, S., Herbert-Voss, A., Krueger, G., Henighan, T., Child, R., Ramesh, A., Ziegler, D. M., Wu, J. C., Winter, C., . . . Amodei, D. (2020)。语言模型是少样本学习者。arXiv(康奈尔大学)。 doi.org/10.48550/arxiv.2005.14165

  15. Singer, G. (2022 年 8 月 17 日). 超越输入输出推理:认知人工智能的四个关键特性。Medium. towardsdatascience.com/beyond-input-output-reasoning-four-key-properties-of-cognitive-ai-3f82cde8cf1e

  16. Singer, G. (2022 年 1 月 6 日). Thrill-K:下一代机器智能的蓝图。Medium. towardsdatascience.com/thrill-k-a-blueprint-for-the-next-generation-of-machine-intelligence-7ddacddfa0fe

在理解量子机器学习时遇到困难吗?

原文:towardsdatascience.com/having-trouble-understanding-quantum-machine-learning-ce8a941d8c70

使用函数式编程实现量子近似优化算法

Frank Zickert | Quantum Machine LearningTowards Data Science Frank Zickert | Quantum Machine Learning

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

--

想要开始量子机器学习吗?请查看动手量子机器学习与 Python

本文将解释量子近似优化算法(QAOA)中最重要的部分。QAOA 是一种机器学习算法,你可以用来解决组合优化问题。

特别之处在于,这个算法迎合了量子计算机的特性——一种承诺在解决问题上提供指数级加速的新型计算机。

尽管量子机器学习(QML)——即使用量子计算来解决机器学习算法——是最有前景的技术之一,它也同样具有挑战性!

因此,本文旨在以一种易于理解的方式解释 QAOA 的基本概念。

量子计算、优化和机器学习都在很大程度上依赖于数学。除非你是数学家,否则这将是一个令人望而生畏的任务。

幸运的是,一些 QML 库,比如 IBM Qiskit,解决了这个问题。它们提供了易于使用的接口,并将所有复杂性隐藏在你之外。

正如我在我之前的帖子中所展示的,它们甚至处理了问题的公式化。

在那篇帖子中,我使用了量子近似优化算法(QAOA)来解决一个组合优化问题——如何应对野火。

图片由作者使用 Stable Diffusion 生成

你唯一需要做的就是指定问题的个体值。

这简直太好了,不是吗?

尽管这些库让你可以使用量子机器学习而不必担心数学、量子力学或其他复杂的东西,但它们同样不会教你太多。

如果你想了解一个算法是如何工作的,你就会回到最初的地方。如果 Qiskit 库如此出色,为什么不查看他们的示例来理解 QAOA 是如何工作的呢?

下图展示了他们示例的一部分。

摘自 Qiskit 文档

我认为没有什么需要添加的,是吗?

也许一点点。

所以,让我给你提供一个替代的解释。一个不需要数学学位的解释。但一个利用函数式编程(在 Python 中)的表达力的解释。

函数式编程的故事讲起来很快。

函数式编程将应用程序分解为一组函数。理想情况下,函数只接收输入并产生输出,并且没有影响给定输入产生的输出的内部状态。

从这个意义上讲,QAOA 算法是一个通过optimize一组params来解决problem的函数。换句话说,我们的目标是找到这些params的最佳值。

为了决定哪些params最佳,我们根据从compute得到的结果来assess这些params,该结果来自使用这些params编码问题(problem_circuit)及其解决方案(ansatz_circuit)的(量子)circuit

这就是 Qiskit 描述中提到的变分算法。它使用一种经典优化算法来向量子计算机发出查询。

这就是代码。

def qaoa(
  problem, optimize, assess, compute,
  to_circuit, problem_circuit, ansatz_circuit):

    return optimize(
        lambda params: assess(
            problem,
            compute(to_circuit(
              problem, params,
              problem_circuit, ansatz_circuit
            ))
        )
    )

相当不错,不是吗?

让我们继续深入到最内层的函数to_circuit

from qiskit import QuantumCircuit

def to_circuit(problem, params, problem_circuit, ansatz_circuit):
    cnt_qubits = problem.size
    qc_qaoa = QuantumCircuit(cnt_qubits)

    # initial_state
    qc_qaoa.h(range(cnt_qubits))

    # append problem circuit
    qc_qaoa.append(problem_circuit(problem, params[0]), range(cnt_qubits))

    # append ansatz circuit
    qc_qaoa.append(ansatz_circuit(problem, params[1]), range(cnt_qubits))

    qc_qaoa.measure_all()
    return qc_qaoa

这个函数接收problemparams。我们使用problem的大小来确定量子电路中量子比特(qubits)的数量。

一个量子比特是量子计算机中的基本计算单位。尽管其内部状态非常复杂,但当你查看它时,它要么是 0,要么是 1——就像普通的比特一样。

我们从对所有量子比特应用 Hadamard 门(h)开始。这将量子比特置于一个状态,其中它们有相等的可能性产生 0 或 1。

然后,我们使用problem_circuitansatz_circuit函数附加两个子电路。这就是 Qiskit 解释中提到的“单位矩阵 U(β,γ)具有特定的形式,并由两个单位矩阵 U(β)和 U(γ)组成……”

第一个函数problem_circuit添加了一个量子电路,表示我们想要解决的问题。

def problem_circuit(problem, gamma):
    qc_p = QuantumCircuit(problem.size)
    for i, j in problem.relations:
        qc_p.rzz(gamma, i, j)
        qc_p.barrier()

    return qc_p

在这种情况下,我们遍历problem中的所有relations。显然,我们期望一个relation由一对整数值(i, j)组成。我们在这些位置的两个量子比特上应用rzz门。rzz门是一个参数化(由参数gamma)的旋转门,绕着两个量子比特系统的 ZZ 轴旋转。

第二个函数ansatz_circuit添加了一个量子电路,表示我们问题的解决方案。

def ansatz_circuit(problem, beta):
    qc_a = QuantumCircuit(problem.size)
    for i in range(problem.size):
        qc_a.rx(beta, i)

    return qc_a

这一次,我们遍历问题的所有部分,并在相应的量子比特上应用rx门。这是一个以参数beta为参数的绕 X 轴的旋转。

from qiskit import Aer, execute

def compute(circuit):
  return execute(
    circuit,
    Aer.get_backend('qasm_simulator'),
    shots=1000
  ).result().get_counts()

本质上,这些电路使用两个params(称为betagamma)来创建一个量子电路,该电路产生 Qiskit 生动描述的特定量子态|𝜓(𝛽,𝛾)⟩。这里,𝜓(“psi”)是量子态的占位符。𝛽和𝛾是定义此状态的参数。

这个量子电路创建了一个状态,可能产生任何值,无论好坏。当然,我们希望生成有意义的结果。因此,我们需要一个“好坏”度量。这就是assess函数的目的。

def assess(problem, result):
    avg = 0
    sum_count = 0
    for solution, count in result.items():
        performance = 0
        for i, j in problem.relations:
            if solution[i] != solution[j]:
                performance -= 1

        avg += performance * count
        sum_count += count
    return avg/sum_count

根据我们的problem,我们计算从执行量子电路得到的结果的performance。我们查看problem定义中的relations,并在量子比特表示的relation不相等时(solution[i] != solution[j])降低(注意,这里较低为更好)性能。记住,量子比特的结果是 0 或 1。所以,solution[i]solution[j]要么是 0,要么是 1。

现在,通过创建电路并评估其结果,我们可以输入一个经典优化算法。这个算法重复评估不同的值及其结果,并基于此向产生更好结果的值移动。

from scipy.optimize import minimize

def optimize(f_params_to_problem):
    return minimize(
        # callable function
        f_params_to_problem,

        # initial guess on beta and gamma
        [1.0, 1.0],

        # optimization method
        method='COBYLA')

那么,让我们看看问题的结构。我们使用了它的两个特征:sizerelations。因此,让我们创建一个class来存储这些数据。

class Problem():
    def __init__(self, nodes, relations):
        self._nodes = nodes
        self._relations = relations

    @property
    def size(self) -> int:
        return len(self._nodes)

    @property
    def relations(self) -> int:
        return self._relations

最后,我们需要制定我们问题的实例并将其输入到qaoa算法中。

problem = Problem([0, 1, 2], [(0, 1), (1, 2)])
result = qaoa(
  problem, optimize, assess, compute,
  to_circuit, problem_circuit, ansatz_circuit
)

我们定义问题由三个节点(0, 1, 2)和几个关系组成。节点0, 11, 2是连接的。以下列表表示输出。

 fun: -1.632
   maxcv: 0.0
 message: 'Optimization terminated successfully.'
    nfev: 32
  status: 1
 success: True
       x: array([1.05618646, 2.28854173])

nfev表示迭代次数。最重要的是,x表示产生最佳结果的params值。

要了解这些值的意义,我们将这些值反馈到电路中并观察结果。

from qiskit.visualization import plot_histogram

plot_histogram(compute(to_circuit(
  problem, result.x, problem_circuit, ansatz_circuit
)))

作者提供的图片

输出显示两个解决方案出现得更频繁:010101。因此,这些表示了指定问题的解决方案。

当我们回顾assess函数时,我们看到如果两个连接的节点具有不同的值,我们将每个relation的值定为-1。此外,我们定义了0, 11, 2为连接的。

因此,最佳解决方案是那些连接节点具有不同值的方案。这些是010101

这个问题被称为 Max-Cut。这是 Qiskit 示例中解决的相同问题,可以被认为是组合优化的“Hello World”。

结论

本文解释了量子近似优化算法(QAOA)的基本部分。虽然这不是第一个也不是唯一的解释,但它不要求你先学习数学。

非数学性的解释有相当多的优点。

  • 这对于我们大多数人来说要容易得多。

  • 这是一门实践课程。我们直接用它解决了一个问题。

  • 你可以看到算法的各个部分如何配合在一起。

想要开始量子机器学习吗?看看用 Python 实践量子机器学习

免费获取前三章 点击这里

全新第 2 卷:组合优化完全讲解了如何使用变分量子本征求解器和量子近似优化算法来解决优化问题。

立即索取您的副本

浓缩咖啡中的 Headspace:优化

原文:towardsdatascience.com/headspace-in-espresso-optimization-4990b24d8073?source=collection_archive---------5-----------------------#2023-04-21

咖啡数据科学

另一个隔离 headspace 的实验

Robert McKeon AloeTowards Data Science Robert McKeon Aloe

·

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

--

Headspace 是指在制作浓缩咖啡时,咖啡床顶部与喷头屏幕之间的空间量。它是与研磨度、剂量和压实压力互动的一个变量。更具体地说,headspace 是指咖啡饼在萃取过程中膨胀的空间,这会影响水和气体在咖啡中的流动。

优化 headspace 的挑战在于它与研磨度、剂量和压实压力相互交织。我通常发现较少的 headspace 是最优的,但我希望通过更好的实验来隔离变量。

进入用过的咖啡

我决定在新鲜咖啡上添加用过的咖啡,这样它在膨胀和水流方面的作用类似于咖啡,但不会干扰萃取。

我在 Decent Espresso 机器上使用了萨拉米测量萃取的射击。萨拉米射击可以用来测量萃取,并更好地理解不同的口味成分何时萃取到杯中。

我也使用了距离烘焙后 3 个月的咖啡,这是我最近一些测试的实验咖啡。目的是使用已经脱气的咖啡,在更理想的环境中专注于 TDS 和 EY 作为数据指标。

总溶解固体(TDS)是使用折射计测量的,结合杯中的输出重量和咖啡的输入重量,用于确定萃取到杯中的咖啡的百分比,称为萃取收率(EY)

小头部空间 vs 大头部空间

我使用的第一个测试集是 Blooming Ramp 轮廓,该轮廓在达到一定压力(4 bar)后开始绽放 30 秒,然后以 2.2 ml/s 注入。然而,这个轮廓对于较大的空间效果不佳,因为在压力建立起来时,目标输出已经达到。

所有图片由作者提供

所以我认为结果并不公平,因为更大的空间(使用了 3 克已用咖啡)是因为基准没有一个花期。

平面轮廓

我重新使用了平坦的 2 ml/s 轮廓进行了实验。这控制了花期,并允许我们只关注头部空间。这表明较小的头部空间更有利于压力的建立。

这些萃取收率的结果更加清晰。

空间是一个易于改变且不需要任何特殊工具的变量。我希望这个小研究能激发人们尝试减少空间,因为数据表明较少的空间是最佳的参数选择。

如果你喜欢,可以在TwitterYouTube,和Instagram关注我,我在那里发布不同机器的浓缩咖啡视频和相关内容。你也可以在LinkedIn找到我。你还可以在Medium上关注我并订阅

我更多的阅读

我的书

我的链接

浓缩咖啡文章集合

工作与学校故事合集

医疗数据本质上是有偏的

原文:towardsdatascience.com/healthcare-is-inherently-biased-b60bf00d4af7

这里是如何避免被它欺骗

Stefany GoradiaTowards Data Science Stefany Goradia

·发表于Towards Data Science ·10 分钟阅读·2023 年 1 月 19 日

--

通常,当你想到偏差时,你可能会想到一个人的信念如何无意中塑造他们的假设和方法。这当然是一个定义。

但偏差也指的是我们用来获得洞察的数据本身可能被无意中扭曲和不完整,扭曲了我们观察一切的视角。

许多——如果不是大多数——我们在医疗领域使用的数据集本质上是有偏的,如果我们不小心,很容易使我们偏离正确的方向。

我将特别关注三个主要概念,这些概念困扰医疗数据,并且可能会低调地使你的整个分析失效。

Kaleb Nimz拍摄,发布在Unsplash

在早前的文章中,我写到了我在医疗数据分析职业生涯中遇到的各种偏差类型,以及它们如何成为数据从业者面临的挑战。对我来说,最大的主题是:个人偏差、数据偏差和确认偏差。

在我发布那篇文章的四天后,一位导师给我发来了这篇关于医疗索赔数据中抽样偏差的 JAMA 文章,以及它如何在某些地区因社会健康决定因素(SDOH)而加剧

文章的结论确实引起了我的注意:

[研究强调]了调查大型医疗索赔数据的抽样异质性的重要性,以评估抽样偏差如何影响结果的准确性和普遍性。

重要的是,调查这些偏差或准确地重新加权数据将需要来自索赔数据库本身之外的外部数据源。

TLDR:

在大型医疗保健数据集中,患者或成员的失衡可能使您的结果无法概括,甚至(在某些情况下)完全无效。

社会决定健康因素数据可能会有所帮助。

让我们来解开这个谜题。

但首先,引用杰出的 Cassie Kozyrkov 的话:“AI 偏差问题开始于——但并不止于——定义。‘偏差’是一个在不同背景下含义截然不同的过载术语。” 进一步阅读她的文章和相关的其他文章:什么是偏差?

作为医疗保健分析师,我们需要注意:

1. 抽样/选择偏差。

抽样偏差当某些成员比其他成员更有可能被选入样本时。 选择偏差 可以通过选择[研究] 人群 的方式、抽样方法 或参与者的招募(即,选择包含内容的设计缺陷)而引入。[1]*

我使用这些术语有些口语化。在传统调查/样本/研究设计中,抽样/选择偏差的定义是有意义的。

但是……第一个警告:在数据中,“人口”有着非常特定的含义,在医疗保健中也是如此,虽然定义相似(但不同),它们可能相同,也可能不同,这取决于具体情况。

在医疗保健的背景下,它们可以指如何构建我们的分析和我们的包含/排除,以了解某个特定的“人口”。在医疗保健中,我们使用关注的人群来指代根据研究的不同而涉及的多个兴趣组,例如:

  • 业务线(LOB),如由政府支付者(医疗补助、医疗保险)覆盖的患者;商业保险线(雇主团体、零售或通过交换购买的保险);自保(自筹资金团体,通常是大型雇主支付员工的医疗索赔)等。

  • 子业务线或团体。例如,医疗补助可能包括多个子组,代表不同的资格、覆盖、福利或人群/他们为何符合该计划的类型(临时援助计划 (TANF) 与医疗补助扩展计划)

  • 人口统计(特定群体、男性或女性、某些地区等)

  • 条件(检查某些慢性病、关注的主要疾病状态或对系统成本最高的疾病、医疗保险关注的疾病等)

  • 上述某些组合的子组,例如:医疗补助、临时援助、母亲与新生儿

  • 对上述某些组合的跨组比较,例如:Medicaid、TANF、新妈妈及其成本/利用/结果趋势 vs. 商业、自保、新妈妈及其成本/利用/结果

  • 上述任何子组合或交叉组合,加上更多内容

正如你所见,分析设计和“相关人群”可能会迅速变得复杂。这并不一定意味着数据不可用或结果总是会令人不安。它确实重申了,意识到这种偏差对于确保你从各个角度考虑和进行相应分析至关重要。

标记 #2:如果医疗行业的人说数据包含了所有感兴趣的群体……这真的是真的吗?也许……

对健康保险公司的一点警示: 一个常见的讨论点是,健康保险公司可以分析他们的“整个群体”,因为他们接收了他们负责的每个成员的索赔,因此可以避免抽样偏差。根据使用案例,这可能潜在地是一个合理的讨论点。但总体来说,我会提醒你要记住,即使这些数据本身也具有固有的偏差,因为它:a) 只包括那些实际发生了事件/事故并且保险公司处理了后续索赔的成员/患者,b) 数据本身往往会过度/不足地代表那些更容易有慢性健康问题、 adverse social determinants of health、寻求医疗、反映你的组织倾向于服务的类型人群或你拥有更大业务量的群体等。更多内容请参见下面的文章总结。

2. 覆盖不足偏差

覆盖不足偏差 发生在你的样本中排除了部分人群时。 [1]

再次强调,这一定义在传统的调查/抽样/研究设计中是合理的。在医疗背景下,如果我们不小心,它可能会以实际的方式影响我们(浪费金钱、浪费精力、羞耻、没有影响或改变结果,或…… 以上所有)。

除了上述最纯粹的定义外,我还考虑到这种情况,即看不到“你四面墙之外发生的任何事情。”我们(通常)只拥有我们组织生成的数据,这本质上只是整个情况的一部分。根据你所做的分析类型和原因,这可能不是决定性问题,但这是另一个需要注意的重要标记。我们的[患者/成员/员工/居民/等]可能不会以相似的方式行动,甚至不能代表[所有患者/其他群体/其他类型的雇主/其他地区/等]

标记 #3:你的数据仅显示了你的组织内部的行为,因此不能总是用来推断你的竞争对手的真实情况、你的社区的真实情况、患者在访问不同的医疗提供者(不是你)时的情况,或者任何一个健康计划成员的行为是否与另一个成员的行为相似,这些都可能受到个人、地区、社会、职业或行为差异(通常我们没有的数据)的影响。所有这些都必须在你寻求结论时予以考虑。

随着更多的医疗保健组织将焦点转向更全面地改善其服务的社区(而不是仅关注具体的患者),我们可能会再次错过大量的数据拼图——这些数据告诉我们“我们四面墙外发生了什么。”

尽管如此,我们的一些分析可能并不完全依赖于了解这些情况。可以通过用来自我们四面墙外的外部数据来增强我们自己的内部数据,从而填补更多的图景来解决这些问题。我们中的一些人已经认识到这一点,并正在进行数据共享/合作,或利用额外的数据源,如健康信息交换(HIE)或购买的基准数据,以比较我们的人群,以便理解我们的情况有何不同或行为如何。请记住,这些数据集可能在更广泛的范围内存在相同的偏见(实际上可以参见那篇JAMA 文章),但所有这些都是朝着至少理解和识别任何潜在“陷阱”的良好第一步。

3. 历史偏见或系统性偏见。

历史偏见发生在社会文化偏见和信仰反映到系统化的过程中[这些偏见随后会反映在数据中]。[3] 系统性偏见来源于那些以不利于某些群体的方式运作的机构。[2]

当来自历史上有偏见的来源的数据用于数据科学模型或分析时,这就变得特别具有挑战性。尤其是在你分析历史上存在缺陷的系统时,比如医疗保健系统,这一点尤其重要。

这是一个非常热门的话题,以至于 NIST 正在开发一个人工智能风险管理框架。NIST 在他们的特别出版物中讨论了人类和系统偏见,朝向人工智能中的偏见识别与管理标准,这张图片来源于此:

图片来源:N. Hanacek/NIST(公有领域)。

在医疗保健领域,某些疾病(或者一般的病人,因为他们是医疗保健的主要消费群体)、人口统计学特征、某个群体或子群体、使用模式(或缺乏使用)、健康/质量/死亡率/参与度/满意度以及我们在医疗数据中看到的许多其他趋势或结果都反映了破碎的医疗保健系统的运作方式。这是一个高度复杂的话题,具有许多不同的方面,将在后续文章中深入探讨。但可以说:

标志#4:医疗保健系统已经破碎,现有的社会结构极大地影响了健康和结果、机会(或缺乏机会)、障碍和行为。这在医疗数据中以多种不同方式不可否认地反映出来。

提到健康公平倡议。

SDOH 数据如何提供帮助?

这部分并不是解决我所概述的所有问题的灵丹妙药,但我认为它足够有趣,值得特别指出,因为我们最近在健康分析中越来越多地讨论健康公平和 SDOH。

JAMA 文章的作者试图理解大型商业数据库中潜在的偏见,这些数据库由多个商业保险公司汇总的索赔数据组成。这些类型的数据集通常被组织用于临床研究、竞争情报,甚至基准测试。作者分析了一个常见的数据集——Optum Clinformatics 数据市场 (CDM)——该数据集来源于几个大型商业和医疗保险优势计划,并商业授权用于各种用途。

在邮政编码级别,作者分析了 CDM 中个人的代表性,与普查估计和这些邮政编码的 SDOH 变异性进行比较。我会指出,他们使用的数据集来自 2018 年;这是你能找到的大多数医疗数据的另一个主要问题,但另当别论。

文章发现:

[即使在调整了州级变异后,我们的高级统计方法] 发现,CDM 中的纳入与拥有更富裕、更年长、更受教育且过度白人居民的邮政编码相关联。

与索赔数据中过度代表相关的社会经济和人口统计特征也已被证明是在广泛的健康结果中起作用的修饰因子。

重要的是,调查这些偏见或准确地重新加权数据将需要来自索赔数据库以外的外部数据来源。

TLDR(再说一遍):

索赔数据在代表更受教育、更富裕和更多白人的患者方面存在严重偏差。

影响邮政编码级别(及子邮政编码)人群的具体 SDOH 因素对健康结果的差异有着明确的影响。

重新加权/标准化数据需要除索赔之外的额外数据。

有些人可能会称那些数据为“在你的四面墙之外”。

那么,现在怎么办?

你的第一个目标:意识。

我提出这些想法仅作为指导,帮助你开始思考潜在的“陷阱”。首先问自己和他人问题。在设计下一个分析时开始思考它。确保你完全理解结果将如何以及在哪里使用、意图是什么,并提前列出风险。开始理解不同类型的偏差及其可能对你的影响。如果你想了解 29 种其他类型的偏差,进一步细分为更多子类型,请查看这篇有用的Scribbr 上的研究偏差知识库文章

你的第二个目标:不要让这导致分析瘫痪。

虽然保持这些在心中很重要,但更重要的是我们不要被这些困扰到导致分析瘫痪。作为分析师,了解细微差别并决定在沙滩上划出那条线无疑是你工作的最难部分:决定哪些见解仍然有价值或有意义尽管存在这些细微差别,以及何时倾向于(或远离?)结论的可靠性,将对你的成功和组织的成功至关重要。

最有可能的是,结果可能会落在中间,所以你的目标是理解细微差别,清晰地传达它们,并在尽可能的范围内提供适当的指导,同时有效防范潜在的误解。

这篇文章突出了一个子集的偏差,这些偏差并非医疗保健所独有,但在医疗保健中应用的方式非常独特。

在你的医疗保健或分析职业中,你遇到过哪些其他类型的偏差?

Stefany GoradiaRS21健康实验室的副总裁,该公司是一家数据科学公司,拥有一个专注于医疗保健+社区的健康实验室。

她在医疗保健分析的前线工作,致力于为内部和外部客户提供价值。她撰写关于如何解读医疗保健数据、与利益相关者沟通,并利用数据支持知情决策和执行的文章。

喜欢健康数据和医疗保健分析?

MediumLinkedIn上关注我。

喜欢我的风格?

了解更多关于我的背景以及对数据的热情,请访问stefanygoradia.bio

其他一些关于偏差的有用参考文章:

[## [1] 研究中的偏差类型 | 定义与示例

研究偏差来源于任何偏离真相的情况,导致结果失真和错误结论。偏差可能会…

## [2] NIST 报告强调:人工智能偏见不仅仅是偏见数据

作为提升我们识别和管理人工智能中偏见有害影响能力的一个步骤……

## [3] 处理数据时最常见的 6 种偏见类型

你正试图做出一个好的决定,并决定查看你的数据来帮助你做出判断。你已经有了……

## 8 种可能毁坏你机器学习模型的数据偏见类型 - Statice

偏见数据:你很可能对这个术语很熟悉。也许你自己就是对使用存在偏见的数据非常怀疑的人……

## 数据驱动?再想想

大多数人缺乏的心理习惯,以及为什么你不能期望使用数据有效地指导你的行动……

## 克服 COVID-19 期间的确认偏误

在疫情期间你的大脑如何干扰你以及你可以采取的应对措施

## 人工智能偏见对代表性不足群体的影响

“无论许多人是否意识到,偏见都是人类思想和经验的普遍方面。然而,曾经的……

## 人工智能偏见对代表性不足群体的影响

排序算法:堆排序

原文:towardsdatascience.com/heapify-with-heap-sort-5df23b5764c1?source=collection_archive---------19-----------------------#2023-01-03

了解堆数据结构及其在排序中的应用

Vyacheslav EfimovTowards Data Science Vyacheslav Efimov

·

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

--

介绍

堆是一种数据结构,表示以基于二叉树的格式组织的数组。堆对其结构施加了以下规则:

  • 完整性。堆的每一层都被完全填满。然而,最后一层可能从左侧开始部分填满。

  • 堆规则。任何父节点的值必须小于或等于其子节点的值。如果满足这一属性,则堆称为最小堆。还有一种最大堆变体,其中父节点的值必须大于子节点的值。

本文中的示例和代码将针对最小堆提供。最大堆的算法工作流程非常类似。下面展示了一个最小堆的示例。

堆通常以数组的形式存储。如果一个父节点的索引是 i,那么其左子节点和右子节点的位置分别是 2 * i + 1 和 2 * i + 2。反之,如果一个非根节点的索引是 i,那么其父节点的索引是 (i - 1) // 2*。遵循这一原则,我们得到上述堆的数组表示:

操作

堆支持多种操作:

  • 插入一个节点

  • 从数组构建堆

  • 提取具有最小值的节点

  • 排序

由于堆数据结构具有多个操作,实际中将其实现为一个类更为方便。目前,我们将实现其基础部分。每次操作后,将提供相应的代码片段。

class Heap:

    def _swap(self, i, j):
        self.heap[i], self.heap[j] = self.heap[j], self.heap[i]

    def _number_of_children(self, i):
        if 2 * i + 2 < len(self.heap):
            return 2
        if 2 * i + 1 < len(self.heap):
            return 1
        return 0
  • heap 字段以堆的形式存储输入数组(稍后将实现)

  • _swap() 方法接受两个数组索引并交换其值。

  • number_of_children() 方法返回节点的子节点数量(0、1 或 2)

插入一个节点

新元素插入堆的最后位置。如果插入的元素低于父节点的值,可能会破坏堆规则。为避免此问题,新节点会递归向上传播,直到不违反堆规则为止。上述过程称为堆化(up)。

从上图中,我们插入了一个值为 3 的节点。

  • 插入后,堆规则被破坏,因为 3 < 15(父节点)。我们交换元素 3 和 15。

  • 现在节点 3 有了一个新父节点,值为 7。再次,堆规则未得到满足,因为 3 < 7。结果,我们交换了 3 和 7。

  • 节点 3 位于索引 2,其父节点值为 1。由于 3 ≥ 1,堆规则是正确的。在这一阶段,插入过程结束。

让我们来探讨插入的时间复杂度。最坏的情况是需要将新节点从树的底部传播到顶部。由于任何树的高度与其元素总数 N 的对数关系成正比,而每次比较花费 O(1) 时间,最终估计结果为 O(logN) 时间。

class Heap:

    def insert(self, value):
        self.heap.append(value)
        self._heapify_up(len(self.heap) - 1)

    def _heapify_up(self, i):
        if i != 0:
            parent_index = (i - 1) // 2
            if self.heap[i] < self.heap[parent_index]:
                self.swap(i, parent_index)
                self._heapify_up(parent_index)

    # ... #
  • insert() 方法将值附加到堆中,然后调用堆化方法。

  • _heapify_up() 方法递归调用自身,直到堆规则正确。

构建堆

对于输入数组的每个元素,调用插入过程。这就是构建堆的方法。

说到复杂度,可能会觉得构建一个堆需要 O(N * logN) 的时间,因为对于每个 N 元素,我们调用一个耗时 O(logN) 的函数。然而,可以改进这个估计,并通过数学证明总时间是 O(N)

class Heap:

    def build(self, array):
        self.heap = []
        for value in array:
            self.insert(value)

   # ... #
  • 对于传递给 build() 方法的数组,通过插入调用构建堆。

提取具有最小值的节点

最小节点位于堆的顶部。我们提取最小值,并用堆的最后一个节点替换顶部节点。由于违反了堆规则,我们将这个元素向下传播。该算法与我们上面使用的插入元素时相似(插入时元素是向上传播的):在每一步中,我们将当前元素与具有最小值的子节点交换。此过程持续到堆规则不再被破坏或当前元素没有子节点为止。

在上图中,值为 1 的节点被提取,值为 15 的最后一个节点取代了它的位置。

  • 由于节点 15 违反了堆规则,我们将其与其最小的子节点 3 交换。

  • 然后节点 15 有子节点 7 和 8,它们都较小。我们再次将 15 与最小的子节点 7 交换。

  • 之后,15 位于索引 5,并且只有一个子节点 20。由于 15 ≤ 20,我们停止堆化过程。

类似于插入部分的堆化算法,该算法具有相同的渐进复杂度,并在 O(logN) 时间内进行。

class Heap:    

    def extract_min(self):
        self._swap(0, len(self.heap) - 1)
        min_element = self.heap.pop()
        self._heapify_down(0)
        return min_element

    def _heapify_down(self, i):
    if self._number_of_children(i) == 2:
        if self.heap[i] > self.heap[2 * i + 1] or self.heap[i] > self.heap[2 * i + 2]:
            if self.heap[2 * i + 1] < self.heap[2 * i + 2]:
                self._swap(i, 2 * i + 1)
                self._heapify_down(2 * i + 1)
            else:
                self._swap(i, 2 * i + 2)
                self._heapify_down(2 * i + 2)
    elif self._number_of_children(i) == 1 and self.heap[i] > self.heap[2 * i + 1]:
        self._swap(i, 2 * i + 1)
        self._heapify_down(2 * i + 1)

    # ... #

排序

排序是通过提取最小节点来实现的。当堆不为空时,我们调用 extract_min() 函数,并将每个最小元素附加到新数组中。这样,数组将由排序的元素组成。

由于堆包含 N 个节点,并且 extract_min() 的时间复杂度为 O(logN),因此总排序时间为 O(N * logN)

class Heap:

    def sort(self):
        array = []
        while len(self.heap) > 0:
            array.append(self.extract_min())
        return array

    # ... #

结论

我们已经覆盖了堆的所有四个主要操作。要使用堆数据结构对数组进行排序,必须首先构建堆,然后调用排序方法。构建堆需要 O(N) 时间,而排序需要 O(N * logN) 时间,这最终导致堆排序的 O(N * logN)渐进复杂度。

堆类的完整实现如下所示。

class Heap:

    def __init__(self, array):
        self.build(array)

    def build(self, array):
        self.heap = []
        for value in array:
            self.insert(value)

    def insert(self, value):
        self.heap.append(value)
        self._heapify_up(len(self.heap) - 1)

    def sort(self):
        array = []
        while len(self.heap) > 0:
            array.append(self.extract_min())
        return array

    def extract_min(self):
        self._swap(0, len(self.heap) - 1)
        min_element = self.heap.pop()
        self._heapify_down(0)
        return min_element

    def _swap(self, i, j):
        self.heap[i], self.heap[j] = self.heap[j], self.heap[i]

    def _number_of_children(self, i):
        if 2 * i + 2 < len(self.heap):
            return 2
        if 2 * i + 1 < len(self.heap):
            return 1
        return 0

    def _heapify_down(self, i):
        if self._number_of_children(i) == 2:
            if self.heap[i] > self.heap[2 * i + 1] or self.heap[i] > self.heap[2 * i + 2]:
                if self.heap[2 * i + 1] < self.heap[2 * i + 2]:
                    self._swap(i, 2 * i + 1)
                    self._heapify_down(2 * i + 1)
                else:
                    self._swap(i, 2 * i + 2)
                    self._heapify_down(2 * i + 2)
        elif self._number_of_children(i) == 1 and self.heap[i] > self.heap[2 * i + 1]:
            self._swap(i, 2 * i + 1)
            self._heapify_down(2 * i + 1)

    def _heapify_up(self, i):
        if i != 0:
            parent_index = (i - 1) // 2
            if self.heap[i] < self.heap[parent_index]:
                self._swap(i, parent_index)
                self._heapify_up(parent_index)

执行示例:

array = [14, 8, 1, 12, 6, 20, 7, 10, 18, 4, 5, 15]
heap = Heap(array)
sorted_array = heap.sort()
print(sorted_array) # [1, 4, 5, 6, 7, 8, 10, 12, 14, 15, 18, 20]

除非另有说明,所有图片均由作者提供

这是我使用 Apache Airflow 6 年学到的东西

原文:towardsdatascience.com/here-is-what-i-learned-using-apache-airflow-over-6-years-15d88b9922d9

从实验到生产无忧的 Apache Airflow 之旅

Chengzhi ZhaoTowards Data Science 程智钊

·发表于Towards Data Science ·8 分钟阅读·2023 年 1 月 9 日

--

照片由Karsten Würth拍摄,来源于Unsplash

Apache Airflow 无疑是多年来最受欢迎的数据工程开源项目。它在与数据工程师崛起的关键时刻获得了流行,其核心理念是将代码作为一等公民,而不是数据管道(即 ETL)的拖放,这标志着一个里程碑。Apache Airflow 于 2016 年 3 月成为 Apache 孵化器项目,并于 2019 年 1 月成为顶级项目。我从 2017 年起作为用户使用 Apache Airflow。在此过程中,我也为 Apache Airflow 做出了贡献。今天,我想分享我与 Airflow 的旅程以及我在 6 年中学到的东西。

什么是 Airflow

Airflow 是由社区创建的平台,用于以编程方式创建、调度和监控工作流。 — Airflow 官方文档

Apache Airflow 由Maxime Beauchemin开发,他曾在 Airbnb 和 Facebook 工作。他在 Airbnb 开发了 Airflow(可以从项目名称中看出)。然而,激发他的核心想法是 Facebook 使用的内部工具。

Apache Airflow 的主要用户是数据工程师或需要调度工作流的工程师,主要是 ETL(提取、转换、加载)作业。这些 ETL 作业通常按日或按小时运行。作业本身执行数据操作,以从未结构化的原始数据中获取见解。

为什么 Airflow 如此受欢迎?

自 2017 年以来,我一直在使用 Apache Airflow,在它成为任何数据工程师必备技能之前。我仍然记得早期我们“黑客”式地将大量努力投入到 Airflow 调度程序系统中,以使其稳定运行的经历。如果你想了解更多,我几年前写了一篇文章。

[## Meetup 的数据管道基础设施,减少噩梦:在 Kubernetes 上运行 Apache Airflow

Meetup 数据平台基础设施——一个起点

medium.com](https://medium.com/making-meetup/data-pipeline-infrastructure-at-meetup-with-fewer-nightmares-running-apache-airflow-on-kubernetes-54cb8cdc69c3?source=post_page-----15d88b9922d9--------------------------------)

从一开始,Airflow 就以一个孵化项目的身份闪耀。它在不断发展和稳定。以下是我记录的几个让 Airflow 一开始就如此迷人的原因:

首先编写代码

编码(尤其是 Python)已成为数据工程师的新常态。然而,10 年前编写数据管道主要是通过像 SSIS 或 Informatica 这样的 UI 工具拖放完成的。拖放一开始很简单。然而,它很快会遇到开发周期难以扩展的情况,UI 有时也会变得更加棘手,并且拖放框时可能会出现问题。

编码是一种很好的抽象方式。我们可以用 Python 代码编写包含所有逻辑的有向无环图(DAG),分享并部署它。这很好地达到了目的。它迫使一些仅了解 SQL 的人学习 Python,但这是值得的。

良好的可视化

无论我们编写多少代码,作为一个协调者,它需要某种方式来可视化过程。最终,良好的可视化看起来既吸引人又解决了多个问题。它可以有效地捕捉故障、理解依赖关系和监控作业状态。

作为最终用户,可视化并不像仪表板那样需要打磨。一旦编码完成,Airflow 会处理剩余部分并为你提供可视化。你的 DAG 可以以七种不同的视图进行可视化,每种视图都有其重点。我每天使用的最常见视图是 Grid view(曾经是树形视图)和 Graph view

在 UI 上,你可以执行诸如重启作业、重置 DAG 中某些任务的状态、检查失败日志或检查长期运行的作业等任务。虽然 UI 并没有提供 100% 的功能,但它覆盖了日常使用的场景。

扩展性

为了让更多人将其用作 ETL 编排工具,该项目必须具有可扩展性,以便其他项目能够轻松集成。集成的丰富性为 Airflow 成为顶级 Apache 项目之一奠定了基础。此外,Airflow 允许用户编写自己的 PythonOperator,这进一步鼓励开发者通过代码构建自己的逻辑,而不是等待插件的新升级来满足他们的 ETL 需求。

出色的可扩展性还促进了更多创新项目和插件在 Airflow 生态系统中入驻。使用插件的核心思想是避免每个人都重新发明轮子来编写自己的逻辑。它还帮助服务或产品与最终用户无缝集成。

完整的 Airflow 插件使最终用户迁移到其他工具变得更难。例如,EDI 835:电子汇款通知是一种在医疗保健行业提供索赔支付信息的特殊格式。它需要特殊的解析逻辑才能正确读取。这些特定领域的用例使 Airflow 形成了一个很好的“护城河”,从而具备竞争力。

社区

最终,Airflow 是 Apache 基金会的一部分。我们都知道成为 Apache 顶级项目意味着什么。名声本身就能吸引更多的人尝试 Airflow。

社区正在健康增长。在 StackOverflow 上,你可以找到超过 9k 个标记为 [Airflow] 的问题。Airflow 每年都会举办 AirflowSummit。支持 Airflow 的公司 astronomer.io 也提供了出色的文档和认证。自从我在 2017 年加入以来,其 Slack 群组一直很活跃,现在已有 25k+ 人加入。

我使用 Airflow 的旅程

当我加入 Meetup 时,Airflow 是由一位首席工程师在 2017 年 4 月作为概念验证带来的实验工具。

当时引入 Airflow 的目的是解决我们使用自定义 Scala 基于 ETL 时遇到的多个问题。我仍然欣赏那些建立复杂 DAG 逻辑依赖关系来使 ETL 首先正常工作的人员。但我们都知道 ETL 有时会失败。这就是自定义 Scala 基于 ETL 的头疼之处。不容易调试和重新启动作业,并且你需要非常小心以确保作业是幂等的。

最终,我们找到了一种运行 Airflow 1.7 的方法,并部署了新的 Python 代码用于 POC。结果非常好。自从引入了 Airflow 1.9 和 Kubernetes 执行器,Airflow 已经处于一个更加成熟的阶段。我还为 Airflow 项目做出了贡献,并获得了对内部核心代码的深入了解。在生产环境中,我们遇到的问题比早期要少得多,我们可以更多地关注于提高可扩展性和增加更多用例。

不过,还有一些事情我希望我能早些知道,以避免一些耗时的调查。

学到的经验

一开始没有利用宏

你没看错。我在 Airflow 的早期阶段不知道宏的概念。因此,像 {{ ds }} 这样的 Jinja 模板我一开始并没有利用。在新的一天中,为了触发 ETL,它只是使用了 Python 函数 date()-1

最大的问题是回填,我使用了 Airflow 的参数进行处理,但用户必须提供这些信息,并且只能逐日回填。整个过程变成了一个不容小觑的过程。在仔细阅读 Airflow 文档后,发现宏非常适合获取作业的元数据。我们将回填过程简化为清除现有的 dag,而无需额外的参数修改。

ETL 在 EST

日期在任何数据系统中都是关键的。我们的数据系统按 EST 分区。是 EST!!! 如果你和我处于同样的情况,你会理解在日光节约时间更改期间我经历了多么痛苦。一切都崩溃了!

我们有多个 DAG 依赖于 external_task_sensor,它依赖于 execution_delta — 与之前执行的时间差来查看。想象一下,你有一些任务完成了,然后突然待处理的任务 +1/-1 小时变得混乱不堪。我必须构建一个解决方案来避免这种情况。这不是一个有趣的维护工作。如果你开始构建任何数据系统,我建议尽可能使用 UTC 时间,以简化工作和保持内心的平静。

在调度 DAG 时要温和。

Airflow 不是数据流处理解决方案。 这在 Airflow 官方文档的 Beyond The Horizon 部分开头就提到过。

一个原因是 Airflow 不会在任务之间传递数据。遇到这种情况时,你需要寻找外部存储。Airflow 的架构并没有以流式设计开始。如果你有一个需要每分钟触发的作业,Airflow 可以完成这个任务,但它的扩展性不好。对于数据流处理的用例,使用 Flink/Spark 进行数据流处理,不要把这个负担放到 Airflow 上。

另一个不常提及的原因是,Airflow 调度器在更新所有 DAG 状态时非常繁重。当 Airflow 调度器触及实时时钟时,它会扫描下一个潜在的 DAG 以触发其执行日期。即使 Airflow 调度器宕机,状态也必须被实际化以跟踪状态。大规模的状态更新在 OLTP 数据库中进行,Airflow 仍建议使用 MySQLPostgres 在这种情况下,scheduler_heartbeat_sec 需要正确配置,默认值为 5 秒。这个值可以作为调节的关键。

当我们面临数百个 DAG 和数千个任务时,作业逐渐被安排和运行。将此值增加到 60 秒以上会使 Airflow 调度器冷却。为了更好地了解 Airflow 调度器,我写了一篇文章 Airflow Schedule Interval 101 帮助你深入学习。

## Airflow Schedule Interval 101

气流调度间隔可能是一个具有挑战性的概念,即使对于正在使用 Airflow 的开发人员也是如此……

towardsdatascience.com

最终想法

Apache Airflow 是一个了不起的开源项目,专为数据工程师设计。回顾过去,许多概念是主导实施的基础,这些实施为其今天的流行和成功铺平了道路。许多公司将 Airflow 作为数据工程师招聘的必备技能。对我来说,早期参与 Apache Airflow 并见证项目的成长是一种幸运。一些新项目已开始挑战 Apache Airflow,例如 mage-aiPerfect。我将在未来的帖子中介绍它们。

Airflow 的创建者,Maxime,也是 Apache Superset 的创建者,是这两个项目中最令人钦佩的数据工程师之一。以下是他在 2015 年关于 Airflow 最佳实践的演讲。这仍然是今天使用 Airflow 和了解 Airflow 设计理念的一个很好的参考。

与 Airflow 的最佳实践 - 一个开源的工作流和调度平台 | 作者 Maxime Beauchemin

我希望这个故事对你有帮助。本文是我工程与数据科学故事系列的一部分,目前包括以下内容:

程智赵

赵成志

数据工程与数据科学故事

查看列表 53 个故事

你还可以 订阅我的新文章 或成为 推荐的 Medium 会员,获取对 Medium 上所有故事的无限访问权限。

如果有任何问题/评论,请随时在此故事的评论区留言,或通过 LinkedinTwitter 直接联系我

我的数据科学工作流程模板

原文:towardsdatascience.com/heres-my-data-science-workflow-template-caa83c1e3032

一份专注于数据科学方面的快速指南

Matt PrzybylaTowards Data Science Matt Przybyla

·发布于 Towards Data Science ·阅读时间 6 分钟·2023 年 2 月 22 日

--

Campaign CreatorsUnsplash 拍摄的照片 [1]。

目录

  1. 介绍

  2. 数据探索、数据汇总和特征工程

  3. 算法比较

  4. 摘要

  5. 参考文献

介绍

虽然我已经写了涵盖整个数据科学过程的文章,包括业务理解和利益相关者协作等方面,但我想专注于数据科学部分。在这篇文章中,我将提供一个供数据科学家使用和构建的模板。我假设更多的中高级数据科学家已经遵循这个通用模式,所以如果你在数据科学方面较为初级、刚刚入门或对数据科学感兴趣,这篇文章适合你。话虽如此,让我们更深入地探讨数据科学过程的主要部分,包括数据探索、数据汇总、特征工程和算法比较的步骤。

数据探索、数据汇总和特征工程

Jason BriscoeUnsplash 拍摄的照片 [2]。

我们要记住,不同的公司和角色有所不同,因此这个模板可能需要根据你的情况进行更新。下面描述的步骤是当你已经定义了业务问题并与利益相关者会面后进行的步骤。根据你的情况,你也可能需要负责数据工程和数据分析的其他部分。

数据探索

  • 定义你的初始数据集 → 你可能在训练模型之前已经对需要的数据有了假设,因此会从这些特征开始,并进一步迭代。

  • 定义该数据集的来源 → 是来自当前的公司数据库表,还是一次性的 Google Sheet/Microsoft Excel 表格,或是第三方数据平台等?

  • 数据目前是否存在? → 如果存在,则继续前进。如果不存在,则你可能需要与数据工程/数据分析等合作,以提供模型所需数据的准确来源。

— 你也可能需要向数据工程师提供理由,例如,请求特定数据点。例如:我希望得到按邮政编码分组的人口数据 — 先自行进行分组,并提供这种分组如何影响准确性以及最终业务问题的理由,以验证数据工程的时间。

数据汇总与特征工程

  • 你的数据是否已经正确汇总? → 如果是,则继续前进。如果不是,则需要对数据进行汇总并以该格式存储。例如,如果你想预测县级人口,可能需要将以州(地理分组)为基础的训练数据行进行汇总。你可能能够在没有数据工程帮助的情况下进行数据存储,并使用如 Python 编程语言中的 pandas 等工具进行一些分组技术。

  • 你是否想创建新的模型特征? → 如果是,则继续前进。如果不是,则需要通过相关性、自动特征选择、SHAP 特征重要性等工具,识别和测试更多可以解释你预测变异性的特征。

反思与可能的惊喜:

本节的关键点是,你可能在开始数据科学项目时并没有预期的那么多相关数据/特征。话虽如此,开始初步数据探索时,要对这种情况保持警惕。

算法比较

图片由Liz Sanchez-Vegas拍摄,发布在Unsplash [3]。

需要记住的一点是,最后一节的最后一步,特征工程,可以与算法比较的顺序来回切换,因为你可能会发现不同的特征对于某些算法更有用或更易于使用。

现在你已经有了你的特征(或至少是一个起点),你会想选择一个特定的算法来构建最终模型。

算法比较

  • 你的目标是一个连续的数字还是一个类别? → 如果答案是一个连续的数字,那么你会比较回归算法;如果是一个类别,那么你会比较分类算法。

  • 你选择什么算法? → 开始时最好选择尽可能多的你了解的算法,然后再从中筛选。比较算法有很多方法,例如分别运行每一个,或者创建一个循环比较你选择的所有算法,但我发现最简单的方法是使用一个已经能并排比较算法的库,如Moez Ali的 PyCaret 库——以下链接展示了极其有价值的 compare_models()函数:

[## Notebook on nbviewer

一旦设置成功执行,它会打印出包含若干重要部分的信息网格……

nbviewer.org

  • 比较不同算法时我应该关注什么? → 根据你的用例,你可以比较一些因素,但通常你会关注损失函数,比如 MAE 或 RMSE,例如,对于回归目标。你还可以查看训练所需的时间,以及是否需要添加超参数调整等。

反思与可能让你惊讶的事:

算法比较曾经是非常繁琐的,随着时间的推移,你可以利用越来越多的工具来轻松比较算法,这样你就可以花更多时间在你最终选择的算法上。你甚至可以在同一时间窗口内训练和比较多个算法,更轻松地针对特定的损失函数进行优化,这样你就不会局限于单一算法,因为你的基础数据和假设可能会发生变化。

总结

虽然公司和角色可能会有所不同,但数据科学流程中有一些关键步骤是被广泛实践的。我们讨论了几个涵盖常规项目的数据科学工作流的步骤。

总结一下,这里是我们在数据科学工作流模板中讨论的步骤:

  • 定义你的初始数据集

  • 定义此数据集的来源

  • 数据是否已经存在?

  • 您的数据是否已经正确汇总?

  • 您是否想创建新的模型特征?

  • 您的目标是连续数字还是类别?

  • 您选择哪个算法?

  • 在比较不同算法时,我需要关注什么?

我希望您发现我的文章既有趣又有用。如果您作为数据科学家的经验与我的相同或不同,请随时在下方评论,或者您对第一次进入数据科学有什么期望?为什么或者为什么不?您认为还应该讨论哪些其他内容,包括更多的步骤?这些内容肯定可以进一步阐明,但我希望能够为您提供一些关于典型数据科学工作流程的预期信息。

我与这些公司没有任何关联。

请随时查看我的个人资料, Matt Przybyla以及其他文章,同时可以通过以下链接订阅以接收我的博客的电子邮件通知,或者 点击屏幕顶部关注图标旁的订阅图标,如果您有任何问题或评论,请在 LinkedIn 上与我联系。*

订阅链接: datascience2.medium.com/subscribe

推荐链接: datascience2.medium.com/membership

(如果您通过 Medium 注册会员,我将获得佣金)

参考文献

[1] 照片由 Campaign Creators 提供,拍摄于 Unsplash,(2018)

[2] 照片由 Jason Briscoe 提供,拍摄于 Unsplash,(2019)

[3] 照片由 Liz Sanchez-Vegas 提供,拍摄于 Unsplash,(2019)

[4] Moez Ali,PyCaret,PyCaret 回归教程 (REG101) — 初学者级别,(2023)

你错过了什么

原文:towardsdatascience.com/heres-what-you-are-missing-cdf4a6294020

AI 行业中可见性的隐藏力量以及如何获得它

Richmond AlakeTowards Data Science Richmond Alake

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

--

什么是行业可见性?

保持可见性 — 图片由 MidJourney 生成。

你多久会考虑一下你职业生涯中的非技术方面?

我们都在一个重视技术技能和生产力的领域工作,因此如果你没有把个人品牌和行业可见性等非技术活动视为首要任务,那也是可以理解的。但在这篇文章的结尾,作为 AI 从业者的你将会了解什么是行业可见性,它的好处,以及如何在 AI 领域建立和提升你自己的可见性。

行业可见性和个人品牌是两个密切相关的术语,在某些情况下,它们是可以互换使用的。你可能是一个中级、高级或经验丰富的 AI 从业者,并且从技术角度上看已经见多识广。你可能已经在不同的团队中工作过,这些团队有不同的数据工程师、科学家和产品经理的结构配置。你了解机器学习管道中的关键过程,包括 AI 模型的生产化。

现在你已经到了职业生涯的一个阶段,你对各种库、工具、框架以及整体技术栈有了较好的理解。

在某些圈子里,你可能(偷偷地)自称为 AI 专家。

还有什么?

还有什么在行业可见性之外?— 图片由 MidJourney 生成

接下来是什么?超越技术专长的领域是什么?

寻找上述问题的答案将引导你走上几条道路,而你的旅程中的一个主要途径就是行业可见性

让我们正式定义一下这个术语。

行业可见性是指个人在特定行业内被同行认可、确认或重视的程度,通常由于知识共享。

从个人角度来看,当你把行业可见度视为某种声誉货币时,这一切都会变得有意义。

那我为什么要在乎呢?

行业可见度 == 职业声誉货币

在一个主要以技术为主的领域,最棒的事情之一就是你可以在 AI 领域取得成功和满足感,而不必担心行业可见度。那么你为什么还要在乎呢?

允许我带你回顾一下过往。

你可以把你的职业生涯视为一场长时间的社会实验。这就是我所做的。

在实验的早期阶段,我担任 Web 开发人员,翻阅前端框架的文档,寻找将更多字母添加到我的缩略技术栈(MEAN、MERN、LAMP 等)中的方法——这段时间非常有趣。

我忽略了行业可见度的观念,或者说,我从未想到过个人可以通过他们的工作和知识在一个组织、一个整个领域内展示自己。

我们可以在这里谈谈数字,对吧?

攀登企业阶梯——图像由 MidJourney 生成

我在 4 家公司作为 Web 开发人员工作了 2.5 年,并将薪水从每年£28,000(全职)提升到约£90,000+(承包商)每年。

然后,我被 AI/机器学习的兴趣所感染,不得不回到大学学习该领域的基础知识。当我完成学位后,我决定在实验的下一个阶段以不同的方式进行。

AI 是一个发展中的领域。早在 2018 年,我就意识到,掌握一些复杂的 AI 主题的技术知识,并仅在你的 9–5 工作中应用它,只能带你走这么远。

所以我决定再进行一次实验:我能否在 AI 领域超越作为 Web 开发人员的最高年收入?回答这个问题的最佳方法是以某种形式对外展示我的专业知识和学习历程。我本想告诉你,我曾战略性地计划写一些文章和教授课程以提升我在 AI 领域的可见度,但那将是不真实的。

我的做法并没有经过精心规划。正如我所说,这只是一个实验。

Medium 曾经(并且现在仍然是)一个很好的平台来展示我的技能。

就这样,我第一次体验了行业可见度。

结果

两年零三个月后,毕业获得硕士学位,这个实验成功了。

我作为 AI 从业者在比作为 Web 开发人员更短的时间内达到了更高的收入水平,并且职业转变更少。

如何做到的?

现在和我一起说……行业可见度。

通过在 Medium 上分享我在计算机视觉和机器学习方面的专业知识和学习内容,经过一段时间,我在 9–5 工作之外获得了一些可见度。这种可见度导致了以下结果:

我是这样看的:你投入的努力来传达你的技能和专业知识,获取了一种专业货币,你可以用这种货币去获得未在招聘板上广告的人工智能职位、外部项目的机会、演讲机会以及那些无法通过行业可见性获得的职业利益。

在人工智能行业获得可见性的实用步骤

学习一些东西。构建一些东西。教授一些东西。

无论你是否意识到,你可能在人工智能领域的特定细分领域中拥有专业知识,无论是工具、行业还是领域相关的。

你的专业知识已经让你在职业生涯中达到了一个不错的水平。如果你需要掌握任何新的人工智能相关技能,成长型思维使得这成为一个周末活动。

“网络”,“个人品牌”,“行业领导者”和“思想领袖”等词汇开始越来越频繁地出现。你认识到这是你职业生涯的下一步。

如果我们将与科技行业网络相关的复杂性投射到一个由简单句子表示的较低维度,那就是:‘谁知道你知道你知道什么?

学习一些东西

如果你以某种方式参与人工智能/机器学习/数据领域,你必须不断学习新的工具、技术和概念,以保持相关性和竞争力。

尽管近年来该领域取得了许多重要进展,人工智能仍在发展中。人工智能领域的行业可见性依赖于知识分享或对话题的独特观点。然而,最近出现了大量的“人工智能专家”,他们整理出了一份值得关注的人工智能工具清单

这些人不是“人工智能专家”。他们更像是工具专家……这没关系。

老实说,我并不太担心你学习的能力。

要在人工智能领域拥有长期职业生涯,你需要具备的关键特质是成长型思维

今天,生成性人工智能是年度热词。去年是 MLOps,几年前曾是大数据和物联网。

关键在于,人工智能领域的变化要求从业者学习和重新学习新的想法、概念和技术。为了为下一个变革——可能是量子人工智能或其他东西——做好心理准备,你需要具备成长型思维,并培养与这种思维相伴的习惯。

行动: 这篇文章Ken Jee概述了从业者获取和保持成长心态的步骤。Ken 介绍了一些基本支柱,使从业者能够开始健康地获得成长心态。

建造一些东西

并非所有的 AI 从业者都能构建完整的产品。

团队成员之间的协作— 图片通过 MidJourney 生成

但这个数字每天都在减少,因为软件开发的各个环节之间的障碍正在崩溃。现在,有些 AI 从业者懂得网页开发。对于网页开发者来说,访问 AI 功能只需一次 API 调用。

你可能听说过一个运动。它叫做#buildinpublic。我并不是其中的大参与者,但我却是那些在公共场合构建产品的从业者的观众。

这一运动很简单。它概括了公开产品开发过程的理念,从编写第一行代码到获得第一个用户,所有这些都是为了透明和开放。

公共宣传方面是这一运动的关键部分,通过在社交媒体平台如 LinkedIn 或 Twitter 上定期发布产品开发更新来实现。

作为一个人,你生活中有许多繁琐且手动的方面;而作为一个 AI 从业者,你具备开发软件产品的技能和资源,这些产品可以处理你在日常活动中观察到的一些繁琐和重复的任务。作为数字原住民,你已经拥有社交媒体账户,这些账户充当你声音的放大器,你可以在上面分享你产品开发旅程的片段。

对于在公开场合构建产品的 AI 从业者有哪些好处?

  • 公开构建是一种传播你的技术专长的方法,它超越了仅仅命名工具、库和包的层面;它展示了你所积累的技术知识的实用性。获得下一份工作或获得客户可能会成为由 LinkedIn 帖子引发的一系列事件。

  • 社区是人际社会互动的一个重要方面,无论在线还是离线。公开构建为 AI 从业者提供了一条通向行业可见性和培养个人品牌的道路,其他从业者可以跟随并学习。

还有更多的好处,但这些将在未来的文章中讨论。

行动: 阅读《Build》由 Tony Fadell 著。

这本书让我以一种独特的方式意识到,构建意在被数百、数千甚至数百万人使用的产品,始于从构思之初就以设计为导向的思维。

对一些人来说,这可能听起来很明显。

但对于技术人员来说,我们问自己的是“我们要如何构建”,而不是“我们要构建什么或为什么构建”。

在书的最后,我明白了培养以产品为导向的思维方式所需的步骤,以及一些开发值得做的产品的实用步骤。

教授一些内容

你的声音和知识是提升你在行业中能见度的最佳工具。将这两者结合的一个活动就是教学。

在 AI 领域,教学有多种形式:

  • 在线学习:这是如今最常见的教学形式之一。这种教学方法将课程内容结构化为几分钟或几小时的视频、教程、课程和长篇文章。有时,在线学习是互动的,并且有支持小组。

  • 其他教学方法包括传统讲座和基于项目的学习。

作者对生成式 AI 和计算机视觉的教学同事

你可能在过去已经说服自己不要教学。也许你认为其他更有经验的专业人士已经多次讲解了你想覆盖的主题,你还能添加什么呢?

让我们把这种悲观的自我谈话扔到一边吧。

你拥有其他从业者无法提供的东西:你对某个话题的独特视角。我在 Medium 上表现最好的文章之一教导 AI 从业者 如何阅读研究论文,已有超过 20 万次浏览。而且,你猜怎么着?我不是 AI 研究员;我目前作为机器学习架构师的角色不需要阅读研究论文,但我对阅读技术研究论文的见解却与成千上万的 AI 从业者产生了共鸣。

当然,我多次说服自己不要写这篇文章。

这篇文章强化了 AI/ML 传奇人物 Andrew Ng 在他众多教学讲座中的观点。尽管如此,我对这个话题的见解仍对一些从业者有价值。

不用说,我期待在接下来的几周内看到你如何讲解生成式 AI 的技术概念。如果我激励了你采取行动,请随时发送你创作的文章或视频给我。

告诉别人(额外奖励)

LinkedIn、Instagram、Medium、Twitter、YouTube…Threads👀

选择以上的一种、两种或所有方法。

最关键的是,你需要放大你的声音来启动你的 AI 行业能见度。你的职业和社交档案展示了你的工作和职业努力,向该领域的其他人展示你的知识财富,包括你的热情、好奇心等。

我喜欢构建产品,我使用 Medium 和 LinkedIn 来分享我的旅程。

我对技术(AI)充满热情和好奇,喜欢讨论和学习相关内容。我使用 YouTube 作为与该领域思想领袖和杰出从业者交流的渠道。

我对产品构建的热情和对 AI 领域的好奇心已在多个社交平台上传达,现在,成百上千的人知道我对这个领域的好奇。

在本文早些时候,我提到过网络关系的核心在于谁知道你知道你知道什么? 本节为你提供了一个实际的方法,用于向外界展示你的专业知识、技能和经验。

让我们以 LinkedIn 作为选择的平台,向别人介绍你做的那些了不起的事情。

一开始,我就承认有一些比我更优秀的从业者在 LinkedIn 上表现出色。我只有几千个粉丝。但以下是对我有效的经验。

  • 如果你在 Medium 上有文章,可以从这些文章中提取片段并分享到 LinkedIn。这可以为你旧的和新的文章带来新的流量。查看你文章中读者高亮的部分。被高亮的部分是 Medium 上的读者认为有用或有趣的内容;这些内容很可能也会对 LinkedIn 上的观众产生同样的效果。

  • 批量处理你的 LinkedIn 帖子。我会在一些周六计划几篇我打算在一周或一个月内分享的帖子。这有助于我保持与观众的互动。但老实说,最近我确实有些掉队了。不过,我很高兴从今天开始与你重新投入其中。

  • 对于帖子创意感到困惑?写一些关于你正在学习的内容的帖子,如果你已经采纳了前面提到的成长心态,你将有很多东西可以分享。写一些关于你职业经历、你读过的书籍和你正在收听的播客的帖子。

我在 LinkedIn 上关注的最优秀的 AI 从业者之一是Vin Vashista,他的 LinkedIn Top Voice 徽章和 150,000+粉丝证明了他几乎每天发布的高质量和信息丰富的帖子。

结论

让我们谈谈未来。

AI 行业的关注度和炒作波动是周期性的;任何行业资深人士都可以确认这一点。但在商业关注的高峰和低谷之间,以及耸人听闻的头条新闻背后,从业者如你我有机会在这些领域中奠定基础,建立职业和公众生涯,开辟路径并推动行业发展。

随着我们接近每一个高峰,新的思想领袖、影响者和公司出现在舞台上。而在降入低谷或 AI 寒冬时,能够提供真实价值的人才能保持相关性。

AI 的寒冬可能会来来去去,但真正进步的温暖从未消退。

在 AI 行业的可见性带来了一些责任和承诺;单纯的行业可见性与思想领导的道路相一致,而思想领导还附带了从业者需要提供价值驱动的成果和信誉的额外要求。

这段旅程本身并不容易。

但这是你在意图提升职业发展时一直缺失的。如果你能在追求工业 AI 可见性的过程中保持一致和稳定,所获得的好处是不可估量的。

如果你有一些有趣的观点想要分享,请使用评论区。

感谢阅读。

LinkedIn | Newsletter | YouTube | Support My Writing

这就是为什么你从数据中提取价值的努力没有进展

原文:towardsdatascience.com/heres-why-your-efforts-extract-value-from-data-are-going-nowhere-8e4ffacbdbc0

数据驱动的领导力与职业生涯

行业内对数据设计和数据质量的忽视(以及你可以做些什么)

Cassie KozyrkovTowards Data Science Cassie Kozyrkov

·发表于Towards Data Science ·8 分钟阅读·2023 年 2 月 25 日

--

我最喜欢的解释数据科学数据工程之间区别的方法是:

如果数据科学是“让数据有用”,那么数据工程就是“让数据可用”。

这些学科非常令人兴奋,以至于我们容易超前,忘记在我们能够使数据可用(更不用说有用)之前,我们首先需要制作数据

那么“制作数据”本身呢?

制作优质数据的艺术被严重忽视了。如果你没有数据——没有输入——可以使用,那么你的数据工程师和数据科学家也帮不了你太多。

即使你确实有一些数据,也有可能你忽视了一个重要因素:数据质量。如果你收集的是非常糟糕的数据,忘记从中提取价值吧。与自然界基本定律“垃圾进,垃圾出”作斗争是徒劳的。

作者从文章“为什么企业在机器学习上失败”中引出的人工智能类比

数据在数据科学和人工智能中的角色,就像食材在烹饪中的角色一样。即使你有一个现代化的厨房,也无法拯救你;如果你的食材是垃圾,那你最好放弃。不管你如何切割和处理,你都不可能做出有价值的东西。这就是为什么在你急于进入项目之前,首先需要考虑投资于优质数据。

如果你关心结果,在追求华丽的算法、模型和一系列数据科学家之前,先投资于优质数据。

说到垃圾进垃圾出,你的作者进入这个地方后出来时一模一样。¯_(ツ)_/¯

让我对你做个小小的猜测,亲爱的读者:你对垃圾进垃圾出(GIGO)并不陌生。或者对于那些性格乐观、看到半杯水的人(Q 是指质量),你可能更喜欢称之为 QIQO。你实际上在迫切希望我说一些你没听过的内容,但我仍在以 GIGO 的讨论磨练你的耐性。再一次。是的,我们都已经厌倦了重复 GIGO 原则。我和你一样厌倦。

但请解答我这个难题。如果我们拥有一整支尊重垃圾进垃圾出(GIGO)原则的行业,同时我们也明白设计高质量数据集并非易事,那我们有什么证据表明我们把钱投入到了我们口中的理念中呢?

如果数据质量如此显而易见地重要——毕竟,它是整个数十亿美元数据/人工智能/机器学习/统计/分析大工程的基础——那我们称负责这方面的专业人员为什么呢?这不是一个难题。我只想知道的是:

负责高质量数据集设计、收集、策划和文档工作的人的职位名称是什么?

不幸的是,这可能也是一个技巧性问题。每当我在会议上与数据专家聊天时,我都会尝试把这个问题偷偷问进去。而每次我问他们谁负责他们组织中的数据质量时,他们从未给出任何接近共识的答案。谁的工作是?数据工程师说是数据工程师,统计学家说是统计学家,研究人员说是研究人员,用户体验设计师说是用户体验设计师,产品经理说是产品经理……的确,GIGO 反复出现。数据质量似乎正是那种“每个人的工作”最终却变成没人做的工作,因为它需要技能(!),但似乎没有人故意在投资这些技能,更不用说分享最佳实践了。

数据质量正是那种“每个人的工作”最终却变成没人做的工作。

也许我对数据科学职业的关注稍显过多。如果我只是为了自己的职业发展,我会通过数据江湖术快速赚钱,但我希望数据职业总体上能有所意义,值得被重视,发挥作用,使世界变得比我们发现时更好。因此,当我看到两个最重要的前提条件(数据质量和数据领导力)被忽视时,我感到心碎。

如果{数据质量专业人员 / 数据设计师 / 数据策展人 / 数据收集员 / 数据管家 / 数据集工程师 / 数据卓越专家}这个职业甚至没有名字(看到了吧?)或社区,那么在简历或大学课程中找不到它也就不足为奇了。你的招聘人员会使用什么关键字来搜索候选人?你会用什么面试问题来筛选核心技能?祝好运找到卓越——你的候选人将需要一整套丰富的技能。

你的招聘人员会使用什么关键字来搜索候选人?你会用什么面试问题来筛选核心技能?

首先,我们要认识到我们不是在谈论你小表弟的“数据标注”暑期工作,这种工作涉及无脑的数据录入和/或从一堆烘焙缩略图中选择所有的杯子蛋糕照片和/或逐门逐户进行纸质调查。我提到这一点是因为“这不就是数据标注吗?”这是我被多次以礼貌的关切语气问到的问题。真是低估了整个天才领域。

“这不就是数据标注吗?”不是的。(真是低估了整个天才领域。)

不,我们说的是那种最初设计数据收集过程的人。它至少需要一点用户体验设计,一点决策科学,一点调查设计经验,一些心理学,一点有实地经验的实验社会科学(任何有实际经验的人都会在睡梦中预见费城问题),以及一块统计学培训(尽管你不需要整一个统计学家),再加上扎实的分析经验,大量的领域专业知识,一些项目/程序管理技能,一点数据产品管理经验,以及足够的数据工程背景来考虑大规模的数据收集。这是一种稀有的融合——我们迫切需要一种新的专业化。

要有任何希望建立成熟的数据生态系统,我们必须为新一代专家提供一个良好的环境,让他们的专业技能得到认可。

但在我们争取一个得到良好认可、良好管理和良好回报的数据制作职业之前,我们将陷入困境。那些对这一系列技能有天赋的新兴高手如果贸然投入其中,就像是跳入悬崖。现在,这几乎是个在地下室工作的职位,如果它还能算作工作的话。要有任何希望建立成熟的数据生态系统,我们必须为新一代专家提供一个良好的环境,让他们的专业技能得到认可。

那么能做什么?

认可你的专家

如果你的组织中已经有那些有这些技能和才华的人,尽管历史上被忽视,他们仍在积极承担数据质量工作,你是否在鼓励他们?你是否在培养他们?你是否在奖励他们?我希望你是在。相反,如果你在创建奖励机制来追逐引人注目的MLOps或带有博士头衔的数据科学,你是在自毁(也是在害我们整个行业)。

贡献知识分享

Google 的 People + AI Research (PAIR)团队最近发布了数据卡片手册,以帮助培训社区进行数据设计、数据透明度、数据质量和数据文档最佳实践。我对我们的工作感到非常自豪,并且很高兴这些材料可以免费供大家使用,但还有很多需要学习的东西。如果你也在这条路上,并且热情支持数据卓越,请与世界分享你正在学习的课程。

获取它:bit.ly/datacardsplaybook(图片由Mahima Pushkarna,手册共同创作者,已获得许可)

告诉别人

如果一篇研究论文在森林里落下而没有人使用,它会发出声音吗?从好点子到建立一个卓越的学科是一个漫长的旅程……一个需要所有鼓励和宣传的旅程。如果你相信这一点,并且你能激励甚至一个人认真对待,你将为构建未来发挥重要作用。谢谢你提前传播这个消息。

摘要

我们的社区在庆祝数据科学家方面做得很好。我们在庆祝MLOps数据工程师方面做得不错。但我们在庆祝所有其他数据职业依赖的那些人方面做得很差:那些设计数据收集并负责数据卓越、文档和策展的人。也许我们可以从命名他们(我很乐意听听你的建议)开始,并至少承认他们的重要性。从这里开始,我们会进步到培训他们、雇用他们并欣赏他们的专业技能吗?我真的希望如此。

感谢阅读!怎么样来一个 YouTube 课程?

如果你在这里玩得很开心,并且你正在寻找一个为初学者和专家设计的完整应用 AI 课程,这里有一个我为你的娱乐而制作的课程:

在 YouTube 上享受这个课程这里

P.S. 你是否尝试过多次点击 Medium 上的拍手按钮看看会发生什么? ❤️

寻找动手的 ML/AI 教程?

这里有一些我最喜欢的 10 分钟教程:

别忘了访问数据卡片手册!

获取链接:bit.ly/datacardsplaybook(图片来源:Mahima Pushkarna,手册共同创作者,经授权使用)

虽然该网站强调数据文档和 AI(要抓住那个时代精神),但数据卡片手册远不止于此。这是我所知道的最强大的通用数据设计资源集合。预览:

获取链接:bit.ly/datacardsplaybook(图片来源:Mahima Pushkarna,手册共同创作者,经授权使用)

感谢阅读!怎么样,一门课程?

如果你在这里玩得开心,并且你在寻找一个有趣的、旨在愉悦 AI 初学者和专家的领导力导向课程,这是我为你准备的小东西

课程链接:bit.ly/funaicourse

[## 加入 Medium

阅读 Cassie Kozyrkov(以及 Medium 上的其他数千位作者)的每一个故事。你的会员费直接支持…

kozyrkov.medium.com

P.S. 你是否尝试过在 Medium 上多次点击“点赞”按钮以查看会发生什么? ❤️

喜欢作者?与 Cassie Kozyrkov 联系

成为朋友吧!你可以在TwitterYouTubeSubstackLinkedIn找到我。想让我在你的活动中演讲吗?使用这个表格联系我。

混合整数规划(MIP)模型的启发式方法

原文:towardsdatascience.com/heuristics-as-warm-start-for-mixed-integer-programming-mip-models-9046781dd21f

在 MIP 模型中设置起始解:一个调度应用程序

Bernardo FurtadoTowards Data Science Bernardo Furtado

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

--

图片由 Nils Geldner 提供,来源于 Unsplash

在计算机科学中,启发式方法是用于寻找给定问题的可行解的技术,通常比精确方法快,但不保证最优性。另一方面,精确方法计算成本高,但可以保证最优解。

将问题建模为混合整数规划(MIP)并使用求解器求解可能会给出最终解决方案。通常,这些求解器在后台使用分支定界算法。分支定界(B&B)被认为是一种解决优化问题的精确方法。

正如名字所示,它将解枚举为树状结构(分支)。B&B 和通常称为蛮力的穷举搜索的主要区别在于界限阶段。在过程中,每个节点(解)与解的下界和上界进行比较。如果分支被证明不能比已找到的最佳解更好,则会被剪枝,算法将转向另一个分支。

本文的目标不是详细介绍分支定界算法;更深入的解释可以在 这里 找到。但我们需要定义几个概念:

  • 现有解:在算法的每一步找到的最佳可行解。如果这是一个最小化问题,它是上界。

  • 最佳界限:解的有效下界。

  • Gap (%):最佳上界与现有解之间的差异。如果现有解等于最佳上界,则差距为 0,表明已找到最优解。

先进的求解器有不同的方法来计算任意 MIP 的现有解和最佳上界。但对于求解器而言,我们的 MIP 只是一个方程组和目标函数。既然我们知道问题的确切结构,那么开发一个专门算法并将结果作为现有解提供会不会更有帮助?是的,这就是启发式方法和热启动发挥作用的地方。

排列流车间调度问题是文献中最经典的优化问题之一。它可以简要描述为:给定一组机器 m 和一组作业 n,如何处理这些作业,使得每个作业必须按照相同的顺序经过所有机器。目标是最小化最后一个作业的完成时间。

假设有 3 台机器(M1M2M3)和 3 个作业(J1J2J3)。作业必须按照给定的顺序进行。下图显示了一个可能的可行解,形式为甘特图:

排列流车间的可行解

这个调度的完成时间,或称 Makespan,为 34,解决方案/顺序是 J1J2J3

这个问题可以被表述为 MIP 模型。我们称 m 为机器集合,n 为作业集合。作业必须按照相同的机器顺序 {0,1,…,m} 进行。作业 j 在机器 i 上的处理时间由 p_ij 给出。我们来用 Python 创建这些参数。我们将从 15 个作业和 5 台机器开始。处理时间在 1100 之间随机生成。M 是解的上界。一个好的上界是所有作业在所有机器上的处理时间之和。

import random
import numpy as np

random.seed(10)

# Number of jobs
n = 15
# Number of machines
m = 5
# Max time for random input generator
max_time = 100

# Generate processing times randomly
times = np.zeros((m, n))
tot_time_job = []
for i in range(m):
    for j in range(n):
        times[i][j] = random.randint(1, max_time)
# Total processing time per job - sum across machines
tot_processing_job = np.sum(times, axis=0)

# Upper bound of the solution - sum of transit matrix
M = sum(times[j][i] for i in range(n) for j in range(m))

变量有两组。变量 x_ij 是连续变量,表示作业 j 在机器 i 上的开始时间。变量 y_jk 是二进制变量,如果作业 jk 之前执行,则等于 1,否则为 0Cmax 是调度的 makespan。现在我们用 gurobipy 来定义这些。

opt_model = grb.Model(name="Flow shop scheduling")

# Start time of job j at in machine i
x = {(j, i): opt_model.addVar(vtype=grb.GRB.CONTINUOUS, lb=0, name="x_{0}_{1}".format(j, i))
     for j in range(n) for i in range(m)}

# 1 if job j is executed before job k. 0 otherwise
y = {(j, k): opt_model.addVar(vtype=grb.GRB.BINARY, name="y_{0}_{1}".format(j, k))
     for j in range(n) for k in range(n) if j != k}

# Makespan - Completion time of last job in last machine
c = opt_model.addVar(vtype=grb.GRB.CONTINUOUS, lb=0, name="c")

约束如下所示:

(1) 确保作业 j 在机器 i 上的处理只能在其在机器 i-1 上完成后开始。(2)(3) 是选择约束——如果作业 j 在作业 k 之后执行,则在给定的机器上应该在其完成后开始。(4) 定义了 makespan,即在机器 m(最后一台机器)上最后一个作业的完成时间。

# Job j in machine i can start only when it is finished in machine i-1
c1 = {(j, i): opt_model.addConstr(x[j, i] - x[j, i - 1] >= times[i - 1][j],
                                  name="c1_{0}_{1}".format(j, i))
      for j in range(n) for i in range(1, m)}

# Disjunctive constraints - if job j is after k, it should start after its completion
c2 = {(j, k, i): opt_model.addConstr(x[j, i] - x[k, i] + M * y[j, k] >= times[i][k],
                                     name="c2_{0}_{1}_{2}".format(j, k, i))
      for j in range(n) for k in range(n) for i in range(m) if k != j}

c3 = {(j, k, i): opt_model.addConstr(-x[j, i] + x[k, i] - M * y[j, k] >= times[i][j] - M,
                                     name="c3_{0}_{1}_{2}".format(j, k, i))
      for j in range(n) for k in range(n) for i in range(m) if k != j}

# Makespan is the completion time of last job in last machine
c4 = {j: opt_model.addConstr(c >= x[j, m - 1] + times[m - 1][j],
                             name="c4_{0}".format(j))
      for j in range(n)}

目标函数是最小化 makespan:

opt_model.ModelSense = grb.GRB.MINIMIZE
opt_model.setObjective(c)
opt_model.setParam('MIPGap', 0.018)
opt_model.optimize()

如果我们将 MIP Gap 设置为 1.8%,最佳目标是 832。分支定界算法的收敛情况见下图:

排列流车间示例的收敛曲线

现有解开始时约为 1,200(Gurobi 能找到的第一个解),并收敛到 832。如果我们能找到一个更好的初始解并将其作为现有解会怎样?

NEH 启发式算法是流车间调度问题中最著名的启发式算法之一。它以其作者(Nawaz, Enscore, 和 Ham)命名为 NEH。它是一种构造性启发式算法,因此,它从一个空解开始,并迭代地构建调度,直到所有任务都被分配。

排列流车间问题的解的表示是一个由 n 个元素组成的列表。列表按开始时间排序——列表中的第一个任务是最早执行的任务,而最后一个任务是最晚的任务。第一步是创建一个函数,该函数接收有序列表(一个解)作为输入,并返回与之相关的总工时。

def get_makespan(solution):
    ''' Calculate the makespan of a sequence of integer (jobs).
        - A job can start only after the previous operation of the same job in machine j-1 ends and
            machine is not processing any other job
        - Finish time of a job in a given machine is its start time plus processing time in current machine
    '''
    end_time = np.zeros((m, len(solution) + 1))
    for j in range(1, len(solution) + 1):
        end_time[0][j] = end_time[0][j - 1] + times[0][solution[j - 1]]
    for i in range(1, m):
        for j in range(1, len(solution) + 1):
            end_time[i][j] = max(end_time[i - 1][j], end_time[i][j - 1]) + times[i][solution[j - 1]]
    return end_time

NEH 从按处理时间递减的顺序对任务进行排序开始(所有机器的处理时间之和)。第一次迭代将处理时间最长的任务添加到调度的开头。对于剩余的任务,它尝试将其插入当前解中的所有可能位置,比较每个位置的(部分)总工时,并存储找到的最佳解。这个过程会重复,直到所有任务都被分配。函数如下:

def neh():
    ''' Heuristic NEH (Nawaz, Enscore & Ham) for flow shop scheduling
        1 - Start from an empty schedule
        2 - Add first the job with highest sum of processing time
        3 - Go through the list of the unassigned jobs, test all in all possible positions in the current solutions
        4 - Assign the best job at the best position (with lowest makespan) at the final solution
        5 - Repeat (3) and (4) until there are no job unassigned
    '''
    initial_solution = np.argsort(-tot_processing_job)
    current_solution = [initial_solution[0]]
    for i in range(1, n):
        best_cmax = 99999999
        for j in range(0, i + 1):
            temp_solution = current_solution[:]
            temp_solution.insert(j, initial_solution[i])
            temp_cmax = get_makespan(temp_solution)[m - 1][len(temp_solution)]
            if best_cmax > temp_cmax:
                best_seq = temp_solution
                best_cmax = temp_cmax
        current_solution = best_seq
    return current_solution, get_makespan(current_solution)[m - 1][n]

NEH 找到的结果的总工时为 891,比 Gurobi 作为第一个现有解找到的 1,200 要好得多。

如果我们想从现有解开始,Gurobi 的 B&B 方法可以跳过许多解较高于 891 的分支。如下面的图表所示:

对比 NEH 解的排列流车间示例的收敛曲线

在 Gurobi 中使用热启动相对容易。唯一的挑战是将启发式返回的数据结构转换为 MIP 变量的值。对于我们的问题,我们必须将有序列表和总工时值转换为 yxc 的变量值。

cmax_heuristicc 变量具有相同的含义,因此唯一的任务是分配变量的初始值。

c.Start = cmax_heuristic

将有序列表(sequence_heuristic)转换为变量 y 也很简单。需要遍历列表,如果任务 j 在列表中排在任务 k 之前,则 y_jk = 1。否则为 0

for j in range(n):
   for k in range(n):
       if j != k:
           y[j, k].Start = 0
for i in range(0, len(sequence_heuristic) - 1):
   for j in range(i + 1, len(sequence_heuristic)):
       j1 = sequence_heuristic[i]
       j2 = sequence_heuristic[j]
       y[j1, j2].Start = 1

我们不需要分配变量 x 的值。Gurobi 可以从约束条件中推断出它们的值。一旦模型执行,用户将能够看到以下消息:

这意味着解是可行的,其目标是 891,并将作为 MIP 开始。那如果我们尝试插入一个不可行的 MIP 开始呢?例如,如果我们尝试将所有 i 和所有 jy_ij = 0 作为开始。当然,这是不可行的,因为所有任务都必须在调度中。在这种情况下,用户将看到以下消息:

具有一个能为MIP找到良好起点的启发式方法并不是MIP启动功能的唯一用途。还有其他几个用途:

  • 在实际问题中,用户可以用现实中发生的场景来为模型提供数据。在这种情况下,基准将是模型的起始点。例如,如果一家公司想要为其路线过程创建一个模型,并且有历史数据可用,那么一天运营所采取的路线可以作为MIP启动点,前提是它符合数学模型中给定的约束条件。这也是一个很好的练习,用于检查设计的约束在现实中是否得到遵守。

  • 正如已解释的,对于求解器而言,我们的模型只是一些方程。如果用户不想花时间开发启发式方法,使用非常简单/天真的方法也可能有帮助。例如,按索引排序的作业列表是一个可行解([1,2,3,..,n]),如果求解器在寻找初始点时遇到困难,这可能会很有用。

  • 如果有一个非常好的已知解,并且想要证明它是否最优,或者离最优解有多远。

最后但同样重要的是,不能保证即使MIP启动比求解器的初始解更好,也会改善收敛性和/或运行时间。这是因为求解器会走一条与原始不同的路径。我们之前展示的案例就是一个很好的例子。使用MIP启动的MIP收敛曲线如下所示:

使用 NEH 作为起点的排列流车间示例的收敛曲线

它确实从一个更好的解开始,但会卡在接近891的值附近。

最后,MIP启动是一个非常有用的资源,几乎所有商业求解器都有这一功能。但它的价值在很大程度上取决于问题、数据集以及开发一个好的启发式方法的时间。

除非另有说明,所有图片均由作者提供。完整代码可在GitHub获取。

嘿,GPU,我的矩阵怎么了?

原文:towardsdatascience.com/hey-gpu-whats-up-with-my-matrix-cb7f6d7ae7d6

了解 GPU 如何执行矩阵乘法的温和指南

Thushan GanegedaraTowards Data Science Thushan Ganegedara

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

--

图片来源:Thomas FosterUnsplash

矩阵乘法;深度神经网络和现代语言理解巨头的圣杯。作为 MLE 或数据科学家,我们的手指太快了,以至于只需敲入 tf.matmultorch.matmul,然后从未回头。但不要告诉我你从未对矩阵进入 GPU 时可能发生的情况感到过一丝兴奋!如果你有,那你来对地方了。跟随我,一起探索 GPU 内部那些迷人的复杂性。

我将向你解释这些计算强者是如何处理数字的。你将了解 GPU 在面对矩阵时做的三件鲜为人知的令人印象深刻的事情。在本文结束时,你将对 GPU 内部的矩阵乘法有一个清晰的理解。

GEMM:一个真正的💎为 GPU 所用

GEMM 或广义矩阵乘法是当 GPU 执行矩阵乘法时运行的内核。

C = a (A.B) + b C

在这里,ab 是标量,A 是一个 MxK 矩阵,B 是一个 KxN 矩阵,因此 C 是一个 MxN 矩阵。就是这么简单!你可能会好奇为什么会有那一个额外的加法。事实证明,这是一种在神经网络中非常常见的模式(例如,添加偏置、应用 ReLU、添加残差连接)。

技巧 #1:外积的效果出乎意料👽

如果你被要求从基本原理出发编写一个矩阵乘法算法,这里是你要做的(除非你天生聪慧,能用 GPU 代替大脑——那样不就能为 MLE 省下不少钱了!)。

for (int i = 0; i < M; ++i)
    for (int j = 0; j < N; ++j)
        for (int k = 0; k < K; ++k) 
            C[i][j] += A[i][k] * B[k][j];

这里有一个动画视觉效果,可以展示这是什么效果。

基于内积的两个矩阵的乘法(由作者重现 — 灵感来源:www.adityaagrawal.net/blog/architecture/matrix_multiplication

但你知道 GPU 讨厌这种实现吗🤔?要理解原因,你需要了解 GPU 内存架构,

所有比较和规格,我将使用 Nvidia A100 GPU 规格。

GPU 有三个主要的内存级别,

  • 全局内存或 HBM(通常指的 GPU 内存,以及你在运行nvidia-smi时看到的内存)

  • 共享内存(专门分配给一个流式多处理器[或 SM]的本地内存,并在该 SM 中运行的线程之间共享)

  • 寄存器(分配给线程以执行其工作负载)

这就是它的样子,

GPU 的典型内存层次结构(为了简便,忽略 L0/L1/L2 缓存)

首先要注意的是,共享内存(从现在起称为 SRAM)比 HBM 小得多,更不用说寄存器了。所以你的矩阵不可能适合其中(在大多数情况下)。如果我们回到我们的动画,对于A的一行,需要检索B的所有列,并对A中的所有行重复此过程。这意味着,GPU 需要进行大量读取来计算输出。HBM(约 1.5TB/s)比 SRAM(约 19TB/s)慢几个数量级。

用数字来说,假设你想乘以一个10x2020x30的矩阵,你需要读取B的列10x30=300次。有没有更好的方法呢?

事实证明一个简单的技巧能大有帮助!只需翻转循环的顺序,使k成为最外层的循环。就完成了!😮

for (int k = 0; k < K; ++k) 
    for (int i = 0; i < M; ++i)
        for (int j = 0; j < N; ++j)
            C[i][j] += A[i][k] * B[k][j];

我们没有触及实际计算,只是调整了循环的顺序,因此结果应该和之前一样。现在矩阵乘法看起来是这样的!

基于外积的两个矩阵的乘法(由作者重现 — 灵感来源:www.adityaagrawal.net/blog/architecture/matrix_multiplication

你看,我们一次只带来A的一个B的一个,并且不会回头看。这需要的读取次数比原始实现少得多。唯一的区别是,我们之前计算的是两个向量之间的内积,现在我们计算的是外积

绿色显示的两个向量(蓝色和黄色)之间的内积和外积的区别。

但仍然,我们需要整个C在 SRAM 中,这可能太大而无法容纳在 SRAM 中。那么 CUDA 是怎么做的呢?这就引出了第二个技巧。

技巧 #2:分而治之(并累积)

不用担心!我不会用复杂的数学或 Leetcode 算法轰炸你。主要要记住的是,矩阵是单独瓦片的二维布局。下面的动画很好地展示了我试图解释的内容。

你可以迭代 A 和 B 中的每个块,仍然可以计算 C 对应块的准确答案

绿色块💚的结果是浅蓝色的 A 条💙和浅黄色的 B 条💛。更进一步,为了计算输出,你可以一次带来一个 A 条的块和一个 B 条的块,计算输出并将结果累积在绿色框中。

这为我们提供了一个灵活的框架,我们可以加载任意大小的 A 和 B 的块(或瓦片),仍然能够计算最终的答案。我们不必止步于此,可以继续递归地将问题划分为更小的问题。即矩阵被拆分成瓦片,瓦片被拆分成片段,片段进一步拆分成单个值。

使用瓦片方法,问题可以递归地拆解。

这很好地适用于 GPU 中的过程执行架构。GPU 中的内核执行有三层。为了简化,我们说一个 SM 运行一个线程块(虽然实际上它们是并行执行的,以减少被称为尾效应的影响)。

  • 线程

  • Warps(32 个线程的集合)

  • 线程块(几个 warp 的集合)

线程块中的确切线程数量取决于特定的架构。例如,A100 具有以下规格

  • 每个 SM 最大支持 2048 个线程

  • 每个块最大支持 1024 个线程

  • 每个 SM 最大支持 32 个线程块

侧边栏 #2:2 的幂的魔力

回到瓦片方法,已发现(启发式地)每个线程块的矩阵瓦片大小为256x128在大多数问题中能提供合理的效率。因此,这是 CUDA 常用的瓦片大小。

你可能听说过将批处理大小、隐藏维度大小保持为 2 的幂的最佳实践。这就是来源!当你的矩阵维度是 2 的幂时,它将被完全划分为一组没有余数的瓦片。如果不是,这会使你的代码效率更低

当你的矩阵维度是 2 的幂时,GPU 计算会更高效

当不是 2 的幂时会发生什么?

侧边栏 #2:瓦片量化

这会导致一种叫做 tile quantization 的现象。换句话说,如果你的 tile 行维度为 128,但矩阵的行中有 257 个元素,你需要的不仅仅是两个,而是三个 tiles(即 256+1)。如下所示。

仅仅因为行中多了一个元素,我们不得不分配两个完整的线程块。

问题在于,无论线程块中有用的数据如何,线程块执行的计算量是相同的。因此,你会把执行有用计算的机会从 GPU 中拿走,导致效率低下。

类似的现象称为 wave 量化,即矩阵过大,SMs 无法一次性容纳。这时 GPU 需要进行 2 次“波”。不过,这对现代 GPU 影响较小,因为它们利用并发性来减少 wave 量化。

当线程块需要部分溢出数据时,就会发生 tile 量化;当 SMs 需要溢出数据时,就会发生 wave 量化。

技巧 #3:一个比两个好

最终的技巧是内核融合。通常情况下,将所有计算放在一个内核中要比一个接一个调用两个内核要快。为什么?因为一个内核需要将数据写入 HBM,另一个需要读取数据。我们已经讨论过这种操作有多慢。更好的方法是将两个操作合并为一个。以下是一些示例,

正如这里所看到的(我相信 Pytorch 也有类似的术语表),TensorFlow 提供了许多融合的内核,将便捷的操作合并到一个内核中。在代码中,这意味着类似于以下内容,

for (int m = 0; m < M; m += Mtile) 
    for (int n = 0; n < N; n += Ntile)
        for (int k = 0; k < K; ++k)
            float tmp = 0
            for (int i = 0; i < Mtile; ++i)
                for (int j = 0; j < Ntile; ++j) 
                    int row = m + i;
                    int col = n + j;
                    tmp += A[row][k] * B[k][col];
                    // Do other things
                    C[row][col] = tmp

换句话说,我们会珍惜我们的 tmp 变量,直到完成所有计算后才将结果写回 C

结论

就这些了,希望这次通过 GPU 的“丛林探险”对你有所帮助。如果你对音频视觉版本感兴趣,这里是我的 YouTube 视频的链接。

总结一下,我们讨论了三件使 GPU 在矩阵乘法中非常快速的事情。

  • GPUs 摒弃了更友好的内积实现,转而采用更高效的外积实现。

  • GPUs 将矩阵分割成更小的块(块再分割成碎片),并将计算负载分配到线程块、warp 和线程上。

  • GPUs 使用内核融合将常见的功能组合在一起,提高 GPU 效率。

如果你喜欢这个故事,可以随时订阅Medium,这样你就会收到我的最新内容通知,并且能够解锁来自其他作者的成千上万的优质故事的完整访问权限。

[## 通过我的推荐链接加入 Medium - Thushan Ganegedara

作为 Medium 会员,你的一部分会员费用将会支持你阅读的作者,并且你可以完全访问每一篇故事…

thushv89.medium.com/membership?source=post_page-----cb7f6d7ae7d6--------------------------------

除非另有说明,否则所有图片均由作者提供

参考文献:

隐藏马尔可夫模型:通过实际例子和 Python 代码进行解释

原文:towardsdatascience.com/hidden-markov-models-explained-with-a-real-life-example-and-python-code-2df2a7956d65?source=collection_archive---------0-----------------------#2023-11-05

Carolina BentoTowards Data Science Carolina Bento

·

关注 发表在 Towards Data Science ·14 分钟阅读·2023 年 11 月 5 日

--

作者提供的图片

隐藏马尔可夫模型是用于解决现实生活中的问题的概率模型,问题范围从每个人每周至少考虑一次的简单问题——明天的天气会如何?[1] — 到复杂的分子生物学问题,例如预测肽与人类 MHC II 类分子的结合位点[2]。

隐藏马尔可夫模型是马尔可夫链的近亲,但其隐藏状态使其成为确定随机变量序列概率时的独特工具。

在本文中,我们将拆解隐马尔可夫模型的所有不同组件,并一步步展示数学和 Python 代码,看看哪些情感状态导致了你狗狗在训练考试中的结果。我们将使用维特比算法来确定观察到特定观测序列的概率,并展示如何使用前向算法来确定观察到序列的可能性,当你给出一个隐藏状态序列时。

现实世界充满了这样的现象:我们可以看到最终结果,但无法实际观察生成这些结果的潜在因素。例如,根据过去的天气观测和不同天气结果的观察概率来预测天气,确定明天是雨天还是晴天。

尽管受我们无法观察的因素驱动,利用隐马尔可夫模型可以将这些现象建模为概率系统。

隐藏状态的马尔可夫模型

隐马尔可夫模型,简称 HMM,是一种统计模型,作为一系列标记问题进行工作。这些问题描述了可观察事件的演变,而这些事件本身依赖于无法直接观察的内部因素——它们是隐藏的[3]。

隐马尔可夫模型由两个不同的随机过程组成,这些过程可以定义为随机变量的序列——这些变量依赖于随机事件。

存在隐形过程可观察过程

隐形过程是一个马尔可夫链,如同将多个隐藏状态串联在一起,随着时间的推移以达到某个结果。这是一个概率过程,因为马尔可夫链的所有参数以及每个序列的得分实际上都是概率[4]。

隐马尔可夫模型描述了可观察事件的演变,这些事件本身依赖于无法直接观察的内部因素——它们是隐藏的[3]。

就像其他马尔可夫链一样,为了知道你接下来会进入哪个状态,唯一重要的因素是你现在的位置——即你目前处于马尔可夫链的哪个状态。你过去的历史状态对于理解你接下来要去的地方并没有意义。

这种短期记忆是 HMM 的关键特征之一,称为马尔可夫假设,表示到达下一个状态的概率仅依赖于当前状态的概率。

马尔可夫假设。(作者提供的图像)

HMM 的另一个关键特征是,它还假设每次观察仅依赖于产生该观察的状态,因此完全独立于链中的其他状态[5]。

马尔可夫假设表明,达到下一个状态的概率仅依赖于当前状态的概率。

这些都是关于 HMM 的极好背景信息,但它们实际上用于哪些问题类别?

HMM 帮助建模现象的行为。除了建模和允许进行模拟,你还可以提出关于这些现象的不同类型的问题:

  • 似然性评分,即确定观察到序列的概率

  • 解码生成特定观察的最佳状态序列

  • 学习 HMM 参数,这些参数用于观察到特定序列,该序列遍历了特定的状态集合。

让我们看看实际操作吧!

作为 HMM 的狗训练表现

今天你不太担心天气预报,思考的重点是你的狗是否可能从训练课程中毕业。经过所有的时间、精力和狗狗零食,你只希望它们能成功。

在狗狗训练过程中,你的四脚朋友会做一些动作或把戏,教练可以观察并评分它们的表现。通过合并三次试验的分数,他们会判断你的狗是否毕业或是否需要额外的训练。

教练只看到结果,但涉及到的几个因素是无法直接观察的,例如你的狗是否疲倦、快乐、是否不喜欢教练或周围的其他狗。

这些状态都无法直接观察,除非你的狗在特定情绪下做出明显的行动。如果它们能用语言表达自己的感受,那就太好了,也许未来能实现!

在脑海中还新鲜的隐马尔可夫模型,这看起来是预测你狗在考试期间感觉如何的绝佳机会。它们可能因为感到疲倦、饥饿或对教练感到烦恼而获得某个分数。

你的狗已经接受了一段时间的训练,基于训练过程中收集的数据,你拥有了建立隐马尔可夫模型所需的所有基础构件。

为了建立一个建模你狗在训练评估中表现的 HMM,你需要:

  • 隐藏状态

  • 转移矩阵

  • 观察序列

  • 观察似然矩阵

  • 初始概率分布

隐藏状态是那些影响观察序列的不可观察因素。你只会考虑你的狗是疲倦还是快乐。

HMM 中的不同隐藏状态。(作者提供的图像)

了解你的狗后,非可观察因素可能影响它们的考试表现,仅仅是疲劳或快乐。

接下来你需要知道从一个状态转移到另一个状态的概率,这些信息被记录在转移矩阵中。这个矩阵也必须是行随机矩阵,意味着链中从一个状态到任何其他状态的概率,每一行的总和必须为一。

转移矩阵:表示从一个状态转移到另一个状态的概率。(图像来源:作者)

不论你解决的是什么类型的问题,你总是需要一个观测序列。每个观测值表示在马尔可夫链中遍历的结果。每个观测值都是从特定的词汇表中抽取的。

词汇表(图像来源:作者)

在你狗的考试中,你会观察到每次试验后的得分,这可能是失败OK完美。这些都是观测词汇表中的所有可能术语

你还需要观测似然矩阵,即从特定状态生成观测值的概率。

观测似然矩阵。(图像来源:作者)

最后是初始概率分布。这是马尔可夫链在每个特定隐藏状态下开始的概率。

在马尔可夫链中,有些状态可能永远不会成为起始状态。在这些情况下,它们的初始概率为零。就像转移矩阵中的概率一样,这些初始概率的总和也必须加起来为一。

初始概率(图像来源:作者)

初始概率分布,加上转移矩阵和观测似然矩阵,组成了隐马尔可夫模型的参数。这些是你在拥有观测序列和隐藏状态的情况下,需要弄清楚的概率,并尝试学习哪个特定的 HMM 可能生成了这些数据。

将这些部分组合在一起,这就是表示你狗在训练考试中表现的隐马尔可夫模型的样子。

隐藏状态以及它们之间的转移概率。(图像来源:作者)

在考试期间,你的狗将进行三次试验,只有在这三次试验中不失败两次,才能毕业。

到头来,如果你的狗需要更多训练,你仍然会照顾它。你脑海中的大问题是它们在考试期间感觉如何

想象一个场景,如果它们以OK — Fail — Perfect的顺序毕业,它们将处于什么样的情感状态?它们会大多数时间感到疲惫,还是饥饿,或者两者的混合?

这种问题正好属于 HMM 可以应用的解码问题。在这种情况下,你希望找出生成特定观察序列OK — Fail — Perfect的最佳状态序列。

解码生成给定观察序列的状态序列的问题利用了维特比算法。然而,值得稍微绕个弯子,了解一下如何使用前向算法计算给定观察序列的概率——这是一项似然任务。这将为更好地理解维特比算法的工作原理奠定基础。

前向算法

如果你将这个问题建模为常规的马尔可夫链,并希望计算观察序列OK, Fail, Perfect的似然,你需要通过每个特定的状态生成期望的结果。在每一步,你会根据前一个观察结果的条件概率来观察当前结果,并将该概率乘以从一个状态到另一个状态的转移概率。

大的区别在于,在常规的马尔可夫链中,所有状态都是已知和可观察的。但在隐马尔可夫模型中却不是这样!在隐马尔可夫模型中,你观察到的是一个结果序列,而不知道为了观察到这个序列需要经过哪个特定的隐藏状态序列。

大的区别在于,在常规的马尔可夫链中,所有状态都是已知和可观察的。但在隐马尔可夫模型中却不是这样!

此时你可能会想,好吧,我可以简单地遍历所有可能的路径,然后最终有一个规则来选择等效路径。 这种方法的数学定义如下所示。

计算观察结果序列的概率,遍历所有可能的隐藏状态序列。(图片来自作者)

这肯定是一种策略!你需要计算观察到序列OK, Fail, Perfect的概率,考虑所有可能生成该序列的隐藏状态组合。

当隐藏状态和观察结果序列的数量足够小时,可以在合理的时间内完成计算。

HMM 中可能路径的概述(图片来自作者)

值得庆幸的是,你刚定义的隐马尔可夫模型相对简单,包含 3 个观察结果和 2 个隐藏状态。

对于长度为 L 的观察序列,在具有 M 个隐藏状态的 HMM 中,你有“M 的 L 次方”种可能的状态,在你的例子中,这意味着2 的 3 次方,即 8 条可能的路径来生成序列OK — Fail — Perfect,涉及到的计算复杂度为 O(M^L L),在大 O 符号中描述。随着模型复杂度的增加,你需要考虑的路径数量呈指数级增长。

随着模型复杂度的增加,你需要考虑的路径数量呈指数级增长。

这就是前向算法的亮点所在。

前向算法计算观察序列中新符号的概率,而无需计算形成该序列的所有可能路径的概率[3]。

算法定义了前向变量,而不是计算所有形成该序列的可能路径的概率,前向变量的值是通过递归计算的。

前向变量是如何递归计算的。(图片来源:作者)

它使用递归是该算法比计算所有可能路径的概率更快的关键原因。实际上,它可以在仅需“L 乘以 M 平方”的计算中计算观察到序列x的概率,而不是“M 的 L 次方乘以 L”。

在你的例子中,使用 2 个隐藏状态和 3 个观察结果的序列,计算概率的次数为 O(MˆL L) = 2³x3 = 8x3 = 24 次,而不是 O(L Mˆ2)=3*2²=3x4 = 12 次。

这种减少计算次数的方式是通过动态规划实现的,这是一种编程技术,使用辅助数据结构存储中间信息,从而确保不会多次进行相同的计算。

每次算法要计算新概率时,它会检查是否已经计算过,如果是,它可以轻松地在中间数据结构中访问该值。否则,计算概率并存储该值。

回到你的解码问题,使用维特比算法。

维特比算法

伪代码的思维方式,如果你要用暴力破解的方法解码生成特定观察序列的隐藏状态序列,你只需:

  • 生成所有可能的路径排列,以达到期望的观察序列

  • 使用前向算法计算每个观察序列的可能性,对于每个可能的隐藏状态序列

  • 选择概率最高的隐藏状态序列

所有可能的隐藏状态序列生成观察序列OK — Fail — Perfect(图片来源:作者)

对于你的特定 HMM,有 8 条可能的路径可以得到OK — Fail — Perfect的结果。再添加一个观察值,你将会有双倍数量的隐藏状态序列!与前向算法所描述的类似,你很容易会遇到指数级复杂度的算法,并达到性能瓶颈。

Viterbi 算法可以帮助你解决这个问题。

当 HMM 中的隐藏状态序列被遍历时,在每一步,概率vt(j)是 HMM 在观察后处于隐藏状态j的概率,并且是通过最可能的状态到达j的。

Viterbi 路径到隐藏状态j在时间步t。(图像来源:作者)

解码生成特定观察序列的隐藏状态序列的关键是最可能路径的概念。也称为Viterbi 路径,最可能路径是从所有可能的路径中,具有最高概率的路径。

解码生成特定观察序列的隐藏状态序列的关键是使用 Viterbi 路径。最可能的路径通向任何给定的隐藏状态。

你可以将前向算法和 Viterbi 算法进行对比。前向算法通过将所有概率相加来获得达到某个状态的可能性,考虑所有到达该状态的路径,而 Viterbi 算法则不想探索所有可能性。它专注于通向任何给定状态的最可能路径。

回到解码隐藏状态序列的任务,以获得考试中 OK — Fail — Perfect 的分数,手动运行Viterbi 算法将会像这样:

Viterbi 路径和解码序列。(图像来源:作者)

Viterbi 算法的另一个独特特点是它必须有一种方法来跟踪所有到达任何给定隐藏状态的路径,以便比较它们的概率。为此,它使用动态规划算法的典型辅助数据结构来跟踪回溯指针到每个隐藏状态。这样,它可以轻松访问过去遍历的任何 Viterbi 路径的概率。

回溯指针是确定通向观察序列的最可能路径的关键。

在你狗的考试示例中,当你计算 Viterbi 路径v3(Happy)v3(Tired)时,你选择概率最高的路径,然后开始向后遍历,即回溯,通过所有到达你所在位置的路径。

Python 中的 Viterbi 算法

手动完成这些工作既费时又容易出错。错过一个重要的数字,你可能需要从头开始并重新检查所有的概率!

好消息是,你可以利用像 hmmlearn 这样的软件库,只需几行代码就可以解码隐藏状态的序列,使你的狗在试验中获得OK — Fail — Perfect,并且按照这个顺序。

from hmmlearn import hmm
import numpy as np

## Part 1\. Generating a HMM with specific parameters and simulating the exam
print("Setup HMM model with parameters")
# init_params are the parameters used to initialize the model for training
# s -> start probability
# t -> transition probabilities
# e -> emission probabilities
model = hmm.CategoricalHMM(n_components=2, random_state=425, init_params='ste')

# initial probabilities
# probability of starting in the Tired state = 0
# probability of starting in the Happy state = 1
initial_distribution = np.array([0.1, 0.9])
model.startprob_ = initial_distribution

print("Step 1\. Complete - Defined Initial Distribution")

# transition probabilities
#        tired    happy
# tired   0.4      0.6
# happy   0.2      0.8

transition_distribution = np.array([[0.4, 0.6], [0.2, 0.8]])
model.transmat_ = transition_distribution
print("Step 2\. Complete - Defined Transition Matrix")

# observation probabilities
#        Fail    OK      Perfect
# tired   0.3    0.5       0.2
# happy   0.1    0.5       0.4

observation_probability_matrix = np.array([[0.3, 0.5, 0.2], [0.1, 0.5, 0.4]])
model.emissionprob_ = observation_probability_matrix
print("Step 3\. Complete - Defined Observation Probability Matrix")

# simulate performing 100,000 trials, i.e., aptitude tests
trials, simulated_states = model.sample(100000)

# Output a sample of the simulated trials
# 0 -> Fail
# 1 -> OK
# 2 -> Perfect
print("\nSample of Simulated Trials - Based on Model Parameters")
print(trials[:10])

## Part 2 - Decoding the hidden state sequence that leads
## to an observation sequence of OK - Fail - Perfect

# split our data into training and test sets (50/50 split)
X_train = trials[:trials.shape[0] // 2]
X_test = trials[trials.shape[0] // 2:]

model.fit(X_train)

# the exam had 3 trials and your dog had the following score: OK, Fail, Perfect (1, 0 , 2)
exam_observations = [[1, 0, 2]]
predicted_states = model.predict(X=[[1, 0, 2]])
print("Predict the Hidden State Transitions that were being the exam scores OK, Fail, Perfect: \n 0 -> Tired , "
      "1 -> Happy")
print(predicted_states)

几秒钟内你就能得到一个与手工计算结果匹配的输出,速度更快,且错误空间更小。

运行上述代码的输出结果。(图片由作者提供)

结论

隐马尔可夫模型令人着迷之处在于,这一统计工具诞生于 20 世纪 60 年代中期[6],却如此强大,并且在从天气预报到找出句子中的下一个词等不同领域中应用广泛。

在这篇文章中,你有机会了解 HMM 的不同组件,它们如何应用于不同类型的任务,以及如何发现前向算法和维特比算法之间的相似性。这两个非常相似的算法使用动态规划来处理涉及的指数级计算。

无论是手工计算还是将参数输入到 TensorFlow 代码中,希望你享受深入探索 HMM 世界的过程。

感谢阅读!

参考文献

  1. D. Khiatani 和 U. Ghose,“使用隐马尔可夫模型进行天气预报”,2017 年国际智能国家计算与通信技术会议(IC3TSN),印度古尔冈,2017 年,第 220–225 页,doi: 10.1109/IC3TSN.2017.8284480。

  2. Noguchi H, Kato R, Hanai T, Matsubara Y, Honda H, Brusic V, Kobayashi T. 基于隐马尔可夫模型的抗原肽预测,这些抗原肽与 MHC II 类分子相互作用。《生物科学与生物工程杂志》。2002;94(3):264–70。doi: 10.1263/jbb.94.264。PMID: 16233301。

  3. Yoon BJ. 隐马尔可夫模型及其在生物序列分析中的应用。《当前基因组学》。2009 年 9 月;10(6):402–15。doi: 10.2174/138920209789177575。PMID: 20190955;PMCID: PMC2766791。

  4. Eddy, S. 什么是隐马尔可夫模型?自然生物技术 22,1315–1316(2004)。doi.org/10.1038/nbt1004-1315

  5. Jurafsky, Dan 和 Martin, James H.,《语音与语言处理:自然语言处理、计算语言学和语音识别导论》。上萨德尔河,新泽西州:Pearson Prentice Hall,2009。

  6. Baum, Leonard E., 和 Ted Petrie。“有限状态马尔可夫链的概率函数的统计推断。《数学统计年鉴》 37, no. 6 (1966): 1554–63。

街道名称中的隐藏模式:数据科学故事 [第一部分]

原文:towardsdatascience.com/hidden-patterns-in-street-names-a-data-science-story-part-1-82c8dd130693?source=collection_archive---------4-----------------------#2023-01-29

Dea BardhoshiTowards Data Science Dea Bardhoshi

·

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

--

照片由 Alexandr Bormotin 拍摄,来源于 Unsplash

你好!

最近我花了一些时间编制数据集,并阅读了关于我祖国阿尔巴尼亚的研究,试图从数据驱动的角度理解它。除了我自己的经验,我还希望有一种系统的方法来分析该国的社会、政治和生活背景。最近引起我好奇的一个方面是街道名称及其模式。在网上查找时,我找不到一个现有的数据集来查看该国的道路和街道是如何命名的,所以我决定创建并分析一个数据集。

对于这个故事,我将使用 Open Street Map 数据来分析阿尔巴尼亚首都地拉那的街道名称中的性别分布,以及其他历史或地理空间模式。为此,我还会使用pandasseaborn和一个名为contextily的地图绘制库来制作更美观的地图。第一部分将重点关注性别和街道名称,但我会在接下来的几周内继续研究第二部分,关注活动年份以及贡献区域。

那么,开始吧!

数据标注与可视化

这是 Open Street Map 提供的数据的一个快照,指定了一组坐标以覆盖整个地拉那区域:

图片由作者提供

数据包含 26950 行,大多数行有几何形状和其他关联的道路类型属性。有趣的是,同一条街道在数据中出现了多次,可能是因为同一条街道的不同部分在不同时间被映射。由于数据中没有分类名字性别的变量,并且没有简单的方法通过代码将阿尔巴尼亚语的名字与特定性别匹配,我利用自己对语言的知识手动标记了大约一半的数据框的行。该列包含以下标签之一:

  • W = 女性名字

  • M = 男性名字

  • O = 其他类型的名字(例如历史事件、抽象概念或著名家族的姓氏)

这里是每个性别的计数情况:

图片由作者提供

如你所见,女性街道名称在地拉那的街道名称中占约 3.3%,而男性名称则占约 71%。这种情况在阿尔巴尼亚并非独特:巴黎有 2%的街道以女性命名,而在罗马这个比例是 3.5%。在这些主要城市中,部分原因是制定这些决定的市政委员会历史上往往是男性和白人的主导。事实上,全球地方政府成员的平均女性比例仅为36%,许多国家甚至远未达到这一值,例如只有约 25 个国家在地方政府中有 40%的女性代表。尽管如此,最近的地拉那市议会立法机构的男女比例为 50–50%,这是朝着正确方向迈出的一步。

街道类型和长度

OpenStreetMap 数据还包括描述每条街道类型的标签,以下是按性别比较这些类型的可视化图:

作者提供的图片

有趣的是,存在如此多的“住宅”街道。根据 OpenStreetMap 的定义,住宅标签是“用于提供访问或在住宅区内的道路,但通常不作为通行路线使用”。“生活”街道也非常常见,定义为“具有较低的速度限制,相较于使用住宅标签的街道,有特殊的交通和停车规则”。这些是地拉那街道中主导的两种类型,即使它们不是主要街道,而是较窄且交通较少的街道。因此,研究这些街道如何命名可能会很有趣。

让我们看看按性别分组的所有街道总长度。为此,我将线串几何体投影到使用米的投影坐标系统中,并按性别平均了它们的长度:

作者提供的图片

有趣的是,“其他”街道的平均长度最长,而以女性命名的街道略短于以男性命名的街道。还发现“其他”街道多位于远离地拉那城市核心的高速公路或外围街道上:

作者提供的图片

这可以通过高速公路或其他城市间街道的名称来解释,如“Tirane-Durres”,它标示了它们连接的两个城市中心,或者仅仅是“SH1”这样的高速公路代码。鉴于此,我将这些街道标记为“其他”。

这里是以男性和女性命名的街道地图:

以下是自定义地图的示例代码

以男性命名的街道地图(图像由作者提供)

以女性命名的街道地图(图像由作者提供)

职业和工作

每位女性的贡献领域是什么?我将职业分为几个类别,结果如下(有些人物在网上没有可用的信息):

我的分类大致为:

  • 艺术、教师/作家/研究员、政治、人道主义和宗教与战争(为提供一些背景,许多这些女性在二战中与男性并肩作战,她们代表了“战争”类别中大多数女性):

图像由作者提供

邻里

除了贡献领域,街道名称在不同邻里中的分布是否存在模式?地拉那有 14 个行政区域,将城市划分为不同的区域。使用来自 OpenStreetMaps 的 GeoJSON 文件,该文件显示了这些区域的多边形,我们可以将这个数据集与街道名称的数据集进行空间连接。这是 14 个区域的地图:

创建下图的代码,带有用户定义的图例

我对每个行政区域中街道名称中女性的比例感兴趣:

有一些有趣的发现(至少对我来说 😃 )。女性名字的比例存在一些差异,如第 4 区的街道中几乎有 10%以女性命名,而第 8 区仅有 0.08%。另一方面,两个区域(12、14)没有以女性命名的街道。再次说,探讨这些命名选择背后的决策过程将是有趣的。

结论

总结来说,这个故事探讨了地拉那(阿尔巴尼亚)的街道命名情况,重点关注了性别构成以及城市不同区域和贡献领域的模式。稍后的第二部分将会讨论街道名称中代表的历史人物的其他方面,但现在这里有 Jupyter Notebook数据集** 的链接。**

感谢阅读!

街道名称中的隐藏模式 [第二部分]

原文:towardsdatascience.com/hidden-patterns-in-street-names-part-2-4ae9af5fdee3?source=collection_archive---------20-----------------------#2023-03-06

使用数据科学分析阿尔巴尼亚地拉那的街道名称

Dea BardhoshiTowards Data Science Dea Bardhoshi

·

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

--

图片来源:Mario BeqollariUnsplash

你好!

这是我关于地拉那街道名称的故事的第二部分(你可以在这里找到第一部分:街道名称中的隐藏模式:数据科学故事 [第一部分])。在第一部分中,我们探讨了整体性别分布以及它们如何根据社区和道路类型发生变化。在这篇后续文章中,我将重点关注职业以及这些人物生活的历史时期。让我们开始吧!

数据与工具

与第一篇博客一样,为了将人物与他们的出生和死亡日期及其职业匹配,我手动标记了从 OpenStreetMaps 获得的数据集的一部分(许可:CC BY-SA 2.0)。具体来说,我使用了维基百科中列出的特定历史人物的主要职业,并依赖于那里列出的出生/死亡日期将这些细节添加到数据中。总共,我标记了643条独特的街道,并将其分为数据中的多个段落,其中相当一部分没有在线信息(更多内容见文末)。

在这第二部分中,我将使用 pandas、seaborn 和一些地理空间 Python 库来分析结果数据集并创建各种可视化图表。

职业和历史时期

在标记数据时,我根据每个人的维基百科页面将其分类为几个广泛的类别。结果发现街道名称中有 62 种独特的职业!以下是前 20 名及其各自计数的条形图:

前 20 名职业(图片来源:作者)

请注意政治家、战士和作家的突出地位:这三个类别共同占据了数据中所有标记街道名称的约 40%。艺术家紧随其后,完成了前 10 名的其余部分。我选择将党派人物(占标记总数的 8%)与其他战士单独标记,因为他们与阿尔巴尼亚***政权时期(1946–1991)有直接关联,以及他们在此期间所参与的宣传。因此,我认为这在我们作为一个国家如何面对历史的更广泛讨论中值得单独考虑。

让我们更详细地了解一下前三大类别:政治家、作家和战士。具体来说,他们生活和工作的历史时期是什么?以下是出生和死亡年份的分布情况,以及它们的平均值:

政治家分布的代码片段

图片来源:作者

图片来源:作者

图片来源:作者

  • 平均出生年份:政治家(1882 年)、作家(1858 年)、战士(1853 年)

  • 平均死亡年份:政治家(1936 年)、作家(1925 年)、战士(1900 年)

在上述图表和平均统计数据中可以注意到一些有趣的模式:首先,似乎大多数这些人物活跃在1850 年代1930 年代。作家的情况则有所不同,他们也出现在 17 世纪和 18 世纪。此外,“战士”出生和死亡年份有一个高峰,这与奥斯曼帝国入侵期间斯坎德培领导的反抗时期相对应。斯坎德培(1405–1468)被认为是我们的民族英雄,至今在许多纪念碑、广场和机构中得到广泛尊敬。另一方面,19 世纪到 20 世纪则对应于阿尔巴尼亚民族觉醒时期,这是一个旨在建立独立阿尔巴尼亚国的文化和政治运动。

以女性命名的街道

上一次,我们查看了以女性命名的街道大约占总街道名称的 3%,但让我们也在历史背景和职业方面来看看这些街道。通过性别筛选,并查看相同的出生/死亡分布:

图片由作者提供

  • 中位出生年: 1912

  • 中位死亡年: 1949

这里也有一些有趣的模式:首先,我选择了中位数而不是均值,因为均值会受到 1400 年代小高峰的显著影响。这导致了女性活动时代的时间移位,相较于整体分布。在第一部分中,我们看到这些女性中的许多人从事艺术或参与过各种战争,这可以解释中位死亡年份的情况。

绘制地图,邻里与道路类型

让我们从地理学的角度来看看这些内容。这是前三个类别的三张地图:

图片由作者提供

图片由作者提供

图片由作者提供

我会说,仅凭数据我无法立即找出任何明显的模式。为了开始识别这些模式,这里还展示了蒂拉纳 14 个行政区的最常见职业(与街道名称合并的行政区的多边形数据也来自 OpenStreetMaps):

图片由作者提供

不出所料,政治家和作家依然占据重要位置:我认为值得更多思考的是,某些地区以宗教或党派人物作为最常见的职业。这可能指向这些邻里的特定人物作为一种地方代表,或者是其他有趣的模式值得探讨。

互联网上没有信息的名字

正如我在开始时提到的,这些街道名称中很大一部分在网上没有 readily available 的信息。根据标记的数据,它们约占以人名命名街道的 55%。我发现这是一个有趣的现象,因为这可能意味着这些名称是本地重要人物但不被广泛认知,或者(显而易见)互联网信息有限,需要查阅其他来源。这里是这些名称所在位置的可视化(请注意这些名称主要出现在城市的边缘,而不是城市的中心):

结论 + 代码

总体来说,这个故事从更具历史性的角度审视了地拉那的街道风貌。我们深入探讨了不同工作区域和历史时期的表现,并揭示了这两个方面的模式。此外,还有一些其他的方向可以推进这个项目:例如,如何随时间推移城市名称的变化,或是命名过程中的具体细节如何导致我们看到的分布。目前,这里是Jupyter Notebook数据集。希望你喜欢!

如果你喜欢这些城市规划主题的帖子,你可能会喜欢我的新闻通讯,在其中我会更深入地讨论这些话题:The Zoned Out Chronicles!

分层 Transformer — 第一部分

原文:towardsdatascience.com/hierarchical-transformers-54f6d59fa8fc

更高效的语言模型

Mina GhashamiTowards Data Science Mina Ghashami

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

--

图片来源于 unsplash.com

在本文中,我们将探讨分层 Transformer:它们是什么、如何工作、与标准 Transformer 的不同之处以及它们的好处。让我们开始吧。

什么是分层 Transformer

“分层 Transformer”指的是在输入序列的多个尺度或分辨率上运行的 Transformer 架构。

我们为什么需要分层 Transformer?

标准 Transformer 尽管非常出色,但在时间上非常消耗。Transformer 内部的注意力机制在处理 n 个标记的输入序列时需要 O(n²) 的时间。这意味着 Transformer 对于长序列不够实用。解决这种低效的一个方法是使用分层 Transformer。它是唯一的解决方案吗?不!另一个方法是提高注意力机制的效率。但这是另一个话题。

分层结构如何帮助 Transformer?

分层 Transformer 使模型能够在不同级别的输入上进行操作,例如单词、句子、段落等。这与人类处理文本的方式相匹配。这迫使注意力机制跨越不同的层次,以建模不同粒度的实体之间的关系。

分层 Transformer 有很多方法;在本文中,我们力图直观地解释其中一种方法。

Hourglass Transformer

Hourglass [1] 网络是 OpenAI、Google Research 和华沙大学的共同研究成果。它是一种分层自回归 Transformer,接收一个输入序列,并形成从全分辨率到越来越小尺度的序列层次;在每个尺度上,它处理该分辨率内的序列,最后将序列扩展回全尺寸。这使得模型更加高效,因为较短的序列处理成本较低。

请注意,在自回归 Transformer 中,第一层和最后一层至少必须在输入的全尺度上操作。这是因为第一层处理输入,所以必须在全尺度上操作,最后一层(由于模型是自回归的)生成输出,因此必须再次在全尺度上操作。

让我们看看这个架构。下图展示了沙漏:

“沙漏”架构——图片来自[1]

我们一步一步描述。我们从上面图片的左侧开始,其中输入令牌被表示为灰色框。

  1. 模型首先使用标准的 Transformer 层处理完整的输入序列。因此,如果输入序列(显示为标题为“输入令牌”的灰色框)有 L 个令牌,则它们都通过标准 Transformer 层(用蓝色表示,称为预处理层)。这些层输出每个令牌的 L 个嵌入向量。

预先处理的层是对缩短前的完整令牌级序列进行操作的 Transformer 层。

因此,如果任务是“文本语言建模”,则输入到预处理层的是一个表示文本的子词令牌序列。如果任务是“图像生成”,则输入将是展平的像素值序列。

第 1 步——沙漏架构——图片来自[1],由作者修改

2. 在第二步中,模型将序列从L 个令牌缩短为更少的令牌。这一步显示为橙色梯形,并且“缩短因子”表示为sf = k₁。注意sf 代表“缩短因子”,如果设置为k₁,则意味着每k₁ 个令牌合并为 1 个令牌。缩短通过使用某种池化操作来实现,例如平均池化线性池化注意力池化。我们很快会讨论这些。此步骤的输出为L/k₁ 个令牌。此步骤也称为下下采样步骤

第 2 步——沙漏架构——图片来自[1],由作者修改

3. 缩短的序列经过更多的 Transformer 层,称为缩短层。在图中,它们由黄色框表示。这些层输出更新后的令牌嵌入。

第 3 步——沙漏架构——图片来自[1],由作者修改

4. 如果还有更多的缩短需要完成,我们简单地重复这个过程。在下图中,在第二个橙色梯形中,我们将输入序列缩短一个sf = k₂的因子。这将把每个k₂ 个令牌合并为 1 个令牌,因此将输出L/(k₁.k₂) 个令牌。输出的令牌经过更多的缩短层,这些层用淡黄色表示。

第 4 步——沙漏结构——图像来自[1],由作者修改

到现在,我们正处于架构的中间…

从这里,上采样开始了!

  1. 上采样层用于将最短的序列扩展回原始的全分辨率。由于我们进行了两次下采样(一次从 L tokens 到L/k₁,第二次从L/k₁ tokens 到L/(k₁.k₂) tokens),我们将执行两次上采样,将 token 数量恢复到L tokens。第一次上采样如下:

第 5 步(第一次上采样)——沙漏结构——图像来自[1],由作者修改

第二次上采样如下:

第 5 步(第二次上采样)——沙漏结构——图像来自[1],由作者修改

每次上采样操作后,我们将 token 嵌入通过 Transformer 层。在图像中,它们被称为 缩短层后置 vanilla 层

第 5 步——上采样涉及到 Transformer 层,无论是作为缩短层还是作为后置 vanilla 层——图像来自[1],由作者修改

最后的上采样通过后置 vanilla 层传递嵌入,从而输出下一个预测 token 的嵌入。

下采样步骤

下采样步骤(也称为论文中的缩短步骤)将输入序列缩短为更少的 token。此步骤通过使用各种池化操作将 tokens 合并为组来完成,例如:1)平均池化,2)线性池化和 3)注意力池化。

1) 平均池化: 从高层次来看,平均池化通过取平均值将 k 个相邻的 token 嵌入合并为单一的嵌入。这种方法有两个超参数:“池化大小”和“步幅”。

“池化大小”是窗口的大小,“步幅”是窗口每次移动的步数。例如,在一个序列“ABCDEF”中,池化大小 = 步幅 = 2,前两个 token 组成第一个窗口,窗口每次移动 2 个 token。所以窗口将是:[AB],[CD],[EF]。

论文将“池化大小”和“步幅”设为相同的数字,并称之为“缩短因子 (sf)”。我们通过一个例子来看看:

如果输入序列是 [x1, x2, x3, x4, x5, x6, x7, x8, x9, x10],且超参数为 pool size=stride=3,则平均池化将序列划分为大小为 3 的块,即 [x1, x2, x3],[x4, x5, x6],[x7, x8, x9],[x10],并对每个窗口中的 token 嵌入取平均值,以获得单一嵌入,如下:

e1 = mean(x1, x2, x3)

e2 = mean(x4, x5, x6)

e3 = mean(x7, x8, x9)

e4 = x10

因此,缩短后的序列将是 [e1, e2, e3, e4]。注意缩短后的序列长度是输入长度/sf = 10/3 = 3。

2) 线性池化:

这种方法设置步幅 = k,并将长度为 L 的输入序列划分为 L/k 个窗口。每个窗口包含 k 个 token,每个 token 具有一个维度为 d 的嵌入向量。然后,该方法将每个窗口展平为 k*d 维的向量,并形成以下矩阵:

线性池化 — 第一部分 — 图片由作者提供

假设输入序列是 [x1, x2, x3, x4, x5, x6, x7, x8, x9, x10],且步幅=3,则我们有以下窗口: [x1, x2, x3] , [x4, x5, x6], [x7, x8, x9], [x10],如果每个 token 具有一个 100 维的嵌入向量,则上述矩阵变为:

线性池化 — 第二部分 — 图片由作者提供

注意,现在我们已经将序列的长度从 10 缩短到 4,但现在每个新 token 的维度是 300 而不是 100!为了恢复到原始维度,线性池化将它们通过学习到的线性变换投影到 100 维空间中。线性变换是一个 300*100 的矩阵,由数据学习得出。

3) 注意力池化:

这种方法与上述两种方法类似开始:输入序列被划分为大小为 k 的窗口,然后在每个窗口内应用注意力,这使得窗口中的 token 可以相互关注。最后,由注意力产生的每个窗口的嵌入被加在一起。经过这一步后,在块嵌入上应用前馈层。

注意力池化 — 图片由作者提供

上采样步骤

上采样步骤将缩短的序列扩展回原始的完整长度。有两种简单的上采样方法:

  1. 重复扩展: 重复扩展 只是简单地多次复制每个嵌入。这在计算上非常高效。

  2. 线性扩展线性扩展将其投影到更高的维度,然后进行扩展。例如,如果缩短后的序列是 [e1, e2, e3, e4] 并且 sf=k=3,那么每个嵌入会线性投影到一个大小为 k * d 的向量,其中 d 是原始嵌入维度。投影权重矩阵是可学习的,并且与完整模型一起端到端地训练。

为了保持对原始输入序列的保真度,残差连接(如红色虚线所示)将缩短之前的输入序列添加到上采样序列中。可以将其视为通过多次缩短-扩展周期来获取声学上下文的一种方式。

为了保持保真度添加的残差连接 — 图片来自 [1],由作者修改

还有一种更高级的上采样方法,称为 注意力上采样,其工作原理如下:

如果缩短的序列是[ e1, e2, e3, e4 ],且sf=k=3,则首先应用线性或重复上采样将其扩展到原始长度。这将得到[ u1, u2, …, u12 ]。

设缩短前的嵌入为[ x1, x2, …, x12 ];这些通过残差连接(红色虚线)添加到上采样的嵌入中,形成[ u1+x1, …, u12+x12 ]。现在,自注意力机制应用于这个序列,其中:

  • 查询(Q)来自求和的嵌入[ u1+x1, …, u12+x12 ]。

  • 键(K)和值(V)来自上采样的嵌入[ u1, u2, …, u12 ]。

这更新了求和的嵌入,这将是最终输出。对上采样序列的注意力有助于放大相关部分,并与预缩短的上下文进行结合。

实验

他们[1]在语言建模(使用 enwik8)和图像生成(使用 ImageNet-32/64)上评估了他们的模型。他们显示,相比于 Transformer-XL 基线,Enwik8 数据集上的困惑度提高了 10-15%;并且他们在 ImageNet-32 图像生成任务上实现了自回归变换器模型的新最先进水平。

实验参数 — 作者提供的图片

这部分结束了沙漏网络的讨论。在下一篇文章中,我们将深入探讨其他层次化的变换器模型。

总结

在这篇文章中,我们回顾了一种改进效率并减少处理长序列时内存使用的变换器层次化架构。这种架构称为 Hourglass [1],由两个主要组件组成:1)缩短或下采样,2)上采样。缩短是通过使用池化操作(如平均池化或线性池化)将令牌合并成组来完成的。网络中间层的序列长度由缩短因子 k 减少。上采样组件使用线性上采样或注意力上采样等方法将缩短的序列扩展回原始长度。Hourglass 模型相比于基线变换器如 Transformer-XL [1],改善了困惑度。实际上,它在 ImageNet32 图像生成任务上达到了变换器模型的新最先进水平。

如果你有任何问题或建议,欢迎随时联系我:

邮箱: mina.ghashami@gmail.com

LinkedIn: www.linkedin.com/in/minaghashami/

参考文献

  1. 层次化变换器是更高效的语言模型

  2. 探索层次化注意力变换器在长文档分类中的高效性

分层变换器 — 第二部分

原文:towardsdatascience.com/hierarchical-transformers-part-2-2616eecacb21

分层注意力更快

Mina Ghashami数据科学前沿 Mina Ghashami

·发表于 数据科学前沿 ·阅读时间 6 分钟·2023 年 10 月 7 日

--

这篇文章要求你具备标准变换器及其工作原理的知识。如果你是初学者,想了解变换器,请查看 变换器入门 文章。

分层变换器 — 第一部分 中,我们定义了“分层变换器”的含义,并回顾了该领域的一项重要工作,即 Hourglass

在这篇文章中,我们将继续探讨另一项著名的工作,即 分层注意力变换器(HAT)。

让我们开始吧。

分层注意力变换器(HAT)

该方法最初是为分类长文档而提出的,通常长达数千个单词。一个应用案例是分类法律文档或生物医学文档,这些文档通常非常长。

分词和分段

HAT 方法通过获取输入文档,并使用字节对编码(BPE)分词器将其拆分成子词/标记来工作。这个分词器被许多著名的大型语言模型使用,如 BERT、RoBERTA 和 GPT 家族。

然后将分词后的文档拆分为 N 个大小相等的块;即如果 S 代表输入文档,则 S = [C1, …., CN]N 个大小相等的块。(在整篇文章中,我们有时将块称为段,但它们是相同的概念。)每个块是一个由 k 个标记组成的序列 Ci = [Wi[cls], Wi1…, Wik-1],其中第一个标记 Wi[cls]CLS 标记,代表该块。

图片由作者提供

如上图所示,每个块是一个由 k 个标记组成的序列,其中第一个标记是 CLS 标记。

模型架构

在对输入序列进行标记化和分段后,将其输入到HAT变换器模型中。HAT 模型是一个编码器变换器,由两个主要组件组成:

  1. 分段编码器(SWE):这是一个共享的编码器块,接收一个段(也称为块)的序列并处理该块。

  2. 交段编码器(CSE):这是另一个编码器块,它处理所有段(也称为块)的 CLS 标记,处理交段关系。

图像来源于[1]

正如上图所示,左侧的分段编码器接收一个段的所有 k 个标记,处理它们并输出更新后的标记表示。在左侧,我们看到交段编码器接收所有段的 CLS 标记嵌入,并输出它们的更新表示。

这两个组件可以用于几种不同的布局。例如,我们可以将它们放在“临时”布局中,其中一个堆叠的 L 层 SWE 编码器放在底部,然后在其上方放置两层 CSE 编码器。请注意,每层的 SWE 共享权重。层与层之间的箭头表示在层之间传递嵌入。

图像来源于[1]

另一种布局是“交错”层,如下所示:

图像来源于[1]

如上所示,交错布局是一个配对的分段编码器和交段编码器的堆叠,其中跨越模型的几层执行交段注意力。

[1]中的作者探讨了几种布局(见下图),并通过实验发现“交错”布局优于其他变体。

图像来源于[1]

模型的完整架构如下:

图像来源于[1]

架构由 N 层组成,每层如上所示。请注意,将它们堆叠在一起会带来“交错”架构。一个层中的所有分段编码器共享权重,它们独立且并行地处理输入段(块)。每个段都有自己的位置嵌入。

分段编码器的输出将是分段标记的更新嵌入。第一个标记,即 CLS 标记嵌入,被添加到分段位置嵌入中,并传递给交段编码器

交段编码器捕捉段之间的关系,并更新每个段的 CLS 嵌入并输出。该层的输出将是所有标记的更新嵌入。

位置嵌入。请注意,架构中有两个位置嵌入:

  1. 段的标记位置嵌入:这是为了指示标记在段中的位置。这仅在段级编码器中使用。

  2. 段的标记位置嵌入:这是为了指示段的顺序,仅在跨段编码器中使用。

模型训练

作者[1]将模型训练分为两个阶段:预训练和微调。

预训练:由于网络是一个编码器变换器,它使用掩码语言建模(MLM)目标进行训练,其中一部分(15%)的标记被掩码,语言模型应该预测这些标记。

微调:他们在几个标注数据集上使用文档分类任务来微调模型。

不同级别的嵌入

使用这个网络,我们可以在不同的尺度上获得嵌入:词、段落、文档。

  • 词嵌入或标记嵌入可以通过模型的最后一层直接获取,

  • 段落或段的嵌入可以通过段的 CLS 标记嵌入获得。

  • 文档嵌入可以通过对所有段 CLS 标记嵌入进行最大池化(或平均池化)来获得。在论文[1]中,作者选择了最大池化。

评估

我真正喜欢这篇论文的地方在于,他们在三个层次上进行了全面的评估:

1)上游评估任务:这些任务旨在以通用方式预训练编码器。对于这个任务,他们采用了 MLM(掩码语言建模)任务。

  1. 中游评估任务:这些任务旨在评估预训练模型学习到的表示的质量。为了这个评估,作者[1]考虑了几个任务,例如:
  • 段顺序预测:这是为了预测几个段的顺序。模型在此任务中的输入是来自文档[1]的打乱的段序列,目标是预测它们之间的正确顺序。由于这是一个回归问题,他们使用平均绝对误差(MAE)作为损失函数。直观上,这个任务评估 CLS 标记嵌入的质量。
  1. 下游评估任务:他们在现实应用中评估模型的性能。为了这个评估,他们考虑了文档分类,用于对美国医院的出院总结进行分类。为了获得文档嵌入,他们对文档中所有段的 CLS 标记嵌入进行了最大池化,并使用交叉熵作为分类的损失函数。

更多细节请参考论文 [1]。

实验结果

论文中有许多实验结果,但一个显著的结果是,他们将自己的模型与 Longformer [2] 和 BigBird [3] 模型进行比较。Longformer 和 BigBird 属于稀疏注意力方法,它们通过强制每个标记仅关注其邻域中的少数标记和少量全局标记来实现高效的注意力机制。这与标准注意力方法相对,后者每个标记都关注每个其他标记。在这个实验中,他们展示了 HAT 方法在性能上超越了 Longformer 和 BigBird。

图片来源 [1]

摘要

在这篇文章中,我们探讨了另一种分层的变换器架构,称为分层注意力变换器(HAT)。这是一种基于编码器的模型,将输入分割成相等长度的片段。每个片段以一个表示该片段的 CLS 标记开始。该模型架构包括两个主要组件:片段级编码器和跨片段编码器。第一个编码器学习单个片段的表示,而第二个编码器学习片段之间的跨关系。它们一起能够学习输入中各种层次的表示,如词表示、句子表示和文档表示。

如果你有任何问题或建议,随时与我联系:

邮箱:mina.ghashami@gmail.com

LinkedIn: www.linkedin.com/in/minaghashami/

参考文献

  1. 对分层注意力变换器在长文档分类中的有效性的探索

  2. Longformer: The Long-Document Transformer

  3. Big Bird: Transformers for Longer Sequences

更高精度的浓缩咖啡篮成像

原文:towardsdatascience.com/higher-precision-imaging-of-espresso-982270300b80?source=collection_archive---------12-----------------------#2023-02-28

咖啡数据科学

为了更好地测量每个孔的顶部和底部之间的差异

Robert McKeon AloeTowards Data Science Robert McKeon Aloe

·

关注 发布于 Towards Data Science ·4 min read·2023 年 2 月 28 日

--

多年来,摄像技术得到了极大的进步,使人们能够拍摄高质量的图像。虽然这对于计算机视觉应用很有用,但实验设计的简单改动往往可以大幅提高这些图像在特定应用中的质量。进入咖啡!

几年来,我一直在将我的图像处理技能应用于意式咖啡滤篮。两年前,我尝试对滤篮的顶部和底部进行成像以测量每个孔的形状。然而,当我遇到自动对齐图像的问题时,这项调查暂停了。最近,我又开始重新进行这个工作,不过我使用了手动对齐来改进这个过程。

在收集一些数据时,我意识到我的成像设置也可以更好,所以我们在这里讨论一下。

挑战

成像意式咖啡滤篮面临几个挑战:

  1. 金属滤篮和反射率

  2. 孔很小

  3. 相机镜头有曲线

我通过数据收集标准操作程序和后处理调整了许多这些问题。

数据收集

我使用了一些标准化工具:

  1. 使用平板屏幕照亮滤篮孔

  2. 在一个黑暗的房间中隔离其他光源

  3. 减少曝光以处理平板屏幕上的反射,使其从滤篮反射回相机。

后处理

我有一个半自动化的过程来简化处理:

  1. 使用蓝色圆圈标记滤篮

  2. 手动阈值处理图像

  3. 自动去除非孔区域

  4. 将任何椭圆形孔调整为圆形。

  5. 调整滤篮上的光照

如何改进这一过程?

首先,用于测量滤篮顶部的光量应与测量底部的光量匹配。

所有图像由作者提供

所以我使用一个纸杯制作了一个领圈,将滤篮固定在上面,使得滤篮顶部(篮子内部)与翻转过来的滤篮底部处于同一高度。

然后我还调整了光照。全屏幕是校准图像所需的(每毫米的像素数量),但任何不在滤篮下方的光线都会反射到相机(我的手机相机)上,再反射到滤篮上。

为了消除这个问题,我使用了全屏亮度进行校准,并拍摄了另一张中间有白色圆圈的图像。这消除了反射问题。

但接下来我需要校准!为了确保图像对齐,因为手机可能会移动(即使在支架上),我在应用程序 Procreate 中手动对齐图像。我以为这会更困难,但使用图层和 50%透明度,这一过程非常简单。我还发现了一些有趣的事情,比如 VST 滤篮的对称性。

我使用校准图像来对齐顶部和底部图像,以便一切都校准到相同的尺度。这涉及到线性缩放和物体旋转,直到孔的对齐最好或最对称。我必须镜像底部图像,以确保孔与顶部图像正确对齐。

然后我将这些图片通过我的算法处理。下面是 Wafo Classic 的假色图像的顶部和底部图像,以描绘孔径大小。图像的顶部(左侧)有一块蓝色斑点,这不是由于其他问题。这是多次拍摄中的一个持久特征。

左:从过滤器顶部,右:从过滤器底部(镜像)

这些分布可用于更好地理解过滤篮。

从根本上讲,这是一个帮助回答关于过滤篮和性能的问题的工具。将这些信息与实际的浓缩咖啡表现连接起来还存在其他挑战,但努力追求对性能关键属性的更深入理解仍然是令人愉快的。

如果你喜欢,可以在TwitterYouTube以及Instagram关注我,在这里我会发布不同机器的浓缩咖啡镜头视频和与浓缩咖啡有关的内容。你也可以在LinkedIn上找到我。也可以在Medium上关注我和订阅

我的其他阅读内容

我的书

我的链接

浓缩咖啡文章合集

工作和学校故事合集

KDD 2023 大型语言模型亮点

原文:towardsdatascience.com/highlights-on-large-language-models-at-kdd-2023-fc53440563c3?source=collection_archive---------3-----------------------#2023-09-11

你无法参加 KDD 吗?阅读我的总结,了解会议上的热门话题:LLMs

Gabriel MoreiraTowards Data Science Gabriel Moreira

·

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

--

作者提供的图片

几周前,我第一次有机会参加了 ACM SIGKDD(简称 KDD)会议。KDD 2023 在加利福尼亚州长滩举行,是数据挖掘领域最古老且最重要的学术会议,开创了与数据科学和大数据相关的主题。

会议持续了 5 天,吸引了超过 2200 人参加,其中有大量来自业界的参与者。我对所涵盖的主题多样性感到印象深刻,但从我的角度来看,热点话题是大语言模型(LLMs)和图学习。同时也发现了很多关于推荐系统(RecSys)的内容,我对此特别关注。

在这篇文章中,我总结了我在参加的研讨会、教程和论文报告中对 LLMs 的亮点,并附上了额外信息的在线资源链接。

警告:接下来是一篇长文,包含大量资源链接!

LLM 革命主题演讲(Ed Chi - Google)

著名科学家及谷歌主任 Ed H. Chi 在备受期待的LLM 革命主题演讲中发表了讲话。他回顾了我们经历的技术革命,从互联网到移动设备,再到深度学习的兴起,现如今是 LLMs,这无疑令人瞩目。

他讨论了人类智能与 ML 的不同之处——(1)从少量示例中学习,(2)解释他们的预测/决策,(3)强大的分布外泛化能力——以及 LLM 如何开始填补这一差距。

随后,他讨论了使 LLM 能够进行一些推理的技术:(1)链式思维提示,(2)自我一致性,(3)最少到最多提示,以及(4)指令微调。更多内容请参见 Denny Zhou 在 LLM 日的演讲(下一部分)。

最后,他分享了他对 LLM 未来挑战的愿景:(1)责任与安全,(2)事实性、基础及归属,(3)人类<->AI 内容循环与生态系统,以及(4)个性化和用户记忆。

LLM 日

KDD 专门设立了一天用于 LLM,有 5 位不同的研究人员就微软、Google DeepMind、Meta、智谱 AI 和 OpenAI 如何推动 LLM 技术的发展、面临的挑战以及他们对该领域未来演变的预见进行了详细讲解。演示幻灯片已经发布,强烈推荐查看。

从文档到对话:LLMs 如何塑造未来工作(Jaime Teevan — Microsoft) (幻灯片)

这次演讲涵盖了关于 LLM 质量问题的不同研究和应用主题(例如,如何处理低资源语言)、云端高效训练、检索增强生成(RAG)作为利用私人知识库(KB)的可持续方式、微调中的差分隐私、提示工程和聊天记录分析的最佳实践。

教会语言模型推理(Denny Zhou - Google DeepMind)(幻灯片)

他关注了 ML 的终极目标——推理——作为仅通过少量示例进行学习的方式。总结了一些使 LLM 如此强大的核心技术:

  • 思维链(CoT)——逐步思考的提示技术,提供一些少量示例以概述推理过程

  • 最小到最大提示(规划 + 推理)——将复杂问题分解为一系列子问题,依次解决

  • 自洽(SC)解码——一种从采样的多样化推理路径生成不同答案的技术。最终答案不是贪婪选择的答案,而是这些不同答案的多数票。这种技术似乎对 LLMs 非常有效,让我想起了模型集成的力量!

  • 指令调优——对预训练 LLMs 进行微调以遵循指令的过程。这使得零样本提示新任务成为可能。这对启用如 Google Bard 或 Open.ai ChatGPT 这样的问答系统至关重要。

Llama 2: 开放基础和微调聊天模型(Vedanuj Goswami - Meta FAIR)(幻灯片

他介绍了 Meta 在训练 Llama 基础模型和使用 SFT 数据(高质量的 27k 收集样本)进行指令微调的历程。他们的奖励模型是在 1M 收集样本上训练的。他还描述了他们的迭代微调与 RLHF,评估(人类,安全)。他在演讲结束时谈到了训练和部署 LLMs 面临的挑战,我在这里进行了记录:

  • 获取更多数据,多语言,多模态

  • 扩展到数千个 GPU,并提高 MFU(模型 FLOPs 利用率)

  • 设计高效的训练和推理架构,硬件-软件共同设计

  • 持续学习和更新知识

  • 改善事实准确性和引用来源

  • 减少幻觉和承认不确定性

  • 删除有害、攻击性或偏见内容

  • 适应超越训练数据的世界知识

从 GLM-130B 到 ChatGLM(Peng Zhang - Zhipu AI)(幻灯片

我了解了 Zhipu AI,这是一家在中文领域挑战 Open.ai 的公司。他们作为钻石赞助商在 KDD 上有强大存在,并在宴会庆典上发表了主题演讲。Zhipu 展示了他们在许多任务中表现最佳的中文 LLM 结果,甚至优于 GPT-4。他们描述了如何在其基础模型(GLM-130B)之上开发 ChatGLM 和 VisualGLRM。他们在HuggingFace 上开源了 ChatGLM-6B

大语言模型复兴:范式与挑战(Jason Wei — OpenAI)(幻灯片

关于规模规律的非常务实的演讲,讲述了如何达到当前 LLMs 的状态,以及当 LLMs 参数超过 100B 时可以观察到的新兴能力(包括推理)。还谈到了通过提示技术进行推理:Chain-of-Thought 和 Least-to-most 提示。

大规模 AI 模型的基础与应用预训练、微调和基于提示的学习研讨会

我认为LLM-AI 研讨会是会议上最具争议的一个。我早上实在无法参加,因为在 KDD 早晨的主旨发言后,小房间被人群完全挤满了。幸运的是,在咖啡休息之后,我找到了一个座位,能够参加几场会议。

LLMs 时代的 NLP 研究(Shafiq Joty - Salesforce)

他描述了 SalesForce 的 XGen LLM — 一个内部的 JaxFormer 库,继承了 LLaMA-7B,并通过 WizardLM 进行了指令微调,能够基于非结构化和结构化数据(例如 Spark 和 SQL 数据库)回答问题。还介绍了一些他们用于推理准备的技术,包括通过Chain-of-Thought来分解问题,并通过在自然句子、SPARQL 和 SQL 上训练模型进行自适应查询生成(LoRA)来选择最相关的知识库。该过程为每个推理步骤生成一个查询,并在知识源上执行。

模块化大型语言模型和最小化人工监督的原则驱动对齐(YiKang Shen — IBM)

这个演讲介绍了 IBM 的基础模型:(1)Sandstone — 适合针对特定任务微调的编码器-解码器架构,(2)Granite— 仅解码器,类似于 GPT 的生成任务,(3)Obsidian — 一种新的模块化架构,提供高效的推理能力和在各种任务中的表现水平

他还描述了他们在 LLM 方面面临的一些挑战:

  • 效率 — 如何训练和服务 Llama 65B 模型。

  • 可扩展性 — 如何用不断增长的训练语料库、不同语言和客户的私人数据来更新 LLM

  • 灵活性 — 能够在不同设备上使用不同复杂度的 LLM 模型,满足不同的延迟要求。

他们展示了他们的ModuleFormer,它通过稀疏专家混合(SMoE)来解决上述问题。它可以为每个输入激活其模块的子集,比密集型 LLMs 对灾难性遗忘的免疫力更强。对 ModuleFormer 进行微调可以使模块子集专门化,而与任务无关的模块可以被修剪,以实现轻量级部署。

教程

这些教程是同时进行的,所以我不得不分配时间去参加两个讲座。幸运的是,他们的优秀幻灯片被提供了,并且非常详细。

面向下一代智能助理,利用 LLM 技术 — Meta (幻灯片)

关于智能助理的非常全面的教程,这些智能助理是多模态的,并且能够利用用户的位置、用户可以听到和看到的内容(例如,使用 Google Glasses、Meta Quest 2)作为上下文。教程描述了不同模块之间的连接方式:ASR、CV、NLU、对话状态跟踪器、NLG、TTS、KB、个性化/推荐和隐私保护等。

预训练语言表示用于文本理解:一个弱监督视角 - 伊利诺伊大学香槟分校 (幻灯片)

介绍了语言模型预训练的进展,将其与传统的 NLU 任务进行了比较,并描述了 LLM 如何用于提取实体和层次关系、主题发现和文档理解。我从这个教程中获得的一个良好见解是使用一些 NLU 技术来评估生成的答案是否回答了问题。

研究论文

这是我喜欢的一些 NLP / LLM 论文的简短列表。

端到端查询术语加权(Google)(论文

这篇出色的论文结合了词汇和语义检索系统。他们在词汇检索器的基础上提出了一种术语加权 BERT(TW-BERT)模型来构建他们的解决方案。TW-BERT 学会预测单个 n-gram(例如,单字和双字)查询输入术语的权重。这些推断出的权重和术语可以直接被检索系统用来执行查询搜索。学习到的权重可以被标准词汇检索器(例如 BM25)以及其他检索技术(如查询扩展)轻松利用。

UnifieR:大型检索的统一检索器(Microsoft)(论文

另一个有趣的提议是将稠密向量和基于词汇的检索统一到一个具有双重表示能力的模型中。它通过两阶段自学习流程进行训练,并在最新的词汇和稠密检索模型上有所改进。

学会与对话搜索中的先前回合相关(论文

通常在多轮对话中,历史查询用于扩展当前查询。然而,并非所有的先前查询都与下一个问题相关或有用。该论文提出了一种方法,用于选择对当前查询有用的相关历史查询。他们使用伪标注机制来标注相关的历史查询,并与检索器训练一起训练选择模型。

GLM-Dialog:面向知识驱动对话生成的噪声容忍预训练(论文

描述了如何通过基于 RAG 的对话系统使用嘈杂的私人知识库。他们提出了一种新颖的评估方法,使人们可以同时与多个部署的机器人对话,并隐性地比较其表现,而不是使用多维度指标进行显式评分。

集群语言模型以改善电子商务检索和排名:利用查询相似性和微调以实现个性化结果(Home Depot)(论文

论文描述了 Home Depot 如何通过使用特定集群的语言模型来改善电子商务的语义搜索,而不是使用典型的双编码器架构。他们的方法首先使用 K-Means 将用户查询映射到集群,并使用所选的集群特定语言模型进行检索。

结论

这些是我在 KDD 2023 上对 LLM 的重点总结。希望你能从这个总结和我编纂的资源中找到一些有用的信息和灵感。

“抱歉,帖子写得太长。如果我有更多时间,我会写得更短一点” 😃

爬山算法优化:简单的初学者指南

原文:towardsdatascience.com/hill-climbing-optimization-algorithm-simply-explained-dbf1e1e3cf6c

最流行的优化算法之一的直觉

Egor HowellTowards Data Science Egor Howell

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

--

照片由Isaac Burke拍摄,发布在Unsplash

背景

许多工业和研究问题需要某种形式的优化来获得最佳解决方案或结果。其中一些问题属于组合优化类别,这意味着它们通常不能在合理的时间内通过暴力破解解决。因此,我们转向启发式元启发式算法,这些算法虽然不能保证找到最佳的全局解决方案,但通常能在合理的时间内计算出足够的解决方案。

其中一种元启发式算法是爬山算法这是本文的主题。我们将深入探讨理论、优缺点,并通过实现该算法来解决著名的旅行商问题 (TSP)

爬山算法

概述

攀登算法是一种元启发式的 迭代 局部搜索 算法。它通过对当前解决方案进行小的 扰动 并继续这一过程,直到找不到更好的解决方案,旨在找到最佳解决方案。此外,它是一种 贪心算法,因为它只关注局部最优的移动,因此常常会陷入 局部最优(见下图)。

一个包含局部和全局最优的函数示例。图由作者提供。

算法

攀登算法的一般流程如下:

  • 生成初始解决方案,该解决方案现在是最佳解决方案。

  • 从最佳解决方案中选择一个邻域解决方案。

  • 如果邻域解决方案优于最佳解决方案,则将最佳解决方案设置为等于邻域解决方案。

  • 重复上述两个步骤,直到邻域解决方案不优于最佳解决方案,或满足其他终止条件,如迭代次数。

如果这仍然显得有些模糊,不用担心!稍后我们将在 Python 中应用上述算法到一个实际的例子中。

类型

爬山算法有多种类型和变体。以下是最常见的几种:

  • 简单爬山算法: 仅考虑最近的邻域。

  • 最陡爬升爬山算法: 考虑所有邻域并选择最佳方案。

  • 随机爬山算法: 随机选择一个邻域。

最陡爬升版本会带来更优的性能,但需要更多的计算资源。

优缺点

让我们简要列出爬山算法的主要优缺点:

优点:

  • 非常直观,易于向同事、利益相关者等解释。

  • 可以应用于 连续 离散 目标函数和问题。

  • 能够解决各种不同的问题。

缺点:

  • 可能会陷入 局部极小值/极大值 ,因此不能保证找到最佳的全局解决方案。

  • 可能出现平坦区域,其中所有邻域具有相同的目标分数。

还有更复杂的算法,如 模拟退火禁忌搜索 类似于爬山算法,但不会陷入局部极小值,并且能更广泛地探索搜索空间。要了解更多关于这些算法的信息,请查看我之前的帖子:

如何用模拟退火算法解决旅行推销员问题 ## 如何用模拟退火解决旅行推销员问题

使用模拟退火优化算法求解旅行推销员问题的最佳解决方案

[towardsdatascience.com ## Tabu Search 简单解释

对 Tabu Search 优化算法的直观解释以及如何将其应用于旅行推销员问题……

[towardsdatascience.com

Python 实现示例

旅行推销员问题

我们将编写爬山算法代码来解决旅行推销员问题(TSP)。不过,在此之前,让我们简要说明和解释一下我们在 TSP 中试图解决的问题。

TSP 是优化中的经典问题,并提出了以下问题:

“访问给定城市列表一次并返回到起点的最短路径是什么?”

这个问题看起来很简单,但由于组合爆炸的问题,随着城市数量的增加,暴力解决方法变得计算上不可行。例如,对于 10 个城市,存在~300,000 个可能的 路径

可行路径的数量作为城市数量n的函数是(n-1)!/2. 因此,它的计算复杂度是 O(n!)。

当城市数量约为 20 时,暴力解决方法变得不可行,因为计算所需的时间为~2,000!令人惊讶的是,对于 61 个城市,这一时间竟达到惊人的 10⁶⁷年!

爬山算法解决 TSP

让我们简要列出我们将用来实现爬山算法解决 TSP 的伪代码。我们将使用最陡上升版本:

  • 生成一个初始路线并将其设为最佳解决方案。

  • 通过交换当前最佳解决方案中的两个城市来生成邻域解列表。

  • 从这些邻域解中获取最佳邻域解(最短距离),并将其设为当前解决方案。

  • 将当前解决方案与最佳解决方案进行比较。如果当前解决方案更短,则将最佳解决方案设为当前解决方案。

  • 重复此过程,直到当前解决方案比最佳解决方案更差。

Python 代码

以下是我们刚刚讨论的算法的HillClimb类的示例代码:

现在让我们运行类并绘制 20 个合成生成城市的初始解决方案和最佳解决方案的结果:

图表由作者在 Python 中生成。

图表由作者在 Python 中生成。

正如我们所见,爬山算法找到的解决方案优于初始解决方案,但显然不是全局最优解。不过,它找到了一个足够的解决方案,而且没有花费 2000 年!

总结与进一步思考

在这篇文章中,我们讨论了元启发式局部搜索爬山算法。该算法对最佳解决方案进行小的增量扰动,直到达到一个变化不再带来更好解决方案的点。该算法在旅行推销员问题上表现良好,但却陷入了局部最小值,这是这种优化算法的主要缺点。

本文使用的完整代码可以在我的 GitHub 上找到:

[## Medium-Articles/Optimisation/hill-climbing at main · egorhowell/Medium-Articles

当前无法执行该操作。您已在另一个标签或窗口中登录。在另一个标签中注销了…

github.com

参考资料与进一步阅读

另一件事!

我有一个免费的通讯,Dishing the Data,我每周分享成为更好的数据科学家的小贴士。没有“虚华”或“点击诱饵”,只有来自实践数据科学家的纯粹可操作的见解。

[## Dishing The Data | Egor Howell | Substack

如何成为更好的数据科学家。点击阅读由 Egor Howell 编写的《Dishing The Data》,这是一个 Substack 出版物,包含…

newsletter.egorhowell.com

与我联系!

招聘杰出的数据科学家

原文:towardsdatascience.com/hiring-exceptional-data-scientists-c454110742f?source=collection_archive---------8-----------------------#2023-02-28

异常值:超越单纯的技术技能

Jaco du ToitTowards Data Science Jaco du Toit

·

关注 发表在 Towards Data Science · 阅读时间 7 分钟 · 2023 年 2 月 28 日

--

布劳贝格斯特兰德(作者提供的图片)

随着对应用数据科学最初兴奋感的减退,回顾一些在热潮高峰期取得成功的举措的因素是很有必要的。值得考虑的是,在这段时期设定的那些大多不切实际的期望中,哪些因素对于超越这些期望至关重要。然而,当前的问题可能更适合用“谁?”而不是“什么?”来框定。在这篇文章中,我分享了我的观察和思考,谈论我认为使一名杰出的数据科学家脱颖而出的因素——那些通过全面的技能和无尽的热情不断带来惊艳表现和业务影响的人。

积极的自我学习者

一位出色的数据科学家会表现出强烈的好奇心和持续学习的动力。在我的经验中,大多数数据科学家面临需要新颖解决方案的问题,这些问题并非普通的监督学习或无监督学习方法可以解决。对于这些问题,解决方案需要在不总是直接相关的主题上进行积极的研究,探索在线资源,跟进开源社区的最新动态,阅读书籍,与同行交流,或参加数据科学聚会以外化思想。这类个人也可能通过开发工具或算法为该领域做出贡献(尽管不一定注册了研究生学位)。他们还会与学术界互动,以保持对研究的最新了解,并支持自己的学习,这在我看来是非常宝贵的资源。

自学的数据科学家能够批判性地评估新信息,并将其与他们正在进行的项目或同行的项目联系起来。此外,他们展现出开放的心态,保持对新思想和观点的接受态度,并愿意评估和挑战自己的假设,以便不断学习和扩展技能。这些人表现出高度的自我效能感,使他们对学习充满动力,并能坚持不懈。我曾与一些出色的数据科学家交谈过,他们还实践元认知。他们会反思自己的认知过程和学习方法,这对于调节和引导自己的思维和知识朝向特定目标非常有益。在我看来,这有助于提高他们的学习效率和解决问题的能力。

我如何识别这样的人呢?我会查看他们的公开代码库,看看他们是否实现了难度较大的理论概念或从科学论文中得出的算法。他们是如何处理手头的问题的?他们参考了哪些文献和资源?他们最终是如何开发出可行的解决方案的?这些见解可以提供有用的信息,帮助理解他们的思维过程和解决问题的方法。我还会关注那些采用“通过教学学习”方法的数据科学家,他们通过教导他人来巩固自己对相关主题的理解和知识。这种候选人对团队非常有益,因为他们愿意分享他们的学习成果!作为面试过程的一部分,我会问候选人他们希望学习(或更好地理解)的方法或算法,以及原因。他们的愿望清单上有什么?

高效的沟通者

杰出的数据科学家拥有能够清晰阐述商业和技术信息的词汇和沟通风格,同时考虑到听众的需求和视角 他们可能会使用主动倾听技巧,例如复述或总结演讲者所说的内容,并提出后续问题。他们能够设身处地为听众着想,展现出同理心。这在数据科学家需要向非技术的业务利益相关者解释复杂概念时尤为必要。他们也对自己的能力充满信心,但能够自信地表达他们的不确定性。他们能够在演讲中保持听众的兴趣,使用讲故事的方式使信息令人难忘且具有影响力。他们是与产品负责人、业务利益相关者和同事的优秀合作者,并且意识到团队动态,这对于识别潜在障碍和成功机会至关重要。他们对他人充满信任,并善于建立稳固的工作关系,保持团队中的高度互依,并以积极和建设性的方式解决冲突。这也增强了团队的凝聚力,促进了成功的成果。

创造性问题解决者

杰出的数据科学家具备高度的流动智力,使他们能够解决新颖的问题、进行抽象思维并适应新情况。这有助于他们的创造力和对新概念的掌握,往往导致产生新的商业理念(如果你愿意,可以称之为创业思维),这些理念可以转化为新的实用产品或解决方案。一些人可能表现出发散性思维,能够在面对挑战或问题时产生多种想法,这涉及超越传统方法、建立新颖的联系和考虑多种视角。他们富有想象力、好奇心强,并且乐于接受新体验。

要识别这样的人,我会关注他们在提出不同问题解决方法时的认知灵活性。这里的重点并不一定是候选人提出正确的解决方案,而是评估他们通过连接看似无关的概念来生成有意义的联想的原创思维能力,以解决问题。

域专业人士

除了技术技能外,有些人可能在特定领域或行业中拥有相当的知识和专业技能,这对于您的招聘目标至关重要。杰出的数据科学家可能对业务(例如,商业战略、目标等)有较好的理解,这帮助他们识别并自主解决与业务相关的核心问题。其他领域(如工程、制造、金融等)也是如此。这些数据科学家在利用其领域知识理解他们所处理的数据的细微差别方面具有优势,并能够开发相关且可操作的模型或洞察。他们还会知道哪些模型假设最能代表领域的基本动态,这可以在面试中评估。招聘领域专业人士可以加速特定领域的数据科学计划。

技术熟练度

以下是一些杰出数据科学家非常重视的关键技术领域。

合理的模型假设: 能够数学地识别和解释基本逻辑、原则和假设,使用连贯的论据和证据来支持他们的观点。杰出的数据科学家能够解释他们的工具,并识别利用这些工具的理想场景。

系统导向推理: 他们会通过考虑解决方案嵌入的整个端到端系统来处理问题。他们仔细分析系统中各种数据和模型组件之间的相互关系,考虑这些组件如何被消费或互动。他们还会考虑系统中某部分的变化对整个系统的影响,并对导致系统行为的基本结构和反馈回路感兴趣。我还观察到,杰出的数据科学家会与系统的最终用户密切合作,以利用用户体验反馈。

指标驱动: 他们非常强调使用数据和定量措施(适当的统计数据)来推动决策和评估绩效。他们专注于跟踪和分析 KPI,并利用这些信息做出明智的决策,以改善模型或系统。例如,他们将准确知道在特定模型中使用哪些指标,如何解释它们,以及需要注意哪些认知偏差和统计偏差(如 Goodhart 定律、Simpson 悖论、Berkson 悖论等)。

数据导向: 他们非常注重数据的收集、分析和解释,以指导决策并获得对各种现象的洞察。个人在处理数据、数据分析和数据可视化方面非常熟练。他们在数据探索过程中注重细节,并会寻找异常发现的解释——绝不会将隐藏的知识掩盖在地毯下。

开源项目: 他们可能有自己的公开开源项目或代码库,在这些项目中,他们和其他贡献者经常进行贡献。这些个人积极地为一个共同目标贡献代码,他们的贡献被社区重视并接受。

面试可以通过不同的方式进行。就个人而言,我不喜欢给数据科学候选人分配时间限制的编码任务。这样做会削弱这篇文章中几乎所有的内容。相反,我会审查他们的公开代码库,以评估他们的编码风格、贡献和解决问题的方法。通过检查他们代码和分析中的评论,可以判断他们的思维过程和观察结果。在面试过程中与他们的公开代码相关的跟进问题可以帮助澄清差距。或者,让数据科学候选人谈谈他们做的一个有趣项目(保护机密信息),或提供给他们一个预定的案例研究以概念性地解决(而非代码)应该能为招聘团队提供充足的机会来识别这篇文章中提到的大部分特征。

附注:请确保你的职位要求是现实的,并能代表实际角色;否则,出色的候选人会对你的公司失去兴趣。了解你目前团队中哪些实践是成功的,你的公司/团队文化如何,你的团队需要什么额外技能,以及你将如何留住并培养那些出色的数据科学家!

以幽默的结尾——一个出色的数据科学家就像一个卡尔曼滤波器 😉

  • 适应性: 他们应该能够随着新信息或数据的出现,调整他们的模型和分析方法。

  • 稳健: 他们应该能够应对噪声和不确定的数据(或人)。他们应该能够利用不确定性,过滤掉噪声和其他无关信息,专注于重要内容。

  • 预测: 他们应该能够基于过去的数据(以及当前的观察)做出准确的预测,识别数据中的趋势和模式,并利用这些信息做出明智的预测/推荐。

  • 平衡准确性和复杂性: 他们应该找到最适合给定数据集的最佳模型,并从中提取最有用的信息,同时通过使用简单(通常是线性的)假设来保持计算时间合理。

直方图均衡化:逐步指南 (CV- 06)

原文:towardsdatascience.com/histogram-equalization-a-step-by-step-guideline-06-527dcb1a7504

图像直方图均衡化详细说明

Md. ZubairTowards Data Science Md. Zubair

·发布于 Towards Data Science ·5 分钟阅读·2023 年 7 月 27 日

--

原图由 Dan Fador 提供,Pixabay (左上图是主图,左下图是图像的灰度版本。右侧的图像是直方图均衡化的结果)

动机

直方图是通过条形图可视化频率分布的过程。在计算机视觉中,图像直方图是表示强度值频率的过程。通过图像直方图均衡化,我们可以轻松调整图像强度值的频率分布。通常,这个过程有助于提高图像的对比度和亮度。该过程简单易行。本文将讨论直方图均衡化的完整过程以及代码示例。

目录

  1. **图像直方图**

  2. **直方图均衡化的完整过程**

  3. **逐步实施**

图像直方图

图像直方图 是用条形图表示图像强度值的频率。在图-1 中,我展示了一幅样本图像及其在二维空间中的强度值。

图-1: 样本图像强度值(作者提供的图像)

值的范围从 0 到 7。让我们计算这些值的频率。

图-2: 图像强度值的频率(作者提供的图像)

图像直方图 是对频率强度值的简单表示,如图-3所示。

图-3: 图像的直方图(作者提供的图片)

直方图均衡化的完整过程

直方图均衡化是通过一些函数均匀分布图像强度值的频率的过程。主要的函数是概率函数 — PDF (概率密度函数) 和 CDF (累积分布函数)

  • PDF 是通过将强度值的频率除以总频率来计算的。

  • CDF 表示小于或等于特定值的概率分布的概率。例如,强度值的 PDF 为*0 → 0.12, 1 → 0.24, 2 → 0.12, 等等。所以,1 的 CDF 为 0.12+0.24 = 0.36,2 为 0.36+0.12=0.48,以此类推。*完整结果编译在图-4中。

图-4: 直方图均衡化计算(作者提供的图片)

最后,我们需要将CDF与一个整数相乘。

选择数字的过程 —

图像的最大强度值为 7,如图 — 1所示。我们必须用以下公式选择一个整数值。

这里,x 是表示最大强度值的最小位数。对于我们的情况,最大值为 7,*(2³ -1 = 7)*。如果我们选择 2 作为 x 的值,就无法表示最大强度值。因此,在我们的案例中,最佳乘法值是 7。

最终,我们将对乘法结果进行四舍五入以获得均衡直方图值。将强度值替换为均衡直方图值以找到最终输出。整个过程见图-4

图-5: 初始强度值和均衡直方图强度值(作者提供的图片)

为了应用直方图均衡化,我们需要用均衡直方图值替换初始强度值。

新的图像如下 —

图-6: 带有均衡直方图强度值的图像(作者提供的图片)

均衡直方图强度值的频率与之前的频率不同,如图-6所示。

图-6: 新的均衡频率(作者提供的图片)

图像强度频率的比较见图-7

图-7: 比较(作者提供的图片)

如果我们观察均衡直方图频率与初始频率的比较,会发现均衡直方图的强度值比初始值高,如图-7所示。

现在是时候用 Python 进行实际操作了……

分步实现

为了实现直方图均衡化,我们使用OpenCV库。

  • 让我们导入必要的库,并以灰度模式读取图像 —

  • 计算图像的频率并绘制直方图——

由于大多数图像部分较暗,低强度值(~0)的频率大于高强度值。

  • OpenCV 函数 ***cv2.equalizeHist()*** 有助于实现直方图均衡化——

如果我们仔细观察输出图像,可能会注意到图像的亮度高于原始图像。

  • 让我们设置均衡化直方图图像——

强度值的频率比原始图像更加均匀分布。

处理彩色图像

RGB/BGR 图像有 3 个通道—— 红色、绿色和蓝色。 要在图像中应用直方图均衡化,我们需要将 RGB/BGR 图像转换为 **HSV** *(色调、饱和度和值)* 图像。最后,我们对 HSV 图像的值参数应用直方图均衡化。

  • 应用直方图均衡化,并使用 **matplotlib** 将其转换为 RGB 图像进行可视化。

显然,图像的亮度和对比度显著提高了。

如果你愿意,可以绘制之前展示的直方图和均衡化直方图。

结论

我们生活在生成式 AI 的世界里。这些基本的计算机视觉技术看起来非常基础和过时,但它们是创建强大模型或创新的基础。所以,我总是强调知识的基础。

我已经开始撰写一系列计算机视觉文章。之前的文章如下嵌入——

## NumPy 和 OpenCV 在计算机视觉中的入门 (CV-01)

用 Python 开始你的计算机视觉编码

towardsdatascience.com ## 计算机视觉中色彩表示的综合指南 (CV-02)

色彩空间和颜色模型的详细解释

towardsdatascience.com ## 图像融合的最简单指南 (CV-03)

最简单的图像融合和粘贴指南

towardsdatascience.com [## 阈值化——使图像更可见的一种方法 (CV-04)

使用阈值化从图像中提取更多信息

阈值处理:使图像更清晰可见 形态学操作:去除图像失真 [## 计算机视觉中的形态学操作及其模拟 (CV-05)

图像处理中的形态学操作最简单解释

形态学操作:去除图像失真

达到时间预测:时间序列概率预测的另一种方式

原文:towardsdatascience.com/hitting-time-forecasting-the-other-way-for-time-series-probabilistic-forecasting-6c3b6496c353

需要多长时间才能达到一个特定值?

Marco CerlianiTowards Data Science Marco Cerliani

·发表于Towards Data Science ·阅读时长 4 分钟·2023 年 6 月 27 日

--

Mick Haupt拍摄,照片来源于Unsplash

准确预测的能力对每个时间序列预测应用都是基础。为此,数据科学家通常选择那些从点预测角度最小化误差的最佳模型。这虽然正确,但可能并不总是最有效的方法。

数据科学家还应考虑开发概率预测模型。这些模型不仅会生成点估计,还会提供上限和下限的可靠性区间,未来的观测值很可能会落在这些区间内。尽管概率预测似乎是统计学或深度学习解决方案的特权,任何模型都可以用来生成概率预测。这一概念在我之前的文章中解释了,我介绍了将符合性预测作为使用任何 scikit-learn 模型来估计预测区间的方法

点预测无疑更容易向非技术利益相关者传达。同时,能够生成关于我们预测可靠性的关键绩效指标(KPI)也是一种附加值。概率输出可能提供更多的信息来支持决策。告知未来几小时有 60%的降雨概率可能比报告降雨毫米数更具信息性。

在这篇文章中,我们提出了一种称为预测到达时间的技术,用于估计特定事件或条件发生的时间。它被证明是准确的,因为它基于合规预测,可解释的,因为它具有概率解释性,并且可重复的,与任何预测技术都适用。

介绍到达时间预测

预测到达时间是一个在各种领域常用的概念。它指的是预测或估计特定事件或条件发生所需的时间,通常是在达到特定阈值或水平的背景下。

模拟的季节性和趋势 [图片由作者提供]

模拟时间序列(季节性 + 趋势)示例 [图片由作者提供]

到达时间的最著名应用涉及可靠性分析和生存分析等领域。它包括估计系统或过程经历特定事件的时间,例如故障或达到特定状态。在金融领域,到达时间通常用于确定信号/指数朝向预期方向的概率。

总体而言,预测到达时间涉及对某一事件发生所需时间的预测,该事件遵循时间动态。

从点预测到概率预测

要正确估计到达时间,我们必须从点预测开始。作为第一步,我们选择所需的预测算法。对于本文,我们采用了来自tspiral的简单递归估计器,这种估计器在 scikit-learn 风格中易于获取。

预测值与实际数据点在测试集上的对比 [图片由作者提供]

model = ForecastingCascade(
    Ridge(),
    lags=range(1,24*7+1),
    use_exog=False,
)

我们的目标是为每个预测点生成预测分布,从中提取概率洞察。这通过三步法完成,并利用合规预测的理论:

  • 预测结果通过交叉验证在训练集上收集,然后进行平均。
CV = TemporalSplit(n_splits=10, test_size=y_test.shape[0])

pred_val_matrix = np.full(
    shape=(X_train.shape[0], CV.get_n_splits(X_train)),
    fill_value=np.nan,
    dtype=float,
)

for i, (id_train, id_val) in enumerate(CV.split(X_train)):

    pred_val = model.fit(
        X_train[id_train], 
        y_train[id_train]
    ).predict(X_train[id_val])

    pred_val_matrix[id_val, i] = np.array(
        pred_val, dtype=float
    )

pred_val = np.nanmean(pred_val_matrix, axis=1)
  • 合规性分数基于交叉验证预测和实际值的绝对残差在训练数据上计算。
conformity_scores  = np.abs(
    np.subtract(
        y_train[~np.isnan(pred_val)], 
        pred_val[~np.isnan(pred_val)]
    )
)
  • 未来的预测分布通过将合规性分数添加到测试预测中获得。
pred_test = model.fit(
    X_train, 
    y_train
).predict(X_test)

estimated_test_distributions = np.add(
    pred_test[:, None], conformity_scores
)

测试数据上的预测分布 [图片由作者提供]

按照上述程序,我们得到一组可能的轨迹,这些轨迹是未来值可能遵循的路径。我们拥有所有需要的东西来提供预测的概率表示。

从概率预测到到达时间预测

对于每个未来的时间点,记录了估计的测试分布中值超过预定义阈值(我们的击中目标水平)的次数。这个计数被转化为一个概率,方法是通过估计测试分布中的值的数量进行归一化。

最后,对概率数组应用了一种转换,以获得一系列单调递增的概率。

THRESHOLD = 40

prob_test = np.mean(estimated_test_distributions > THRESHOLD, axis=1)

prob_test = pd.Series(prob_test).expanding(1).max()

测试集上的预测数据点与实际数据点以及击中时间概率[作者提供的图片]

无论我们尝试预测的事件是什么,我们都可以仅从点预测开始生成概率曲线。解释仍然很直接,即对于每个预测的时间点,我们可以推导出目标序列达到预定义水平的概率。

总结

在这篇文章中,我们介绍了一种为预测模型提供概率结果的方法。这不需要应用奇怪且复杂的额外估计技术。从一个点预测问题出发,可以通过应用击中时间方法,添加任务的概率概述。

查看我的 GITHUB 仓库

保持联系:Linkedin

利用 SageMaker 多模型端点和 GPU 实例托管数百个 NLP 模型

原文:towardsdatascience.com/host-hundreds-of-nlp-models-utilizing-sagemaker-multi-model-endpoints-backed-by-gpu-instances-1ec215886248

将 Triton 推理服务器与 Amazon SageMaker 集成

Ram VegirajuTowards Data Science Ram Vegiraju

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

--

图片来源于 Unsplash

过去,我们曾探索过SageMaker 多模型端点 (MME)作为在单一端点后面托管多个模型的经济有效的选项。虽然在 MME 上托管较小的模型可以使用基于 CPU 的实例,但随着这些模型变得越来越大和复杂,有时可能需要 GPU 计算。

MME 由 GPU 支持的实例是一项特定的 SageMaker 推理功能,我们将在本文中利用它展示如何在单个端点上高效托管数百个 NLP 模型。请注意,在本文发布时,SageMaker 上的 MME GPU 目前支持以下单 GPU 基于的实例系列:p2、p3、g4dn 和 g5。

MME GPU 目前还由两个模型服务堆栈提供支持:

  1. Nvidia Triton 推理服务器

  2. TorchServe

为了本文的目的,我们将利用 Triton 推理服务器和 PyTorch 后端,在我们的 GPU 实例上托管基于 BERT 的模型。如果你对 Triton 不太熟悉,我们会稍作介绍,但我建议参考我的入门文章 这里。

注意:本文假设你对 SageMaker 部署和实时推理有中级理解。我建议你参考这篇 文章 以深入了解部署/推理。我们也会概述多模型端点,但要进一步了解,请参考这份 文档

免责声明:我是 AWS 的机器学习架构师,我的观点仅代表我个人。

什么是 MME?解决方案概述

为什么使用多模型端点(MME),以及何时使用它们?MME 是一种成本和管理上有效的托管选项。传统的 SageMaker 端点设置将如下所示:

作者提供的图片

当你有数百或甚至数千个模型时,管理如此多的不同端点变得困难,而且你需要为每个持久端点背后的硬件付费。使用 MME,这变得简单,因为你只需要管理一个端点和一组硬件:

作者提供的图片

你可以将这些不同的模型打包成一个模型 tarball(model.tar.gz)。这个模型 tarball 实质上会包含所有模型元数据,格式符合模型服务解决方案的要求。在这个例子中,我们使用 Triton 作为我们的模型服务器,所以我们的 model.tar.gz 将如下所示:

- model.tar.gz/
  - linear_regression_model
    - 1
       - model.pt
       - model.py (optional, not included here)
    - config.pbtxt

在这个例子中,我们将制作 200 份我们的模型 tarball,以展示如何在单个端点上托管多个模型。对于实际应用,这些 tarballs 会根据你推送到端点后的模型有所不同。这些 tarballs 都被捕获在 SageMaker 能理解的一个通用 S3 路径中:

MME 分桶(作者提供的图片)

MME 背后的模型如何管理?SageMaker MME 将接收请求并动态加载和缓存你调用的特定模型。如果你期望你的端点会有大量流量,最好在端点后面配置多个初始实例或设置自动扩展。例如,如果单个模型接收了大量调用,这个模型将被加载到另一个实例上,以处理额外的流量。要进一步了解 SageMaker MME 的负载测试,请参考这个 指南。

本地设置与测试

在这个例子中,我们将在一个 SageMaker Classic Notebook 实例中工作,使用 conda_python3 内核和 g4dn.4xlarge 实例。我们使用基于 GPU 的实例在本地测试 Triton,然后再部署到 SageMaker 实时推理中。

在这个例子中,我们使用了流行的 BERT 模型。我们首先要创建本地模型文件,因此我们使用 PyTorch 进行跟踪,然后保存序列化的模型文件。

import torch
from transformers import BertModel, BertTokenizer
device = "cuda" if torch.cuda.is_available() else "cpu"

# Load bert model and tokenizer
model_name = 'bert-base-uncased'
model = BertModel.from_pretrained(model_name, torchscript = True)
tokenizer = BertTokenizer.from_pretrained(model_name)

# Sample Input
text = "I am super happy right now to be trying out BERT."

# Tokenize sample text
inputs = tokenizer(text, return_tensors="pt", padding=True, truncation=True)

# jit trace model
traced_model = torch.jit.trace(model, (inputs["input_ids"], inputs["attention_mask"]))

# Save traced model
torch.jit.save(traced_model, "model.pt")

我们可以通过加载保存的模型并用标记化的文本运行示例推理来确认模型推理是否正常。

# sample inference with loaded model
loaded_model = torch.jit.load("model.pt")
res = loaded_model(input_ids=inputs["input_ids"], attention_mask=inputs["attention_mask"])
res

我们现在可以专注于设置 Triton 来托管这个特定的模型。为什么在实现 SageMaker 之前需要 本地测试 Triton?我们希望在创建 SageMaker 端点之前捕获任何设置问题。创建 SageMaker 端点可能需要几分钟,直到你在日志中看到失败之前,你无法知道你的设置中出现了什么问题,即使是一个小的脚本错误或模型 tarball 的不当结构。通过首先本地测试 Triton,我们可以快速迭代我们的配置和模型文件,以捕获任何错误。

对于 Triton,我们首先需要一个 config.pbtxt 文件。这捕获了我们的输入和输出维度以及其他你希望调整的 Triton 服务器属性。在这种情况下,我们可以从描述 BERT 架构的 transformers 库中获取输入和输出形状。

from transformers import BertConfig
bert_config = BertConfig.from_pretrained(model_name)
max_sequence_length = bert_config.max_position_embeddings
output_shape = bert_config.hidden_size
print(f"Maximum Input Sequence Length: {max_sequence_length}")
print(f"Output Shape: {output_shape}")

我们可以使用这些值来创建我们的 config.pbtxt 文件。

name: "bert_model"
platform: "pytorch_libtorch"

input [
  {
    name: "input_ids"
    data_type: TYPE_INT32
    dims: [1, 512]
  },
  {
    name: "attention_mask"
    data_type: TYPE_INT32
    dims: [1, 512]
  }
]

output [
  {
    name: "OUTPUT"
    data_type: TYPE_FP32
    dims: [512, 768]
  }
]

然后我们用以下 Docker 命令启动 Triton 推理服务器,指向我们的模型库。

docker run --gpus all --rm -p 8000:8000 -p 8001:8001 -p 8002:8002 -v
/home/ec2-user/SageMaker:/models nvcr.io/nvidia/tritonserver:23.08-py3
tritonserver --model-repository=/models --exit-on-error=false --log-verbose=1

一旦容器启动,你可以进行示例请求以确保我们能够成功地使用现有模型文件进行推理。

import requests
import json

# Specify the model name and version
model_name = "bert_model" #specified in config.pbtxt
model_version = "1"

# Set the inference URL based on the Triton server's address
url = f"http://localhost:8000/v2/models/{model_name}/versions/{model_version}/infer"

# sample invoke
output = requests.post(url, data=json.dumps(payload))
res = output.json()

一旦这项工作成功进行,我们可以专注于 SageMaker MME 部署。

SageMaker MME GPU 部署

现在我们已经将模型文件转换为模型服务器所理解的格式,我们可以将其封装成 SageMaker 预期的 model.tar.gz 格式。

!tar -cvzf model.tar.gz bert_model/

我们还在一个公共 S3 路径中创建了 200 个模型副本,以支持我们的 MME。

%%time
# we make a 200 copies of the tarball, this will take about ~6 minutes to finish (can vary depending on model size)
for i in range(200):
    with open("model.tar.gz", "rb") as f:
        s3_client.upload_fileobj(f, bucket, "{}/model-{}.tar.gz".format(s3_model_prefix,i))

除了模型文件的位置,我们还需要指定我们用于 SageMaker 部署的管理 Triton 容器。

triton_image_uri = "{account_id}.dkr.ecr.{region}.{base}/sagemaker-tritonserver:23.07-py3".format(
    account_id=account_id_map[region], region=region, base=base
)

print(f"Triton Inference server DLC image: {triton_image_uri}")

接下来的几个步骤是常见的 SageMaker 端点创建流程:

在我们的 EndpointConfiguration 对象中,我们指定了基于 GPU 的实例:在本例中为 g4dn.4xlarge。

endpoint_config_response = client.create_endpoint_config(
    EndpointConfigName=endpoint_config_name,
    ProductionVariants=[
        {
            "VariantName": "tritontraffic",
            "ModelName": model_name,
            "InstanceType": "ml.g4dn.4xlarge",
            "InitialInstanceCount": 1,
            "InitialVariantWeight": 1
        },
    ],
)

endpoint_name = "triton-mme-gpu-ep" + time.strftime("%Y-%m-%d-%H-%M-%S", time.gmtime())

create_endpoint_response = client.create_endpoint(
    EndpointName=endpoint_name, EndpointConfigName=endpoint_config_name
)

print("Endpoint Arn: " + create_endpoint_response["EndpointArn"])

端点可能需要几分钟来创建,但创建完成后,你应该能够运行示例推理。在 TargetModel 头部,你可以指定 1 到 200 之间的任何模型,因为我们将这个范围设定为不同 model.tar.gz 文件的分隔符。

response = runtime_client.invoke_endpoint(
    EndpointName=endpoint_name, ContentType="application/octet-stream", 
    Body=json.dumps(payload), TargetModel='model-199.tar.gz'
)
print(json.loads(response["Body"].read().decode("utf8")))

在运行推理时,你也可以通过 CloudWatch 监控硬件和调用指标。特别是由于这是一个基于 GPU 的端点,你可以通过 API 或 SageMaker 控制台监控 GPU 使用率。

SageMaker 控制台的监控标签(作者截图)

硬件 GPU 指标(作者截图)

要了解所有其他 MME CloudWatch 指标,请参阅以下文档

附加资源与结论

[## SageMaker-Deployment/RealTime/Multi-Model-Endpoint/Triton-MME-GPU/mme-gpu-bert.ipynb at master ·…

对 SageMaker 推理选项及其他功能的示例汇编。…

github.com](https://github.com/RamVegiraju/SageMaker-Deployment/blob/master/RealTime/Multi-Model-Endpoint/Triton-MME-GPU/mme-gpu-bert.ipynb?source=post_page-----1ec215886248--------------------------------)

示例的完整代码可以在上述链接中找到。MME 已经是一个非常强大的功能,但当与基于 GPU 的硬件配合使用时,可以让我们在 NLP 和 CV 领域托管更大的模型。Triton 也是一个动态服务选项,它支持多种不同的框架和多样的硬件,从而大大增强了我们的 MME 应用程序。有关更多 SageMaker 推理示例,请参阅以下链接。如果你有兴趣更好地了解 Triton,请参阅我的 PyTorch 模型入门指南。

一如既往,感谢你的阅读,欢迎留下任何反馈。

如果你喜欢这篇文章,欢迎在 LinkedIn 上与我联系,并订阅我的 Medium Newsletter。如果你是 Medium 的新用户,可以使用我的 会员推荐链接注册。

在 Colab 上免费托管你的 Google Earth Engine RESTful API

原文:towardsdatascience.com/host-your-google-earth-engine-restful-apis-on-colab-for-free-3a95abc729d0

使用 FastAPI 和 ngrok

黄四兴Towards Data Science 黄四兴

·发表于Towards Data Science ·阅读时长 8 分钟·2023 年 1 月 6 日

--

图片由NASA提供,Unsplash上的照片

地理空间数据的需求一直很高。它揭示了我们星球随时间变化的情况。当我们谈到地理空间时,我们会想到Google Earth Engine (GEE)。该服务有几个优势。它托管了跨度超过 37 年的大量数据集合。所有计算都在谷歌强大的云基础设施上运行。更重要的是,对于非营利项目,它是免费的。通过 GEE,我们可以免费研究土地利用和土地覆盖 (LULC)、植被、本地气候(这里这里),甚至美国的农作物生产

然而,GEE 确实有较高的门槛。首先,必须精通 JavaScript 或 Python。其次,我们需要熟悉许多地理空间概念,如图像集合、几何体和卫星波段。第三,它的异步请求-响应模式对于新手来说需要一些时间来适应。

这对许多数据科学家来说是一个小挑战。大多数情况下,他们只希望快速获得一组坐标的某些值,例如土壤 pH 值或平均地表温度。截止目前,他们需要进行相当多的编码,因为 GEE 没有提供 RESTful API。如果我们能自己填补这个空白岂不是很好(视频 1)?我们的 API 应该封装一些常见的 GEE 计算,并提供互联网 HTTP 访问。

视频 1. 使用 FastAPI 在 Google Colab 上创建的 Google Earth Engine API。视频由作者提供。

让我们在本文中完成它。我选择了FastAPI来完成这项工作。这个流行的库允许我们用 Python 构建高性能的 API,Python 是 GEE 中的两种服务语言之一。Google Colab 是首选的平台。Colab 不仅与 GEE 无缝集成,而且在ngrok的帮助下,还可以通过 HTTP 公开提供 API(视频 1)。在本文中,我将描述两个 API 的构建。其中一个返回土地温度,另一个计算给定坐标集的土壤 pH 值。之后,我将演示如何使用这些 API 从BacDive获取一些细菌的元数据。

这里定义了 API。

[## Google Colaboratory GEE APIs

colab.research.google.com](https://colab.research.google.com/drive/1BNsFLHA_ISC7sDSLF4-znBNY9YSIUbh1?usp=sharing&source=post_page-----3a95abc729d0--------------------------------)

这是一个 Python 应用程序,它通过我们的 GEE API 在 BacDive 中验证数据。

[## Google Colaboratory BacDive

colab.research.google.com](https://colab.research.google.com/drive/12NXlV6Q8Qrs6hLqEIeEmQ1cm_CElCkOB?usp=sharing&source=post_page-----3a95abc729d0--------------------------------)

1. Google Earth Engine API 与 FastAPI 和 ngrok 的结合

首先,你需要一个Google Earth Engine 账户和一个ngrok 账户。在你的账户页面找到 ngrok 的 Authtoken(图 1)。

图 1. 将 Authtoken 存储在你的 ngrok 账户页面。图片由作者提供。

1.1 初始化

创建一个 Colab 笔记本。在库导入后,让我们初始化 GEE 和 ngrok。首先,按照 Google 的说明进行身份验证和初始化 GEE(第 1 和第 2 行)。之后,input函数会提示你输入 ngrok 的 Authtoken(第 3 行)。将你的 Authtoken 粘贴到输入框中并确认。然后,代码将在第 4 行授权你的 ngrok 实例。

# Trigger the authentication flow.
ee.Authenticate()

# Initialize the library.
ee.Initialize()

ngrok_key = input("Your ngrok Authtoken")

!ngrok authtoken {ngrok_key}

1.2 GEE 封装函数

接下来,我们可以创建一些封装函数来封装 GEE 交互。

def generate_collection(geometry, dataset, startDate, endDate):
    collection = ee.ImageCollection(dataset).filterDate(startDate, endDate).filterBounds(geometry);
    return collection

def get_mean(image_collection, point, property, scale_factor):
    image = image_collection.select([property]).reduce(ee.Reducer.mean()).multiply(scale_factor)

    fcPoint = ee.FeatureCollection([ee.Feature(point)])

    geojson = image.sampleRegions(collection = fcPoint,  scale = 1000, geometries = True)

    return geojson.getInfo()["features"][0]["properties"]

def generate_image(dataset):
    return ee.Image(dataset)

def get_image_value(image, point, property, scale_factor):
    return image.reduceRegion(ee.Reducer.mean(), point, 100).get(property).getInfo() * scale_factor

一般来说,GEE 将测量结果存储在图像中。某些测量,例如海拔和土壤 pH,只做了一次,它们被存储在单个图像中。相比之下,其他测量,如土壤温度和降水量,是周期性进行的,因此它们被存储在图像集合中。两个函数generate_collection(第 1-3 行)和generate_image(第 14-15 行)分别返回图像集合和单个图像。接下来,我们想获取目标区域的平均测量值(第 5-12 行 & 第 17-18 行)。

1.3 FastAPI

现在是时候在我们的笔记本中设置 FastAPI 了。

app = FastAPI()

@app.get('/')
async def root():
    return {'hello': 'world'}

@app.get("/land-surface-temperature")
async def get_land_surface_temperature(lat: float, lon: float, start_date: datetime.date, end_date: datetime.date):
    dataset = "MODIS/061/MOD11A1"

    point = ee.Geometry.Point([lon, lat])

    image_collection = generate_collection(point, dataset, start_date.strftime("%Y-%m-%d"), end_date.strftime("%Y-%m-%d"))

    result = get_mean(image_collection, point, "LST_Day_1km", 0.02)

    return {'result': result}

@app.get("/soil-ph")
async def get_soil_ph(lat: float, lon: float):
    dataset = "OpenLandMap/SOL/SOL_PH-H2O_USDA-4C1A2A_M/v02"

    point = ee.Geometry.Point([lon, lat])

    image = generate_image(dataset)

    scale_factor = 0.1

    ph = get_image_value(image, point, "b0", scale_factor)

    return {"result": {"pH": ph}}

在这个代码块中,我们首先初始化一个 FastAPI 应用。然后定义了三个路由:根路由,land-surface-temperaturesoil-ph。在后两个路由中,我们使用了第 1.2 节中的函数来请求 GEE 中的测量值。温度值来自于MOD11A1.061 Terra Land Surface Temperature and Emissivity Daily Global 1km数据集(MODIS 数据和产品在 LP DAAC 中获取,无后续使用、销售或再分发的限制),而 pH 值由OpenLandMap Soil pH in H2O数据集提供(CC-BY-SA-4.0)。

1.4 ngrok

现在让我们开始 API 服务。这里的代码借鉴自一个回答在 stackoverflow.com 上。

ngrok_tunnel = ngrok.connect(8000)
print('Public URL:', ngrok_tunnel.public_url)
nest_asyncio.apply()
uvicorn.run(app, port=8000)

代码将生成一个端点 URL 并继续运行(视频 1)。

1.5 测试

我们现在可以测试服务。点击端点 URL,你将看到根路由的“hello world”消息(视频 1)。

令人惊讶的是,FastAPI 会自动生成文档。你可以在/redoc路由下访问它(图 2)。

图 2. 文档由 FastAPI 准备。图片由作者提供。

让我们测试land-surface-temperature API。在 URL 中的“?”标记后,将所需参数编码为键值对。例如,以下查询字符串允许我们获取 2020 年 1 月 1 日到 2020 年 5 月 1 日(52.72389418745157, -92.03593750000002)的地表温度。

/land-surface-temperature?lat=52.72389418745157&lon=-92.03593750000002&start_date=2020-01-01&end_date=2020-05-01

图 3. 2020 年 1 月 1 日到 2020 年 5 月 1 日(52.72389418745157, -92.03593750000002)的地表温度。图片由作者提供。

请注意,返回的值是以开尔文为单位的(图 3)。

soil-ph API 的工作原理类似。但它不需要采样周期。因此,以下查询字符串可以单独请求相同坐标对的土壤 pH 值(视频 1)。

/soil-ph?lat=52.72389418745157&lon=-92.03593750000002

2. 比较 BacDive 的实验室结果与 GEE 的野外元数据

BacDive数据库由 DSMZ 创建,收集了关于细菌分离株的信息,包括其生长温度、pH 值和代谢特征。请注意,它几乎所有的信息都是在实验室中生成的。微生物在野外的行为很可能有所不同。

最近,BacDive已集成了Microbeatlas。嵌入的Microbeatlas地图显示了许多细菌 16S 序列的全球分布(图 4)。

图 4. BacDiveMicrobeatlas。作者提供的图片。

这个交叉验证非常棒。现在,研究人员不仅可以了解细菌在实验室中的行为(BacDive),还可以了解它们在全球范围内的分布(Microbeatlas)。但我们可以做得更多。我们可以从新推出的 GEE APIs 中收集野外的元数据,并将其与 BacDive 的实验室结果进行比较。这种比较可以向我们展示细菌在实验室和野外的生活是否有所不同。

以细菌Rhodopseudomonas palustris R1为例。它的 Microbeatlas 页面 显示该细菌可以在长长的样本列表中找到(图 5)。

图 5. 根据Microbeatlas,全球范围内的Rhodopseudomonas palustris R1分布。作者提供的图片。

截至 2023 年 1 月 5 日,Microbeatlas中的DOWNLOAD按钮无法使用。因此,我使用“soil”关键词过滤了这些样本。然后我检查了前几个样本,并选择了那些具有完整位置和时间数据的样本(表 1)。

表 1. 五个样本Rhodopseudomonas palustris R1丰富存在的土壤。

使用以下 Python 代码,我们可以获取这些样本的土地温度和 pH 值(也可以在我上面的 Colab 链接中找到)。

sheet_id = "YOUR GOOGLE SHEETS ID"
sheet_name = "YOUR GOOGLE SHEETS NAME"

url = f"https://docs.google.com/spreadsheets/d/{sheet_id}/gviz/tq?tqx=out:csv&sheet={sheet_name}"
df = pd.read_csv(url)

api_url = "YOUR ngrok ENDPOINT"

land_temperature_route = "/land-surface-temperature/?"
land_ph_route = "/soil-ph/?"

for index, row in df.iterrows():
    date = datetime.strptime(row["date"], "%Y-%m-%d")

    start_date = datetime.strptime(f"{date.year}-{date.month}", '%Y-%m').date()
    end_date =  datetime.strptime(f"{date.year}-{date.month + 1}", '%Y-%m').date()

    lat = row["lat"]

    lon = row["lon"]

    temp_url = f"{api_url}{land_temperature_route}lat={lat}&lon={lon}&start_date={start_date}&end_date={end_date}"
    ph_url = f"{api_url}{land_ph_route}lat={lat}&lon={lon}"

    temp = requests.get(temp_url).json()["result"]["LST_Day_1km_mean"] -273.15
    ph = requests.get(ph_url).json()["result"]["pH"]

    print (f'{row["sample"]}\t{temp}\t{ph}')

结果见表 2。

表 2. 根据 GEE 的五个样本的温度和 pH 值。

一方面,这些前五个样本的温度值范围为 11 至 19°C,远低于 BacDive 中显示的 28–30°C 的生长温度。另一方面,BacDive 没有记录该细菌的生长 pH 值。但我们的结果表明,Rhodopseudomonas palustris R1在酸性土壤中丰富存在,从而填补了 BacDive 中的信息空白。

结论

本文展示了使用 FastAPI 和 ngrok 在 Colab 上为 GEE 原型化自己的 RESTful API 是多么简单。只需几行代码,我们就可以在互联网上免费设置功能完善的 API。现在我们可以通过简单的 URL 请求 GEE 的地理空间结果。希望这些 API 也能吸引更多用户和研究人员使用 GEE。我在本文中仅展示了两个 API。我鼓励你为你的项目构建更多的 API。你也可以修改查询字符串的设计。但请注意,这种设置并不具备可扩展性。在生产环境中,我们最好将 API 部署到 Deta 或其他云基础设施上。

正如你在 BacDive 部分看到的,这些简单的 API 非常有用。我们使用它们填补了 BacDive 中的信息空白。但为何止步于此呢?例如,我们可以对全球微生物 DNA 进行测序,并将结果与 GEE 元数据结合。这些分析可能揭示有助于或限制某些微生物传播的环境因素。这些知识可以帮助我们对抗传染病和遏制疫情。

来自 BacDive 的数据采用 知识共享 4.0 国际许可证

## 通过我的推荐链接加入 Medium - 黄思行

作为 Medium 会员,你的会员费的一部分将用于支持你阅读的作者,你还可以完全访问每一个故事…

dgg32.medium.com

德国住房租赁市场:使用 Python 的探索性数据分析

原文:towardsdatascience.com/housing-rental-market-in-germany-exploratory-data-analysis-with-python-3975428d07d2

使用 Python、Pandas 和 Bokeh 获取统计见解

Dmitrii EliuseevTowards Data Science Dmitrii Eliuseev

·发表于 Towards Data Science ·27 min 阅读·2023 年 4 月 4 日

--

Salzbrücke, Germany, 图片来源 en.wikipedia.org/wiki/German_Timber-Frame_Road

德国不仅是欧洲最大的经济体,而且还是一个拥有美丽风景和有趣文化的国家。不足为奇的是,德国是来自世界各地的游客和外籍人士的热门目的地。对德国住房租赁市场的探索性数据分析不仅对数据分析师有趣,也对即将移居和在此工作的人有意义。我将展示一些可以通过 Python、Pandas 和 Bokeh 发现的有趣趋势。

让我们开始吧。

数据收集

为了找到数据,我决定使用 ImmoScout24,它不仅是最大的(在撰写本文时列出了大约 72K 套公寓和房屋),也是最早的网站之一。根据 web.archive.org,第一个版本是在 1999 年制作的,已有 20 多年。ImmoScout24 还有一个 API 和开发者页面。我联系了公关部门,他们允许我使用该网站的数据进行出版,但无法提供 API 密钥。这个 API 可能仅供合作伙伴使用,以添加或编辑住房数据,而不是用于批量读取。不过,这不是问题;可以使用 Python 从网页中检索数据,这使得任务更加具有挑战性。

在以类似方式收集数据之前,请先征得所有者的许可,并且要“做一个好房客”:不要使用过多的线程以避免服务器过载,使用本地保存的 HTML 文件调试代码,在使用网页浏览器时尽可能禁用图片加载。

首先,我尝试使用 requests 获取页面数据:

import requests

url_berlin = "https://www...."
print(requests.get(url_berlin))

可惜,没能成功——页面对机器人有保护,在获得搜索结果之前,用户必须确认自己不是机器人。像更改“user-agent”这样简单的方法没有帮助。好吧,我们确实不是机器人,这没问题。 Selenium Python 库允许使用真实的 Chrome 浏览器来获取数据并自动化读取页面:

from selenium import webdriver
import time

def page_has_loaded(driver: webdriver.Chrome):
    """ Check if the page is ready """
    page_state = driver.execute_script('return document.readyState;')
    return page_state == 'complete'

def page_get(url: str, driver: webdriver.Chrome, delay_sec: int):
    """ Get the page content """
    driver.get(url)
    time.sleep(delay_sec)
    while not page_has_loaded(driver):
        time.sleep(1)
    return driver.page_source

options = webdriver.ChromeOptions()
driver = webdriver.Chrome(executable_path="./chromedriver", chrome_options=options)

# Get the first page
url_page1 = "https://www...."
html1 = page_get(url_page1, driver, delay_sec=30)

# Get next pages
url_page2 = "https://www..."
html2 = page_get(url_page1, driver, delay_sec=1)
...

当我们运行代码时,浏览器窗口将被打开。正如我们在代码中看到的,在处理第一页之前,我添加了一个 30 秒的延迟,这足以确认我不是机器人。在此期间,最好通过按右侧的三个点打开浏览器“设置”,并禁用图像加载;这会使处理速度更快。在请求下一页时,浏览器保持打开状态,进一步的数据可以在没有“机器人”检查的情况下进行处理。

在获取 HTML 内容后,数据提取或多或少是直接的。首先,我们必须通过使用 Web 浏览器中的“检查”按钮找到 HTML 元素的属性:

浏览器中的 HTML 输出,图片由作者提供

然后我们可以通过使用 Beautiful Soup 库在 Python 中获取这些元素。此代码从页面中提取所有公寓的 URL:

from bs4 import BeautifulSoup

soup = bs.BeautifulSoup(html1, "lxml")
li = soup.find(id="resultListItems")
links_all = []
children = li.find_all("li", {"class": "result-list__listing"})
for child in children:
    for link in child.find_all("a"):
        if 'data-go-to-expose-id' in link.attrs:
            links_all.append(base_url + link['href'])
            break
links_all.append(base_url + link['href'])

现在让我们找出可以获得什么数据。

数据字段

对于每个房地产对象,我们可以得到一个这样的页面(出于隐私原因,所有值和公司名称均被模糊处理):

页面示例,图片由作者提供

让我们看看可以获得什么数据:

  • 标题。在这张图片中,我们可以看到(当然是德文)“一个美丽的单间公寓在一个美丽的(地方)赫姆斯多夫”。我认为这段文字对于分析没有用处,不过只是为了好玩,稍后我们将从中构建一个词云。

  • 类型。在这个例子中,类型是“Etagenwohnung”(楼层上的公寓)。

  • 冷租是所谓的“冷价格”。这是一个不包含公用费用的租金价格,例如取暖费或电费。

  • 暖租,或称“暖价格”。这个名字可能有些误导,因为我们可以看到在图片中,“暖价格”不仅包含取暖费用(“heizkosten”),还包括其他额外费用(“nebenkosten”)。

  • 楼层。在这个页面上我们可以看到文本“0 of 3”——需要进行少量解析。在德国,第一层是第一个 升高 的楼层,所以我猜 0 意味着“底层”,或在德语中称为“Erdgeschoss”。从“0 到 3”的文本中,我们还可以提取出建筑物的总楼层数。

  • 押金。一个可以覆盖可能损坏的金额,并在租赁结束时退还给租户。在这里我们可以看到一个“3-冷租”的值。我们立即记住一些解析工作是必需的。

  • Flasche(面积)。顾名思义,这是房屋或公寓的面积。

  • Zimmer(房间)。在这个例子中,是 1。

其他数据字段也可以从页面中提取,比如车库的额外租金或允许养宠物的费用,但对于我们的任务,这些字段应该足够了。

HTML 解析过程通常与之前描述的相同。例如,要获取房产标题,可以使用以下代码:

soup = bs.BeautifulSoup(s_html, "lxml")

title = soup.find_all("h1", id="expose-title")
if len(title) > 0:
    str_title = title[0].get_text().strip()

其他字段也可以以相同的方式找到。在运行了所有页面的代码后,我得到了一个这样的数据集,并将其保存为 CVS 格式:

property_id;logging_date;property_area;num_rooms;floor;floors_in_building;price_cold_eur;price_warm_eur;deposit_eur;property_type;publisher;city;title;address;region;
13507XXX1;2023-03-20;7.0;1;None;None;110;110;None;Sonstige;Private;Berlin;Lagerraum / Kellerraum / Abstellraum zu vermieten;None;Moabit, 10551 Berlin;
13613XXX2;2023-03-20;29.0;1;None;None;189;320;None;None;XXXXXXXX Sverige AB;Berlin;Wohnungstausch: Luise-Zietz-Straße 119;Luise-Zietz-Straße 119;Marzahn, 12000 Berlin;
...
14010XXXn;2023-03-20;68.0;1;None;None;28000;28000;1000;None;HousingXXXXXXXXX B.V;Berlin;Wilhelminenhofstraße, Berlin;Wilhelminenhofstraße 0;Oberschöneweide, 12459 Berlin;

现在让我们看看可以获取哪些信息。

数据转换与加载

正如我们在上一段中看到的,住房数据肯定需要一些清理和转换。

我从德国不同地区的 6 个城市收集了数据:柏林、德累斯顿、法兰克福、汉堡、科隆和慕尼黑。作为例子,我们检查柏林;对于其他城市,方法是一样的。首先,让我们将 CSV 加载到 Pandas 数据框中:

import pandas as pd

df_berlin = pd.read_csv("Berlin.csv", sep=';', 
                        na_values=["None"], parse_dates=['logging_date'], 
                        dtype={"price_cold_eur": pd.Int32Dtype(), 
                               "price_warm_eur": pd.Int32Dtype(), 
                               "floor": pd.Int32Dtype(), 
                               "floors_in_building": pd.Int32Dtype()})
display(df_berlin)

过程很简单,但有一些有用的技巧。首先,解析是在 Python 中完成的,对于缺失值,“None” 被写入 CSV。我不希望有“None”作为文本字符串,因此我将其指定为“na_values”参数。我还指定了“;”作为分隔符,并为整数字段,如价格或楼层号,设置了“pd.Int32Dtype”类型。顺便提一下,我最初尝试使用 UInt32,因为价格无论如何不可能是负数,但结果发现计算差异时,有时会出现负值,这导致一些单元格得到类似 4,294,967,295 的值。实际上,保持 Int32 更简单;幸运的是,房价不会高于最大 Int32 值 😉

如果一切都做得正确,作为输出,我们应该得到类似这样的结果:

让我们检查一下维度和 NULL 值的数量:

display(df_berlin.shape)
display(df_berlin.isna().sum())

输出看起来是这样的:

我们可以看到,柏林有 3556 套房产待租,每套房产都有“冷”价和“热”价、一个面积和一个房间数量;这些字段可能是必需的。2467 套房产缺少“类型”字段,2200 套房产没有“楼层”值,等等。如前所述,某些字段,如“押金”值,将需要转换,例如,我们需要一种方法将类似“3 Nettokaltmieten”的文本字符串转换为数值。

基本分析

首先,让我们看看在不费太多劲地编写代码的情况下,使用 Pandas 能做些什么。作为热身,使用 Pandas 的“describe”方法获取数据集的描述性统计

display(df_berlin.drop(columns=['property_id']).describe().style.format(precision=0, thousands=","))

在这里,我只稍微调整了一下输出:我从结果中移除了“property_id”,并通过添加“千位”分隔符来调整输出样式。柏林的数据结果如下:

我们可以看到,柏林有 3,556 处房产上市。我们在前一步已经得到了这个值,并且有某种验证结果正确性的方法是好的。那些 3,556 处房产的中位数(第 50 百分位数)面积是 60 平方米,中位数价格是 1,645€。第 75 百分位数是 2,271€,这意味着 75%的租金价格低于这个值。有趣的是,平均房间数量是 2,这在直观上是正确的,但即便 11 间房的公寓也有(最高价格 28,000€暗示这些 11 间房的地方不便宜)。

下一步,让我们制作散点矩阵,用于一些字段:房产面积、房间数量和价格。可以通过 Pandas 的一个方法调用来完成:

pd.plotting.scatter_matrix(df_berlin[["property_area", "num_rooms", 
                                      "price_warm_eur", "price_cold_eur"]][(df_berlin['price_cold_eur'] > 0) & (df_berlin['price_cold_eur'] < 5000)], 
                           hist_kwds={'bins': 50, 'color': '#0C0786'}, 
                           figsize=(16, 16))

在这里,我还调整了可视化参数——我调整了直方图的箱数,将价格限制在 0-5000€范围内(否则图表因为一些离群值而过小),并且调整了颜色。结果对于 4 行代码来说还不错:

散点矩阵,作者图片

为了进一步可视化,我将使用Bokeh库,它适合制作美丽且互动的图表。让我们首先导入所需的文件:

from bokeh.io import show, output_notebook, export_png
from bokeh.plotting import figure, output_file
from bokeh.models import ColumnDataSource, LabelSet, Label, Whisker, FactorRange
from bokeh.transform import factor_cmap, factor_mark, cumsum
from bokeh.palettes import *
from bokeh.layouts import row, column
output_notebook()

我们准备好了;开始吧。

房产类型

第一个令我感兴趣的问题是德国有哪些类型的房产。如前所述,我从 6 个不同的德国城市收集了数据。让我们加载 CSV 文件并将它们合并成一个数据框:

df_berlin = pd.read_csv("Berlin.csv", sep=';', na_values=["None"], parse_dates=['logging_date'], dtype={"price_cold_eur": pd.Int32Dtype(), "price_warm_eur": pd.Int32Dtype(), "floor": pd.Int32Dtype(), "floors_in_building": pd.Int32Dtype()})
df_munchen = pd.read_csv("Munchen.csv", sep=';', na_values=["None"], parse_dates=['logging_date'], dtype={"price_cold_eur": pd.Int32Dtype(), "price_warm_eur": pd.Int32Dtype(), "floor": pd.Int32Dtype(), "floors_in_building": pd.Int32Dtype()})
df_hamburg = pd.read_csv("Hamburg.csv", sep=';', na_values=["None"], parse_dates=['logging_date'], dtype={"price_cold_eur": pd.Int32Dtype(), "price_warm_eur": pd.Int32Dtype(), "floor": pd.Int32Dtype(), "floors_in_building": pd.Int32Dtype()})
df_cologne = pd.read_csv("Cologne.csv", sep=';', na_values=["None"], parse_dates=['logging_date'], dtype={"price_cold_eur": pd.Int32Dtype(), "price_warm_eur": pd.Int32Dtype(), "floor": pd.Int32Dtype(), "floors_in_building": pd.Int32Dtype()})
df_frankfurt = pd.read_csv("Frankfurt.csv", sep=';', na_values=["None"], parse_dates=['logging_date'], dtype={"price_cold_eur": pd.Int32Dtype(), "price_warm_eur": pd.Int32Dtype(), "floor": pd.Int32Dtype(), "floors_in_building": pd.Int32Dtype()})
df_dresden = pd.read_csv("Dresden.csv", sep=';', na_values=["None"], parse_dates=['logging_date'], dtype={"price_cold_eur": pd.Int32Dtype(), "price_warm_eur": pd.Int32Dtype(), "floor": pd.Int32Dtype(), "floors_in_building": pd.Int32Dtype()}) 

df = pd.concat([df_berlin, df_munchen, df_hamburg, 
                df_cologne, df_frankfurt, df_dresden])

这段代码显然可以优化;例如,我可以使用glob.glob(‘.csv’)*来获取文件夹中所有 CSV 文件的列表,但对于仅 6 个文件来说,我实在是懒得这样做。

现在,让我们找出房产类型的分布:

 data_pr = df[['property_type']].fillna('Unbekannt').groupby(['property_type'], as_index=False).size().sort_values(by=["size"], ascending=True)

types = data_pr['property_type']
amount = data_pr['size']

palette = Viridis10 + Plasma10
p = figure(y_range=FactorRange(factors=types), width=1200, height=500, title="Apartment Types")
p.hbar(y=types, right=amount, height=0.8, color=palette[:len(types)])
p.xaxis.axis_label = 'Amount'
p.x_range.start = 0
show(p)

我使用了一些技巧来改进结果。我将“NA”值替换为“Unbekannt”,这是德语中的“未知”(其他类型都是德语的,所以名称应该统一)。然后,我按房产类型分组并按数量排序。作为最终调整,我指定了颜色调色板,以避免在 Matplotlib 风格中出现乏味的蓝色条形。最终输出如下:

房产类型分布,作者图片

结果很有趣。许多列表中的房产没有指定类型。在其他类型中,“Etagenwohnung”(楼层公寓)是最受欢迎的。第三和第四种类型是“dachgeshoss”(屋顶下的地方)和“erdgeschosswohnung”(底层公寓)。像“maisonette”(小房子)或“hochparterre”(提高的底层)这样的类型较少;读者可以自行查找更详细的描述。

让我们检查不同房产类型的价格。我猜“顶层公寓”应该比“标准”公寓更贵,而底层公寓应该比“标准”公寓便宜。让我们验证一下。我们可以通过按类型分组并在 Pandas 中聚合结果来找到价格分布:

def q0(x):
    return x.quantile(0.01)

def q1(x):
    return x.quantile(0.25)

def q3(x):
    return x.quantile(0.75)

def q4(x):
    return x.quantile(0.99)

agg_data = {'price_cold_eur': ['size', 'min', q0, q1, 'median', q3, q4, 'max']}

prices = df[df['price_cold_eur'].notna()][['property_type', 'price_cold_eur']].fillna('Unbekannt').groupby('property_type', as_index=False).agg(agg_data)
prices = prices.sort_values(by=('price_cold_eur', 'size'), ascending=True)
display(prices.style.hide(axis="index"))

我们可以在表格形式中查看结果:

使用箱线图,我们可以以可视化的形式呈现结果:

prices = prices.sort_values(by=('price_cold_eur', 'size'), ascending=True)

p_types = prices["property_type"]
q1 = prices["price_cold_eur"]["q1"].astype('int32')
q3 = prices["price_cold_eur"]["q3"].astype('int32')
v_min = prices["price_cold_eur"]["q0"].astype('int32')
v_max = prices["price_cold_eur"]["q4"].astype('int32')
median = prices["price_cold_eur"]["median"].astype('int32')

palette = Viridis10 + Plasma10
source = ColumnDataSource(data=dict(p_types=p_types, 
                                    lower=v_min, 
                                    bottom=q1, 
                                    median=median, 
                                    top=q3,                                     
                                    upper=v_max,
                                    color=palette[:p_types.shape[0]]))

p = figure(x_range=p_types, width=1400, height=500, title="Property types distribution") 
whisker = Whisker(base="p_types", upper="upper", lower="lower", source=source)
p.add_layout(whisker)
p.vbar(x='p_types', top='top', bottom='median', width=0.9, color='color', 
       line_color="black", source=source)
p.vbar(x='p_types', top='median', bottom='bottom', width=0.9, color='color', 
       line_color="black", source=source)

p.left[0].formatter.use_scientific = False
p.y_range.start = 0
p.y_range.end = 4000
p.yaxis.axis_label = 'Rent Price, EUR'
show(p)

我为每组使用了相同的大小排序,因此箱线图的调色板与之前的图表相同:

房产类型的须状图,作者提供的图像

我的猜测部分正确。顶层公寓确实是最贵的,但在标准公寓(“etagenwohnung”)、屋顶公寓(“dachgeshoss”)和底层公寓(“erdgeschosswohnung”)之间,没有显著差异。

房产价格

从整个数据集中获取分布是有用的,但让我们深入一些,比较不同地方的数据。正如之前所述,我从 6 个德国城市收集了数据:

城市位置地图,作者提供的图像

让我们看看从德国相对两端收集的数据之间的差异有多大。

每平方米价格

以一定金额租赁的房产大小是多少?使用散点图可以轻松获得结果;这种方法通常只需要两个 X 和 Y 的数组。但我们可以让它看起来更好。

首先,让我们创建一个按数量排序的可能房产类型列表,就像我们之前做的那样:

pr_types = df[['property_type']].fillna('Unbekannt').groupby(['property_type'], as_index=False).size().sort_values(by=["size"], ascending=True)['property_type']

然后我们可以为特定城市创建 3 个数组,数据将包括平方米面积、价格和类型:

price_limit, area_limit = 3000, 200  

df_city = df_berlin  
data_pr = df_city[df_city['price_cold_eur'].notna()][['property_area', 'price_cold_eur', 'property_type']].fillna('Unbekannt')
data_pr = data_pr[(data_pr['property_area'] > 0) & (data_pr['property_area'] < area_limit) & (data_pr['price_cold_eur'] > 0) & (data_pr['price_cold_eur'] < price_limit)] 
values_x = data_pr["property_area"].astype('float').values
values_y = data_pr["price_cold_eur"].astype('float').values
types = data_pr["property_type"]

在这里,我用“Unbekannt”替换了 NULL 房产类型,这在散点图本身中并不需要,但对图例有用。我还只使用了非 NaN 的价格值。选择了€3,000 和 200 m²的限制;我认为这是大多数读者感兴趣的合理范围。

作为一个可选步骤,我创建了一个线性回归模型并使用数据点进行了训练;这将允许绘制线性近似:

from sklearn.linear_model import LinearRegression

linear_model = LinearRegression().fit(values_x.reshape(-1, 1), values_y)

现在我们准备绘制结果:

palette = (Viridis10 + Plasma10)[:len(pr_types)]

name = "Berlin"
df_city = df_berlin

source = ColumnDataSource(dict(property_area=values_x, 
                               price_cold_eur=values_y, 
                               property_type=types))

title = f"Property prices: {name} ({df_city.shape[0]} items, {data_pr.shape[0]} displayed)"
p = figure(width=1200, height=550, title=title)

# Draw scatter
p.scatter("property_area", "price_cold_eur",
              source=source, fill_alpha=0.8, size=4,
              legend_group='property_type',
              color=factor_cmap('property_type', palette, pr_types)
             )
# Draw approximation with a linear model
linear_model = LinearRegression().fit(values_x.reshape(-1, 1), values_y)
p.line([0, 9999], linear_model.predict([[0], [9999]]), line_width=4, line_dash="2 2", alpha=0.5)

p.xaxis.axis_label = 'Area, m²'
p.yaxis.axis_label = 'Rent Price, EUR'
p.x_range.start = 0
p.x_range.end = area_limit
p.y_range.start = 0
p.y_range.end = price_limit
p.toolbar_location = None
p.legend.visible = True
p.legend.background_fill_alpha = 0.9

我决定将不同城市显示在一个图表上,因此我将所有这些代码放在一个单独的“get_figure_price_per_area”方法中。然后,我可以绘制几个 Bokeh 图形,将它们按行和列组合:

p1 = get_figure_price_per_area("Berlin", df, df_berlin)
p2 = get_figure_price_per_area("München", df, df_munchen)
p3 = get_figure_price_per_area("Hamburg", df, df_hamburg)
p4 = get_figure_price_per_area("Köln", df, df_koeln)
p5 = get_figure_price_per_area("Frankfurt", df, df_frankfurt)
p6 = get_figure_price_per_area("Dresden", df, df_dresden)
show(column(row(p1, p2), 
            row(p3, p4), 
            row(p5, p6)))

结果相当有趣:

价格与面积的散点图,作者提供的图像

首先,我们可以通过视觉上比较市场上可用物业的数量。柏林(图表左上角)是一个大城市,也是一个受欢迎的地方;那里的市场最大,价格差异也最大。例如,50 平方米的公寓可以在不同的价格类别中找到,从 400 欧元预算到 3000 欧元以上的高档。在比较中,德累斯顿(图表右下角)的房地产便宜得多,几乎没有昂贵的房产。也许德累斯顿的租赁需求低得多,可能与薪资和可用工作的数量有关。其次,在柏林的数据中,两个独立的类别明显可见。我不清楚解释,或许这与城市历史上被划分为西部(BRD)和东部(DDR)有关。

价格和面积的直方图

我们还可以通过使用直方图以更紧凑的形式查看价格。制作直方图很简单;NumPy 的“histogram”方法可以完成所有计算:

def get_figure_histogram_price(name: str, df_city: pd.DataFrame):
    price_limit = 10000
    prices = df_city['price_cold_eur'].dropna().values
    hist_e, edges_e = np.histogram(prices, density=False, bins=50, range=(0, price_limit))

    # Create figure
    palette = Viridis256[::3][0:len(hist_e)]  # Take every 3rd item from array
    p = figure(width=1400, height=500, 
               title=f"Property prices: {name} ({df_city.shape[0]} total)")
    p.quad(top=hist_e, bottom=0, left=edges_e[:-1], right=edges_e[1:], fill_color=palette)
    p.x_range.start = 0
    p.x_range.end = price_limit
    p.y_range.start = 0
    p.y_range.end = 360
    p.xaxis[0].ticker.desired_num_ticks = 20
    p.xaxis.axis_label = "Rent Price, EUR"
    p.yaxis.axis_label = "Amount"
    p.toolbar_location = None
    return p

我使用了相同的方法来绘制图表,将几个城市放在一起:

p1 = get_figure_histogram_price("Berlin", df_berlin)
p2 = get_figure_histogram_price("München", df_munchen)
p3 = get_figure_histogram_price("Hamburg", df_hamburg)
p4 = get_figure_histogram_price("Köln", df_koeln)
p5 = get_figure_histogram_price("Frankfurt", df_frankfurt)
p6 = get_figure_histogram_price("Dresden", df_dresden)
show(column(row(p1, p2), 
            row(p3, p4), 
            row(p5, p6)))

结果显然与之前看到的散点图有关:

可用价格的直方图,作者提供的图片

慕尼黑似乎是最昂贵的地方,分布的峰值约为 1500 欧元,并且正如之前提到的,柏林有两个峰值看起来很有趣。分布右偏,至少对于像柏林或慕尼黑这样的大城市,我们可以看到一个长尾,其中一些物业的价格甚至高于 10000 欧元/平方米。

至于平方米面积,我只展示柏林的结果;其他城市总体上看起来是一样的:

可用面积的直方图,作者提供的图片

大多数房屋和公寓的面积在 30 到 70 平方米之间,这看起来直观上是正确的,但正如我们所见,有些物业的面积甚至小于 10 平方米,有些则大于 250 平方米。

公用事业费用

接下来感兴趣的是公用事业费用是多少。如前所述,所有列出的公寓有两个价格:所谓的“暖”(包括供暖和电力等公用事业)和“冷”值。让我们计算差异并制作散点图:

name = "Berlin"
df_city = df_berlin

df_area = df_city[['property_area', 'price_warm_eur', 'price_cold_eur', 'property_type']].copy()
df_area['property_type'] = df_area['property_type'].fillna('Unbekannt')

pr_types = df_area[['property_type']].groupby(['property_type'], as_index=False).size().sort_values(by=["size"], ascending=True)['property_type']

df_area['price_diff'] = df_area['price_warm_eur'] - df_area['price_cold_eur']
df_area = df_area[df_area['price_diff'] > 0] 
values_x = df_area['property_area'].astype('float').values
values_y = df_area['price_diff'].astype('float').values

p = figure(width=1200, height=500, title=f"Utilities cost: {name}")

source = ColumnDataSource(dict(property_area=values_x, 
                               price_diff=values_y, 
                               property_type=df_area["property_type"]))

palette = (Viridis10 + Plasma10)[:len(pr_types)]

# Draw scatter
p.scatter("property_area", "price_diff", source=source,
          fill_alpha=0.8, size=4,
          legend_group='property_type',
          color=factor_cmap('property_type', palette, pr_types))
# Draw approximation with a linear model
model = LinearRegression().fit(values_x.reshape(-1, 1), values_y)
p.line([0, 9999], model.predict([[0], [9999]]), line_width=2, line_dash="2 2", alpha=0.5, color='red')

p.xaxis.axis_label = 'Area, m²'
p.yaxis.axis_label = '"Warm" - "Cold" price, EUR'
p.x_range.start = 0
p.x_range.end = 200
p.y_range.start = 0
p.y_range.end = 800
p.toolbar_location = None
p.legend.visible = True
p.legend.background_fill_alpha = 0.9
show(p)

结果很有趣:

公用事业费用与平方米面积的散点图,作者提供的图片

显然,结果存在很大差异:不同的房屋可能有不同类型的供暖、绝缘等。但一般来说,50 平方米的物业每月的公用事业费用约为 200 欧元,面积翻倍也会成比例地翻倍,我们的例子中,100 平方米的房子或公寓每月费用为 400 欧元。至于物业类型,图表上的点分布得较为均匀,我没有看到成本与不同类型之间的任何视觉关联。

押金

押金是租赁合同中的一个重要部分,因为其价值可能相当大。在德国,法律规定的最高押金为 3 个“冷”价格;让我们看看实际情况如何。

首先,让我们看看我们拥有的数据:

display(df_berlin[["title", "price_cold_eur", "deposit_eur"]])

结果如下:

我们可以看到这些值是不同的——一些房主使用数字形式的金额,如“585 €”,而其他人则使用文字描述,如“3 Nettokaltmieten”或“3 MM”。显示唯一值很简单:

print(df['deposit_eur'].unique().tolist())

在输出中,我们可以看到文本描述,如“Drei Nettokaltmieten”,“Zwei Monatsmiete”等。为了解析这些值,我创建了 2 种将文本字符串转换为数值的方法。也许这个检查没有涵盖所有可能性,但在大多数情况下,它能完成工作:

def value_from_str(price_str: str) -> Optional[float]:
    """ Convert string price like "7.935,60 EUR" to 7936 """
    try:
        # '4.800,00 EUR' => '4.800,00'
        if price_str.find(' ') > 0:
            price_str = price_str.split(" ")[0]
        s_filtered = ''.join(c for c in price_str if c in "0123456789,.")
        # "1000.0" => 1000.0
        if s_filtered.find(".") != -1 and s_filtered.find(",") == -1:
            return float(price_str)
        # 7.935,60 => 7935.60
        return float(s_filtered.replace(".", "").replace(",", "."))
    except ValueError as _:
        return None

def convert_price(s_deposit: str, cold_price: int) -> float:
    """ Convert text string and a price to a new value """
    try:
        if s_deposit is None:
            return None
        if isinstance(s_deposit, int) or isinstance(s_deposit, float):
            return s_deposit
        if '€' in s_deposit:
            # 585 € => 585
            return value_from_str(s_deposit)
        if 'zwei' in s_deposit.lower():
            return 2*cold_price
        if 'drei' in s_deposit.lower():
            return 3*cold_price
        if 'kalt' in s_deposit.lower() or 'km' in s_deposit.lower() or 'monat' in s_deposit.lower():
            # 2x Monatsnettokaltmieten => 2
            return cold_price*value_from_str(s_deposit)
        return value_from_str(s_deposit)
    except:
        return None

使用这些方法,我可以进行这样的转换:

convert_price("3 Nettokaltmieten", 659)
convert_price("1.274,00 EUR", 659)

最后,我们可以在数据集中创建一个押金与价格比率的列:

def get_deposit_ratio(s_deposit: str, cold_price: int) -> float:
    """ Calculate the ratio between deposit and price """
    try:
        deposit_price = convert_price(s_deposit, cold_price)
        if deposit_price is not None and cold_price != 0:
            return deposit_price/cold_price
    except:
        pass
    return None

df_berlin["deposit_price_ratio"] =  df_berlin.apply(lambda x: get_deposit_ratio(s_deposit=x['deposit_eur'], 
                                                                                cold_price=x['price_cold_eur']), 
                                                    axis=1)

使用这个新列,我们可以像以前一样轻松地制作直方图:

def get_deposit_histogram(name: str, df_city: pd.DataFrame):
    """ Get Bokeh figure from a dataframe """
    prices = df_city['deposit_price_ratio'].dropna().values
    hist_e, edges_e = np.histogram(prices, density=False, bins=60, range=(0, 5))

    # Draw
    palette = Viridis256[::2][0:len(hist_e)]  # Take every 2nd item from array
    p = figure(width=1400, height=400, 
               title=f"Property area: {name} ({df_city.shape[0]} total)")
    p.quad(top=hist_e, bottom=0, left=edges_e[:-1], right=edges_e[1:], fill_color=palette)
    p.x_range.start = 0
    p.x_range.end = 5
    p.y_range.start = 0
    p.y_range.end = 400
    p.xaxis[0].ticker.desired_num_ticks = 20
    p.xaxis.axis_label = 'Deposit amount, relative to the "cold" price'
    p.yaxis.axis_label = 'Number of objects available'
    p.toolbar_location = None
    return p

p1 = get_deposit_histogram("Berlin", df_berlin)
p2 = get_deposit_histogram("München", df_munchen)
p3 = get_deposit_histogram("Hamburg", df_hamburg)
p4 = get_deposit_histogram("Köln", df_koeln)
p5 = get_deposit_histogram("Frankfurt", df_frankfurt)
p6 = get_deposit_histogram("Dresden", df_dresden)
show(column(row(p1, p2), row(p3, p4), row(p5, p6)))

结果很有趣:

存款价值分布的直方图,作者提供的图片

许多房东要求最高可能的押金,这在德国法律限制为 3 个“冷”价格。尽管也可以找到要求 2 倍、1 倍甚至完全不需要押金的地方(德语中为“kautionsfrei”)。令人惊讶的是,有些房主要求的押金高于 3 倍的值。有时,当这个值为 3.05 时,可以通过计算错误来解释,但如果押金值约为 5 倍,那肯定不是这种情况。

物业发布者

一些房主喜欢自己出租他们的房产;其他人则与中介合作。这些金额有多大?让我们用饼图来展示分布。

def get_figure_publisher(name: str, df_city: pd.DataFrame) -> figure:
    publishers = df_city[['publisher']].groupby(['publisher'], as_index=False).size().sort_values(by=["size"], ascending=False)
    publishers = publishers[publishers['size'] > 5]

    # Put private first
    data_private = publishers[(publishers['publisher'] == "Private")]
    data_non_private = publishers[(publishers['publisher'] != "Private")]
    data = pd.concat([data_private, data_non_private])

    palette = RdGy3[:1] + Viridis11 + BrBG11 + Plasma11 + Cividis11 + RdYlBu11 + RdGy11 + PiYG11

    data['angle'] = data['size']/data['size'].sum()*2*np.pi
    data['percentage'] = data['size'] / data['size'].sum() * 100
    data['color'] = palette[:data.shape[0]]

    p = figure(width=1100, height=750, title=f"Property publishers: {name}", toolbar_location=None, x_range=(-0.5, 1.0))
    pie_chart = p.wedge(x=0, y=1, radius=0.4,
            start_angle=cumsum('angle', include_zero=True), end_angle=cumsum('angle'),
            line_color="white", 
            fill_color='color', 
            legend_field='publisher', 
            source=data)

    p.axis.axis_label = None
    p.axis.visible = False
    p.grid.grid_line_color = None
    return p

在这段代码中,我按发布者对数据框进行了分组,并按大小排序。唯一的技巧是将“私人”组放在首位,为了清晰起见,我还将该组标记为不同颜色。

2 个城市的结果,柏林和慕尼黑,看起来像这样:

显示每个发布者的房地产对象数量的饼图,作者提供的图片

有趣的是,只有 8.5%的柏林房地产由私人个人挂牌;在慕尼黑,这个比例是 27%。另一个有趣的点是,超过 50%的房产是由少数几家中介发布的。

楼层数量

正如我们之前看到的,公寓的位置,如底层或屋顶下的楼层,通常不会影响租金价格。但了解德国大多数建筑物的楼层数仍然很有趣。

最初,我没有预期到这个任务会有任何困难,但挑战在于进行自定义排序。许多公寓或房屋没有指定楼层号码,我想把“未知”值放在最左边。这可以通过在 Pandas 中实现自定义排序键来完成。这里的难点是,当进行 DataFrame 排序时,custom_key 是由 Pandas 应用于“pd.Series”对象而不是单个值。因此,我们需要第二种方法来更新系列中的值:

def to_digit(v):
    """ Convertion for string or digit """
    if v.isdigit():
        return f"{int(v):02d}" # "1" => "001"     
    return ' ' + str(v)

def custom_key(v):
    """ Custom key for the dataframe sort """
    # v: pd.Series, convert items for proper sort
    # ["1", "10", "2", "U"] => [" U", "001", "002", "010"]
    return v.apply(to_digit)

def get_figure_floors(city_name: str, df_city: pd.DataFrame) -> figure:
    """ Get Bokeh figure """
    floors = df_city[['floor']].astype(str).mask(df_city.isnull(), None).fillna("?").groupby(['floor'], as_index=False).size()
    floors = floors.sort_values(by=["floor"], key=lambda x: custom_key(x), ascending=True)

    # Draw
    palette = Viridis11 + Plasma11 + Cividis11
    values = floors['floor']
    amount = floors['size']
    p = figure(x_range=FactorRange(factors=values), width=1200, height=400, title=f"{city_name}: apartments floor")
    p.vbar(x=values, top=amount, width=0.8, color=palette[:len(values)])
    p.xaxis.axis_label = 'Floor №'
    p.yaxis.axis_label = "Amount"
    p.toolbar_location = None
    return p

p1 = get_figure_floors("Berlin", df_berlin)
p2 = get_figure_floors("München", df_munchen)
show(row(p1, p2))

前两个城市,柏林和慕尼黑的结果如下:

楼层分布直方图,作者提供的图片

正如我们所看到的,这两个城市的大多数公寓位于 1 到 5 层。但也有几套公寓位于 10 到 20 层,还有一套柏林的公寓位于 87 层(虽然我没有检查是否是打印错误,或该建筑是否真的存在)。

地理可视化

构建直方图或多或少是直接的;让我们进入有趣的部分:在地理地图上显示房产对象。在这里,我们面临两个挑战。首先,我们需要获取坐标,其次,我们需要绘制地图。

地理编码

让我们再检查一下数据:数据框中有“region”和“address”字段,我们可以用来进行地理编码请求:

获取坐标时,我将使用一个GeoPy库;它是免费的,不需要任何 API 密钥(例如 Google Maps API):

from geopy.geocoders import Nominatim
from functools import lru_cache

geolocator = Nominatim(user_agent="Python3.9")

@lru_cache(maxsize=None)
def get_coord_lat_lon(full_addr: str):
    """ Get coordinates for address """
    # Remove brackets: "Mitte (Ortsteil), 10117" => "Mitte, 10117"
    p1, p2 = full_addr.find('('), full_addr.find(')')
    if p1 != -1 and p2 != -1 and p2 > p1:
        full_addr = full_addr[:p1].strip() + full_addr[p2 + 1:]  
    # Make request
    pt = geolocator.geocode(full_addr)
    return (pt.latitude, pt.longitude) if pt else (None, None)

代码很简单;唯一的挑战是在地址中去掉“(”和“)”括号;结果发现,库在处理像“Nauen, Havelland (Kreis)”这样的地址时没有返回任何数据。我还使用了“lru_cache”以避免对相同地址进行多次请求(有些中介在同一栋楼里有几套待租公寓)。

使用这种方法,我可以轻松请求位置:

df_city = df_berli
df_addrs = df_city[["address", "region", "price_cold_eur"]].dropna().copy()
# Combine the address from 2 fields
df_addrs["address_full"] = df_addrs[["address", "region"]].apply(lambda x: x[0] + ", " + x[1], axis=1)

points = []
for index, row in df_addrs.iterrows():
    addr, price = row['address_full'], row['price_cold_eur']
    lat, lon = get_coord_lat_lon(addr)
    if (index % 50) == 0:
        print(f"{index} of {df_addrs.shape[0]}: {addr}, {price}, {lat}, {lon}")
    if lat and lon:
        points.append((addr, price, lat, lon)) 

print(f"Points added: {len(points)}")
return points

地图

为了绘制地图,我将使用一个免费的Folium库。作为使用该库的简单示例,可以通过几行代码显示带标记的地图:

import folium
from folium.plugins import HeatMap
from branca.element import Figure

fig = Figure(width=1200, height=1000)
m = folium.Map(location=(50.59, 10.38), tiles="openstreetmap", zoom_start=7)

folium.Marker(location=(52.59, 13.37), popup="Berlin").add_to(m)

fig.add_child(m)
display(fig)

这段代码将生成一个美观的互动地图,无需 API 密钥:

Folium 地图和标记示例,作者提供的图片

但我们的可视化会复杂一些。我将使用 Folium 的“Circle”对象来表示每个房产,并使用“FeatureGroup”来对不同价格进行分组:

import folium
from folium.plugins import HeatMap
from branca.element import Figure
import matplotlib
import matplotlib.cm as colormap

def value_to_color(value: int) -> str:
    """ Convert price value to the HTML color """
    if value >= 5000:
        # Mark high values in special colors
        return "#FF00FF"

    norm = matplotlib.colors.Normalize(vmin=0, vmax=3000, clip=True)
    mapper = colormap.ScalarMappable(norm=norm, cmap=colormap.inferno)
    r, g, b, _ = mapper.to_rgba(value, alpha=None, bytes=True)
    return "#" + f"{(r << 16) + (g << 8) + b:#08x}"[2:]

def add_to_map(fmap, lat, lon, price):
    """ Add point to map """
    color_str = value_to_color(price)
    folium.Circle(
        location=[lat, lon],
        radius=100,
        popup=addr + ": " + str(price),
        color=color_str,
        fill=True,
        fill_color=color_str
    ).add_to(fmap)

def get_html_text_label(text: str, value1: int, value2: int=999999):
    """ Prepare HTML label with a gradient text """
    color1 = value_to_color(value1)
    color2 = value_to_color(value2-1)
    return f'<span style="background: linear-gradient(to right, {color1}, {color2}); padding-left: 2%; padding-top: 3%; padding-bottom: 1%;">&nbsp;&nbsp;&nbsp;&nbsp;</span><span>&nbsp;{text}</span>'

def generate_map(points: list, location: Tuple, name: str):
    """ Draw a city map """
    fig = Figure(width=1200, height=600)
    m = folium.Map(location=location, zoom_start=12)

    heat_map = folium.FeatureGroup(name='Heat Map')
    price_less_500 = folium.FeatureGroup(name=get_html_text_label('< 500', 0, 500))
    price_less_1000 = folium.FeatureGroup(name=get_html_text_label('500..1000', 500, 1000))
    price_less_2000 = folium.FeatureGroup(name=get_html_text_label('1000..2000', 1000, 2000))
    price_less_5000 = folium.FeatureGroup(name=get_html_text_label('2000..5000', 2000, 5000))
    price_more_5000 = folium.FeatureGroup(name=get_html_text_label('> 5000', 5000))

    heat_data = []
    for addr, price, lat, lon in points:
        if price < 500:
            add_to_map(price_less_500, lat, lon, price)
        elif price <= 1000:
            add_to_map(price_less_1000, lat, lon, price)
        elif price <= 2000:
            add_to_map(price_less_2000, lat, lon, price)
        elif price <= 5000:
            add_to_map(price_less_5000, lat, lon, price)
        else:
            add_to_map(price_more_5000, lat, lon, price)
        heat_data.append((lat, lon))

    heat_map.add_child(HeatMap(heat_data, min_opacity=0.3, blur=50))
    m.add_child(heat_map)
    m.add_child(price_less_500)
    m.add_child(price_less_1000)
    m.add_child(price_less_2000)
    m.add_child(price_less_5000)
    m.add_child(price_more_5000)

    folium.map.LayerControl('topright', collapsed=False, style=("background-color: grey; color: white;")).add_to(m)

    fig.add_child(m)
    return fig

我还使用了热图作为背景,以使结果看起来更好。可视化还需要调整颜色和 CSS 样式以显示渐变;最终结果如下:

柏林的房产对象,图片由作者提供

结果或多或少是直观的。在柏林,中心周围的区域更贵,但没有特别的“高档”地方。例如,价格高于€5,000/m 的房产大致上是均匀分布的:

柏林价格≥€5,000/m 的房产,图片由作者提供

租金动态

这个问题有点挑战性。租赁过程的速度如何?房产对象可租赁的时间是多长?这个问题具有挑战性,因为网站上没有房产的发布日期。但我们可以通过比较不同日期获得的结果间接估计这些数据。

这个想法很简单。每个房产都有一个唯一的 ID。我保存了同一城市的数据两次,间隔 7 天。然后我显示了两个价格直方图:一个是所有房产的,另一个是那些在 7 天内被移除的房产(存在于第一个数据框中但在第二个数据框中不存在):

def get_two_histograms(name: str, df_all: pd.DataFrame, df_closed: pd.DataFrame):
    """ Draw two dataframe histograms on the same graph """
    price_limit = 10000
    prices = df_all['price_cold_eur'].values
    hist_e1, edges_e1 = np.histogram(prices, density=False, bins=40, range=(0, price_limit))
    prices = df_closed[(df_closed['price_cold_eur'] < price_limit)]['price_cold_eur'].values
    hist_e2, _ = np.histogram(prices, density=False, bins=edges_e1)

    # Draw
    palette1 = Viridis256[::3][0:len(hist_e1)]  # Take every 3rd item from array
    p = figure(width=1400, height=500, 
               title=f"Property prices: {name} ({df_city.shape[0]} total)")
    p.quad(top=hist_e1, bottom=0, left=edges_e1[:-1], right=edges_e1[1:], fill_color=palette1, fill_alpha=0.8, legend_label='All properties')
    p.quad(top=hist_e2, bottom=0, left=edges_e1[:-1]+20, right=edges_e1[1:]-20, fill_color=palette1, legend_label='Properties removed from listing within 7 days')
    # Add percentage labels
    for i in range(hist_e1.shape[0]):
        pos_x = (edges_e1[i] + edges_e1[i + 1])/2
        pos_y = hist_e1[i]
        value = 100*hist_e2[i]/hist_e1[i] if hist_e1[i] != 0 else 0        
        value_str = f'{value:.1f}%' if value > 0.5 else "0%"
        p.add_layout(Label(x=pos_x, y=pos_y + 2, text_align="center",
                 text=value_str, text_font_size='7pt',
                 background_fill_color='white', background_fill_alpha=0.0))

    p.x_range.start = 0
    p.x_range.end = price_limit
    p.y_range.start = 0
    p.y_range.end = 480
    p.xaxis[0].ticker.desired_num_ticks = 20
    p.left[0].formatter.use_scientific = False
    p.below[0].formatter.use_scientific = False
    p.xaxis.axis_label = "Rent Price, EUR"
    p.yaxis.axis_label = "Amount"
    return p

df_berlin0 = pd.read_csv("Berlin_00.csv", sep=';', na_values=["None"]).drop_duplicates(subset='property_id', keep="first")
ids0 = df_berlin0["property_id"].unique().tolist()
df_berlin6 = pd.read_csv("Berlin_06.csv", sep=';', na_values=["None"]).drop_duplicates(subset='property_id', keep="first")
ids6 = df_berlin6["property_id"].unique().tolist()

df_f = df_berlin0.copy()
df_f["deal_closed"] = df_f['property_id'].apply(lambda pr_id: pr_id in ids0 and pr_id not in ids6)
df_f_closed = df_f[df_f['deal_closed'] == True]

p1 = get_two_histograms("Berlin", df_berlin0, df_f_closed)
show(p1)

我还在条形图上添加了百分比标签,使条形图更易读。结果如下:

从列表中移除的房产直方图,图片由作者提供

显然,这个结果在统计上并不显著,因为实验仅进行了一次。至少,我可以说在进行这个测试时,柏林约 20%的€800–1,200 范围内的房产在一周内被从列表中移除。更贵的房产显然留存时间更长;在同一时期内,只有约 9%的€3,000 价格范围内的房产被移除。

异常检测

接下来的步骤,让我们来点乐趣,尝试找出“异常”情况,即一些不寻常和非标准的情况。为此,我将使用Isolation Forest算法,该算法在Scikit-Learn Python 库中实现。为了寻找异常,我将使用 3 个特征:面积、价格和房间数量:

from sklearn.ensemble import IsolationForest

df_city = df_berlin[["region", "address", "price_cold_eur", "num_rooms", "property_area"]].dropna().copy().reset_index(drop=True)
anomaly_inputs = ["price_cold_eur", "num_rooms", "property_area"]
for field in anomaly_inputs:
    df_city[field] = df_city[field].astype(float)

display(df_city[anomaly_inputs])

model_if = IsolationForest(contamination=0.01)
model_if.fit(df_city[anomaly_inputs])

# anomaly_scores: generated by calling model_IF.predict() and is used to identify if a point is an outlier (-1) or an inlier (1)
df_city['anomaly_scores'] = model_if.decision_function(df_city[anomaly_inputs])
# anomaly : generated by calling model_IF.predict() and is used to identify if a point is an outlier (-1) or an inlier (1)
df_city['anomaly'] = model_if.predict(df_city[anomaly_inputs])

正如我们在代码中看到的,算法只需要一个参数,即所谓的“污染”,它决定了数据集中异常值的比例。在这里我将其设置为 1%;显然,参数可以根据需要进行调整。

调用“fit”方法后,我们可以得到结果。“decision_function”方法返回异常分数,“predict”方法返回+1(如果特定对象被视为正常点)或-1(如果是异常点)。让我们只显示异常点:

display(df_city[df_city['anomaly'] == -1])

结果如下所示:

对于这些属性,有些参数不寻常,通过观察结果,我可以猜测房间数量、面积或价格较大。但是,“隔离森林”方法的一个优点是其可解释性。一个SHAP Python 包允许使用Shapley 值以图形化的方式解释结果:

import shap
shap.initjs()

explainer = shap.Explainer(model_if.predict, df_city[anomaly_inputs])
shap_values = explainer(df_city[anomaly_inputs])

分析本身大约需要一分钟。之后,我们可以得到列表中每一项的结果。例如,让我们检查编号为 3030 的房产:

shap.plots.waterfall(shap_values[3030])

Shapley 解释器结果,图像由作者提供

我们可以看到,价格是合适的,但 211 平方米的房产面积和 5 间房的数量被算法视为不寻常。

还可以通过显示散点图来查看算法的工作情况。例如,让我们看看“房间数量”和“价格”如何影响 Shapley 值:

display(shap.plots.scatter(shap_values[:,'num_rooms'], color=shap_values[:,"price_cold_eur"]))

结果如下所示:

SHAP 值的散点图,图像由作者提供

在这里,我们可以看到房间数量超过 4 的情况对得分的影响最大。

词云

我不会将最后一步视为真正的分析,但为了趣味,我们来构建词云,看看在房地产标题中哪些词最为流行。通过 Python WordCloud 库,我们只需几行代码即可做到这一点:

from wordcloud import WordCloud
import matplotlib.pyplot as plt

from nltk.corpus import stopwords
stop_words_de = stopwords.words('german')

# Generate
text = ""
for s in df.title:
    s_out = s.replace('/', ' ').replace(':', ' ').replace(',', ' ').replace('!', ' ').replace('-', ' ')
    text += s_out + " "

wordcloud = WordCloud(width=1600, height=1200, stopwords=set(stopwords_de), collocations=False, background_color="white").generate(text)

# Show
plt.figure(figsize=(16, 12), dpi=100)
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.show()

结果如下所示:

房地产对象标题的词云,图像由作者提供

词如“wohnung”(公寓)、“zimmer”(房间)和“balkon”(阳台)最为流行,我们还可以看到“helle”(明亮)、“moderne”(现代)、“schöne”(美丽)等词。

结论

正如英国国家统计局前研究员艾伦·史密斯在 2017 年的TED 演讲中所说,我们应该热爱统计学,因为它是关于我们的科学。探索数据集,如这个租房数据,并发现有趣的模式确实很有趣。我希望这对读者也很有趣,不仅作为使用 Pandas 或 Bokeh 的例子,而且作为对另一个国家的生活和文化的一个小见解。显然,我没有测试所有的数据;例如,了解多少房东允许租户在公寓里养宠物可能会很有趣。读者可以自行测试;本文中的代码片段应该足够了。

如果你喜欢这个故事,欢迎订阅Medium,你将会收到我新文章发布的通知,并且可以全面访问其他作者的数千篇故事。

感谢阅读。

如何用 25,000 台计算机训练 ChatGPT

原文:towardsdatascience.com/how-25-000-computers-trained-chatgpt-11104686a24d

照片由 Volodymyr Hryshchenko 提供,来源于 Unsplash

ChatGPT 背后的突破

Jerry QuTowards Data Science Jerry Qu

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

--

作者提供的图片

“Good” 之后是什么词?

你可能会想到早上好,或者再见。但你绝对不会说 声音很大。这没有意义。几十年来,计算机科学家们一直在训练 AI 解决这个确切的问题。

作者提供的图片

给定一个词,我们的 AI 预测下一个词。这样做几次,你就生成了一句完整的句子。

这就是 ChatGPT 的工作原理。

经过整个互联网的训练,ChatGPT 学会了像人类一样聊天。然而,这一巨大的成就只有在 2010 年代末期的突破之后才得以实现。这一突破支撑了 ChatGPT,并永远改变了我们生活的世界。

这是一个 AI 从整个互联网的每本书、每条推文、每个网站中阅读并学习的故事。以及它是如何实现的。

句子很长。

当我们超越单个词时,下一个词的预测就变得困难得多。看这个例子。

作者提供的图片

在这种情况下,说我吃了一个好的 早晨是没有意义的。但是我们的 AI 只关注“good”,并且输出“morning”。即使是人类也需要许多词来预测下一个词。因此,AI 也需要这些额外的信息。

我们的 AI 需要阅读许多词来预测下一个词。ChatGPT 一次可以读取超过 8,000 个先前的词。自然的做法是逐个将每个词输入 AI。

作者提供的图片

这就是过去 AI 的工作方式。递归神经网络(RNN)一次处理一个词,在读取句子时存储信息。

这个 AI 的一个问题是它极其缓慢。每个词都必须等待前一个词处理完成,这在大规模时是个问题。想象一下,如果你的洗衣机一次只能洗一件衬衫,这种顺序处理将需要几天时间。但每个人都知道我们可以同时放入所有衬衫,几分钟就能完成。这就是并行的概念。通过并行执行工作而不是顺序执行,我们可以显著加速洗衣机、计算机和 AI。

RNN 只能在数百万个词上进行训练,远远不及互联网上的万亿词汇。我们需要一种更快、更高效的句子阅读方法。

Transformer 是解决方案。

2017 年,一篇题为Attention Is All You Need的论文被发表。这篇论文有效地改变了句子的处理方式。这些研究人员发明了一种可以一次读取整句的 AI。

图片由作者提供

这种新的 AI 被称为Transformer,它的高效性使得它能够从互联网上的每本书和每个网站中学习。要理解它是如何做到这一点的,我们需要退一步了解计算机如何读取文本。

AI 如何读取文本?

计算机以 1 和 0 的形式工作。这些 1 和 0 被称为二进制,它们组成了数字。计算机科学家需要一种将词语表示为数字的方法。这在 2013 年得以实现,当时 Google 的科学家们创建了word2vec

词语包含语义意义。狗与猫相关。国王与王后相关。Word2vec 能够在向量或数字列表中表示这些语义。

使用 word2vec,你可以取“King”,减去“Man”,加上“Woman”,然后得到“Queen”这个词。

图片由作者提供

这个数字向量称为词嵌入。它们将词语的意义嵌入到这个向量中。在训练 AI 处理文本时,我们实际上是将这些词嵌入输入给 AI。AI 进行一些数学运算,变换这些向量,并生成下一个词。变换这些向量是非常耗时的。

图片由作者提供

Transformer 完成了这一切的并行处理。

不再等待前一个词处理完成,我们同时变换所有这些词嵌入,进行平均以将它们汇总。这将顺序操作的数量从句子的长度减少到一个常数。

图片由作者提供

Lambda Labs估计训练 ChatGPT 在单个 GPU 上需要355 年。但通过利用其并行性,ChatGPT 在25,000 个 GPU上训练,且仅用了几天时间。

Transformer 引发了人工智能领域的范式转变。

随着并行性的增加,越来越大的 AI 可以被训练。尽管过去最大的序列模型是基于数百万字进行训练的,但 ChatGPT 是在接近 一万亿 字的数据上训练的。

图片来自作者,数据来源于原始论文。

ChatGPT 在 CommonCrawl 上进行了训练,这是自 2008 年以来的整个互联网的集合。使用 超过 25,000 台计算机,该模型阅读并学习了整个互联网的每个网站。想象一下阅读每本书、每条推文和每段代码。

今天,ChatGPT 正在被用来编写代码,生成电视广告,并帮助你完成几乎可以想象的任何事情!通过将句子转向侧面,我们开创了 AI 的新时代,这一时代突破了曾经认为可能的界限。

但我们可能已经达到了极限。

在 GPT-4 发布后,OpenAI 的 CEO Sam Altman 说,

“我认为我们正处于这些巨大模型时代的尽头……”

在从整个互联网学习之后,接下来会是什么?ChatGPT 的影响正在渗透到每个行业。但像任何突破一样,进展会趋于平稳。AI 的下一个拐点,只有时间才能告诉我们。

图片来自作者,数据来源于原始论文。

如果你喜欢这篇文章:

  • 关注我的 Medium, LinkedIn,和 Twitter 以获取我的最新进展。

一个优秀的数据科学家如何看待矩阵乘法

原文:towardsdatascience.com/how-a-good-data-scientist-looks-at-matrix-multiplication-33a3128cd1bb

四种不同的视角

Sekhar MTowards Data Science Sekhar M

·发表于数据科学前沿 ·阅读时间 5 分钟·2023 年 2 月 23 日

--

作者提供的图片

介绍:

数据科学是通过科学计算方法、过程和算法从结构化或非结构化数据中提取知识和洞察的领域。

数据——无论是结构化数据(如数据表)还是非结构化数据(如图像)——都被表示为矩阵。

这些数据在计算过程(如机器学习模型)中的处理主要涉及矩阵乘法。

因此,对矩阵乘法操作的深入理解对从事数据科学和机器学习领域的人员大有裨益。

矩阵的乘法可以从 4 个不同的角度来看待:

  1. 行和列的点积

  2. 列的线性组合

  3. 行的线性组合

  4. 排名为 1 的矩阵之和

AB为相乘的矩阵,表示为AB。设AB的维度分别为*m*x*p**p*x*n*。我们知道,为了能够相乘AB,矩阵A的列数应与矩阵B的行数匹配。

为了简单起见,并且不失一般性,我们考虑以下示例维度。

如我们所知,下面的AB是两个矩阵AB的乘积。

1. 行和列的点积:

在矩阵AB中,element-11可以看作是矩阵Arow-1和矩阵Bcolumn-1的点积。

同样,element-12可以看作是矩阵Arow-1和矩阵Bcolumn-2的点积。

矩阵乘法作为行和列的点积(图片由作者提供)

视角:

AB中的Element-*ij*Arow-*i*Bcolumn-*j*的点积。

2. 列的线性组合:

为了形成矩阵乘法的列视角,将矩阵AB重新组织如下。

AB的第 1 列可以看作是*b11*倍的A的第 1 列和*b21*倍的第 2 列的和。即,AB的第 1 列是A列的线性组合(加权和),其中组合的权重是B的第 1 列的元素。

同样,column-2ABA列的线性组合,其中组合的权重是Acolumn-2的元素。

矩阵乘法作为列的线性组合(图片由作者提供)

视角:

AB中的每一列是A的列的线性组合,其中组合的权重是B中对应列的元素。

3. 行的线性组合:

现在,让我们从行的角度来看AB,将其重写如下。

AB的第 1 行可以看作是*a11*倍的B的第 1 行和*a12*倍的第 2 行的和。即,AB的第 1 行是B行的线性组合(加权和),其中组合的权重是A的第 1 行的元素。

同样,row-2ABB行的线性组合,其中组合的权重是Arow-2的元素。

矩阵乘法作为行的线性组合(图片由作者提供)

视角:

AB中的每一行是B行的线性组合,其中组合的权重是A中对应行的元素。

4. 秩-1 矩阵的和:

AB重写如下,可以得到两个秩-1 矩阵,每个矩阵的大小与AB相同。

很明显,上述两个矩阵是秩-1 的,因为它们的行(和列)都是线性相关的,即所有其他行(列)都是某一行(列)的倍数。因此,秩为 1。

矩阵乘法作为秩-1 矩阵的和(图片由作者提供)

视角:

矩阵ABp个秩-1 矩阵的和,每个矩阵的大小为mxn,其中第*i_*个矩阵(在p中)是将Acolumn-*i*Brow-*i*相乘的结果。

结论:

这些不同的视角在不同的场合中都有其相关性。

例如,在Transformer神经网络架构中的Attention机制中,注意力矩阵的计算可以从“行和列的点积”角度看作矩阵乘法。

更多关于注意力机制和 Transformer 的信息可以在下面的文章中找到。

## 走进变压器

数据流、参数和维度

towardsdatascience.com

我希望这些关于矩阵乘法的视角能够帮助读者更直观地理解机器学习和数据科学算法与模型中的数据流。

人工智能如何改变我们的编码方式

原文:towardsdatascience.com/how-ai-is-changing-the-way-we-code-36ff30262e65?source=collection_archive---------5-----------------------#2023-08-23

来自 ChatGPT 和 Stack Overflow 的证据

Quentin Gallea, PhDTowards Data Science Quentin Gallea, PhD

·

关注 发布在 Towards Data Science ·11 分钟阅读·2023 年 8 月 23 日

--

图片来源:Pavel Danilyuk,来自 Pexels

简而言之: 在本文中,您将找到我关于人工智能和工作的最新研究总结(探讨人工智能对生产力的影响,同时开启关于长期效果的讨论),一个使用 ChatGPT 和 Stack Overflow 说明的准实验方法(差异中的差异),以及如何通过简单的 SQL 查询从 Stack Overflow 提取数据。

完整科学文章的链接(请引用):arxiv.org/abs/2308.11302

与大多数技术革命一样,ChatGPT 的发布伴随着惊奇和恐惧。一方面,仅仅两个月内,应用程序就以 1 亿月活跃用户打破了历史上增长最快的消费应用记录。另一方面,高盛的一份报告声称,这项技术可能会取代全球超过 3 亿个工作岗位 [1]。此外,埃隆·马斯克和 1000 多名科技领袖及研究人员签署了一封公开信,呼吁暂停最先进的人工智能发展 [2]。

``我们只能看得见短远的距离,但我们可以看到很多需要做的事情。’’ 艾伦·图灵

根据艾伦·图灵的名言,本文并不试图英勇预测人工智能的遥远未来及其影响。然而,我专注于一个主要的可观察后果:人工智能如何改变我们的编码方式。

随着 ChatGPT 的诞生,世界发生了变化。至少,作为一个每天都在编码的人,我的世界在一夜之间发生了改变。我不再需要花费数小时在谷歌上寻找正确的解决方案,或在 Stack Overflow 上深入挖掘答案并将解决方案翻译成适合我具体问题的变量名称和矩阵维度,我只需询问 ChatGPT。这个聊天机器人不仅能在眨眼间给出答案,而且答案会适应我的具体情况(例如,正确的名称、数据框维度、变量类型等)。我感到惊讶,我的生产力瞬间提升。

因此,我决定探索 ChatGPT 发布的大规模影响及其对生产力的潜在影响,最终对我们工作的方式。 我定义了三个假设(Hs),并使用 Stack Overflow 数据进行了测试。

H1: ChatGPT 减少了 Stack Overflow 上的问题数量。 如果 ChatGPT 能在几秒钟内解决编码问题,我们可以预期在编码社区平台上提出问题和获得答案的时间会减少。

H2: ChatGPT 提高了提问的质量。 如果 ChatGPT 被广泛使用,那么 Stack Overflow 上剩余的问题必须得到了更好的记录,因为 ChatGPT 可能已经提供了一些帮助。

H3: 剩余问题更加复杂。 我们可以预期剩余的问题会更具挑战性,因为这些问题可能无法通过 ChatGPT 得到解答。因此,我们正在测试未解答问题的比例是否增加。此外,我还测试了每个问题的浏览量是否发生变化。如果每个问题的浏览量保持稳定,这将是一个额外的迹象,表明剩余问题的复杂性增加,并且这一发现不仅仅是由于平台活动减少所致。

为了验证这些假设,我将利用 ChatGPT 在 Stack Overflow 上的突然发布。2022 年 11 月,当 OpenAI 公开发布他们的聊天机器人时,没有其他替代品可用(例如 Google Bard),并且可以免费访问(不像 OpenAI ChatGPT 4 或 Code Interpreter 那样限于付费订阅)。因此,可以观察到这一冲击如何改变在线编码社区的活动。然而,尽管这种冲击很“清晰”,其他影响可能会混淆,因此需要质疑因果关系。特别是季节性因素(例如发布后的年末假期)以及问题越新,浏览量越低且找到答案的概率越小。

理想情况下,为了减少季节性等潜在混淆变量的影响并测量因果效应,我们希望观察没有 ChatGPT 发布的世界,但这是不可能的(例如因果推断的基本问题)。尽管如此,我将通过利用 ChatGPT 对编码相关问题的答案质量在不同语言之间变化,并使用准实验方法来限制其他因素混淆效应的风险(差异中的差异)来应对这一挑战。

为此,我将比较 Stack Overflow 上 Python 和 R 的活动。Python 是一个明显的选择,因为它可以说是编程社区中最流行的语言之一。

使用最广泛的编程语言(例如在 TIOBE 编程社区中排名第一)

编程社区指数)。Python 在线资源的丰富提供了像 ChatGPT 这样的聊天机器人的丰富训练集。现在,为了与 Python 进行比较,我选择了 R。Python 通常被引用为 R 的最佳替代品,两者都可以免费使用。然而,R 的流行度稍低一些(例如在 TIOBE 编程社区指数中排名第 16),因此训练数据可能较少,这意味着 ChatGPT 的性能可能较差。经验性证据证实了这种差异(更多细节请参见方法部分)。因此,R 对于 Python 来说是一个有效的反事实(它受季节性影响,但我们可以预期 ChatGPT 的影响可以忽略不计)。

图 1:ChatGPT 对 Stack Overflow 每周问题数量的影响

Overflow(作者提供的图表)

上图显示了每周原始数据。我们可以看到在 ChatGPT 3.5 发布后,关于 Python 的 Stack Overflow 每周问题数量突然减少了重要的 21.2%,而对 R 的影响略小(减少了 15.8%)。

这些‘定性’观察结果得到了统计模型的确认。后面描述的计量经济学模型发现,Python 在 Stack Overflow 上的每周问题数平均下降了 937.7(95%置信区间:[-1232.8,-642.55];p 值=0.000)。随后,利用差分中的差分方法的分析进一步揭示了问题质量的改善(通过平台上的得分进行衡量),同时未回答问题的比例增加(虽然每个问题的平均浏览次数似乎没有变化)。因此,这项研究为之前定义的三个假设提供了证据。

这些发现强调了 AI 在我们工作方式中的深远作用。通过处理常规查询,生成性 AI 使个人能够将精力转向更复杂的任务,同时提高他们的生产力。然而,长期的重要潜在不利影响也在讨论部分中进行了讨论。

文章的其余部分将介绍数据和方法,然后是结果,并以讨论结束。

数据

数据是通过在Stack Overflow 数据浏览器门户上执行 SQL 查询提取的(许可证:CC BY-SA)。以下是使用的 SQL 命令:

SELECT Id, CreationDate, Score, ViewCount, AnswerCount
FROM Posts
WHERE Tags LIKE '%<python>%'
AND CreationDate BETWEEN '2022–10–01' AND '2023–04–30'
AND PostTypeId = 1;

然后,我将数据按周汇总,以减少噪音,从而获得了从 2022 年 10 月 17 日星期一到 2023 年 3 月 19 日的数据集,其中包含每周帖子数量、浏览次数、每个问题的浏览次数、每个问题的平均得分以及未回答问题的比例。得分由平台用户定义,他们可以投票表示问题是否表现出“研究努力;它是有用且清晰的”

方法

为了测量因果效应,我使用了一个差分中的差分模型,这是一种计量经济学方法,通常利用时间上的变化,将处理组与未处理组进行比较。为了了解更多关于这种方法的内容,我推荐你阅读两个免费的电子书中的相关章节:勇敢与真实的因果推断因果推断:混音带

简而言之,Diff-in-Diff 模型计算双重差分以识别因果效应。以下是简化的解释。首先,这个想法是计算两个简单的差分:处理组和未处理组在处理前(ChatGPT 发布前)和处理后期间的“平均”差异(这里分别为 Python 和 R 问题)。我们关心的是处理对处理单位的影响(这里是 ChatGPT 发布对 Python 问题的影响)。然而,如前所述,可能还有另一个效应与处理效应混淆(例如季节性)。为了解决这个问题,该模型的想法是计算双重差分,以检查处理组(Python)的第一个差分与控制组(R)的第二个差分的差异。由于我们期望控制组没有处理效应(或微不足道),尽管仍受季节性影响,我们可以排除这个潜在的混淆因素,最终测量因果效应。

这是一个稍微更正式的表示。

处理组的第一个差分:

E[Yᵢₜ| Treatedᵢ, Postₜ]-E[Yᵢₜ| Treatedᵢ, Preₜ] = λₜ+β

这里的 i 和 t 分别指代语言(R 或 Python)和周数。而 treated 指的是与 Python 相关的问题,Post 指的是 ChatGPT 可用的时间段。这个简单的差分可能代表了 ChatGPT 的因果效应(β)+ 一些时间效应 λₜ(例如季节性)。

控制组的第一个差分:

E[Yᵢₜ| Controlᵢ, Postₜ]-E[Yᵢₜ| Controlᵢ, Preₜ] = λₜ

控制组的简单差分不包括处理效应(因为它是未处理的),仅包括 λₜ。

因此,双重差分将给出:

DiD = ( λₜ+β) — λₜ = β

在假设 λₜ 对两个组相同(平行趋势假设,如下所述)的情况下,双重差分将允许我们识别 β,即因果效应。

该模型的本质在于平行趋势假设。为了声明因果效应,我们应该确信如果没有 ChatGPT,Stack Overflow 上 Python(treated)和 R(untreated)的问题演变在处理期(2022 年 11 月之后)会是相同的。然而,这显然无法直接观察和测试(参见因果推断的基本问题)。(如果你想了解更多关于这个概念和因果推断的信息,请查看我在 Towards Data Science 上的视频和文章:因果关系的科学与艺术)。然而,我们可以测试在冲击发生前趋势是否平行,这表明控制组是一个潜在的良好“反事实”。用数据进行的两次不同的安慰剂测试表明,我们不能拒绝 ChatGPT 前期的平行趋势假设(测试的 p 值分别为 0.722 和 0.397(见在线附录 B))。

正式定义:

Yᵢₜ = β₀ + β₁ Pythonᵢ + β₂ ChatGPTₜ + β₃ Pythonᵢ × ChatGPTₜ + uᵢₜ

“i” 和 “t” 分别对应 Stack Overflow 上问题的主题(i ∈ {R; Python})和周数。Yᵢₜ 代表结果变量:提问数量(H1)、平均问题评分(H2)和未答复问题的比例(H3)。Pythonᵢ 是一个二元变量。

如果问题与 Python 相关,则变量取值为 1,否则为 0

否则(与 R 相关)。ChatGPTₜ 是另一个二元变量

从 ChatGPT 发布及以后取值为 1,之前为 0

否则。uᵢₜ 是一个以编程语言为聚类的误差项

层级 (i)。

这个模型的核心在于平行趋势假设。为了声称因果效应,我们应该确信如果没有 ChatGPT,Python(处理组)和 R(对照组)在处理期(2022 年 11 月以后)的帖子演变将是相同的。然而,这显然无法直接观察和测试(参见因果推断的基本问题)。(如果你想了解更多关于这一概念和因果推断的信息,请查看我在 因果性科学与艺术 上的视频和文章)。然而,可以测试在冲击之前的趋势是否平行,从而表明对照组是一个良好的“反事实”。在这种情况下,两次不同的安慰剂测试表明,我们不能拒绝 ChatGPT 前期的平行趋势假设(测试的 p 值分别为 0.722 和 0.397(见在线附录 B))。

结果

H1: ChatGPT 减少了在 Stack Overflow 上提问的数量。

如引言中所述,Diff-in-Diff 模型估计在 Stack Overflow 上 Python 的每周提问平均减少了 937.7(95% CI: [-1232.8, -642.55]; p 值 = 0.000),这代表了每周提问减少了 18%。

图 2: ChatGPT 对每周提问数量的影响(图片来源:作者)

H2: ChatGPT 提高了提问的质量。

ChatGPT 可能对回答问题有帮助(参见 H1)。然而,当聊天机器人无法解决问题时,它可能让人进一步探索问题或解决方案的某些元素。该平台允许我们测试这一假设,用户可以对每个问题投票,如果他们认为 “这个问题展示了研究努力;它有用且清晰”(增加 1 分) 或不认为(减少 1 分) 这第二次回归估计问题的分数平均增加了 0.07 分(95% CI: [-0.0127 , 0.1518]; p 值: 0.095)(见图 3),这代表了 41.2% 的增加。

图 3: ChatGPT 对提问质量的影响(图片来源:作者)

H3:剩下的问题更加复杂。

现在我们有一些证据表明 ChatGPT 能够提供显著帮助(解决问题并帮助记录其他问题),我们希望确认剩下的问题更复杂。为此,我们将关注两件事。首先,我发现未回答问题的比例在上升(没有答案可能是问题更复杂的标志)。更具体地说,我发现未回答问题的比例增加了 2.21 个百分点(95% 置信区间:[0.12, 0.30];p 值:0.039),这代表了 6.8% 的增长。其次,我们还发现每个问题的浏览量没有变化(我们无法拒绝其未变化的零假设,p 值为 0.477)。这个第二个测试使我们部分排除由于流量较低导致未回答问题增多的替代解释。

图 4:ChatGPT 对未回答问题比例的影响(作者提供的图像)

讨论

这些发现支持了这样的观点:生成 AI 可能会通过处理常规问题来彻底改变我们的工作,让我们可以专注于需要专业知识的更复杂问题,同时提高我们的生产力。

虽然这个承诺听起来很令人兴奋,但也有其反面。首先,低技能的工作可能会被聊天机器人取代。其次,这种工具可能会对我们的学习方式产生负面影响。就个人而言,我把编程看作是骑自行车或游泳:观看视频或上课程是不够的,你必须亲自尝试和失败。如果答案太好,而我们不强迫自己学习,很多人可能会难以掌握。第三,如果 Stack Overflow 上的问题数量减少,这可能会减少生成 AI 模型训练集的一个宝贵来源,从而影响其长期性能。

所有这些长期不利影响尚不明确,需要仔细分析。请在评论中告诉我你的想法。

[0] Gallea, Quentin. “从平凡到重要:AI 对工作动态的影响——来自 ChatGPT 和 Stack Overflow 的证据” arXiv econ.GN(2023 年)

[1] Hatzius, Jan. “人工智能对经济增长的潜在巨大影响(Briggs/Kodnani)。” 高盛(2023 年)。

[2] www.nytimes.com/2023/03/29/technology/ai-artificial-intelligence-musk-risks.html

[3] Bhat, Vasudev 等. “Min (e) d 你的标签:对 Stack Overflow 问题响应时间的分析。” 2014 IEEE/ACM 国际社会网络分析与挖掘会议(ASONAM 2014)。IEEE,(2014 年)

《阿波罗 8 号如何赢得月球竞赛》

原文:towardsdatascience.com/how-apollo-8-won-the-moon-race-829f14b4b6e1

快速成功的数据科学

使用 Python 模拟自由返回轨迹

李·沃恩走向数据科学 李·沃恩

·发表于 走向数据科学 ·阅读时长 10 分钟·2023 年 11 月 21 日

--

“地球升起”由阿波罗 8 号宇航员比尔·安德斯(NASA)拍摄

五十五年前的今年 12 月,阿波罗 8 号的机组人员成为首批离开地球引力范围、前往其他天体的人。本月,那次任务的指挥官弗兰克·博曼在 95 岁时去世。为了纪念这一历史事件及其杰出的机组人员,我将回顾阿波罗 8 号飞行路径的计算机模拟。这样的模拟对于任务规划、教学辅助手段以及媒体和公众的传播都是非常有用的。

为什么阿波罗 8 号如此重要?

在 1968 年夏季,美国在太空竞赛中处于劣势。苏联的 Zond 飞船看起来已准备好前往月球,CIA 拍到了停在发射台上的巨大苏联 N-1 火箭,而美国陷入困境的阿波罗计划仍需进行三次测试飞行。

但在那年的 8 月,NASA 的经理乔治·洛提出了一个大胆的想法。现在就去月球。与其在地球轨道上进行更多测试,不如在 12 月绕月飞行,让成为测试!

在那个时刻,太空竞赛实际上已经结束。不到一年后,苏联就已经投降,尼尔·阿姆斯特朗完成了他对全人类的伟大飞跃。

决定将阿波罗 8 号飞船送上月球并非易事。1967 年,阿波罗 1 号的舱内三名宇航员遇难,多次无人驾驶任务也发生了爆炸或其他失败。

在这种背景下,且事关重大,一切都取决于自由返回轨迹的概念。任务设计如此,以便如果服务模块引擎未能点火,飞船将简单地绕月球一圈,像回旋镖一样返回地球。这个概念的优雅之美甚至被融入了官方任务徽章中。

阿波罗 8 号任务徽章(NASA,CC BY-SA 4.0,来自维基媒体共享资源)

在本文中,我们将使用用 Python 编程语言构建的计算机模拟来探讨自由返回轨道。虽然不是必需的,如果你想在阅读过程中参与模拟,可以从这个GitHub 仓库下载源代码和支持文件。你将需要文件apollo_8_free_return.pyearth_100x100.gifmoon_27x27.gif

了解阿波罗 8 号任务

阿波罗 8 号任务的目标仅仅是绕月,因此不需要带上登月舱组件。宇航员们乘坐的是指令舱和服务舱,这两个部分合称为 CSM。

阿波罗指令舱和服务舱(来源:Real-world Python)

在 1968 年秋季,CSM 引擎仅在地球轨道中经过测试,对其可靠性存在合理的担忧。要绕月球运行,引擎必须点火两次,一次是减速使航天器进入月球轨道,再一次是离开轨道。

通过自由返回轨道,如果第一次机动失败,宇航员仍然可以顺利返回家园。事实证明,引擎在两次点火时都表现完美,阿波罗 8 号绕月球运行了 10 圈。(不过,命运多舛的阿波罗 13 号却充分利用了其自由返回轨道!)

自由返回轨道

尽管计算自由返回轨道需要大量复杂的数学运算(毕竟这是火箭科学),但你仍然可以使用一些简化的参数在二维图中绘制,如下所示。

自由返回轨道(不按比例缩放)(来源:Real-world Python)

为了在二维中模拟自由返回,我们需要几个关键值:CSM 的起始位置(R₀)、CSM 的速度和方向(V₀),以及 CSM 与月球之间的相位角(gamma₀)。相位角,也称为引导角,是从起始位置到最终位置所需的 CSM 轨道时间位置的变化。

跨月注入速度(V₀)是一种推进机动,用于将 CSM 设置为前往月球的轨道。这是在地球的停泊轨道上完成的,在此过程中,航天器进行内部检查,并等待与月球的相位角达到最佳时机。此时,土星五号火箭的第三级点火并脱落,留下 CSM 前往月球。

由于月球在移动,你必须预测它的未来位置或引导它,就像用猎枪打飞碟一样。这需要知道在跨月注入时的相位角(gamma₀)。

引导月球与射击霰丨弹丨枪有所不同,因为太空是弯曲的,你需要考虑地球和月球的引力。这两个天体对航天器的牵引力会产生难以计算的扰动。实际上如此困难,以至于计算在物理学领域获得了一个特殊的名称:三体问题

三体问题

三体问题是预测三个相互作用的天体行为的挑战。艾萨克·牛顿的引力方程对于预测两个天体的行为(如地球和月球)效果很好,但如果再加入一个天体,无论是航天器、彗星、卫星等,情况就会变得复杂。牛顿从未能将三个或更多天体的行为简化为一个简单的方程。275 年来——即使国王提供奖金以寻找解决方案——世界上最伟大的数学家们也徒劳无功。

三体问题(Mgolden96, CC BY-SA 4.0, via Wikimedia Commons)

问题在于三体问题不能使用简单的代数表达式或积分来解决。计算多个引力场的影响需要在不使用高速计算机的情况下进行数值迭代,这在没有高速计算机的情况下是不可行的,例如你的笔记本电脑。

1961 年,迈克尔·米诺维奇(Michael Minovitch),一位在喷气推进实验室的暑期实习生,利用当时世界上最快的计算机 IBM 7090 找到了第一个数值解。他发现,数学家们可以通过使用修正圆锥方法来减少求解限制性三体问题(如我们的地月-CSM 问题)所需的计算次数。

修正圆锥方法

修正圆锥方法是一种分析性近似方法,它假设你在地球引力影响范围内处理一个简单的两体问题,而在月球引力影响范围内处理另一个问题。这是一种粗略的“简便计算”,提供合理的出发和到达条件估计,减少了初始速度和位置向量的选择。剩下的就是通过反复的计算机模拟来优化飞行路径。

因为研究人员已经找到了并记录了阿波罗 8 号任务的修正圆锥解,我们不需要再计算它。我已经将其调整到我们将要查看的 2D 模拟中。

如果你下载源代码,你可以通过调整参数如 R₀ 和 V₀ 来实验不同的解决方案。

模拟

自由返回轨迹模拟将捕捉飞行路径的基本要素,同时为了“观赏效果”做出若干近似。这些近似包括在 2D 环境中运行模拟,忽略阿波罗 8 号的十次月球轨道,并扭曲时间和距离的尺度。作为简化处理,地球和月球都不旋转。

时间尺度失真

在 1968 年,往返月球大约需要 六天。没有人愿意实时观看这个过程,所以模拟每次循环将时间单位增加 0.001。这意味着,当运行 4,100 次循环的模拟时,每个时间步代表现实世界中约 两分钟 的时间。时间步长越长,模拟速度越快,但结果的准确性越低,因为小错误会随着时间积累。

你可以通过首先运行一个小时间步以获得最大准确性,然后使用结果找到产生类似结果的最大时间步,来优化飞行计划模拟中的时间步。

距离尺度失真

模拟不是按比例的。虽然地球和月球的图像将具有正确的 相对尺寸,但它们之间的距离比实际距离 更小,因此需要相应调整相位角。我在模型中减少了距离,因为外太空很大。真的很大。如果你想按比例显示模拟并将其全部放在计算机显示器上,那么你必须接受一个荒谬的小地球和月球。

地球和月球系统在最近距离,或近地点,按比例显示(作者图像)

为了保持两个天体的可识别性,模拟使用了更大的、适当缩放的图像,并减少了它们之间的距离。这种配置将更容易让观众理解,同时仍然可以复现自由返回轨迹。

模拟中的地球和月球系统,仅按比例缩放了天体大小(真实世界 Python)

由于在模拟中地球和月球距离较近,月球的轨道速度将比实际生活中更快,这符合开普勒第二定律。为了补偿这一点,月球的起始位置被设计为适当减少相位角。

为了最终提高可视性,CSM 的大小被大幅增加。按比例显示时,它会比一个像素还小。

运行模拟

模拟追踪了阿波罗 8 从土星五号火箭的第三阶段停止点火进行月球注入直到指令模块在太平洋溅落的整个过程。初始加速代表了飞船在自由返回机动期间需要点燃推进器的唯一时间。

注意:下面的动画是 GIF 格式,根据 Medium 的要求。直接从 Python 脚本运行程序时,图像质量更好。

自由返回轨迹模拟(作者)

如果你在 Python 脚本中取消注释 self.pendown() 选项,你可以通过在 CSM 和月球后面绘制一条线来追踪旅程。自由返回轨迹的“8”字形模式非常清晰可见,以及飞船接近月球时的情况,距离仅为 69 英里(110 公里)。

模拟慢动作并记录路径(作者)

如果你调整模拟输入,你会发现对参数的最小调整可能会导致任务出现偏差。负面结果包括撞击月球、以过陡的角度进入地球大气层,或是永远飞向太空。

要模拟月球撞击,将代表 V₀月球注入速度的Vo_X变量从其初始值 485 更改为 475。

将 Vo_X 速度变量从 485 更改为 475 会导致 CSM 坠毁(作者)

尽管这种敏感性部分是由于小型模拟的自由度有限,但现实世界中也存在相同的问题。在关键时刻,太空旅行变成了秒和厘米的游戏,这也是为什么在轨道插入点燃等重要机动中需要计算机来控制推进器的原因。

模拟重力推进

迈克尔·米诺维奇解决三体问题的方案引领他发现了另一个重要的发现:重力推进。也称为重力助推弹弓机动,该技术涉及利用大型行星的重力场来加速或减速航天器,使其进入不同的轨道。最著名的应用是在 1970 年代,将旅行者号航天器送往太阳系的壮丽旅行。

你可以通过将Vo_X变量设置为 520 到 540 之间的值来模拟重力推进。这会导致 CSM月球后方通过,并偷取一些月球的动量,从而增加飞船的速度并改变其飞行轨迹。

模拟重力推进概念。再见,阿波罗 8 号!(作者)

有趣的是,以这种方式加速 CSM 将减缓月球在其轨道上的速度,因为物理法则要求能量必须守恒。然而,由于两者质量的巨大差异,对月球的影响微不足道。

总结

本文提到的 Python 模拟允许你尝试著名的自由返回轨道,这一轨道使 NASA 决定冒险提前登月。模拟的好处在于,如果失败了,你还能活着再试。NASA 为所有拟议任务运行无数次模拟,结果帮助 NASA 在竞争的飞行计划之间做出选择,找到最有效的路线,决定在出现问题时该如何处理,等等。

模拟对于外太阳系探索尤其重要,因为巨大的距离使实时通信变得不可能。关键事件的时机,如点燃推进器、拍摄照片或释放探测器,都是基于详细的模拟预编程的。

参考文献及进一步阅读

现实世界的 Python: 黑客解决问题的代码指南(无淀粉出版社,2020)由李·沃恩编著,专门用一章详细解释自由返回模拟代码。

阿波罗 8 号: 第一次登月任务的惊险故事(亨利·霍尔特公司,2017),由杰弗里·克卢格编著,涵盖了阿波罗 8 号历史性任务的从不太可能的开始到“不可思议的胜利”。

PBS Nova 阿波罗 8 号如何离开地球轨道 是一个关于阿波罗 8 号转月注入机动的短视频剪辑,标志着人类首次离开地球轨道,前往另一个天体。

NASA Voyager 1 & 2 维修手册(海恩斯,2015),由克里斯托弗·赖利、理查德·科菲尔德和菲利普·道林撰写,提供了关于三体问题以及迈克尔·米诺维奇对太空旅行众多贡献的有趣背景。

维基百科的引力助推页面包含了许多有趣的引力助推机动和历史行星掠过的动画,这些动画可以通过本文提到的阿波罗 8 号模拟器来重现。

追逐新视野: 探索首个冥王星任务的史诗(皮卡多出版社,2018),由艾伦·斯特恩和大卫·格林斯普恩编著,记录了模拟在 NASA 任务中的重要性和普遍性。

谢谢

感谢阅读,请关注我以获取更多未来的快速成功数据科学文章。

posted @ 2024-10-12 19:54  绝不原创的飞龙  阅读(428)  评论(0)    收藏  举报