TowardsDataScience 2023 博客中文翻译(二百零七)

原文:TowardsDataScience

协议:CC BY-NC-SA 4.0

大型语言模型在图灵测试和中文房间论证下的考量

原文:towardsdatascience.com/large-language-models-in-light-of-the-turing-test-and-the-chinese-room-argument-f0b34585280e

继续探讨最现代科技、AI 的哲学层面以及科幻小说之间的前沿话题

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

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

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

红色的那个人是在和另一个人交流还是和机器交流?本文讨论了现代大型语言模型在这个问题的背景下,这直接涉及“智能”是什么。本文中的所有其他图示均由作者使用 Dall-E-2 生成。

AI 近期成为热点话题,ChatGPT、Bard 及其他大型 AI 语言模型在自然语言对话方面取得了显著进展。让我们探讨 AI 的历史以及它最早和最著名的测试和思想实验之一:图灵测试中文房间论证,并在现代语言模型的背景下讨论它们的观点。

这篇分析延续了我之前写的一篇文章,似乎在读者中引起了相当大的兴趣:

## 如果口头和书面交流使人类发展了智能……那语言模型怎么回事?

我们是否也是随机的鹦鹉,只是训练得更好?AI 语言模型是否沿袭了人类的…

towardsdatascience.com

现代大型语言模型、图灵测试以及中文房间论证

我们刚刚迈过 21 世纪的前二十年,我们有像 ChatGPT 和 Bard 这样的语言模型,坦白说,当世纪初开始时,我们甚至不曾想到这些模型可能会出现。这些模型使用先进的机器学习技术来吞噬大量文本,然后通过应用从训练文本中“学习”的模式来执行高度复杂的与文本相关的任务,形式上是一种用户与计算机模型之间的自然对话。

这些模型一出现就令人震惊,因为它们似乎真的很“智能”。如果你觉得我在夸张,那是因为你被像我一样对科学和技术过于投入的人包围了……但只需去问问这些圈子之外的人。

尽管有人声称现代语言模型可能通过图灵测试(见下节),但理解这种测试的局限性至关重要。最重要的是,图灵测试依赖于智能的幻觉,而不是涉及任何实际理解的真正智能。此外,鉴于此,发现一个程序通过测试真的那么令人惊讶吗?

现代大型语言模型只是一个统计模型,它读取输入的标记并输出一个新的标记集合,这些标记集合具有非常好的语法,甚至还有一些有意义的内容。虽然它们能够进行连贯和上下文恰当的对话,确实令人惊讶,但这完全无法等同于真正的理解,更不用说意识了。然而,除了在最新版本中,它们会不断地重复警告你它是一个语言模型,我认为我们都可以相当确信 ChatGPT 能够完美地欺骗任何人,让他们认为它是另一个人——也就是说,它可以通过定义的图灵测试。然而,“中文房间论证”则认为语言模型只是根据在大量训练数据集中观察到的模式来处理语言输入并生成响应,但它当然缺乏对意义的真正理解,即使在某些条件下一些语言模型似乎能够通过类似于逻辑思维的步骤来解决问题。你同意吗?还是不同意?

关于如何区分模拟行为和人工智能系统中的真正认知能力的辩论仍在继续。即使是一个基本的、达成共识的智能定义也仍在追寻中。

“图灵测试”和聊天机器人的演变

图灵测试以著名数学家、逻辑学家和密码学家艾伦·图灵的名字命名,是用来判断机器是否能够表现出与人类无法区分的行为的测试。图灵被广泛认为是计算机科学之父,他在第二次世界大战期间在破解纳粹恩尼格码方面发挥了关键作用,这一点在电影《模仿游戏》中得到了很好的展现,这一概念与测试的理念直接相关。

在 1950 年发表的开创性论文《计算机 Machinery and Intelligence》中,图灵提出了一个基本问题,即机器是否能够思考或表现出智能。由于定义智能本身就是一个令人望而却步的挑战,图灵没有被定义机器是什么或智能包含什么而困扰,而是选择了一种更简单和实用的方法:确定一台机器是否能够在对话中令人信服地模仿人类。这导致了图灵测试的概念,也称为“模仿游戏”——电影标题也由此而来。

在“模仿游戏”中,两个人,一男一女,分别待在不同的房间里。第三个人,即审问者,与这两位个体互动,旨在仅通过书面消息确定他们的性别。图灵建议将“模仿游戏”中的一个参与者替换为机器,并评估审问者是否能够根据他们的回应区分人类和机器。

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

原始提出的图灵测试方案可以简化为一个人和另一个实体进行对话,并尝试确定对方是人还是机器,如引导照片所示。

然而,进行图灵测试存在挑战。没有固定的规则或标准来通过测试,因此对特定机器是否成功展示了类似人类的行为存在不同的意见。早期尝试,如 1966 年约瑟夫·魏岑鲍姆的 ELIZA,旨在通过用通用问题或观察回应用户输入来模拟对话。虽然 ELIZA 成功地欺骗了一些评委,但它更像是一个巧妙编程的聊天机器人,而不是一个真正智能的实体。实际上,你可以与它聊天,你会很快意识到它远不如 ChatGPT、Bard 或任何其他现代 AI 聊天机器人“智能”。

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

你可以在线尝试的 Eliza 机器人示例(链接在文章末尾)。

其他值得注意的图灵测试尝试包括 PARRY,一个模拟偏执型精神分裂症的 AI 程序,以及 Eugene Goostman,一个设计为乌克兰青少年的聊天机器人。虽然它们取得了一些成功,但最终还是依赖于操控语言而没有真正理解它。并不是说 ChatGPT 能理解它所说的内容……但去尝试这些旧聊天机器人,你会明白我的意思!

“中国房间论证”和 AI 是否真的能理解它所读到和写的内容的可能性

1980 年,哲学家约翰·塞尔提出的“中国房间论证”挑战了通过图灵测试即等同于真正的智能或理解的观念。今天看来这似乎非常合理,但将这些想法应用于现代大型语言模型时会变得非常有趣,如我们之前所预期的那样。

Searle提出了一个被称为“中文房间”的思想实验。设想一个人被放在一个房间里,房间里充满了装有中文符号的篮子。这个人对中文没有任何了解,但拥有一本包含逐步指令的手册,用于正确组合这些符号。我们不关心这个手册是如何创建的,重点在于这些指令可以完美地生成有序的输出符号系列,使其作为对给定输入集的响应完全合理。

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

根据中文房间实验的方案,一个能对特定输入产生有意义输出的黑箱,无论任务看起来多么复杂,都不一定需要理解它在做什么,尽管它从外部看起来非常智能,但并不一定意味着具有任何形式的智能。

从房间外部,人们发送符号组合作为问题。房间内的人按照手册的指示,回应相应的符号组合。这是没有理解回应的含义;实际上,这个人甚至不在乎理解:他或她只是按照手册的指示来解释如何根据收到的输入来构建输出。然而,对于房间外的观察者来说,出于实际原因,这个房间整体上表现得像是理解了语言。

结论是,尽管这个房间内的人能使外面的人相信他或她理解中文,但按定义来说,这个人并不真正理解语言。Searle的论点质疑那些通过图灵测试的 AI 系统是否真正拥有智能,还是只是通过机械地操作符号或语言来模拟智能——正是我们之前推测的那种情况。这一观点挑战了图灵关于“强 AI”的信念,后者在某种程度上断言,一个正确编程的机器可以真正思考并拥有思想。

批评者对这种思想实验进行了广泛的辩论,有些人提出了“系统反应”理论,认为房间内的占有人类似于计算机的中央处理器。塞尔反驳说,理解不能仅仅从系统的部分中产生。另一个反对意见认为,具有传感器和与环境互动能力的机器人可能会像人类儿童一样学习语言,这与我之前讨论的这里和这里的观点相似。塞尔认为,感官输入也将包括机器可以操作但无法理解的符号。但那……这难道也适用于我们人类吗?毕竟,我们从感官的符号输入中建立了一个现实,这些输入被信念、先入之见和经验所扭曲。你甚至不能确定这个现实对每个人来说是否相同,但我们可以以看似“智能”的方式交换信息。

抛开讨论,图灵测试仍然是人工智能发展中的一个重要里程碑,像 OpenAI 的 ChatGPT、Google 的 Bard 或 Meta 的 Llama 这样的语言模型展示了在模拟类人对话方面的显著进展,甚至可能会通过测试。但中国房间论点仍然存在,警告我们不要过早地将这种行为与真正的智能等同起来,这一点看似合理但需要特别强调,尤其是在你讨论或听到那些远离技术的人的讨论时,他们中的许多人已经把“人工智能”中的“智能”部分当成了现实。

随着研究和技术的进步,政策需要跟上,以缓解人工智能语言模型的负面影响;公众需要了解这些含义——“人工”,“智能”,“技术”,“生命”

人工智能的未来可能会看到一些进展,这些进展模糊了模拟智能与真正理解之间的界限,但目前我们必须认识到这种区别。继续探索人工智能的潜力并了解其局限性对于在各个领域促进负责任和伦理的应用至关重要,同时也值得推动科学与科幻之间的界限,甚至涉及生命本质的问题。

相关文献及进一步阅读

图灵原始文章提出了图灵测试:

[## I.-计算机械与智能

我提议考虑这样一个问题:“机器能思考吗?”这应该从定义这一问题的意义开始……

academic.oup.com](https://academic.oup.com/mind/article/LIX/236/433/986238?source=post_page-----f0b34585280e--------------------------------)

约翰·塞尔讨论中文房间论证的文章:

[## 心智、大脑和程序 | 行为与脑科学 | 剑桥核心

心智、大脑和程序 - 第 3 卷 第 3 期

www.cambridge.org](https://www.cambridge.org/core/journals/behavioral-and-brain-sciences/article/abs/minds-brains-and-programs/DC644B47A4299C637C89772FACC2706A?source=post_page-----f0b34585280e--------------------------------)

中文房间论证由《大英百科全书》解释:

[## 中文房间论证 | 定义、机器智能、约翰·塞尔、图灵测试、反对意见等…

中文房间论证,由美国哲学家约翰·塞尔提出的思想实验,首次发表于他的期刊…

www.britannica.com](https://www.britannica.com/topic/Chinese-room-argument?source=post_page-----f0b34585280e--------------------------------)

与像 Eliza 和 Parry 这样的早期流行聊天机器人聊天 - 期待不到像 ChatGPT 或 Bard 这样的效果!:

[## Eliza,计算机治疗师

与 Eliza 聊天!

psych.fullerton.edu](https://psych.fullerton.edu/mbirnbaum/psych101/eliza.htm?source=post_page-----f0b34585280e--------------------------------) [## Bot Libre

Bot Libre 是一个免费的开源平台,适用于聊天机器人和网页、移动端、社交媒体的人工智能…

www.botlibre.com](https://www.botlibre.com/bot?instance=857177&dynamicChat=Chat&source=post_page-----f0b34585280e--------------------------------)

我的一些其他文章,您可能会感兴趣:

## Gato,来自 Deepmind 的最新成果。迈向真正的人工智能?

Gato 可以玩游戏、生成文本、处理图像和控制机器人手臂。它的体积也不大。是否真正的人工智能…

towardsdatascience.com ## 在击败物理学建模原子和分子后,机器学习现在正在与…

将两种最佳的世界结合起来

[towardsdatascience.com

www.lucianoabriata.com 我撰写和拍摄关于我广泛兴趣范围内的一切内容:自然、科学、技术、编程等。 成为 Medium 会员 以访问所有故事(平台的附属链接,通过这些链接我会获得少量收入,但不会对你产生费用),以及 订阅以通过邮件获取我的新故事 。要 咨询小型工作 请查看我的 服务页面。你可以 在这里联系我

大型语言模型在分子生物学中的应用

原文:towardsdatascience.com/large-language-models-in-molecular-biology-9eb6b65d8a30?source=collection_archive---------0-----------------------#2023-06-02

破解生物学的语言,从 DNA 到细胞再到人类健康

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

·

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

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

作者提供的图像,使用 Midjourney 生成,提示为“DNA”。

引言

我们是否能破解分子生物学的语言?在这里,我认为我们距离拥有准确的计算模型,模拟从 DNA 到基因表达再到蛋白质的主要生物分子信息通道只有几年时间,这些模型能与实验准确度相媲美,并可用于医学和药物发现。

自从 1996 年开始我的博士研究以来,计算生物学界已经接受了“生物学正在成为一种计算科学”的口号。我们的终极目标是以类似于工程学科的精确性和可重复性来预测细胞内的生物分子的活动,以及我们身体内的细胞。我们的目标是创建生物系统的计算模型,从而在计算机上进行准确的生物分子实验。深度学习,尤其是大型语言模型(LLMs)以及负担得起的大规模数据生成的最新进展,正在将这一愿景逐步变为现实。

LLMs 已经被证明在建模人类语言方面具有非凡的能力,表现出诸如通过律师资格考试、编写代码、以多样风格创作诗歌等惊人壮举,甚至可以说让图灵测试变得过时。然而,它们在建模生物分子系统方面的潜力甚至可能超越它们在建模人类语言方面的能力。人类语言反映了人类的思维,给我们带来了内在的优势,而分子生物学则复杂、混乱且反直觉。尽管生物分子系统的组成混乱,但它们却是稳健且可重复的,由数百万个组件组成,这些组件以经过数十亿年进化的方式相互作用。由此产生的系统极为复杂,超出了人类的理解能力。生物学家们通常依赖于一些简单的规则,这些规则只有 60%或 80%的时候有效,从而导致了易于理解但不完整的叙述。我们生成庞大的生物分子数据的能力目前已超出了我们理解这些系统的能力。

本文将概述一些基于深度学习的语言模型在分子生物学领域的最新突破。我们将讨论这些进展如何在未来几年内与直接在大规模生物分子和人群健康数据上训练 LLMs 相结合,推动该领域向前发展。鉴于 LLMs 和深度学习比分子生物学更为广泛的受众,我们首先简要介绍 LLMs,然后更详细地介绍分子生物学,接着描述一些近期在分子生物学领域的 LLM 进展,最后展望未来。

在我们讨论的核心是生物学中正在进行的范式转变。尽管“范式转变”这个术语经常被滥用,但在这里确实非常恰当。传统上,生物学是假设驱动的:研究人员识别模式,提出假设,设计实验或研究以测试这些假设,并根据结果调整他们的理论。这种方法正在逐渐被数据驱动的建模方法所取代。在这种新兴的范式中,研究人员从假设无关的大规模数据生成开始,然后训练一个像 LLM 这样的模型,或者将数据整合到现有的 LLM 中。一旦 LLM 能够准确地模拟系统,接近实验复制中看到的保真度,研究人员可以询问 LLM 以提取关于系统的见解,并识别出底层的生物学原则。这种转变将越来越明显,并允许精确地对生物分子系统进行建模,超出人类能力所及的细微度。

大语言模型

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

作者提供的图片,使用 Midjourney 创建。

大语言模型(LLM)是一种通过审查大量文本数据获得生成与人类语言相仿文本能力的神经网络类型。它运行在“自我监督”的原则上,模型学习基于先前单词预测句子中接下来的单词。这个过程使 LLM 能够识别文本中的模式、关系和上下文,从而使其能够回答查询、生成新内容,甚至进行预测。LLM 可以被视为高级的自动补全形式,预测你可能要输入的下一个单词,但具有出人意料的能力,表现得好像它们对语言、上下文和含义有着扎实的理解。这使它们能够跨多种主题生成连贯而富有知识性的响应。

语言模型的发展看到每一代新的模型都具有增强的建模能力。让我们简要地介绍一下主要类型的语言模型及其独特特征:

  1. Word grams: 这些基本模型根据训练数据中词对或词袋(无序词组)的频率来预测句子中的下一个单词。它们忽略上下文或词序,导致生成的文本是不连贯的句子,与人类文本几乎没有相似之处。

  2. CNNs (Convolutional Neural Networks): 这些模型通过考虑固定窗口内相邻单词之间的关系来分析文本数据。窗口可以非常宽,使用类似扩展的技术。虽然 CNN 在识别局部模式方面表现出色,但在捕捉长距离依赖或理解复杂的句子结构方面则显得不足。

  3. LSTM(长短期记忆网络): 这些是能够存储和处理来自文本早期部分信息的递归神经网络(RNN)的变体。LSTM 在理解上下文和处理长距离依赖方面优于 CNN,但在复杂句子和长文本方面仍然存在不足。

  4. 注意力机制 使模型在进行预测时能够集中于输入的相关部分。一些注意力“头”允许模型在预测下一个词时关注前面文本的不同部分。它们的功能类似于你在长篇文章中重新访问关键点或细节的方式,使模型能够回顾文本中的相关部分,并将这些信息融入当前的上下文中。变换器是一类实现注意力机制的语言模型。

  5. 大型语言模型(LLMs): 如 GPT-3 这样的模型是利用注意力机制的变换器,并在大量数据上进行训练。它们的巨大规模使得学习文本中的复杂模式、关系和上下文成为可能。LLMs 代表了当前最先进的语言模型,能够在广泛的主题上生成非常准确和连贯的回应。

有两个使用变换器架构并在该领域引入重大突破的 LLM 值得特别提及:BERT 和 GPT 系列。

BERT(双向编码器表示从变换器)(Devlin 等,2018)是谷歌在 2018 年推出的一系列 LLM,并开源,代码可在 GitHub 上获取,并发布了多个预训练模型。BERT 使用 掩蔽语言建模 进行训练。其思想是随机隐藏或“掩蔽”输入标记的某些百分比,然后预测这些被掩蔽的标记。这迫使模型从输入的左右两侧理解上下文(因此称为“双向”)。BERT 训练还使用了下一句预测任务。在训练过程中,模型会接收到一对句子,并必须预测该对句子中的第二个句子是否是原始文档中的下一句。

GPT(生成预训练变换器) 是由 OpenAI 推出的一系列 LLM。与 BERT 不同,GPT 使用传统的语言建模任务——自动补全进行训练:预测句子中的下一个词。与 BERT 不同,GPT 在训练过程中只关注左侧上下文(或先前的标记),因此它是单向的。GPT 是一个生成模型,在涉及文本生成的任务中表现特别强大,如写作、生成诗歌或完成句子。最新一代 GPT,即 GPT-4,在多个领域的各种任务中表现出色,导致它被描述为展现出一些通用智能的火花(Bubeck 等,2023)。值得注意的是,并不是每个人都认为 GPT 和类似的 LLM 表现出通用智能。引用 Rodney Brooks 的话,“不要把表现与能力混淆” (spectrum.ieee.org/amp/gpt-4-calm-down-2660261157)。然而,正如我们将在本文中看到的,这并不是它们在分子生物学中有效应用的限制。

遗传法则

人类或其他任何生物的生物轨迹,从胚胎发育到其整个生命周期,是遗传与环境之间复杂的相互作用:个体的 DNA 与个体所暴露的环境之间的对话(图 1)。

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

图 1. 基因型-表型-环境。 个体的表型是个体的 DNA 与环境之间的对话。图像由作者提供。

分子生物学的中心法则描述了遗传信息在生物体内的流动。这些遗传信息的来源是我们的 DNA,它在我们体内每个细胞的核内都有一个精确的副本。人类 DNA 由约 30 亿个核苷酸组成,排列在 23 条染色体上,其中 22 条是常染色体,1 条是性染色体,分别为 X 或 Y。每个人拥有两个几乎相同的人类基因组副本:一个由母亲遗传,一个由父亲遗传。我们体内大约 30 万亿个细胞中的每一个都在其细胞核内保留了一个几乎相同的母系和父系基因组副本。

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

图 2. 人类染色体。 染色质以层级螺旋结构紧密包装。在底层,146 对核苷酸缠绕在组蛋白上,类似于珠子。组蛋白然后被螺旋状缠绕并超螺旋形成一个紧凑的染色体,这个染色体适合于细胞核内。图像来源于 VectorMine,iStock 内容许可协议。

基因组中包含大约 20,000 个基因,这些基因是负责蛋白质合成的 DNA 片段。基因组中约有 1%编码蛋白质,其余部分包括控制基因表达的区域、基因内部不编码蛋白质的区域、对 DNA 结构有贡献的区域,以及“垃圾”区域,这些自私的 DNA“学会”了自我复制。

分子生物学的中心法则描述了从基因组到基因表达以及随后的蛋白质生产的分子信息流,这是生命的基本构件。

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

图 3. 分子生物学的中心法则。 我们的 DNA 由大约 20,000 个基因和基因间区域组成。基因在细胞内通过转录过程被表达,将基因复制成单链分子 mRNA,以及翻译过程,将 mRNA 序列翻译成由氨基酸组成的蛋白质序列。因此,DNA 片段的 4 个字母的核苷酸代码被翻译成蛋白质序列的 20 种氨基酸代码。然后,蛋白质序列在三维空间中折叠成功能性蛋白质结构。图片由作者提供。

蛋白质合成包括三个主要步骤:转录(图 3)、剪接(图 4)和翻译。在转录过程中,与基因对应的 DNA 片段作为模板被复制成名为信使 RNA(mRNA)的分子。mRNA 分子经过剪接,这一过程将某些片段剪切掉或剪接出来,其余片段被连接在一起形成成熟的 mRNA。被剪切的区域称为内含子,而保留的区域,即外显子,构成了 mRNA 的蛋白质编码部分。每个成熟的 mRNA 由平均 7 个外显子组装而成,尽管在人体内的数量从 1 到 79 不等,例如人类的肌营养不良蛋白基因。剪接在高级生物中至关重要,因为一个基因可以通过在剪接过程中组装不同的外显子组合,产生多种不同的蛋白质。20,000 个基因产生了大约 70,000 种已知的标准剪接形式,以及大量的稀有或异常剪接形式。每种蛋白质变体的表达时机是细胞分子控制工具包的一部分。

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

图 4. mRNA 的剪接。 在人类和其他真核生物中,转录与翻译之间的一个重要过程是剪接。mRNA 的某些区域被切除,这些区域称为内含子,其余部分则按顺序粘合在一起,称为外显子。相同的基因可以以多种方式进行剪接,从而产生不同的剪接形式,增加了蛋白质的多样性。图片由作者提供。

转录后,mRNA 被运送到细胞的蛋白质合成机制,即核糖体,在那里发生翻译。在翻译过程中,mRNA 序列按三核苷酸一组进行解码,这些组称为密码子。每个密码子对应于 20 种氨基酸中的一种,这些氨基酸是蛋白质的基本构建块。这些氨基酸被链接在一起形成蛋白质序列,随后折叠成一个功能性三维蛋白质结构。

蛋白质是生命的基本构件,在几乎所有生物过程中的作用都至关重要。它们提供了细胞的结构组件,作为酶催化化学反应,并在细胞内部促进沟通和运输。

基因调控(图 5)涉及决定基因在细胞内何时、何地以及以何种数量表达的复杂过程。这确保了正确的蛋白质在正确的数量上及时生产。基因调控发生在不同的层次上,包括染色质的结构化、化学修饰以及特定蛋白质(称为转录因子)的作用。

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

图 5. 基因调控。 基因的启动子区域,即基因起始点的上游(左侧)区域,包含包括与某些称为转录因子的蛋白质结合的基序在内的控制元素。这些转录因子在招募 RNA 聚合酶和控制基因的表达时间、位置和数量方面发挥作用。开放的染色质是转录发生所必需的。图片改编自 Anshul Kundaje 的演示文稿,并在此处获得许可。

转录因子(TFs) 是在基因调控中发挥重要作用的蛋白质。它们结合到基因附近或基因内的特定 DNA 序列上,这些序列被称为转录因子结合位点,从而影响 RNA 聚合酶的招募,RNA 聚合酶是负责 mRNA 合成的酶。因此,转录因子调节目标基因的表达,确保基因在响应不同细胞信号和环境条件下的适当表达。转录因子本身也受到转录因子的调节,形成复杂的基因调控途径。

启动子和增强子 是在基因表达调控中发挥作用的 DNA 区域。启动子位于基因起始点的邻近区域(在化学方向上位于基因起始点的上游或左侧),而增强子则是较远的调控元件,位于内含子内或基因间。启动子和增强子都含有多个转录因子结合位点。在转录因子的协助下,基因的启动子和增强子形成三维结构,招募并调控负责 mRNA 合成的 RNA 聚合酶。

染色质结构(图 2)是 DNA 和蛋白质(组蛋白)的混合物,组成了我们的染色体。为了紧凑地容纳在每个细胞的细胞核中,DNA 绕着被称为组蛋白的蛋白质缠绕。组蛋白是由四个组蛋白蛋白质副本组装成的四聚体结构。每个这样的结构绕着 146 对核苷酸的 DNA 缠绕,形成一种念珠状的结构,随后折叠成更高阶的螺旋结构,即染色质。染色质的组织决定了哪些 DNA 区域对基因表达是可及的。要发生基因表达,染色质必须展开。相反,紧密打包的染色质则阻止基因表达。

组蛋白修饰 是指一些化学修饰,例如乙酰化或甲基化,这些修饰可以影响组蛋白珠子,从而影响染色质结构和基因可及性。这些修饰可以促进或抑制基因表达,具体取决于修饰的类型和位置。它们也是组蛋白密码的一部分,组蛋白密码是一种表观遗传密码,即在 DNA 遗传密码上叠加的额外编码层。(“epi-”是一个希腊词根,意为“在……之上”。)

DNA 甲基化 是一种化学修饰,其中一个甲基基团被添加到 DNA 分子上,通常在特定的胞嘧啶碱基处。甲基化可以通过影响转录因子的结合或改变染色质结构来影响基因表达,使其更加紧凑,减少转录的可及性。甲基化和其他 DNA 化学修饰也是表观遗传密码的一部分。基因调控是一个动态过程,特定于每种细胞类型。我们身体内的不同细胞展示了独特的基因表达谱,使它们能够执行特化的功能。通过精确控制基因表达,细胞可以响应环境刺激,维持稳态,并执行生命所需的复杂过程。

信息的双向流动。 传统上,中心法则被描述为单向信息流动:DNA 到 RNA 到蛋白质。然而,这种情况存在例外,我们对其潜在机制的了解仍在不断发展,这一主题超出了本简要综述的范围。值得提及一些例外。 (1) 逆转录的发现,即 RNA 被转化回 DNA 的过程,挑战了中心法则的单向性。这个过程由酶逆转录酶促进,并在逆转录病毒中很常见,例如 HIV。 (2) DNA 还可以转录成其他 RNA 分子,而不仅仅是 mRNA,例如转运 RNA(tRNA)、核糖体 RNA(rRNA)以及其他类型的非编码 RNA,这为遗传信息的流动增加了另一层复杂性。 (3) 最后,有关表观遗传学的证据不断增加,表观遗传学机制如 DNA 甲基化和组蛋白修饰,以及对表观遗传变化是否可以遗传的研究。

我们 DNA 的变异

每个人都在其一生中,从受孕到现在,都受到其 DNA 与环境影响之间复杂相互作用的生物学塑造。我们的 DNA,加上女性生殖系统,确保我们出生为人类,而不是例如黑猩猩,虽然它们的 DNA 与我们的 DNA 有 98.8%的相似性。任何两个成年人共享超过 99.9%的相同 DNA。然而,我们的 DNA 变异决定了我们所有特征的遗传,包括对健康和疾病的遗传贡献。

DNA 变异的起源。 产生 DNA 变异的主要机制是通过两个父母的基因组之间的突变,以及两位父母共同贡献给后代基因组的生殖系基因组。在人类中,孩子的 DNA 与父母的 DNA 相比,大约包含 50-100 个突变;这些突变中的大多数来自父亲,并且与父亲的年龄有关(Kong et al. 2012)。生殖系突变主要驱动遗传变异,占据了我们与例如黑猩猩和松鼠等物种的差异。大多数这些新变异是良性的,要么对表型没有影响,要么产生的影响既无利也无害。少数变异可能是有害的,特别是如果它们损坏了一个功能区域,这可能是蛋白质编码区、调控区,甚至与染色质结构相关。更少数的变异可能是有益的,例如,某个变异恰好改善了一个功能元素。

选择。 有害变异,或有害的基因突变,通常使一个有机体在进化上“适应性”降低,适应性定义为预期的存活后代数量。随着时间的推移,有害变异倾向于从种群中被统计学上淘汰。因此,在人类中常见的遗传变异——那些在至少 1%的人群中发现的变异——要么是良性的,要么是导致晚年才表现出来的疾病,这些变异超出了自然选择的范围。这也是为什么稀有变异通常比常见变异更可能是有害的原因。

共 alescence 和 DNA 序列保守性。 在较长的进化时间跨度中,如人类与黑猩猩或狗之间的时间,选择对 DNA 的影响非常有意义。以今天的任何两个人为例。例如,我和我的狗 Murzik(一只马尔济斯和贵宾犬混种)。取任何共享的 DNA 区域,例如我们与狗共享的大多数人类基因。取我母系的那一个基因副本,以及 Murzik(假设)的父系那一个基因副本。它们的相似度约为 84%。现在如果我们追溯这个区域的历史(我母亲从她的母亲那里继承了它,她又从她的父亲那里继承,以此类推;Murzik 的父亲从他的母亲那里继承了它,她又从她的母亲那里继承,以此类推),最终这两个区域会融合:存在一个祖先哺乳动物个体,他有两个孩子都继承了完全相同的 DNA 片段:其中一个孩子导致了我,另一个孩子导致了 Murzik。16%的序列差异反映了数百万代之间发生的所有生殖系突变,这些突变使我们与这个共同的祖先曾祖父分离。重要的是,发生在基因重要部分的突变往往使个体适应性降低,因此不太可能导致今天的我或 Murzik。因此,DNA 区域中更保守的部分更可能具有功能重要性,而不那么保守的部分更可能耐受突变。

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

图 6. 人类基因组测序的成本。 图中不包括过去两年,期间成本大幅下降。使用最新仪器的成本今天低至$200。Wetterstrand KA. DNA 测序成本:来自 NHGRI 基因组测序计划(GSP)的数据。可访问: www.genome.gov/sequencingcostsdata。访问时间:2023 年 5 月 25 日。

数据生成。 自 30 多年前启动人类基因组计划以来,已经开发了大量 DNA 测序技术,使得 DNA 数据的生成既快速又具成本效益。如今,一个完整的人类基因组的测序费用低至 200 美元(图 6)。值得注意的是,用于测序整个基因组的技术也能生成关于多种分子功能的数据,例如涉及分子生物学核心教义的功能。例如,通过将 DNA 测序与单细胞微流控技术结合,研究人员可以测量生物样本中数千个单独细胞内每个基因的转录水平。基于测序的方法可以揭示染色质结构、组蛋白修饰、转录因子与 DNA 的结合以及其他关键的分子信息。如何实现这些超出了本文的范围,但简而言之,具有特定兴趣属性的短 DNA 片段——例如结合某种转录因子或是开放的可及染色质的一部分——会在实验中被分离并测序。

除了 DNA 测序,其他技术如质谱(MS)和基于亲和力的蛋白质组学也能测量生物样本中所有蛋白质的水平。虽然 X 射线晶体学的通量较低,但它提供了蛋白质的高分辨率 3D 结构。

在过去的 20 至 30 年里,我们测量分子功能的能力已经显著超越了摩尔定律的进展,主要是因为 DNA 测序技术的进步,这些技术还使得各种分子读取方法得以实现,如基因表达、染色质可及性和组蛋白修饰。这种数据生成的迅速进展使科学家能够在生物样本中测量大多数遗传学方面,通常具备单细胞或空间精度。

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

图 7. 全基因组关联研究目录。 最新版本的标志性图示总结了迄今为止已知的 23 条染色体上的位点与表型之间的所有关联。来源:www.ebi.ac.uk/gwas/。图示可以实时浏览,关联信息是公开可用的。图示在 CC0 下提供:www.ebi.ac.uk/gwas/docs/about

将变异与功能关联起来。二十多年来,研究人员一直致力于通过将个体基因组中的遗传变异与特定表型(如某种疾病的存在或缺失)进行关联,以阐明基因功能和疾病的分子机制。这些研究被称为全基因组关联研究(GWAS),通过识别与研究表型显著相关的基因组位置(可能是基因或调控区域)来进行。GWAS 目录(https://www.ebi.ac.uk/gwas/),一个公共资源,目前包含了超过 6,300 篇出版物和 515,000 个这样的关联(见图 7)。当测量的表型不是二元的,而是可以量化的实体,如身高时,可以在基因组变异与表型之间进行回归分析,所识别的遗传位点称为定量性状位点。除了像疾病状态、身高或发色等宏观表型,遗传变异还可以与分子表型相关,如基因表达水平(导致表达定量性状位点或 eQTLs)、蛋白质丰度(结果为蛋白质定量性状位点或 pQTLs)以及几乎所有其他分子测量。这些分析提供了对调控细胞功能和人体生理的分子机制的宝贵见解。然而,正如我们将要讨论的,这些传统的关联分析可能会被 LLMs 的应用所超越。

分子生物学中的语言模型

在过去几年中,我们在建模分子生物学中心法则的每一步方面取得了显著进展。虽然我们尚未完全将分子生物学转变为计算科学,也未将医学和人类健康变为工程学科,但目前的势头表明,仅仅需要大量额外的数据和进一步的发展,我们就能实现这一愿景。这一进展在某种程度上与其他 AI 应用领域有所不同。个人而言,我认为人工通用智能(AGI),即使是小型哺乳动物水平的 AGI,仍然在视野之外。此外,组合学、离散算法和

为了说明这一点,让我们审视一些最近在分子生物学中心法则不同阶段的深度学习突破。

注意:我在以下一些工作中是合著者,特别是 SpliceAI 和 PrimateAI-3D 方法。因此,我的阐述可能存在偏见。

预测基因结构

根据分子生物学的基本教义,DNA 的主要功能是编码基因,这些基因转录并翻译成蛋白质。每个基因中翻译成蛋白质的特定片段由剪接机制决定;这些片段在基因组的大多数基因中都得到了良好的注释。然而,突变可能会干扰剪接的精确边界,即剪接位点。罕见的突变会干扰剪接,可能显著影响所产生的蛋白质功能,因为它们通常会产生完全不同的蛋白质序列。因此,它们占大约 10% 的罕见遗传疾病(Jaganathan 等,2019)。预测剪接位点和推断基因结构因此是一个基本的计算任务,对遗传疾病的诊断具有重要意义。事实上,这是我在博士期间探讨的第一个问题之一,并且在整个职业生涯中持续发表相关研究。关于剪接位点预测的文献非常广泛。然而,直到 2018 年左右,这个问题仍然是一个重大挑战,最好的方法的准确度约为 30%,这一水平不足以用于遗传诊断等应用。

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

图 8. SpliceAI 模型。 图像由 Kishore Jaganathan 创建,已获许可。

2019 年,Illumina AI 实验室(Jaganathan 等,2019)推出了 SpliceAI。SpliceAI 并不使用 transformer 技术或作为 LLM,而是采用早期的语言建模技术,其中语言是 DNA 序列。它是一个深度残差 CNN,利用扩张卷积有效扩展其处理的窗口大小。它接受 10,000 个核苷酸窗口的人类基因组作为输入,预测内含子-外显子边界的准确位置,即所谓的供体和受体位点——外显子-内含子和内含子-外显子的边界。就精确度-召回率曲线下的面积(PR-AUC)而言,SpliceAI 在整个人类基因组中的得分为 0.98,而之前的最佳得分为 0.23。重要的是,SpliceAI 足够准确,可以在计算机上进行突变分析:它可以人工改变 DNA 的任何位置,并确定该变化是否在 10,000 个核苷酸范围内引入或消除了剪接位点。因此,它可以用于辅助遗传诊断:对于具有遗传疾病的患者,例如患有儿科疾病的年轻个体,可以汇总所有在父母身上未出现的变异,并将每个变异输入 SpliceAI 以询问它是否可能改变邻近基因的剪接,从而破坏基因的功能。迄今为止,它已解决了在 Genomics England 100,000 基因组项目背景下的数百个之前未解决的罕见未诊断儿科疾病案例(Farh K,个人通讯)。

SpliceAI 是如何实现高准确性的?简而言之,它学习了 DNA 序列的复杂生物分子特性,这些特性可靠地引导剪接机械到剪接位点。这些特性之前未知或仅不精确地了解;SpliceAI 的深度残差网络具有足够的容量来准确捕捉这些特性。这提出了一个有趣的问题:如何提取 SpliceAI 学到的生物分子规则,以深入了解其潜在的生物分子机制?一般来说,神经网络是黑箱,不解释如何做出预测。然而,存在用于探测网络和提取其关注特征的技术。SpliceAI 团队进行了这样的分析,并描述了大量学到的特征(Jaganathan et al. 2019)。

预测蛋白质结构

分子生物学的中心法则讲述了我们的 DNA 中的信息如何产生蛋白质,这些蛋白质是生命的基本构建块。蛋白质序列直接从拼接的 mRNA 序列中根据遗传密码翻译出来,然后折叠成功能性 3D 形状——蛋白质结构。根据蛋白质序列预测蛋白质结构,被称为蛋白质折叠问题,长期以来被认为是分子生物学的圣杯,因其重要性极大且难度似乎难以逾越。蛋白质结构的黄金标准是 X 射线晶体学的实验数据,由于生产高质量蛋白质晶体的困难和衍生蛋白质结构所需的复杂数据处理,这些数据难以获得。尽管结构预测方法的准确性未能接近 X 射线晶体学,计算预测仍然是研究的重点。

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

图 9. CASP 竞赛中蛋白质结构预测的准确性结果。 每种方法都在结构此前未知且在竞赛结束时通过实验确定的多种蛋白质中进行评分。评分反映了氨基酸的百分比,这些氨基酸几乎完美地匹配实验确定的结构。尽管许多年来方法的准确率徘徊在 40% 或更低,AlphaFold 2 已实现 89% 的准确率,这接近实验级别的准确度。图像来源于作者。

半年一度的比赛,CASP(蛋白质结构预测的关键评估),一直在跟踪该领域的进展。在 2019 年的比赛中,DeepMind 的 AlphaFold 方法在准确性上相比之前的基准取得了巨大的飞跃。2021 年,AlphaFold 2(Jumper et al. 2021)又实现了另一项重要的飞跃,几乎达到了 X 射线晶体学的准确性。随后,DeepMind 与欧洲分子生物学实验室(EMBL)合作,基于 AlphaFold2 发布了一个全面的开源数据库,称为 AlphaFold 蛋白质结构数据库。该数据库提供了各种生物体的高准确性结构预测,包括人类蛋白质、模式生物和重要病原体。这些预测的结构有望加速研究,并为生物过程、药物发现和疾病理解提供宝贵的见解。截至目前,数据库中已有 214,683,829 个蛋白质结构。从本质上讲,曾经的分子生物学圣杯现在由于深度学习而接近被解决。AlphaFold 2 无论从哪个角度来看,都是一项重大的科学进步。

DeepMind 的蛋白质折叠 AI 解决了一个存在了 50 年的生物学重大挑战” Will Heaven,技术评论,2020 年 11 月 30 日

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

图 10. AlphaFold 2 的架构. 图片经 AlphaFold 团队(Jumper et al. 2021)许可使用。 (a) AlphaFold 在 CASP14 数据集上的表现。 (b) AlphaFold 对目标 T1049(PDB 6Y4F,蓝色)的预测与实验结构(绿色)对比。 © CASP14 目标 T1056(PDB 6YJ1)。一个预测准确的锌结合位点的例子。 (d) CASP 目标 T1044(PDB 6VR4),一个 2,180 氨基酸的单链,预测准确。 (e) 模型架构。详细解释请参阅原始论文。

AlphaFold 如何实现如此显著的准确性?其方法值得总结(见图 10)。AlphaFold 论文中使用的技术与徐锦波及其同事们早期开发的方法(Wang et al 2017)有相似之处。该方法结合了卷积神经网络对蛋白质序列的操作和成对共进化特征。该特征识别不同物种中相关蛋白质序列上共变的序列位置对,以预测蛋白质序列中的 2D 接触图。接触图是对序列中每对位置的评分,指示这两个位置在 3D 中可能相邻的可能性。AlphaFold 2 方法在这些算法基础上进行了改进,经过专业工程设计和训练,显著提高了结构预测的准确性。AlphaFold2 引入了几个额外的创新改进:(1)基于 Transformer LLM 架构,增强了其捕捉蛋白质序列中长距离相互作用的能力。(2)引入了一种新的基于能量的评分,Amber 能量,在结构优化步骤中直接优化 3D 蛋白质结构,允许在结构优化步骤中进行端到端的可微分处理。(3)通过整合多序列比对(MSA)数据,改进了共进化特征的利用,增强了模型识别同源蛋白质序列中保守结构特征的能力。(4)通过在第一个模型输出上训练的第二个模型进行的精细化阶段,调整了预测的蛋白质结构,从而实现更准确和一致的预测。

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

图 11. 预测(蓝色)和实验确定的(绿色)蛋白质结构。 图片经 AlphaFold 团队许可使用。(Jumper et al. 2021, Varadi et al. 2021)。

自 AlphaFold 问世以来,深度学习在蛋白质结构预测、建模和设计应用方面的进展突飞猛进。ESMFold(Lin et al. 2023)是一种用于蛋白质结构预测的 LLM,提供高达 60 倍的加速而不损失准确性。ProteinGenerator(Lyayuga Lisanza et al. 2023)是基于 RoseTTAfold(Baek et al. 2021)蛋白质结构预测方法的序列空间扩散模型,由同一实验室开发。ProteinGenerator 同时生成满足任何给定序列和结构特性的蛋白质序列及其伴随结构,作者通过实验证明。RosettaFold2(Baek et al. 2023)结合了 AlphaFold2 和 RosettaFold 的特性,在改进计算效率的同时提供了与 AlphaFold2 可比的准确性。我们正处在蛋白质设计的创新之初,未来将在药物设计和生物工程方面取得突破性进展。

一个重要的结论是,尽管几十年的基础研究,包括蛋白质结构能量最小化和蛋白质动力学建模,未能提供准确的结构预测,但实际折叠的复杂分子信息存在于数据中,且 LLMs 能够学习这些信息。

预测蛋白质变异的影响

超过 400 万个基因组中的位置在任何两个个体之间存在差异,其中有超过 20,000 个变异位点位于蛋白质编码区域。绝大多数这种遗传变异是良性的,并显著贡献于人类观察到的表型多样性。然而,这些遗传多样性中的一小部分是有害的,并导致遗传疾病。了解遗传变异的影响并将其分类为良性或有害,对于遗传疾病的诊断、药物开发的基因靶点识别以及疾病的分子机制理解都有直接应用。不幸的是,绝大多数变异是“意义不确定的变异”(VUSs),它们对疾病的影响尚不清楚。对这些变异进行注释是人类遗传学中一个关键的未解问题。

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

图 12. 灵长类动物谱系。 人类最亲近的亲属是大猿。我们与黑猩猩和倭黑猩猩的共同祖先大约在 500 万到 700 万年前分开,与大猩猩的分开时间稍长。我们与黑猩猩的 DNA 相似度为 98.8%,与大猩猩为 98.4%,与猩猩为 97%。像图中的谱系树展示了现存物种的进化历史。例如,大猿包括人类、黑猩猩和倭黑猩猩,它们大约在 500 万到 700 万年前从人类谱系分裂开来,东部和西部大猩猩大约在 800 万年前分裂,猩猩大约在 1500 万到 1900 万年前分裂。我们下一个最亲近的亲属是非洲和亚洲猴子。比较物种间的 DNA 序列时,功能重要的位置,若发生突变则可能导致遗传疾病,更可能被保守。相反,我们在今天的灵长类动物与我们基因组之间观察到差异的位置,更可能对突变有容忍性,并且在突变时不导致遗传疾病。图像由 Lukas Kuderna 生成并获得许可。

确定给定变异是否良性,或至少不太有害的一个重要线索来自将人类遗传学与近亲如黑猩猩和其他灵长类动物的遗传学进行比较(图 12)。我们的人类基因组与其他灵长类动物的基因组非常相似:例如,与黑猩猩的基因组相似度为 98.8%,与大猩猩的基因组相似度为 98.4%,与猩猩的基因组相似度为 97%。进化上保守的蛋白质平均上更为相似。我们的生物学也非常相似,当人类蛋白质中的突变是致命的或导致严重遗传病时,相应的灵长类动物蛋白质中的相同突变也可能是有害的。相反,在健康的灵长类动物中观察到的蛋白质变异在人体中也可能是良性的。因此,我们能访问的灵长类基因组越多,我们就能获得关于人类基因组的信息:我们可以编制一个在灵长类动物中频繁观察到的蛋白质变异列表,并推测这些变异在人体中可能是良性的。因此,寻找导致严重遗传病的突变应该从不在此列表中的突变开始。

灵长类动物蛋白质中的变异列表永远无法足够用于将人类突变分类为良性或致病性。简单来说,将有太多良性人类突变没有机会出现在灵长类动物的变异列表中。然而,这份列表可以以更具生产力的方式利用:通过观察在蛋白质序列和结构中倾向于耐受变异的模式以及倾向于不耐受变异的模式。通过学习区分这两类蛋白质位置,我们可以获得注释变异为可能良性或可能致病的能力。

由 Kyle Farh 领导的 Illumina AI 实验室开发了 SpliceAI 方法,采用这种方法对人类蛋白质中的变异进行注释(Gao 等人,2023 年)。最初,他们与其他团队合作,收集了灵长类动物的血液样本,并对他们能够接触到的尽可能多的灵长类动物进行了基因组测序,包括 233 个不同灵长类物种中的 809 个个体。这一测序工作是一个重要的保护举措:一些灵长类动物物种濒临灭绝,保存这些物种的遗传信息对基础科学以及人类遗传学的研究至关重要。

团队在灵长类动物中确定了 430 万个常见蛋白变体的目录,对应的蛋白质也存在于人类中。然后,他们构建了一个转换器,学习区分人类蛋白质中良性和致病性变体。这是通过学习灵长类动物变体通常出现的蛋白质位置模式,与灵长类动物变体通常不出现的蛋白质位置形成对比来实现的。这个转换器名为 PrimateAI-3D,是前一个深度学习工具 PrimateAI(Sundaram 等人,2018 年)的新版本,由同一实验室开发。PrimateAI-3D 利用了蛋白质序列数据以及通过 AlphaFold 和 HHpred 等工具实验重建或计算预测的蛋白质 3D 模型,分辨率为 2 埃。 (图 13)。

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

图 13. PrimateAI-3D 的架构。 人类蛋白质结构被体素化,与多序列比对一起作为输入传递给三维卷积神经网络,预测目标残基的所有可能点突变的致病性。该网络使用三个组成部分的损失函数进行训练:(1)语言模型使用周围多序列比对预测缺失的人类或灵长类动物氨基酸;(2)三维卷积“填空”模型预测 3D 结构中缺失的氨基酸;(3)基于分类观察变异与具有匹配统计特性的随机变异之间的语言模型分数进行训练。图由 Tobias Hemp 创建,并获得授权包含。

在人类注释变异及其影响的 ClinVar 数据集中,PrimateAI-3D 实现了 87.3%的召回率和 80.2%的精确度,AUC 为 0.843,这在最先进的方法中表现最佳,尽管与其他方法不同,它并未在 ClinVar 上进行训练。此外,检查 ClinVar 各个版本的更正暗示了 PrimateAI-3D 和 ClinVar 存在分歧的一些变异中,PrimateAI-3D 的召回可能是正确的。

PrimateAI-3D 可用于罕见疾病的诊断,它能够优先考虑可能有害的变异,并筛选出可能无害的变异。另一个应用是发现与复杂疾病相关的基因:在某一特定疾病的患者队列中,可以寻找根据 PrimateAI-3D 预测可能有害的变异,然后在队列中的特定基因中寻找这些变异的丰度。表现出这种模式的基因,即在某一特定疾病患者中受到许多可能有害的变异影响的基因,被认为有一种遗传“负担”,这是一种可能在疾病中发挥作用的信号。Gao 和 PrimateAI-3D 团队的同事们采用这一方法研究了几种遗传疾病,发现了许多之前未被认识到与这些疾病相关的基因。利用 PrimateAI-3D,Fiziev 等人(2023)开发了改进的罕见变异多基因风险评分(PRS)模型,以识别高风险个体。他们还将 PrimateAI-3D 整合到 UK Biobank 的罕见变异负担测试中,识别出了有前景的新药物靶点候选者。

基因调控建模

如前所述,基因调控的复杂过程涉及许多相互作用的分子组件:DNA 染色质结构、DNA 包绕的组蛋白中的化学修饰、转录因子与启动子和增强子结合、涉及启动子、增强子、结合的转录因子以及 RNA 聚合酶招募的 3D DNA 结构的建立。从理论上讲,基因附近的精确 DNA 序列携带了触发这一机制所需的所有信息,以确保在正确的时间、适量的情况下,在适当的细胞类型中启动。实际上,仅凭 DNA 序列预测基因表达是一项艰巨的任务。然而,语言模型最近在这一领域取得了重大进展。

基因调控信息的数据生成。 在过去二十年中,基因组研究人员付出了巨大的努力,生产适合理解基因调控的大规模分子数据。已经开发了数百种不同的检测方法,用于揭示中心法则的各个方面,这里无法一一列举。以下是获得的一些信息示例,均与人类细胞系或组织类型相关(前者通常是永生化细胞系,后者通常来源于已故捐赠者):(1)识别整个基因组中具有开放染色质的精确位置和具有紧密堆积染色质的位置。与此相关的两种检测方法是 DNAse-seq 和 ATAC-seq。(2)准确定位基因组中一个特定转录因子结合的所有位置。(3)识别基因组中一个特定组蛋白化学修饰发生的所有位置。(4)确定特定基因的 mRNA 水平,即特定基因的表达水平。这种数据已经从众多人类和小鼠细胞系中获得。总的来说,已经在多年的国际项目如 ENCODE、modENCODE、Roadmap Epigenomics、Human Cell Atlas 等中收集了几千次这样的实验。每个实验在整个基因组中都有数万到数十万个数据点。

一系列语言模型,最终形成了基于变压器的 Enformer 工具(Avsek 等人,2021),已经被开发用来接受基因附近的 DNA 序列作为输入,并输出基因组中任何基因的细胞类型特异性表达水平。Enformer 的训练任务如下:给定一个包含 100,000 个核苷酸的基因组区域和一个特定细胞类型,它被训练来预测该区域的每种实验数据类型,包括开放或紧密堆积的染色质状态、当前的组蛋白修饰、特定结合的转录因子和基因表达水平。语言模型非常适合这个任务:与掩蔽语言建模不同,Enformer 以监督方式进行训练,从 DNA 序列中同时预测所有轨迹。通过整合注意力机制,它可以有效地汇总来自远离区域的信息(最多 100,000 个核苷酸),以预测给定位置的状态。实际上,Enformer 学习了这些多样分子实体之间的所有复杂关联。

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

图 14. Enformer 与早期系统 Basenji2 的预测与实验结果的比较。 图像已获得对应作者 Ziga Avsec 的许可。

Enformer 仅通过序列预测基因表达表现相当不错。如果我们使用特定实验测定(例如 CAGE 实验)在同一细胞系中测量所有基因的基因表达,同一实验的两次重复通常在平均 0.94 的相关性水平上。一个达到这一水平的计算方法可能会减少收集实验数据的需求。Enformer 目前并未完全达到这一水平,其与实验数据的相关性仅为 0.85,这相当于两个实验复制的误差的三倍。然而,随着数据的积累和模型的改进,这一性能有望得到改善。值得注意的是,Enformer 能够预测由不同个体的突变或通过 CRISPR 实验引入的人为突变引起的基因表达变化。然而,它仍然存在一些局限性,例如在预测远端增强子(距离基因起始位置较远的增强子)的效果方面表现不佳(Karollus 等人,2023 年),以及正确确定个人变异在基因表达中的影响方向(Sasse 等人,2023 年)。这些缺点可能是由于训练数据不足所致。随着数据生成步伐的加快,预计在可预见的未来,我们将拥有能够以实验水平精度从序列预测基因表达的大型语言模型(LLMs),并因此准确和全面地描绘参与分子生物学中心法则的复杂分子机制的模型。

如上所述,细胞内的 DNA 呈复杂的分层三维染色质结构,这在基因调控中起到作用,因为只有开放染色质内的基因才会被表达。Orca(Zhou 2022)是一种最近的语言模型,基于卷积编码器-解码器架构,从 Hi-C 实验提供的接近数据预测 3D 基因组结构。这些数据集跨越细胞系或组织样本的整个基因组,在这些数据中,接近的基因组位置会被揭示为将 DNA 片段粘合到每个区域的 DNA 片段。Orca 模型是一个分层多级卷积编码器和多级解码器,用于预测从 4kb 到 1024kb 分辨率的 9 个级别的 DNA 结构,适用于与最长人类染色体长度相当的输入 DNA 序列。

基础模型

基础模型是大型深度学习架构,如 OpenAI 的基于 Transformer 的 GPT 模型,它们编码了来自多源的大量知识。研究人员和从业者可以针对特定任务对这些预训练模型进行微调,从而为各种下游应用提供高性能系统。在分子生物学中已经开始出现几种基础模型。在这里,我们将简要介绍两种刚刚作为 biorXiv 预印本出现的模型。(因为这些论文尚未经同行评审,我们暂时不报告它们与其他最先进方法的比较表现。)

scGPT 是为单细胞转录组学、染色质可及性和蛋白质丰度设计的基础模型。该模型是在来自 1000 万个人类细胞的单细胞数据上训练的。每个细胞包含大约 20,000 个人类基因的表达值。该模型学习这个大型细胞 × 基因矩阵的嵌入,这些嵌入提供了对潜在细胞状态和活跃生物通路的洞见。作者们创新地将 GPT 方法论适应到这个非常不同的环境中(图 15)。具体来说,在基因组中基因的顺序,不像在句子中单词的顺序那样具有意义。因此,虽然 GPT 模型是训练来预测下一个词,但在单细胞数据中,“下一个基因”的概念是不清楚的。作者们通过训练模型根据基因提示(已知基因值的集合)和细胞提示生成数据来解决这个问题。从已知的基因开始,模型预测剩余的基因以及它们的置信度值。对于 K 次迭代,将它们分成 K 个箱,置信度值最高的 1/K 个基因作为下一次迭代的已知基因。训练完成后,scGPT 可以针对多个下游任务进行微调:批次校正、细胞注释(其中地面真实是各种细胞类型的注释集合)、扰动预测(预测在给定一组基因实验扰动后的细胞状态)、多组学(其中每个层次,转录组、染色质组、蛋白质组,被视为不同的语言)、生物通路预测等等。

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

  • 图 15. scGPT 概述。 A. scGPT 的工作流程。该模型在来自细胞图谱的大量细胞上进行训练,然后针对聚类、批次校正、细胞注释、扰动预测和基因网络推断等下游应用进行微调。B. 输入嵌入。有基因令牌、基因表达值和条件令牌。C. Transformer 层。图像由王博提供。

核苷酸变换器是一个基础模型,专注于原始 DNA 序列。这些序列被分割成每个六个字符的词(长度为 6 的 k-mer),并使用 BERT 方法进行训练。训练数据包括参考人类基因组、3200 个额外的多样化人类基因组以捕捉人类基因组学中的变异,以及 850 个其他物种的基因组。然后将核苷酸变换器应用于 18 个下游任务,这些任务包括许多之前讨论的任务:启动子预测、剪接位点供体和受体预测、组蛋白修饰等。预测通过探测完成,其中不同层的嵌入作为简单分类器(如逻辑回归或感知器)的特征,或者通过轻量、计算上不昂贵的微调来实现。

展望未来

解读连接我们基因组与体内各种细胞中的复杂生物分子途径的生物分子代码,并随后与环境互动结合到我们的生理功能,并不需要 AGI。虽然有许多 AI 任务可能会出现在未来,但我认为理解分子生物学并将其与人类健康联系起来并不是其中之一。LLMs 已经足够满足这一总体目标。

这里有一些我们没有要求 AI 做的任务。我们并没有要求它生成新内容;而是要求它学习现有生物系统的复杂统计特性。我们没有要求它以目标导向的方式导航复杂环境,维持内部状态,形成目标和子目标,或通过与环境的互动学习。我们没有要求它解决数学问题或发展深层反事实推理。然而,我们确实期望它学习一步因果关系:如果发生某种突变,特定基因会失效。如果这个基因表达不足,级联中的其他基因会增加或减少。通过简单的一步因果关系,这些关系可以通过在不同模态(如 DNA 变异、蛋白质丰度和表型)之间的相关性进行三角测量(这是一种称为孟德尔随机化的技术)以及越来越普遍的大规模扰动实验来学习,LLMs 将有效地建模细胞状态。这一联系从基因组一端延伸到表型另一端。

总之,今天的 LLMs 已经足够先进,能够对分子生物学进行建模。进一步的方法学改进始终受欢迎。然而,障碍不再是深度学习方法;更重要的障碍是数据。

幸运的是,数据变得越来越便宜且丰富。DNA 测序技术的进步将测序一个人类基因组的成本从第一个基因组的 30 亿美元,降至几年前约 1000 美元,现在更低至 200 美元。这些成本下降同样适用于所有以 DNA 测序为主要读出的分子检测,包括用于定量基因表达、染色质结构、组蛋白修饰、转录因子结合等的检测,以及过去 10-20 年中开发的数百种其他巧妙检测。单细胞技术、蛋白质组学、代谢组学、脂质组学及其他-组学检测的进一步创新,允许对 DNA 与人类生理之间的各种分子层进行越来越详细和高效的测量。

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

图 16. 英国生物库。 英国生物库是一个大规模的生物医学数据库和研究资源,包含约 50 万名英国志愿者的深入遗传和健康信息。这些参与者在 2006–2010 年招募时年龄均在 40 至 69 岁之间。收集的数据包括血液、尿液和唾液样本,详细的参与者背景、生活方式和健康信息,以及通过健康记录获取的随后的医疗历史。对于一部分参与者,还收集了成像数据(大脑、心脏、腹部、骨骼和关节)。470,000 人的外显子组数据于 2022 年 6 月发布,所有人的全基因组数据预计将在 2023 年底前发布。图片由英国生物库提供,并获得许可使用。

那么,这些数据如何整合在一起呢?一种关键的数据倡议是将大量志愿参与者集合起来,以深入探索他们的-omic 数据、表型和健康记录。一个领先的例子是英国生物库项目(UKB),这是一个大规模生物库、生物医学数据库和研究资源,包含来自 50 万名英国参与者的全面遗传和健康信息(见图 16)。参与者的生物样本已在广泛同意下收集,并持续生成大量数据。几乎所有参与者的外显子(基因组中编码蛋白质的部分)已被释放,整个基因组的数据也将随之发布。此外,包括 COVID-19 抗体数据、代谢组学、端粒、影像学、基因型、临床测量、初级护理、疼痛问卷等在内的各种数据类型均可获得。额外的数据类型也在不断添加。UKB 数据对任何研究目的的研究者开放。All Of Us是美国的一个类似倡议,截至目前已对 25 万名参与者的基因组进行了测序。FinnGen(芬兰基因组计划)旨在创建一个包含 50 万芬兰参与者的类似生物库,这极具价值,因为在基因上更为同质的队列中进行遗传研究要容易得多。deCODE Genetics在冰岛领导了类似的努力,超过三分之二的冰岛成年人口参与了这一计划。还有其他已测序参与者的队列,包括由 Regeneron Pharmaceuticals(一个私人倡议)测序的数百万个外显子,以及全球许多国家的多项国家级倡议。

癌症特别是一种基因组疾病,许多公司正在建立大量关于癌症患者和癌症样本的基因组信息,以及额外的临床信息。虽然覆盖这一领域超出了范围,但值得一提的是Tempus,一家基于人工智能的精准医学公司,拥有大量不断增长的癌症临床和分子数据,Foundation Medicine,一家提供全面基因组分析检测的分子信息公司,用以识别患者癌症中的分子变化,并与相关靶向治疗、免疫治疗和临床试验匹配,以及GRAILGuardant Health,这两家开创性的诊断公司专注于通过“液体活检”或分析患者血液样本中的基因组内容来早期检测肿瘤,这些样本中常常含有癌细胞的分子脱落。每家公司都有大量不断增长的患者队列数据。

除了这些队列倡议,还有许多其他大规模的数据倡议。值得注意的是,人类细胞图谱项目已经为来自 6,300 名捐献者的 4200 万个人类细胞生成了基因表达数据。ENCODE 项目,作为一个庞大的功能基因组数据集,涵盖了数百个人类细胞系和各种分子量,生成了关于基因表达、染色质可及性、转录因子结合、组蛋白标记、DNA 甲基化等的数据。

LLMs 完美适用于整合这些数据。展望未来,我们可以设想一个庞大的 LLM 跨越所有这些数据集进行整合。那么,这样一个模型的架构和训练可能会是什么样的呢?让我们进行一个思想实验,尝试拼凑出它的全貌:

  • 基因组中的基因,包括像不同异构体这样的重要变异,进行标记化处理。

  • 不同类型的细胞和组织被进行标记化处理。

  • 人类表型,如疾病状态、临床指征和药物治疗依从性,也被进行标记化处理。

  • DNA 序列在固定长度的核苷酸水平上进行标记化处理。

  • 基因组中的位置性信息将基因与核苷酸内容连接起来。

  • 蛋白质序列使用氨基酸字母表进行标记化处理。

  • 来自人类细胞图谱和其他单细胞数据集的数据以类似 GPT 的自回归方式或类似 BERT 的掩码语言建模方式训练 LLM,突出细胞类型特异性和细胞状态特异性的基因通路。

  • ENCODE 和类似的数据教会 LLM 将不同的分子信息层如原始 DNA 序列及其变异、基因表达、甲基化、组蛋白修饰、染色质可及性等以细胞类型特异的方式关联起来。每一层都是一种独特的“语言”,具有不同的丰富性和词汇量,提供独特的信息。LLM 学会在这些语言之间进行翻译。

  • PrimateAI-3D 的非人类灵长类基因组学倡议以及其他物种测序工作等项目为 LLM 提供了关于人类基因组中突变潜在良性或有害效应的知识。

  • 整个蛋白质组,包括蛋白质变异,丰富了蛋白质的 3D 结构信息,这些信息要么是实验获得的,要么是由 AlphaFold、RoseTTAfold 和其他结构预测方法预测的。

  • 来自英国生物库(UKB)及其他队列的数据使 LLM 能够将基因组变异信息和其他分子数据与人类健康信息相关联。

  • LLM 利用参与者的完整临床记录来理解常规实践及其效果,并将其与所有数据集中的其他“语言”关联起来。

  • LLM 利用基础生物学、遗传学、分子科学和临床实践的广泛现有文献,包括所有已知的基因与表型的关联。

开发这样的 LLM 是一项重大的挑战,与 GPT 系列的 LLM 不同。这需要技术创新来表示和整合各种信息层,并扩大模型处理的 token 数量。这样的 LLM 具有广泛的潜在应用。列举几项:

  • 临床诊断。 它可以利用所有可用的患者信息,包括其基因组、其他测量、完整的临床历史和家庭健康信息,帮助医生做出准确的诊断,即使是对罕见疾病。它在诊断罕见疾病和癌症亚型时尤其有用。

  • 药物开发。 LLM 可以帮助识别不同临床指示的有前景的基因和通路靶点,预测对某些药物可能有反应的个体,以及那些不太可能受益的个体,从而提高临床试验的成功率。它还可以协助药物分子开发和药物重新定位。

  • 基础分子生物学。 每个分子信息层将以类似语言翻译的方式与其他层连接,LLM 将被探测以提供显著的预测能力。虽然深度学习模型的解释是一个挑战,但研究社区不断取得令人印象深刻的进展,致力于使 AI 可解释。在 OpenAI4 的最新进展中,GPT-4 刚刚被部署来解释 GPT-2 的每个神经元的行为。(https://openai.com/research/language-models-can-explain-neurons-in-language-models)

  • 额外实验的建议。 该模型可以用于识别训练数据中的“空白”,例如细胞类型、分子层次,或特定遗传背景或疾病指示的个体,这些在其他数据中预测的置信度较低。

在开发这些技术时,必须考虑潜在的风险,包括与患者隐私和临床实践相关的风险。患者隐私仍然是一个重大关注点。这对于 LLM 尤其如此,因为根据模型的能力,原则上可以通过包含部分数据的提示或其他信息来检索用于训练模型的参与者数据。因此,在用参与者数据训练 LLM 时,特别重要的是要获得针对这些模型预期用途和访问的适当知情同意。

然而,许多人,如英国生物银行队列中的参与者,愿意慷慨地分享他们的数据和生物样本,为研究和社会提供了巨大的好处。至于临床实践,目前尚不清楚 LLM 是否可以独立用于诊断和治疗建议。这些模型的主要目的是辅助,而非取代医疗专业人员,提供强大的工具以便医生验证和审计医学信息。引用 Isaac Kohane 的话:“信任,但要验证”(Lee, Goldberg, Kohane 2023)。

那么,完全实施一个大型语言模型(LLM)以桥接遗传学、分子生物学和人类健康的障碍是什么呢?主要障碍是数据的可获得性。功能基因组数据的生产,如来自 ENCODE 和人类细胞图谱的数据,需要加快进度。幸运的是,生成这些数据的成本正在迅速下降。同时,多组学队列和临床数据必须被生产并公开获取。这个过程需要参与者的同意,同时考虑到合法的隐私问题。然而,除了不可剥夺的隐私权之外,还有一个同样重要的参与者数据透明权:许多人想要通过分享他们的数据来做出贡献。这在罕见遗传疾病和癌症患者中尤为真实,他们希望通过贡献研究和治疗发展来帮助其他患者。英国生物银行的成功证明了参与者在数据共享方面的慷慨,旨在对人类健康产生积极影响。

结论

分子生物学不是一组整洁的概念和明确的原则,而是经过数亿年的试错积累而成的数万亿个小事实的集合。人类生物学家擅长讲述故事,将这些事实转化为描述和故事,这有助于直观理解和实验规划。然而,要将生物学转变为计算科学,需要大量数据的获取和具有适当容量的计算模型来从数据中提取这些数万亿个生物学事实。凭借 LLM 和数据获取速度的加快,我们确实离拥有准确的计算预测模型来连接我们的 DNA、细胞生物学和健康还差几年的时间。我们可以合理地预计,在接下来的 5-10 年内,大量生物医学诊断、药物发现和健康寿命公司和项目将使这些模型在人体健康和医学中得到应用,产生巨大的影响。我们也很可能会见证跨越从基因组到医学信息的开放基础模型的发展。这些模型将极大地加速研究和创新,并促进精准医学。

致谢

我要感谢 Eric Schadt 和 Bo Wang 对本文的许多建议和修改。我还要感谢 Anshul Kundaje、Bo Wang 和 Kyle Farh 提供的想法、意见和图表。我还要感谢 Lukas Kuderna 为本手稿创建了灵长目系统发育图。我是 Seer, Inc 的雇员,但这里表达的所有观点都是我自己的。

参考文献

Avsek Z 等人。通过整合远程相互作用从序列有效地预测基因表达。《自然方法》2021 年。

Baek M 等人。使用三轨神经网络准确预测蛋白质结构和相互作用。《科学》2021 年。

Baek M 等人。使用 RoseTTAFold2 高效准确地预测蛋白质结构。biorXiv doi: doi.org/10.1101/2023.05.24.542179,2023 年。

Bubeck S 等人。具有 GPT-4 的人工智能通用智能的火花:初步实验。arXiv:2303.12712,2023 年。

Cui 等人。scGPT:利用生成式人工智能构建单细胞多组学的基础模型。biorXiv doi.org/10.1101/2023.04.30.538439,2023 年。

Dalla-Torre H 等人。核苷酸变换器:构建和评估人类基因组学的稳健基础模型。biorXiv doi.org/10.1101/2023.01.11.523679,2023 年。

Devlin J 等人。BERT:用于语言理解的深度双向转换器的预训练。arXiv:1810.04805,2018 年。

Fiziev P 等人。罕见的穿透突变导致常见疾病的严重风险。《科学》2023 年。

Gao 等人。人类和灵长目可容许基因变异的景观。《科学》2023 年。

Jaganathan 等人。使用深度学习从原始序列预测剪接。《细胞》2019 年。

Jumper,J.,Evans,R.,Pritzel,A. 等人。使用 AlphaFold 高度准确地预测蛋白质结构。自然 596,2021 年 583-589 页。

Karollus 等人。当前基于序列的模型捕捉启动子中的基因表达决定因素,但大多数忽略远端增强子。《基因组生物学》2023 年。

Kong 等人。新生突变率及父亲年龄对疾病风险的重要性。《自然》2012 年。

Lee P,Goldberg C,Kohane I. 医学界的人工智能革命:GPT-4 及其后续。Pearson,2023 年。

Lin Z 等人。用语言模型预测原子级蛋白质结构的进化尺度。《科学》2023 年。

Lyayuga Lisanza S 等人。使用 RoseTTAFold 序列空间扩散的蛋白序列和结构的联合生成。biorXiv doi.org/10.1101/2023.05.08.539766,2023 年。

Sasse 等人。使用序列-表达深度神经网络,个性化基因表达预测有多远?biorXiv doi.org/10.1101/2023.03.16.532969,2023 年。

Sundaram 等人。使用深度神经网络预测人类突变的临床影响。《自然遗传学》2018 年。

Varadi M 等. AlphaFold 蛋白质结构数据库:通过高精度模型大幅扩展蛋白质序列空间的结构覆盖。核酸研究,2021 年。

Wang S 等. 通过超深度学习模型准确预测蛋白质接触图。PLoS 计算生物学,2017 年。

Wolfram S. ChatGPT 在做什么……以及它为何有效?Wolfram 媒体公司,2023 年。

Zhou J. 基于序列的三维基因组结构建模,从千碱基到染色体尺度。自然遗传学,2022 年。

大型语言模型,MirrorBERT——将模型转化为通用的词汇和句子编码器

原文:towardsdatascience.com/large-language-models-mirrorbert-transforming-models-into-universal-lexical-and-sentence-511bd592da48?source=collection_archive---------10-----------------------#2023-12-12

了解镜像增强如何生成数据,并在语义相似性任务中提升 BERT 的性能

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

·

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

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

介绍

毫无疑问,类似 BERT 的模型在现代自然语言处理应用中扮演着基础性角色。尽管它们在下游任务上的表现非常出色,但大多数模型在特定问题上并不是那么完美,需要进行微调。从原始预训练模型构建的嵌入通常会导致指标远离最先进的结果。同时,微调是一个繁重的过程,通常需要至少几千个标注数据样本才能使模型更好地理解领域数据。在某些情况下,当我们无法简单地收集已标注的数据或数据价格高昂时,这一问题就会变得很棘手。

MirrorBERT 旨在克服上述问题。与标准的微调算法不同,MirrorBERT 通过智能地增强初始数据而不依赖外部知识来进行自我监督。这种方法使 MirrorBERT 在 语义相似性问题 上表现出可比的性能。此外,通过使用其创新的对比学习技术,MirrorBERT 可以在不到一分钟的时间内将像 BERT 或 RoBERTa 这样的预训练模型转换为通用词汇编码器!

大型语言模型:RoBERTa — 一种鲁棒优化的 BERT 方法

了解用于 BERT 优化的关键技术

towardsdatascience.com

借助官方的 MirrorBERT 论文,我们将深入了解其关键细节,以理解其内部工作原理。所获得的知识是通用的,因为讨论的技术也可以用于处理相似性任务的其他 NLP 模型。

方法论

简单来说,MirrorBERT 是与 BERT 模型相同的模型,只不过在其学习过程中引入了几个步骤。让我们逐一讨论这些步骤。

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

MirrorBERT 学习过程

1. 自我重复

如其名称所示,MirrorBERT 只是简单地重复初始数据。

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

自我重复

然后,这些重复的数据用于进一步构建相同字符串的两种不同嵌入表示。

2. 数据增强

论文的作者提出了两种直观的技术,这些技术略微修改了数据集文本。根据他们的说法,在绝大多数情况下,这些文本破坏不会改变其含义。

2.1. 输入增强

给定一对字符串 (xᵢ, x̄ᵢ),算法随机选择其中一个,并应用 随机跨度掩码,即用 [MASK] 令牌随机替换文本中固定长度 k 的子字符串。

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

通过随机跨度掩码进行输入增强

2.2. 特征增强

随机跨度掩蔽操作在句子/短语级别。为了使模型也能在词级任务中表现良好,还需要另一种机制来处理较短的文本片段。特征增强通过使用 dropout 解决了这个问题。

Dropout 过程指的是在某一网络层中关闭一定百分比的 p 神经元。这可以视为将网络中对应的神经元置零的等效操作。

论文的作者建议使用 dropout 进行数据增强。当一对字符串 (xᵢ, x̄ᵢ) 传递到具有 dropout 层的网络中时,如果每次前向传递时 dropout 层总是禁用不同的神经元,则它们的输出表示会略有不同。

使用 dropout 进行特征增强的一个很棒的方面是 dropout 层已经包含在 BERT / RoBERTa 架构中,这意味着无需额外的实现!

虽然随机跨度掩蔽仅应用于数据集中的每第二个对象,但 dropout 是应用于所有对象的。

3. 对比学习

对比学习 是一种机器学习技术,旨在学习数据表示,使得相似的对象在嵌入空间中彼此接近,而不相似的对象彼此远离。

对比学习实现的一种方法是使用 对比损失函数。MirrorBERT 选择的损失函数是 InfoNCELoss。让我们理解它是如何工作的。

InfoNCELoss

初看起来,InfoNCELoss 的公式可能令人畏惧,所以让我们一步步逐渐理解。

  1. 两个向量之间的余弦相似度衡量它们彼此对齐的程度,取值范围从 -1 到 1,值越大表示相似度越高。

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

两个向量之间的余弦相似度

2. 为了更好地理解下一步,必须了解 InfoNCELoss 使用了 softmax 转换,其中温度参数 T 控制输出 softmax 分布的平滑度。这就是为什么相似度除以 T。

关于 softmax 温度的更多信息,请参阅 这篇文章 以了解更详细的解释。

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

余弦相似度除以温度

3. 与标准 softmax 公式一样,预测(相似度)会被转换为指数形式。

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

余弦相似度的指数

4. 在普通的 softmax 公式中,分子包含了类别概率的指数,而分母则是所有分布概率的指数和。在 InfoNCELoss 中,类似度的公式也遵循类似的逻辑:

  • 分子包含两个稍微修改的相同字符串 (xᵢ, x̄ᵢ) 的指数相似度,可以被视为 正例

  • 分母包括 xᵢ与所有其他数据集字符串 xⱼ之间的指数相似度之和,这可以看作是所有负面样本的集合。

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

余弦相似度的 softmax 公式。Nᵢ表示除 xᵢ和 x̄ᵢ之外的所有数据集字符串。

  1. 在理想情况下,我们希望相同字符串(xᵢ,x̄ᵢ)之间的相似度高,而 xᵢ与其他字符串 xⱼ之间的相似度低。如果这是真的,则上述公式中的分子会增加,而分母会减少,从而使整个表达式增大。

损失函数的工作方式是相反的:在理想情况下,它们取较小的值,而在较差的情况下,它们会对模型进行严厉惩罚。为了使上述公式与这一损失原则兼容,让我们在整个表达式前添加负对数

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

负的 softmax 相似度对数。这个表达式可以看作是单个字符串 xᵢ的损失值。

  1. 上一步的表达式已经对应于单个字符串 xᵢ的损失值。由于数据集由多个字符串组成,我们需要考虑所有这些字符串。为此,我们需要对所有字符串求和这个表达式。

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

InfoNCELoss

得到的公式正是InfoNCELoss

InfoNCELoss 试图将相似的对象聚集在一起,同时在嵌入空间中推开不相似的对象。

SBERT中使用的三元组损失是对比学习损失的另一个示例。

## 大型语言模型:SBERT — Sentence-BERT

了解 siamese BERT 网络如何准确地将句子转换为嵌入

towardsdatascience.com

训练资源

关于 MirrorBERT 的一个令人惊讶的事实是,它不需要大量的数据进行微调。此外,这些数据不需要是外部的,因为整个训练过程是自监督的。

研究人员报告称,为了微调词汇表示,他们仅使用每种语言中最频繁的 1 万词汇。对于句子级任务,使用 1 万个句子。

训练细节

MirrorBERT 训练的细节如下:

  • 温度在句子级任务中设置为T = 0.04,在词汇级任务中设置为T = 0.2

  • 在随机跨度掩蔽中,k设置为 5。

  • Dropout 设置为p = 0.1

  • 使用 AdamW 优化器,学习率为2e-5

  • 批量大小设置为 200(或 400 个重复样本)。

  • 词汇模型训练 2 个周期,句子级模型训练 1 个周期。

  • 不同于对所有输出标记表示进行均值池化,创建了[CLS]标记表示。

单次 MirrorBERT 训练周期仅需 10–20 秒。

评估

作者通过应用镜像微调在一组基准测试上评估了指标。结果在三种任务类型上进行了报告:词汇级别、句子级别和跨语言。在每种任务中,MirrorBERT 展现了与其他 BERT 类微调模型相当的性能。

结果还显示,10k 到 20k 的训练样本范围是微调的最优范围。随着训练样本数量的增加,模型的性能逐渐下降。

结论

镜像微调实际上就像一个魔法咒语:与繁重的微调程序不同,镜像框架所需的时间要少得多,而且不需要外部数据,其性能与 BERT、SBERT 或 RoBERTa 等其他微调模型在语义相似性任务上相当。

因此,MirrorBERT 可以将类似 BERT 的预训练模型转变为通用编码器,以高效捕捉语言知识。

资源

除非特别说明,所有图像均由作者提供

大型语言模型,StructBERT — 将语言结构融入预训练

原文:towardsdatascience.com/large-language-models-structbert-incorporating-language-structures-into-pretraining-be3058ab23b3?source=collection_archive---------3-----------------------#2023-11-22

通过融入更好的学习目标来使模型更智能

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

·

关注 发表在 Towards Data Science ·5 分钟阅读·2023 年 11 月 22 日

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

介绍

自 BERT 首次出现以来,它在各种 NLP 任务中显示了惊人的效果,包括情感分析、文本相似度、问答等。从那时起,研究人员就尝试通过修改架构、增加训练数据、扩充词汇量或改变层的隐藏大小等方式,使 BERT 变得更加高效。

## 大型语言模型:BERT — 双向编码器表示转换器

了解 BERT 如何构建最先进的嵌入

[towardsdatascience.com

尽管创建了其他强大的基于 BERT 的模型如 RoBERTa,研究人员发现了另一种提升 BERT 性能的有效方法,这将在本文中讨论。这导致了新模型 StructBERT 的发展,该模型在顶级基准上自信地超越了 BERT。

StructBERT 的想法相对简单,重点在于略微修改 BERT 的预训练目标。

在本文中,我们将深入探讨 StructBERT 论文的主要细节,并理解最初修改的目标。

预训练

在很大程度上,StructBERT 具有与 BERT 相同的架构原则。然而,StructBERT 提出了两个新的预训练目标,以扩展 BERT 的语言知识。该模型在此目标下进行训练,配合掩码语言建模。让我们看看下面的这两个目标。

1. 单词句子目标

实验表明,掩码语言建模(MSM)任务在 BERT 设置中发挥了关键作用,帮助其获得广泛的语言知识。预训练后,BERT 可以以高准确率正确猜测掩码词。然而,它无法正确重建单词被打乱的句子。为实现这一目标,StructBERT 开发者通过部分打乱输入标记来修改 MSM 目标。

与原始 BERT 一样,输入序列会被标记化、掩码处理,然后映射到标记、位置和段嵌入。这些嵌入会被求和以产生组合嵌入,然后输入到 BERT。

在掩码处理过程中,与 BERT 一样,15% 的随机选择标记会被掩盖并用于语言建模。但在掩码处理后,StructBERT 随机选择 5% 的 K 个连续未掩盖标记,并在每个子序列内对其进行打乱。默认情况下,StructBERT 对三元组(K = 3)进行操作。

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

三元组打乱示例

当计算最后一层隐藏层时,掩码和打乱标记的输出嵌入被用于预测原始标记,同时考虑其初始位置。

最终,单词句子目标与 MLM 目标以相等的权重结合。

2. 句子结构目标

下一句预测,作为 BERT 的另一个预训练任务,被认为相对简单。掌握它并不会显著提升 BERT 在大多数下游任务上的表现。因此,StructBERT 研究人员通过让 BERT 预测句子顺序来增加这一目标的难度。

通过在文档中取一对连续句子 S₁ 和 S₂,StructBERT 使用它们以三种可能的方式之一构建训练样本。每种方式发生的概率均为 1 / 3:

  • S₂ 后跟 S₁ (标签 1);

  • S₁ 后跟 S₂ (标签 2);

  • 从随机文档中抽取另一个句子 S₃,并且 S₁ (标签 0) 跟在其后。

这三种过程中的每一种都会生成一个有序的句子对,然后将它们连接起来。在第一个句子开始前添加了 [CLS] 标记,并使用 [SEP] 标记标记每个句子的结束。BERT 将该序列作为输入,并输出最后隐藏层的嵌入集。

[CLS] 嵌入的输出,最初在 BERT 中用于下一句预测任务,现在在 StructBERT 中用于正确识别与输入序列构建方式相对应的三种可能标签之一。

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

训练样本的组成

最终目标

最终目标由词和句子结构目标的线性组合组成。

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

BERT 预训练包括词和句子结构目标

StructBERT 设置

BERT 和 StructBERT 的所有主要预训练细节相同:

  • StructBERT 使用与 BERT 相同的预训练语料库:英语维基百科(2500M 单词)和 BookCorpus(800M 单词)。分词由 WordPiece 分词器完成。

  • 优化器:Adam(学习率 l = 1e-4,权重衰减 L₂ = 0.01,β₁ = 0.9,β₂ = 0.999)。

  • 学习率热身在总步骤的前 10% 中进行,然后线性减少。

  • 在所有层上使用 Dropout(α = 0.1)。

  • 激活函数:GELU。

  • 预训练过程运行 40 个周期。

StructBERT 版本

与原始 BERT 类似,StructBERT 提供了基础版和大型版。所有主要设置,如层数、注意力头、隐藏层大小和参数数量,都分别对应 BERT 的基础版和大型版。

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

StructBERT 基础版与 StructBERT 大型版的比较

结论

通过引入一对新的训练目标,StructBERT 在 NLP 中达到新的极限,在各种下游任务上始终超越 BERT。研究表明,这两个目标在 StructBERT 设置中发挥了不可或缺的作用。词结构目标主要提高了模型在单句问题上的性能,使 StructBERT 能够重建词序,句子结构目标则改善了对句间关系的理解,这对句子对任务特别重要。

资源

除非另有说明,否则所有图片均为作者提供

大型模型遇见大数据:Spark 和 LLMs 的和谐

原文:towardsdatascience.com/large-models-meet-big-data-spark-and-llms-in-harmony-5e2976b69b62

数据工程与生成式 AI

使用 Apache Spark 和大型语言模型的逐步指南

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

·发布于 Towards Data Science ·6 分钟阅读·2023 年 12 月 5 日

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

该图像由 Midjourney 生成。

生成式 AI,包括大型语言模型(LLMs),正在革新人类生活的各个方面。在过去五年里,生成式 AI 从一个研究项目发展成为许多人日常生活中的实际应用。作为一名对生成式 AI 感兴趣的数据工程师,我一直在问自己,这项技术为我的工作和数据工程应用带来了什么?对于工程师来说,生成式 AI 和 LLMs 的一些常见应用包括自动编码、协助文档编写等等。但是,在这里,我正在评估生成式 AI 和 LLMs 在数据工程中的一些更专业的使用。如果你对这个话题感兴趣,请阅读这篇文章,并关注我在 MediumLinkedin 上的其他文章,以获取更多关于其他用例的内容。

LLMs:强大的变革工具

数据工程师喜欢结构化和抽象化数据,这已经不是什么新鲜事了。但是,世界上充满了需要数据工程师关注的非结构化和杂乱的数据。对非结构化数据的转换总是很复杂,有时用传统工具是无法完成的。历史上,其中一种具有挑战性的非结构化数据是文本(例如评论、评价、对话)。对文本的简单转换并不难,但复杂的转换可以从文本中提取更多信息,我们可以生成更丰富的数据集。

复杂文本转换的例子可能包括从文本中提取姓名和对象、对评论或意见进行情感分析、在存储的文本中掩盖重要信息(例如私人数据、用户数据)、从一种语言翻译到标准语言、文本摘要等。好消息是现在的 LLM 可以完成所有这些转换。因此,我相信在数据工程中,LLM 的应用之一就是作为复杂数据(如文本)的转换函数。

在这篇文章中,我将通过 Apache Spark 展示 LLM 的这一能力,Apache Spark 是一个强大的分布式数据处理系统。更具体地说,我将使用 Hugging Face 的一个小型 LLM(t5-small)作为 Apache Spark UDF 函数,并对一个示例数据集应用特定的转换(情感分析)。

设置项目

首先,我们需要一个 Apache Spark 系统。你可以在本地系统上安装 Apache Spark,也可以使用 AWS EMR 等服务。我使用了 AWS EMR 并设置了一个小型集群进行测试。有很多关于如何在本地机器上安装 Apache Spark 或使用 AWS EMR 的文章,你可以参考它们。在这里,我假设你已经在系统上安装了 Apache Spark。

我还为这个项目选择了 Python 3.8。我们将使用 Hugging Face 库,它们与 Python 的兼容性很好。如果你使用 AWS EMR,你的系统上应该已经有 Python 3.8。否则,你需要安装 Python 3.8。

现在你已经在系统上安装了 Apache Spark 和 Python 3,接下来是安装测试此项目所需的库。运行以下 pip 命令来安装库。我们在这里安装 PySpark 以运行 Spark 任务,以及来自 Hugging Face 的 Transformers 库(这使我们能够使用包括 LLM 在内的数百种模型)。

pip3 install torch==1.13.1
pip3 install transformers==4.30.2
pip3 install pyspark==3.4.2
pip3 install urllib3==1.26.6

编码时间

首先创建一个新的 Python 文件。我将其命名为 spark_llm_test.py。首先,我们需要导入库并创建一个新的 Spark 会话。

from pyspark.sql import SparkSession, Row
from pyspark.sql.functions import udf
from pyspark.sql.types import StructType, StructField, StringType, LongType
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

# Create a Spark session
spark = SparkSession.builder.appName("FlanT5Seq2SeqExample").getOrCreate()

对于这个测试,我创建了一个仅有两列的虚拟表。第一列是索引,第二列是随机句子。我为这个项目设定的目标是对第二列进行情感分析。显然,在实际使用情况下,你可能会从一个包含更复杂数据的表(例如 Hive 表)中读取 Spark 数据帧。

# Create an Example Spark DataFrame
schema = StructType([
  StructField("id", LongType(), nullable=False),
  StructField("sentence", StringType(), nullable=False)
])

data = [
  Row(1, "It is a good test for Spark."),
  Row(2, "Spark DataFrames are powerful."),
  Row(3, "LLMs could be very slow."),
  Row(4, "It is a naive statement.")
]

input_df = spark.createDataFrame(data, schema=schema)

为此,我使用了“Flan T5”模型,该模型经过针对各种任务的微调,包括情感分析。如你所见,使用 Hugging Face Transformers 库,模型和分词器的设置非常简单。

# Loading Flan T5 Model and Tokenizer
model = AutoModelForSeq2SeqLM.from_pretrained("google/flan-t5-small")
tokenizer = AutoTokenizer.from_pretrained("google/flan-t5-small")

现在是定义我们的 Spark UDF 函数的时候了。Spark 将在每一行数据上调用这个函数,以按照我们在函数中指定的方式转换数据。

关于这个 UDF 函数,有一点很重要。正如你所见,我们没有在 UDF 函数内部实例化模型和分词器。原因是定义模型和分词器(即使是这样一个小的 LLM 模型)需要较大的处理开销。这意味着每次调用这个 UDF 函数时,模型和分词器都需要在工作节点上加载,然后进行转换(这里是推断)。这无疑会显著减慢我们的处理速度。为避免这种情况,我们在 UDF 函数外部加载了模型和分词器。

# Defining the Spark UDF
def t5_seq2seq_udf(input_text):
  prompt = f"sentiment of the text: {input_text}"
  input = tokenizer(prompt, return_tensors="pt")
  output = model.generate(**input)
  output_text = tokenizer.decode(output[0], skip_special_tokens=True)
  return output_text

最后,我们需要注册 UDF 函数并创建一个新列以保存情感分析结果。

t5_udf = udf(t5_seq2seq_udf, returnType=StringType())

results_df = input_df.withColumn('output_column', t5_udf(input_df['sentence']))

results_df.show(truncate=False)

下面,你可以找到完整的代码。

from pyspark.sql import SparkSession, Row
from pyspark.sql.functions import udf
from pyspark.sql.types import StructType, StructField, StringType, LongType
from transformers import AutoTokenizer, AutoModelForSeq2SeqLM

# Create a Spark session
spark = SparkSession.builder.appName("T5Seq2SeqExample").getOrCreate()

# Create an Example Spark DataFrame
schema = StructType([
  StructField("id", LongType(), nullable=False),
  StructField("sentence", StringType(), nullable=False)
])

data = [
  Row(1, "It is a good test for Spark."),
  Row(2, "Spark DataFrames are powerful."),
  Row(3, "LLMs could be very slow."),
  Row(4, "It is a naive statement.")
]

input_df = spark.createDataFrame(data, schema=schema)

# Loading t5 Model and Tokenizer
model = AutoModelForSeq2SeqLM.from_pretrained("google/flan-t5-small")
tokenizer = AutoTokenizer.from_pretrained("google/flan-t5-small")

# Defining the Spark UDF
def t5_seq2seq_udf(input_text):
  prompt = f"sentiment of the text: {input_text}"
  input = tokenizer(prompt, return_tensors="pt")
  output = model.generate(**input)
  output_text = tokenizer.decode(output[0], skip_special_tokens=True)
  return output_text

t5_udf = udf(t5_seq2seq_udf, returnType=StringType())

results_df = input_df.withColumn('output_column', t5_udf(input_df['sentence']))

results_df.show(truncate=False)

如果你使用 spark-submit 命令运行代码,你将得到以下结果。正如你所见,Flan T5 模型在识别正面句子与负面句子方面表现良好。

+---+------------------------------+-------------+                              
|id |sentence                      |output_column|
+---+------------------------------+-------------+
|1  |It is a good test for Spark.  |positive     |
|2  |Spark DataFrames are powerful.|positive     |
|3  |LLMs could be very slow.      |negative     |
|4  |It is a naive statement.      |negative     |
+---+------------------------------+-------------+

Spark 和 LLMs 的未来

恭喜!你已成功将 Flan T5 作为 Spark 作业执行。为了简化,我们省略了一些细节,包括主节点和工作节点如何传输模型权重以进行推断。这个话题可能会在未来的讨论中涉及,即 Spark 是否可以高效地用于使用 LLMs 进行推断。

此外,我们在这里使用了 Spark 进行数据转换(一个应用)。但如果 Spark 可以在模型开发过程中高效使用,那将是 Apache Spark 的另一个重大胜利。

最后,这只是一个简单的批处理示例。Spark 和 LLMs 的另一个应用可能是流处理,并对这些数据流提供实时分析。毫无疑问,这是使用 Spark 和 LLMs 的良好开端,但这两个工具之间的应用和协作是无穷无尽的。

大型图像模型中的最新 CNN 核

原文:towardsdatascience.com/latest-in-cnn-kernels-for-large-image-models-b48b27e75638?source=collection_archive---------3-----------------------#2023-08-04

对可变形卷积网络中的最新卷积核结构、DCNv2、DCNv3 进行的高级概述

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

·

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

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

澳大利亚拜伦湾灯塔 | 作者拍摄

随着 OpenAI 的 ChatGPT 的显著成功引发了大型语言模型的热潮,许多人预测下一次突破将发生在大型图像模型领域。在这个领域,视觉模型可以像我们目前对 ChatGPT 的提示一样被提示去分析甚至生成图像和视频。

最新的大型图像模型深度学习方法已分为两个主要方向:基于卷积神经网络(CNN)的方法和基于变换器的方法。本文将重点介绍 CNN 方面,并提供改进的 CNN 内核结构的高级概述。

目录

  1. DCN

  2. DCNv2

  3. DCNv3

1. 可变形卷积网络(DCN)

传统上,CNN 内核在每一层中应用于固定位置,导致所有激活单元具有相同的感受野。

如下图所示,为了在输入特征图 x 上执行卷积,计算每个输出位置 p0 的值是通过内核权重 w 和在 x 上的滑动窗口之间的逐元素乘法和求和来完成的。滑动窗口由网格 R 定义,这也是 p0*** 的感受野。*** 在 y 的同一层中的所有位置,R 的大小保持不变。

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

常规 3x3 内核的卷积操作。

每个输出值的计算如下:

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

论文中的常规卷积操作函数。

其中 pn 枚举滑动窗口(网格 R)中的位置。

RoI(兴趣区域)池化操作也在每一层中固定大小的 bin 上操作。对于 (i, j)-th bin 包含 nij 像素,其池化结果计算如下:

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

论文中的常规平均 RoI 池化函数。

每一层中的 bin 的形状和大小再次保持不变。

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

3x3 bin 的常规平均 RoI 池化操作。

因此,对于编码语义的高层,如具有不同尺度的对象,这两个操作特别具有挑战性。

DCN 提出了可变形卷积和可变形池化,这些方法对建模这些几何结构更加灵活。两者都在 2D 空间域上操作,即操作在通道维度上保持不变。

可变形卷积

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

3x3 内核的可变形卷积操作。

给定输入特征图 x,对于输出特征图 y 中的每个位置 p0,DCN 在枚举常规网格 R 中的每个位置 pn 时添加 2D 偏移量 △pn

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

论文中的可变形卷积函数。

这些偏移量从前面的特征图中学习得到,通过在特征图上应用额外的卷积层获得。由于这些偏移量通常是小数,它们通过双线性插值来实现。

可变形 RoI 池化

类似于卷积操作,池化偏移量 △pij 被添加到原始 bin 位置。

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

可变形 RoI 池化函数来自 论文

如下图所示,这些偏移量通过原始池化结果后的全连接(FC)层进行学习。

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

可变形平均 RoI 池化操作,使用 3x3 bin。

可变形位置敏感(PS)RoI 池化

在将可变形操作应用于 PS RoI 池化 (Dai et al., n.d.) 时,如下图所示,偏移量应用于每个分数图而不是输入特征图。这些偏移量通过卷积层而不是全连接层进行学习。

位置敏感 RoI 池化 (Dai et al., n.d.):传统的 RoI 池化丢失了每个区域所代表的对象部分的信息。PS RoI 池化通过将输入特征图转换为每个对象类别的 k² 分数图来保留这些信息,其中每个分数图表示一个特定的空间部分。因此,对于 C 个对象类别,总共有 k² (C+1) 个分数图。

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

3x3 可变形 PS RoI 池化的示意图 | 来源于 论文

2. DCNv2

尽管 DCN 允许更灵活地建模感受野,但它假设每个感受野中的像素对响应的贡献是相等的,这种情况通常不成立。为了更好地理解贡献行为,作者使用三种方法来可视化空间支持:

  1. 有效的感受野:节点响应关于每个图像像素的强度扰动的梯度

  2. 有效的采样/ bin 位置:网络节点相对于采样/ bin 位置的梯度

  3. 错误界限显著区域:逐步遮蔽图像的部分,以找到与整个图像产生相同响应的最小图像区域

为了将可学习的特征幅度分配到感受野中的位置,DCNv2 引入了调制可变形模块:

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

DCNv2 卷积函数来自 论文,符号已修订以匹配 DCN 论文中的符号。

对于位置 p0,偏移量 △pn 及其幅度 △mn 通过应用于相同输入特征图的单独卷积层进行学习。

DCNv2 通过为每个 (i,j)-th bin 添加一个可学习的幅度 △mij 来修订可变形 RoI 池化。

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

DCNv2 池化函数来自 论文,符号已修订以匹配 DCN 论文中的符号。

DCNv2 还扩展了变形卷积层的使用,以替代 ResNet-50 中 conv3 到 conv5 阶段的常规卷积层。

3. DCNv3

为了减少参数大小和内存复杂度, DCNv3 对卷积核结构进行了以下调整。

  1. 灵感来自于深度可分离卷积(Chollet,2017)

深度可分离卷积将传统卷积解耦为:1. 深度卷积:每个输入特征通道与一个滤波器单独进行卷积;2. 点卷积:在通道上应用 1x1 卷积。

作者提出将特征幅度 m 设为深度卷积部分,将投影权重 w 在网格中的位置共享作为点卷积部分。

2. 灵感来自于组卷积(Krizhevsky,Sutskever 和 Hinton,2012)

组卷积:将输入通道和输出通道拆分为多个组,并对每个组分别应用卷积。

DCNv3(Wang 等,2023)提出将卷积拆分为 G 组,每组具有单独的偏移量 △pgn 和特征幅度 △mgn

因此,DCNv3 被表述为:

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

DCNv3 卷积功能来自于论文,符号修订以匹配 DCN 论文中的符号。

其中 G 是卷积组的总数,wg 与位置无关,△mgn 通过 softmax 函数进行标准化,使得网格 R 上的和为 1。

性能

迄今为止,基于 DCNv3 的 InternImage 在检测和分割等多个下游任务中表现优越,如下表所示,以及 papers with code 上的排行榜。有关更详细的比较,请参考原论文。

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

COCO val2017 上的目标检测和实例分割性能。FLOPs 是用 1280×800 输入测量的。AP’ 和 AP’ 分别表示框 AP 和掩码 AP。“MS”代表多尺度训练。来源于论文

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

来自paperswithcode.com的目标检测排行榜截图。

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

来自paperswithcode.com的语义分割排行榜截图。

摘要

在这篇文章中,我们回顾了常规卷积网络的核结构及其最新改进,包括可变形卷积网络(DCN)及两个更新版本:DCNv2 和 DCNv3。我们讨论了传统结构的局限性,并突出了在前一版本基础上的创新进展。欲深入了解这些模型,请参阅参考文献中的论文。

致谢

特别感谢Kenneth Leung,他激发了我创作这篇文章的灵感,并分享了惊人的想法。对 Kenneth、Melissa Han和 Annie Liao 致以深深的谢意,他们对改进这篇文章做出了贡献。你们的深刻建议和建设性反馈显著提升了内容的质量和深度。

参考文献

Dai, J., Qi, H., Xiong, Y., Li, Y., Zhang, G., Hu, H. 和 Wei, Y. (n.d.). 可变形卷积网络。[在线] 可在:arxiv.org/pdf/1703.06211v3.pdf.

‌Zhu, X., Hu, H., Lin, S. 和 Dai, J. (n.d.). Deformable ConvNets v2: 更可变形,效果更佳。[在线] 可在:arxiv.org/pdf/1811.11168.pdf.

‌Wang, W., Dai, J., Chen, Z., Huang, Z., Li, Z., Zhu, X., Hu, X., Lu, T., Lu, L., Li, H., Wang, X. 和 Qiao, Y. (n.d.). InternImage: 利用可变形卷积探索大规模视觉基础模型。[在线] 可在:arxiv.org/pdf/2211.05778.pdf [访问日期 2023 年 7 月 31 日]。

Chollet, F. (n.d.). Xception: 深度学习中的深度可分离卷积。[在线] 可在:arxiv.org/pdf/1610.02357.pdf.

‌Krizhevsky, A., Sutskever, I. 和 Hinton, G.E. (2012). 使用深度卷积神经网络的 ImageNet 分类。ACM 通讯, 60(6), 页 84–90. doi:https://doi.org/10.1145/3065386.

Dai, J., Li, Y., He, K. 和 Sun, J. (n.d.). R-FCN: 基于区域的全卷积网络进行物体检测。[在线] 可在:arxiv.org/pdf/1605.06409v2.pdf.

数据质量的层次

原文:towardsdatascience.com/layers-of-data-quality-320bf3770db5

解决数据问题的地点和方法

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

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

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

图片来源:斯蒂芬·道森Unsplash

随着对生成性人工智能和大型语言模型的兴趣激增,数据质量也重新受到关注。并不是说这个领域需要太多帮助:像Monte CarloSodaBigeyeSiffletGreat Expectationsdbt Labs等公司一直在开发各种解决方案,从专有的到开源的。虽然这些解决方案中的一些是直接竞争对手,但它们并不全是解决相同的问题。例如,定义一个明确的dbt 测试以确保某一列包含唯一值,与对指标进行异常检测(例如,你的 dim_orders 过程一天生成了 500,000 条记录,而通常是 50,000 条)的情况是非常不同的。数据可能以各种显著而复杂的方式失败。

你可能听说过数据质量的维度;我特别喜欢理查德·法恩沃斯的见解¹,但一个简单的谷歌搜索会得到数十种不同的看法。然而,核心思想是数据在某些方面可能是“正确的”,但在其他方面却是错误的。如果你的数据是正确的但却迟到,它还有价值吗?如果这些数据从客观上来看是错误的,但却是一致的²呢?这是数据产品管理中的一个重要方面,以及识别你的利益相关者的优先事项。

很多人关注数据的格式错误、缺失、延迟、不完整等问题,却很少关注数据质量问题的根本原因。我们花费了过多的时间测试和观察数据本身,而不是寻求改进产生、转化和使用这些数据的系统。我想深入探讨这些数据质量问题的“层面”、解决方案以及应该参与解决这些问题的团队。

第 1 层:数据生产

所有数据都来源于某处,而源头通常是数据质量问题的根本原因。遵循“垃圾进,垃圾出”的原则,你无法从糟糕的源系统数据中制造出有用的数据产品。

这一层有三个基本的数据质量问题来源:模式漂移、语义漂移以及系统可用性和可靠性。它们都非常重要,但会导致不同的数据质量故障。更重要的是,它们需要不同的解决方案,通常不同的团队需要参与解决这些问题。

模式变化在许多方面是最容易识别和解决的。如果产品工程师(无论是内部还是通过供应商)更改了你使用的表的模式,你的下游流程可能会中断。由于 SaaS API 遵循更成熟的变更管理协议,这种情况在 SaaS API 中较少出现。不过,这并不是对开发者的贬低;通常,这些内部团队甚至不知道下游团队正在使用他们的数据资产。打破 ETL 流程的模式变化通常是组织沟通不畅的症状。

语义漂移更具隐蔽性,对整个企业的影响也更广泛。如果你的开发团队更改了某个字段的度量单位怎么办?或者如果他们更改了通过下拉菜单填充字段的值怎么办?你的产品并不是唯一暴露的东西。像 SFDC、Zuora、NetSuite 和 Zendesk 这样的企业系统的运营团队也可能更改他们使用操作系统的方式,这可能会产生影响。数据质量问题也可能完全是偶然的;我想我们都见过因错别字而输入了数十亿美元交易的可怜销售代表。更有趣的是,你可能会遇到模式变化和语义漂移的结合;例如,is_enterprise_customer 替代了 customer_type 字段中的有效值‘Enterprise’。与模式变化一样,沟通是缺失的一环。

最终,源系统的可用性和可靠性也是一个问题。如果源系统宕机,数据可能在一段时间内无法生成。根据架构的不同,数据可能仍会生成,但用于检索的端点可能会宕机。在这些情况下,数据质量故障与模式变化和语义漂移的性质完全不同。

Chad Sanderson 一直是 数据契约³ 概念的倡导者:数据从(通常是)操作系统发出的正式、程序化强制定义。数据契约是解决模式变化和某些类型语义漂移的良好起点。请注意,数据契约不涉及 SaaS 运维团队;他们需要一个单独的变更管理系统。

产品团队熟悉如 DatadogSplunk 等可观测性和监控解决方案。SaaS 供应商通常有状态门户,并且一些提供服务中断时的通知。挑战在于,这些系统故障经常不会超出其开发和/或运维团队,即使它们对下游数据团队至关重要。创建良好的流程以在整个组织中沟通故障和问题与提供我们初始可见性的可观测性系统同样重要。

干预措施:数据契约、通信渠道、变更管理、系统可观测性和监控以及向下游消费者的通信

首选团队:产品工程师、SaaS 系统操作员(例如,AR 专家、客户经理)、平台工程师、平台操作员(例如,SaaS 管理员、SaaS 商业分析师)

第 2 层:数据处理 — 提取/转换

假设源系统数据是原始的,或者至少足够干净以供使用。仍然有很多问题可能发生。这次,我们面临三方面的挑战:开发中的逻辑错误、低韧性设计和平台稳定性。

在开发数据管道时,有时我们会出现错误。也许 Airflow 任务没有建立正确的依赖关系,或者分析工程师在连接操作中犯了错误。具体错误的结果可能是管道完全崩溃(通常以戏剧性和火爆的方式);然而,微妙的逻辑错误可能在系统中存在多年,可能会时常轻微影响指标,或者偶尔以重大方式影响指标。

在需求收集过程中,了解利益相关者的优先级至关重要。在某些情况下,如果数据缺失或存在某种质量问题,管道失败是至关重要的。在其他情况下,交付速度比绝对准确性更重要。另一方面,显示部分或不完整的数据是否可以,还是只有在数据经过处理后的某个时间段才展示所有数据?数据产品团队和利益相关者之间的对齐不一致可能意味着我们没有满足期望。

然后,还有一个现实问题,与产品和 SaaS 解决方案一样,我们的数据管道实际上运行在平台上。如果 Snowflake 宕机怎么办?如果 BigQuery 或 EMR 中有影响资源分配的错误怎么办?这些情况会发生,尽管平台团队可能知道,但下游团队仍存在沟通/可见性差距。

解决方案虽然丰富,但实施起来并不一定容易。像单元测试和集成测试这样的策略对在生产前捕捉错误至关重要。数据领域的工具仍然滞后于更广泛的软件工程生态系统,数据团队文化也在追赶中。

另一方面,许多适用于源系统的可观察性解决方案也适用于数据处理系统。然而,关键点在于确保这种可见性在整个组织中共享。可以通过通知渠道或仪表板进行沟通,但无论采取何种方式,我们都需要确保受影响的团队知道发生了什么。

同样值得指出的是,数据生产者引起的相同问题也适用于核心资产开发者;他们的消费者,如分析师、数据科学家和 BI 开发者,也同样容易受到模式变化、语义漂移和系统(ETL)故障的影响。

干预措施:单元测试、集成测试、清晰的设计文档、沟通渠道、变更管理

首选团队:数据工程师、分析工程师、数据平台工程师

第三层:数据消费 — 分析、人工智能和机器学习

在数据和信息之间的最后一公里,仍然有很多出错的空间。在这里,问题更多的是关于数据如何被理解和使用,而不是如何处理、生产和转化数据。具体来说,我想关注应用技术中的错误和对数据本身的误解。

统计学、机器学习和人工智能是复杂的。真的非常复杂。在选择模型时,有很多因素需要考虑:变量是连续的还是离散的、数据分布、异方差性、样本大小等,甚至还有数十种其他因素。即使你做出了所有正确的决策,也可能会在你的机器学习库中出现 obscure 实现错误。

数据使用者也可能不了解上游数据的背景。也许他们没有查看数据目录,或者根本没有数据目录。熟悉数据资产是关键,但并不总是足以避免这些错误,并最终得出不正确的结论。

此外,无法保证业务中的某个人会正确解读仪表板、分析或报告。这也不一定是出于恶意。大多数数据使用者在数据领域之外有其他工作。数据团队通常深陷细节,以至于未能理解哪些指标可能不够明确。

你可能已经注意到这里的主题是以人为本的解决方案。被聘用从事高级分析和机器学习角色的人员需要接受正确的培训和积累经验才能成功。至于在既有资产基础上创建产品,数据目录甚至仅仅与数据资产拥有者的对话也可以提供巨大帮助。最后,确保像仪表板和报告这样的文档清晰至关重要,而温馨交接和利益相关者教育则更为理想。

干预措施:数据目录、仪表板标签、利益相关者教育、从业者教育和培训

主要团队:数据分析师、数据科学家、BI 开发人员、机器学习工程师

结论

虽然认识到数据可能出现错误的不同方式(维度)很重要,但同样重要的是理解数据错误的具体方式和位置(层次)。我们讨论了源头错误(层次 1)、数据处理过程中的错误(层次 2)以及使用过程中的错误(层次 3)。我们还讨论了干预措施以及哪些团队离问题和解决方案最近。

数据质量对于从数据中获得价值至关重要,无论是通过分析、自动化还是外部数据产品。数据质量维度帮助我们识别数据在哪些方面存在问题;它是否过时?是否有误?但同样重要的是理解数据为何出现问题。了解“为何”可以为我们提供解决方案的洞察,无论是立即纠正还是长期的系统性修正。

¹理查德·法恩沃斯。 (2020 年 6 月 28 日)。 数据质量的六个维度——以及如何应对它们towardsdatascience.com/the-six-dimensions-of-data-quality-and-how-to-deal-with-them-bdcf9a3dba71

²本·斯坦西尔。 (2023 年 6 月 9 日)。 我只想知道有什么不同benn.substack.com/p/all-i-want-is-to-know-whats-different

³查德·桑德森。 (2023 年 1 月 25 日)。 仓库的数据合同dataproducts.substack.com/p/data-contracts-for-the-warehouse

懒惰评估使用递归 Python 生成器

原文:towardsdatascience.com/lazy-evaluation-using-recursive-python-generators-9ee6af0dd803

递归函数可以使用*“懒惰评估”*吗?——是的,它们可以——通过使用 Python 的生成器函数!

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

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

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

生成自稳定扩散

我们都熟悉 Python 的生成器及其所有优点。但是,如果我告诉你我们可以通过将生成器与递归结合起来,使它们变得更好呢?所以,让我们看看如何利用它们来实现*“懒惰递归”*,并提升我们在 Python 中使用生成器的效果!

为什么还要费心?

在我们进入代码之前,先问自己*“为什么还要费心?我们真的需要递归生成器吗?”*答案是……这要视情况而定。自然,递归生成器将会兼具生成器和普通递归函数的优缺点。

对于生成器而言,使用它们的首要原因是*“懒惰”*评估——即一次计算一个元素,而不是一次计算全部。至于递归,它对某些算法或问题的解决方式是自然的,例如树的遍历。

因此,递归生成器适合的情况自然是递归算法,这些算法可能处理大量数据或元素,因此如果*“急切”*地运行,会消耗大量内存。

基本示例

现在我们知道了为什么我们会使用递归生成器,让我们来看一个*“简单”的例子,以理解如何*编写一个递归生成器:

这个简短的函数——正如其名称所示——产生连续的二进制数字。当调用时,它首先简单地生成 "1",然后进行递归调用。递归调用也生成 "1",但这会作为 prefix 传递给之前的非递归调用。在计算出前缀后,非递归调用生成两个值 "10""11"。之后,递归调用继续执行,进行另一个递归调用,深入一层,循环继续——前缀向上冒泡,因此外部帧总是先以 "0" 结束,然后是 "1"

现在,如果我们运行它,我们会得到:

当涉及递归时,仅仅解释代码并不足以真正理解发生了什么。所以,如果你不确定 binary_counter 实际上是如何工作的,那么我们来逐步演练一下:

上面的修改版本添加了一个 depth 参数和几个打印语句,以帮助演示代码的作用。如果我们现在调用这段代码,我们会得到以下结果:

我希望这能让事情变得更清晰一些,如果不行,可以考虑手动逐步演练,或者使用你选择的 IDE 中的调试器,以便实时查看堆栈帧和变量。

你可能也会问,“以这种方式计算二进制数字有什么意义?” —— 答案是,没有什么好的理由。确实有更好、更易读的方法来做这件事,但我认为它很好地演示了这个概念。话虽如此,让我们现在看看更多如何使用递归生成器的有用示例…

将其发挥到最佳使用

在递归的例子中,显而易见的候选者是各种数学函数,或者——如这里所示——组合数学,具体来说是 幂集

这里的函数使用了类似于之前二进制计数器的流程。为了更好地理解它,我们可以将递归部分翻译为:

  • 对于更小的幂集 (sequence[1:]) 中的每个结果…

  • … 返回未使用的值 ([sequence[0]]) + 结果 (item)

  • … 然后仅返回结果 (item)

虽然数学函数可以通过递归很好的实现,但它们并不是我们日常使用的内容,所以现在我们来看看其他不同的东西:

上面的 accumulate 函数计算其列表参数的累积总和(和)。虽然上面的代码可以工作,但我不建议在实际中使用它,因为你可以且应该使用以下代码:

在讨论 itertools 的话题时,我们也来看看如何重新实现其他常见函数:

flatten 函数可以用来展开嵌套列表(或其他可迭代对象)。我在这里展示这个函数是因为它使用了与之前不同的流程——它利用 try/ except 来分离基本/非递归部分和递归代码。

然而,如果需要,也可以在没有 try/ except 的情况下重写:

说到递归时,我们显然要展示递归数据结构的示例,在这个案例中是二叉树:

上述代码实现了一个二叉树,包括__iter__方法中的递归生成器。inorder函数中也实现了相同的功能,使递归调用更加清晰。

为了展示上述代码的使用方法,让我们创建一个简单的树结构:

与(二叉)树的遍历类似,我们也可以在遍历 JSON 时使用递归生成器:

以这种方式遍历 JSON 可能是实用的,如果你处理的是非常大的数据,一次性加载会消耗大量内存。

到现在为止,你可能已经掌握了这些奇怪的生成器是如何工作的,但我们还是看看调用上述代码时会发生什么:

最后,另一个常常递归遍历的树状数据结构是文件树:

在这里我们实现了get_paths函数,该函数递归地生成指定路径中的所有文件。话虽如此,对于这个任务,使用内置的path.rglob("*")会更好,因为它也返回生成器。

此外,虽然在这个实例中不太有用,但值得注意的是,send()函数也可以与递归生成器一起使用。因此,上述函数的另一种实现方式:

这种生成器的风格在你需要控制递归或与协程通信时会很有用。

结论

我认为本文中的示例展示了许多可以递归表达的优雅解决方案。然而,优雅并不总是意味着更好。通常,使用不那么*“优雅”*或简洁的解决方案会产生更具可读性和普遍更好的代码。

所以,我们不要试图*“强行”*将递归生成器融入代码中,应该只在适当的情况下使用——也就是说——在实现从延迟评估中受益的递归函数时。

这篇文章最初发布于 martinheinz.dev

成为会员并阅读 Medium 上的每一个故事。你的会员费直接支持我和你阅读的其他作家。 你还将获得 Medium 上每个故事的完全访问权限。

[## 使用我的推荐链接加入 Medium - Martin Heinz

阅读 Martin Heinz(以及 Medium 上的其他数千名作家)的每一个故事。你的会员费直接支持…

medium.com](https://medium.com/@martin.heinz/membership?source=post_page-----9ee6af0dd803--------------------------------)

你可能还会喜欢…

## 用 Python 入门 Google API

使用 Python 和 Google API 来自动化你在 Gmail、Google Drive、日历等上的所有操作的速成课程:

[towardsdatascience.com 在这里 [## 你可能没听说过的 Python 魔法方法]

你可能不知道的许多 Python 魔法方法 — 让我们来了解它们的功能以及如何使用它们…

towardsdatascience.com

我们应该了解的重要 MySQL 数据定义语言(DDL)命令,用于管理我们的表

原文:towardsdatascience.com/learn-common-database-managing-commands-as-a-data-engineer-4d199cfb15ae

作为数据工程师,学习常用的数据库管理命令

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

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

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

图片由 geralt 提供于 Pixabay

作为数据工程师,检查和更新表的模式是我们的日常工作。虽然网上已经有很多教程,但很少有教程关注应该遵循的约定。SQL 非常灵活,可以以“稳健”的方式工作。你可以使用小写或大写的查询,并以任何你想要的方式命名数据库/表/列/索引/视图。然而,代价是可读性降低,维护变得困难,因为不同的人可能会以不同的方式编写 SQL 查询。

在这篇文章中,我们将介绍一些用于管理 MySQL 表模式的常用命令,重点讲解每个操作的约定和最佳实践。它可以作为新数据工程师的手册(需进行必要的调整)。

准备工作

我们将使用 Docker 启动一个 MySQL 8 容器,它将作为这篇文章的 MySQL 服务器:

# Create a volume to persist the data.
$ docker volume create mysql8-data

# Create the container for MySQL.
$ docker run --name mysql8 -d -e MYSQL_ROOT_PASSWORD=root -p 13306:3306 -v mysql8-data:/var/lib/mysql mysql:8

# Connect to the local MySQL server in Docker.
$ docker exec -it mysql8 mysql -u root -proot

mysql> SELECT VERSION();
+-----------+
| VERSION() |
+-----------+
| 8.0.31    |
+-----------+
1 row in set (0.00 sec)

我们将在这个 MySQL 服务器中创建我们的数据库(在 MySQL 中也称为模式)和表。首先,让我们创建一个数据库来存储我们的虚拟数据:

CREATE DATABASE sales;

数据库名称应具有描述性、简洁明了,并且不包含特殊字符,除了下划线之外。它最好是小写的,以便与 MySQL 关键字区分开来。相同的命名约定也适用于表名和列名。

在这篇文章中,我们将使用 DBeaver 来编写查询并查看表数据。

创建表

现在让我们创建第一个表,它将存储客户数据。

CREATE TABLE `sales`.`customers` (
  `id` SMALLINT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(50) NOT NULL,
  `job` VARCHAR(50) DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `ix_name` (`name`)
);

你可以使用单数或复数表名。我更喜欢使用复数,因为表可以被视为数据记录的容器。

可以通过以下命令查找现有表的数据定义语言(DDL)查询:

SHOW CREATE TABLE `sales`.`customers`;

推荐在编写查询时指定模式名称,以便更好地支持自动补全。

默认情况下,MySQL 对数据库名称、表名称和别名是区分大小写的。然而,对列名称不区分大小写。因此,列名称的命名可以非常灵活。然而,我们应该遵循一些相同数据库的命名约定。无论你使用驼峰命名还是蛇形命名,你只需要保持一致。然而,你可能会根据你的后端编程语言有一些偏好。例如,作为 Python 开发者,我们更倾向于使用蛇形命名。

此外,如你所见,我们将前缀ix(表示索引)加到索引名称上。我们通常应该避免给列名称加前缀,以使查询更简洁。然而,我们应该为索引或约束提供前缀,以便在某些错误中更具指示性。我们很少需要显式引用索引或约束,因此简洁不是问题。

一些常用的前缀是:

  • ix用于索引。

  • fk用于外键约束。

  • uq用于唯一键约束。

此外,还有一些关于如何命名索引或约束的约定:

  • 索引:ix_%(column_0_label)s

  • 外键:fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s

  • 唯一键:uq_*%(table_name)s*_*%(column_0_name)s*

我们将在本帖中稍后介绍其中的一些。

重命名或复制一个表

我们可以使用RENAME命令重命名一个表:

RENAME TABLE sales.customers TO sales.customer  -- Not executed as I prefer plural
;

如果你想将一个表从一个模式复制到另一个模式,你需要分两步进行。例如,让我们创建一个新的customers_data模式,并将customers表复制到那里。

CREATE DATABASE customers_data;

CREATE TABLE customers_data.customers LIKE sales.customers;
INSERT INTO customers_data.customers
SELECT * FROM sales.customers
;

这样,旧表的数据类型和索引将保留在新表中。如果你按照下面所示使用CREATE TABLE … SELECT…,则数据类型(实际上会被推断)和索引会丢失,这在几乎所有情况下都是不希望的:

CREATE TABLE customers_data.customers_copy_direct
SELECT * FROM sales.customers 
;

你可以使用SHOW CREATE TABLE命令检查新创建表的模式。

如果你需要将一个表从一个数据库(不同的主机或端口)移动到另一个数据库,你可以将表导出到一个 SQL 文件中,然后在另一个数据库中加载:

mysqldump -h HOST_1 -P PORT_1 -u USERNAME_1 -p \
    --single-transaction --skip-triggers --skip-column-statistics \
    SCHEMA_1 TABLE_NAME > TABLE_NAME.sql
mysql -h HOST_2 -P PORT_2 -u USERNAME_2 -p SCHEMA_2 < TABLE_NAME.sql

安装了 MySQL 客户端后,可以使用mysqldump

sudo apt-get update
sudo apt-get install mysql-client

两个数据库的主机可以相同。在这种情况下,端口会不同。

注意mysqldump指定的选项,这在大多数情况下是必要的。特别是,使用--single-transaction时,表在导出时不会被锁定。

添加/删除/更改列

为了演示命令,让我们执行以下操作。这些操作可能没有太大意义,重点是使用的命令:

  • 使用DROP删除name列,

  • 使用ADD重新添加name列,

  • 使用MODIFY更改列的数据类型;

ALTER TABLE `sales`.`customers`
DROP `name`,
ADD `name` VARCHAR(50) NOT NULL AFTER `id`,
MODIFY `job` VARCHAR(100) DEFAULT ''
;

注意,我们可以使用 AFTER column_name 关键字来改变列的顺序。如果列应该被改为第一个,则需要使用 FIRST 关键字,而不是 AFTER column_name

例如,让我们将 name 列改为第一个:

ALTER TABLE `sales`.`customers`
MODIFY `name` VARCHAR(50) NOT NULL FIRST
;

重命名列

我们可以仅使用 RENAME COLUMN A TO B 来重命名列而不更改数据类型:

ALTER TABLE sales.customers 
RENAME COLUMN `job` TO `address`
;

注意,COLUMN 关键字可以在 ADDDROPMODIFY 以及即将介绍的 CHANGE 命令中省略,但在 RENAME 命令中不能省略。

我们还可以使用 CHANGE 命令重命名列并更改数据类型。让我们将 name 改为 username 并将长度改为 100:

ALTER TABLE sales.customers 
CHANGE `name` `username` VARCHAR(100) NOT NULL
;

使用外键

让我们创建两个新表来演示外键的使用。一个新的 products 表将存储产品信息,orders 表将存储客户的订单:

CREATE TABLE `sales`.`products` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(50) NOT NULL,
  `price` DECIMAL(12,2),
  PRIMARY KEY (`id`),
  KEY `ix_name` (`name`),
  KEY `ix_price` (`price`)
);

CREATE TABLE `sales`.`orders` (
  `customer_id` SMALLINT NOT NULL,
  `product_id` INT NOT NULL,
  `quantity` SMALLINT NOT NULL,
  PRIMARY KEY (`customer_id`, `product_id`),
  KEY `ix_product_id` (`product_id`),
  KEY `ix_quantity` (`quantity`),
  CONSTRAINT `fk_orders_customer_id_customers` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`) ON DELETE CASCADE,
  CONSTRAINT `fk_orders_product_id_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE
);

注意,customersproducts 表中的 id 列没有前缀,但在 orders 表中有前缀。这是因为那里的 ID 有两个,一个用于客户,另一个用于产品。

如果检查 id 列的数据类型,你会发现它们与 customersproducts 表中的数据类型相同。这是添加外键的要求,因为列的数据类型必须在当前表和引用表中相同。

使用 customer_idproduct_id 创建了一个复合主键。注意,我们需要为 product_id 创建一个单独的索引,但不需要为 customer_id 创建,因为它被复合主键覆盖,因为它是复合键中的第一个。

还要注意外键约束的命名约定,遵循了此链接中介绍的命名约定。

要检查表的索引,我们可以运行以下两个查询之一:

SHOW INDEX FROM sales.orders;

SELECT
  s.TABLE_SCHEMA,
  s.TABLE_NAME,
  s.INDEX_NAME,
  s.COLUMN_NAME,
  s.SEQ_IN_INDEX 
FROM INFORMATION_SCHEMA.STATISTICS s
WHERE 1
  AND s.TABLE_SCHEMA = 'sales'
  AND s.TABLE_NAME = 'orders'
;

注意,上述查询不会返回外键。如果需要检查外键,我们需要运行以下查询:

SELECT 
  TABLE_SCHEMA,
  TABLE_NAME,
  CONSTRAINT_NAME,
  COLUMN_NAME,
  REFERENCED_TABLE_SCHEMA,
  REFERENCED_TABLE_NAME,
  REFERENCED_COLUMN_NAME
FROM
  INFORMATION_SCHEMA.KEY_COLUMN_USAGE
WHERE 1
  AND TABLE_SCHEMA = 'sales'
  AND TABLE_NAME = 'orders'
;

注意,两个特殊的表 INFORMATION_SCHEMA.STATISTICSINFORMATION_SCHEMA.KEY_COLUMN_USAGE 是系统表,通常以大写字母引用。

添加和删除索引及约束

让我们演示如何添加和删除索引和约束。我们不能修改索引或约束,因为一旦条件改变,索引/约束必须重新生成。

首先,让我们删除 orders 表的主键、索引和外键:

ALTER TABLE sales.orders 
DROP PRIMARY KEY,
DROP INDEX `ix_product_id`,
DROP INDEX `ix_quantity`,
DROP FOREIGN KEY `fk_orders_customer_id_customers`,
DROP FOREIGN KEY `fk_orders_product_id_products`
;

注意我们指定如何删除外键的方式。应该使用 DROP FOREIGN KEY …,而不是 DROP CONSTRAINT …

现在让我们将它们添加回来:

ALTER TABLE sales.orders 
ADD PRIMARY KEY (`customer_id`, `product_id`),
ADD KEY `ix_product_id` (`product_id`),
ADD KEY `ix_quantity` (`quantity`),
ADD CONSTRAINT `fk_orders_customer_id_customers` FOREIGN KEY (`customer_id`) REFERENCES `customers` (`id`) ON DELETE CASCADE,
ADD CONSTRAINT `fk_orders_product_id_products` FOREIGN KEY (`product_id`) REFERENCES `products` (`id`) ON DELETE CASCADE
;

语法类似于上面的 DDL 查询。

创建或更新视图

MySQL 视图与表的工作方式相同,并且被视为表。视图通常包含一个或多个表中的一些选定列,基于一些过滤条件。它可以用来快速查看一个或多个表中的特定数据,而无需编写 JOINWHERE 条件。

让我们为 orders 表创建一个视图,以便可以直接获取订单的客户和产品详情:

CREATE OR REPLACE VIEW sales.orders_with_details AS
SELECT
  o.customer_id,
  c.username,
  c.address,
  o.product_id,
  p.name,
  p.price,
  o.quantity,
  p.price * o.quantity AS total
FROM sales.orders o 
JOIN sales.customers c 
  ON c.id = o.customer_id 
JOIN sales.products p 
  ON p.id = o.product_id 
;

SELECT * FROM sales.orders_with_details;

视图的名称应能指示其用途。在这种情况下,orders_with_detailsorders_view 更好,因为前者更能说明视图中包含的内容。

编写 SQL 查询的标准

我们应以易于阅读的方式编写 SQL 查询。虽然没有严格的标准,但遵循以下规则将使你的查询更易于阅读和维护:

  • 将所有 SQL 关键字写成大写字母。

  • 将所有数据库名称、列名和别名写成小写字母。

  • 为你的表提供标准缩写作为别名。例如,products => pproduct_attributes => pa 等。不要使用任意的表别名,因为这会使查询变得更加难以阅读。

  • SELECTFROMJOINWHEREGROUP BYORDER BY 等语句另起一行。

  • 每个 ONAND 条件应另起一行。

  • 相同的格式化标准适用于嵌套查询。

你可以在 DBeaver 或 VS code(配合一些 SQL 扩展)中自动格式化 SQL 查询。不过,格式化效果并不完美,我们通常需要根据上述规则进行一些手动调整。

在这篇文章中,我们介绍了一些用于管理 MySQL 表模式的常见且实用的命令。我们涵盖了如何创建和更新数据库、表、列、索引和视图,并重点介绍了每个操作的约定和最佳实践,这些可以作为新数据工程师的起始指导。

相关文章:

通过“刻意练习”学习数据科学(或任何技能)

原文:towardsdatascience.com/learn-data-science-or-any-skills-with-deliberate-practice-47eb21bd2c8

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

图片由Brett Jordan提供,来源于Unsplash

回顾我做对了什么

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

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

最近我读了一本书,名为顶尖:来自新科学的专业秘密,作者是 Anders Ericsson 和 Robert Pool。这本书挑战了“专业来自天赋”的常见神话。相反,通过在不同领域的众多例子,它证明了顶级表现是可以通过一种称为“刻意练习”的技巧来实现的,技能也可以通过这种技巧来学习。在书中,作者提供了各种技能的例子,如音乐表演、战时飞机战斗以及像国际象棋和围棋这样的策略游戏。阅读这本书让我感到似曾相识,因为我在反思自己快速掌握数据科学技能的学习历程时有了类似的感觉。

在我之前的文章中,我分享了我如何成为数据科学家的经历:

## 我是如何成为一名数据科学家的

我的经济学博士之旅

towardsdatascience.com

以及我遵循的七个原则,以成为更好的数据科学家:

## 我遵循的七个原则,以成为更好的数据科学家

设定我的北极星

towardsdatascience.com

在这篇文章中,我想分享在Peak中讨论的作为刻意练习的技巧,并通过我自己的学习历程来说明它们。希望对那些希望学习数据科学或其他技能的人有所帮助。

刻意练习与幼稚练习

首先,什么是刻意练习?与幼稚练习相比,幼稚练习本质上是重复做某事,并希望重复本身能够完成工作。例如,每天弹奏同一首歌来成为吉他大师,或者每天以相同的速度跑同一条小径来为马拉松做准备。确实,重复带来了熟悉感。你可能会在弹奏一首歌方面成为专家,通过每日跑步减轻体重。然而,对一首歌或小径的专业知识并不会使你成为顶级吉他演奏者或跑者。毫无疑问,我们有爱好,只是想做些有趣的事情,而不是为了竞争或谋生,但对于那些你确实希望提高并最终成为顶尖表现者的技能,刻意练习是实现目标的更好方法。刻意练习是在练习过程中有目的、深思熟虑和专注。具体来说,它具有以下特点:

  • 刻意练习有明确、具体的目标。例如,如果你的目标是成为马拉松中的顶级跑者,你需要设定具体的目标,比如你应该多快完成马拉松,并制定计划以实现这一目标。

  • 刻意练习是专注的。在练习时,你必须全神贯注地意识到自己在做什么,而不是处于自动驾驶模式。如果你在完成任务时发现自己分心,很可能是因为你没有认真对待目标,或者任务太容易完成。做简单的任务不会帮助你提高技能。

  • 刻意练习涉及反馈。你必须不断接受评估,以找出需要改进的地方。在大多数情况下,你必须跟随导师,他们会在练习中提供即时反馈。导师不一定非要是一个人。也可以是精心设计的教程,当找不到导师时。举个例子,这本书解释了熟练的作家如何通过将自己的文章与他们期望发表的刊物进行对比来提高写作能力。此外,随着技能的提升,更换导师也是重要的。例如,在一篇文章被理想的刊物接受后,作家会在设定改进词汇、简洁性、逻辑结构等目标时改变练习方法。

  • 刻意练习要求你走出舒适区。除非你的目标是保持当前的技能水平,否则如果你希望每次练习都有所提升,你应该在练习时让自己感到稍微“不舒服”。始终挑战自己跑得更快、表现更好、写得更清晰、执行得更快。走出舒适区可以拓展你的边界并打开机会。

然而,刻意练习可能并不适用于所有领域。如上所述,该领域必须有明确且可衡量的标准来定义顶尖表现者的技能。

这本书提供了许多例子,说明了来自不同领域的顶尖表现者是如何通过刻意练习达到他们的水平的。在这篇博客文章中,我想分享一个我如何从零开始成功发展数据科学技能的例子。

我有具体且明确的目标。

设定明确的目标是我数据科学旅程的第一步。我从来不是单纯地“学习数据科学”,而是总是设立具体的目标,例如:

  • 精通 Python 编程,以便我能更有效地为我的研究项目进行数据分析;

  • 理解统计概念和机器学习算法,以便我知道为什么某些模型在某些情况下效果更好,以及我应该为我的研究项目选择哪个模型;

  • 建立一个数据科学作品集,以便我能更容易地获得面试机会。

  • 学习这些模型,以便我可以用它们解决工作中的问题。

这些目标作为指引,帮助我有效地导航广阔的数据科学领域,它们随着我面临更大挑战和不同环境而不断演变。

建议:在开始之前设立目标是很重要的。了解你希望如何运用这些技能将指导你学习什么以及如何学习。如果你是学生,你可能会学习某些知识以通过考试;如果你在工作,你可能会学习一些东西以便为正在进行的项目做好准备,或者保持在你所在行业的最新技术前沿。我们需要短期目标和长期目标。任何熟悉时间管理的人都会知道 艾森豪威尔的紧急/重要原则 的概念。我们根据任务的紧急性和重要性对其进行分类。虽然许多人优先处理紧急且重要的任务,但也重要的是不要忽视那些重要但不紧急的任务,这些任务在长期中使我们与众不同。例如,学习和掌握一项新技能。短期目标就像这些紧急且重要的任务,它们能提高我们的效率,但长期目标激励我们持续不断地成长,这在不断变化的世界中尤其宝贵。

我在练习时保持专注。

有了明确的目标后,保持专注变得至关重要。我不再盲目地消费教程和资源,而是结构化了我的学习时间。我每天花固定时间专注地进行在线课程学习。在这些在线课程中,容易误以为因为观看了视频或阅读了文章就理解了内容。我强迫自己去做项目,亲自实践。一开始,我会花费几个小时搜索不同的语法,并在 DataFrame 上试验它们的用法。一个简单的可视化任务需要我很长时间才能完成。然而,你不能仅仅通过复制和粘贴来学习编程语言。我们需要知道它的用途是什么,以及为什么在特定任务中选择这个而不是其他的。通过亲自参与项目,我帮助自己保持专注,深入吸收信息。

建议:在练习时,重要的是要让大脑参与其中。虽然这似乎很明显,但不要假设你会自动保持专注。保持专注比单纯地完成动作需要更多的能量。这类似于跑步——在跑一英里时维持一定的心率比毫无目的地跑步需要更多的能量。在练习过程中,尽量多思考,多问自己一些问题。问自己是否真正理解自己在做什么以及为什么这样做。

我寻求练习中的反馈

在我过去的几篇博客文章中,我强调了参加数据科学训练营在我的旅程中的重要性。

[## 如何从参加数据科学训练营中获益?

讨论了基于个人经验的七个方面的好处

pub.towardsai.net](https://pub.towardsai.net/how-to-benefit-from-attending-a-data-science-bootcamp-289db43e2d7c?source=post_page-----47eb21bd2c8--------------------------------)

我从知识渊博的导师那里获得的反馈是无价的。尽管是通过虚拟方式交流,我定期收到有关讲座问题、作业和项目的反馈。此外,我的小组同学来自不同背景,相互提供反馈。我们从彼此的专长和错误中学习。他们的见解和批评帮助我完善了技术技能,并激发了我以战略性和创造性的方法来解决问题。尽管在参加在线教程时,我会因为完成在线作业而获得分数,但没有指导和即时反馈,我感到很挣扎。建设性的反馈将我的错误转化为成长的机会,加速了我的学习曲线。感谢这个为期 8 周的密集训练营,我得到了指数级的成长。

建议:花时间进行练习和反思固然重要,但也要寻求有经验人士的指导和反馈。这是提高技能的最快、最有效的方法。在学习新事物时,考虑找一个比你更成熟的导师,以便在实践过程中获得即时反馈。如果找不到导师,拥有练习伙伴或加入社区也是有帮助的。

我走出了舒适区

虽然舒适区可以提供安全感,但真正的成长只能通过走出它的边界来实现。对我来说,学习数据科学是走出舒适区的一段旅程。在经济学领域,Stata 是一个熟悉的软件,学者和学生常与 R 一起用于数据分析和建模。然而,随着大数据的日益流行,我认识到需要学习 Python 以提高生产力。为了真正掌握数据科学,我挑战了自己做那些最初看似困难的项目。这让我能够在面对挑战的过程中提高解决问题的能力。数据科学发展迅速。去年新颖且前沿的东西很容易被取代,变成遗留物。在这个领域,我们永远不能自满。走出舒适区不仅扩展了我的技能,还让我具备了接受挑战的勇气和解决问题的自信。

建议:每次走出舒适区都会扩展你的技能并提升你的自信。不要害怕在实践中面对挑战。挑战自己做更复杂的项目,写更简洁的代码,建立更高准确度或更易解释的模型。这是成长过程中最痛苦但也是最有价值的部分。爬坡总是比在平地上绕圈更累。你可以对自己撒谎说绕圈也是练习,但这不会带你到达山顶的美丽风景。

实质上,刻意练习是一种有目的和结构化的技能发展方法。它包括设定具体目标、保持专注的练习、积极寻求和采纳导师的反馈,并通过走出舒适区来挑战自己。这种练习方法将学习从一个被动的过程转变为主动的成长和掌握之旅。在这篇文章中,我分享了我学习数据科学技能的经历,以进一步证明刻意练习的重要性,希望能激励那些有兴趣掌握新技能的人。

感谢阅读!最后,别忘了:

学习离散傅里叶变换(DFT)

原文:towardsdatascience.com/learn-discrete-fourier-transform-dft-9f7a2df4bfe9?source=collection_archive---------1-----------------------#2023-02-08

数学与编码视角

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

·

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

数字信号处理(DSP)是用于操控信号数据的数学方法的计算[1]。在数字信号处理中,最重要的工具之一就是离散傅里叶变换(DFT)。它通常用于生成信号的频域(谱)表示[2]

在这篇文章中,我们将讨论离散傅里叶变换(DFT)的工作原理以及如何实现它以输出信号的频谱。

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

照片由 Pawel Czerwinski 提供,来源于 Unsplash

离散傅里叶变换(DFT)

傅里叶变换是 DFT 的数学基础和谱分解的主要思想,谱分解得出一个信号实际上只是不同频率分量的正弦波之和 [3]。由于我们处理的所有信号数据都是数字形式的,信号是一组时间域中的样本。对这些离散信号进行傅里叶变换可以使用 DFT,它可以用来在时间域和频率域之间来回切换。时间域包含信号的样本,而频率域表示构建信号的正弦波的谱 [4]。下图描述了使用 DFT 和 IDFT 时间域与频率域之间的关系。

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

时间域与频率域之间的关系 [4] [作者提供的图像]

从数学角度来看,如果我们有一个包含 N 个样本的信号(xn),该信号的 DFT 定义为 [5]

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

DFT 方程 [5]

其中:

  • N:样本数量

  • n:当前样本

  • k:当前频率,其中 k ∈ [0, N−1]

  • xn:样本 n 的正弦值

  • Xk:包含幅度和相位信息的 DFT

DFT(Xk)的输出是一个包含复数的数组,这些复数包含了构建输入信号的正弦波的频率、幅度和相位的信息。DFT 数组(Xk)的前半部分包含正频率项,而后半部分包含负频率项。此外,当输入信号仅为实值信号时,前半部分是后半部分频率项的共轭,谱是对称的。因此,在实值信号的情况下,我们只关注前半部分(正频率项) [5]。下图表示当输入样本数量(N)为奇数或偶数时的正频率和负频率项。

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

正频率和负频率项 [5] [作者提供的图像]

每个正弦波的振幅和相位,构成信号时加起来的这些值,可以从复数数组(Xk)中计算得到(Im 和 Re 分别代表复数的虚部和实部)[5]

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

振幅和相位信息可以从这些方程中计算得到 [5]

开始编码:

我们将建立一个基于上述第一个方程计算 DFT 的函数。但首先,我们需要生成一个作为 DFT 输入的信号。我们将生成一个由 3 个正弦波组成的信号,其频率分别为(1,20,10)Hz,振幅分别为(3,1,0.5)。采样率将为每秒 200 个样本。为了生成信号,我使用了一个类 Signal,你可以参考这个 GitHub gist使用此类。你可以轻松生成任何信号,但我使用了这个 Signal 类以获得更多的控制。

注意

  • 我们将在下面编写的 DFT 和 IDFT 函数是根据 MIT 许可证发布的,所有荣誉归这本书的作者。

  • 我们将讨论的这两个函数只是为了理解 DFT 的数学原理以及这种变换的输出。在实际应用中,有更快更高效的算法可以计算傅里叶变换,即快速傅里叶变换(FFT)及其逆变换。

让我们开始吧…

# Import the required packages
import numpy as np
import matplotlib.pyplot as plt
# Generate the three signals using Signal class and its method sine()
signal_1hz = Signal(amplitude=3, frequency=1, sampling_rate=200, duration=2)
sine_1hz = signal_1hz.sine()
signal_20hz = Signal(amplitude=1, frequency=20, sampling_rate=200, duration=2)
sine_20hz = signal_20hz.sine()
signal_10hz = Signal(amplitude=0.5, frequency=10, sampling_rate=200, duration=2)
sine_10hz = signal_10hz.sine()

# Sum the three signals to output the signal we want to analyze
signal = sine_1hz + sine_20hz + sine_10hz

# Plot the signal
plt.plot(signal_1hz.time_axis, signal, 'b')
plt.xlabel('Time [sec]')
plt.ylabel('Amplitude')
plt.title('Sum of three signals')
plt.show()

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

我们使用 Signal 类生成的信号。 [作者提供的图片]

现在我们将建立 DFT 函数,以提供构成我们上面生成的信号的正弦波。请确保仔细阅读下面代码的注释,因为它们有助于你了解每一行的输出。

# Build a function that calculates the discrete Fourier transform
def DFT(signal):
  # Number of samples, 100 samples in our example
  N = len(signal)
  # The samples from 0 to N-1, [0, 1, 2, ..., 199] in our example
  n = np.arange(N)
  # Generate the frequencies, [[0], [1], [2], ..., [199]] in our example
  k = n.reshape((N,1))
  # e is a matrix of complex numbers with a shape of (N, N), (200, 200) in our example
  e = np.exp(-2j * np.pi * k * n / N)
  # dft is a matrix of complex numbers with a shape of (N,), (200,) in our example
  dft = np.dot(e,signal)
  return dft

# Let's use the function
dft = DFT(signal= signal)

# Calculate the amplitude spectrum of the signal
amp = np.abs(dft)

# Generate the frequency axis
N = len(dft)
n = np.arange(N)
T = N/signal_1hz.sampling_rate
freq = n/T

# Plot the spectrum
plt.figure(figsize = (8, 6))
plt.stem(freq, amp, 'b', markerfmt='o', basefmt='b')
plt.xlabel('Frequency [Hz]')
plt.ylabel('DFT Amplitude |X(freq)|')
plt.title('Spectrum of the signal')

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

DFT 函数的输出是信号的频谱。 [作者提供的图片]

x 轴包含构成信号的频率成分。y 轴表示每个频率成分的强度。频谱中最低的频率成分通常称为基频,而振幅最大的频率成分称为主频 [3]。在我们上面的例子中,1Hz 的频率成分是基频和主频。

我们可以注意到频谱在采样率的一半处的对称性(尝试不同的采样率);这通常称为 折叠频率。当记录一个真实世界的信号(f(t)),其 FN 为最高频率分量时,折叠频率绝不应低于 FN,以检索信号的所有信息。这是根据 奈奎斯特-香农定理 [5]

我们可以通过将频谱归一化到输入样本数(N)来获得实际的幅度。但是,当我们只关注频谱的前半部分时,如果输入是实值信号,我们将频谱归一化为 N/2 [5]。下面的代码用于归一化频谱的前半部分 [0, N/2] 并绘制频谱。

# Get the length of one side of frequencies
n_oneside = N//2
# Get the one side frequency
f_oneside = freq[:n_oneside]
# Normalize the amplitude by N/2
one_side_dft = dft[:n_oneside]/n_oneside

# Plot the first half
plt.stem(f_oneside, np.abs(one_side_dft))
plt.xlabel('Freq (Hz)')
plt.ylabel('DFT Amplitude |X(freq)|')
plt.title('The spectrum of the signal after normalization')
plt.show()

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

归一化后的信号频谱。 [作者提供的图片]

离散傅里叶变换(IDFT)

同样地,正如你可以从时域转换到频域,你也可以通过离散傅里叶逆变换将信号从频域转换回时域。这一过程在信号处理中非常有用,当你想要使用 DFT 过滤特定的频率成分,然后使用 IDFT 将信号恢复到其时域时。可以从 Xk 序列中计算 IDFT,按照方程 [5]

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

IDFT 方程 [5]

让我们构建一个可以使用上述方程计算 IDFT 的函数。

def IDFT(dft):
  # Number of frequencies, 200 components in our example
  N = len(dft)
  # The frequencies from 0 to N-1, [0, 1, 2, ..., 199] in our example
  k = np.arange(N)
  # Generate the samples, [[0], [1], [2], ..., [199]] in our example
  n = k.reshape((N,1))
  # If your input was a first half spectrum, 2j should be 1j to retrieve the signal
  e = np.exp(2j * np.pi * k * n / N)
  # dft is a matrix of complex numbers with a shape of (N,), (200,) in our example
  signal = np.dot(e,dft)/N
  return signal

# Apply the Inverse Fourier Transform on the spectrum [dft]
sig = IDFT(dft)

# Generate the time axis from sampling rate and length of dft
N = len(dft)
duration = N/signal_1hz.sampling_rate
time_axis = np.arange(0, 2, 1/200)

# Plot the results of IDFT along with the original signal
plt.plot(time_axis, sig,'b')
plt.plot(time_axis, signal, 'r')
plt.xlabel('Time [sec]')
plt.ylabel('Amplitude')
plt.title('Output of the IDFT')
plt.show()

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

IDFT 函数恢复的信号与原始信号相同。 [作者提供的图片]

DFT 的极限

在样本数量很大的信号情况下,DFT 函数的执行时间会很长,因为需要对信号的所有数据点应用傅里叶变换。幸运的是,已经开发出一种高效的算法来计算信号的 DFT,即快速傅里叶变换(FFT)。该算法将执行复杂度从 O(N²) 降低到仅 O(NlogN),其中 N 为数据的大小。通过使用 FFT 显著降低的计算复杂度使傅里叶变换在工程、科学和数学领域得到广泛应用 [5]

Python 提供了多个功能,用户可以使用 Numpy 或 Scipy Python 包来应用傅里叶变换。下面的代码表示了使用我们之前构建的 DFT 函数、使用 Numpy 包的 FFT [6] 和使用 Scipy 包的 FFT [7] 进行时间执行的比较。使用 Scipy 包中的 FFT 是最快的。

# Import the scipy package
from scipy.fftpack import fft

# Estimate the execution time of DFT using the function we've built
print('Execution time of DFT Function:')
%timeit DFT(signal)
# Estimate the execution time of DFT using FFT from numpy package
print('\nExecution time of FFT using Numpy pacakge:')
%timeit np.fft.fft(signal)
# Estimate the execution time of DFT using FFT from scipy package
print('\nExecution time of FFT using Scipy package:')
%timeit scipy.fftpack.fft(signal)
Execution time of DFT Function:
17.3 ms ± 2.65 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)

Execution time of FFT using Numpy pacakge:
8.72 µs ± 2.2 µs per loop (mean ± std. dev. of 7 runs, 100000 loops each)

Execution time of FFT using Scipy package:
8.27 µs ± 137 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)

结论

  • 我们已经了解了傅里叶变换在信号处理领域的有用性,并且理解了其主要思想。

  • 我们已经指出了离散傅里叶变换的数学视角以及如何对离散信号进行计算。

  • 我们构建了一个使用数学方程计算 DFT 的函数,并将该函数应用于我们使用在上一篇文章中构建的 Signal 类生成的信号。

  • 我们已经了解到 DFT 的输出是一个包含 N 个元素的复数数组,元素数量与输入信号的样本数量相同。这些复数包含了频谱的幅度和相位信息。

  • 我们已经看到,如果输入信号是实值信号,DFT 的输出将对半个采样率对称。这就是我们只关注正频率分量的原因。

  • 已经指出了计算离散傅里叶变换的逆变换(IDFT)的方程。我们也构建了一个函数来计算 IDFT,以从频谱中恢复原始信号。

  • 我们讨论了我们构建的函数的局限性,并且有一个高效的算法可以计算傅里叶变换,即快速傅里叶变换。

参考文献

[1] R. Toulson, R., & Wilmshurst, T. (2012). 数字信号处理入门。快速有效的嵌入式系统设计(第 219–242 页)。Elsevier。 doi.org/10.1016/B978-0-08-097768-3.00011-8

[2] T. Giannakopoulos, T., & Pikrakis, A. (2014). 信号变换与滤波基础。音频分析简介(第 33–57 页)。Elsevier。 doi.org/10.1016/B978-0-08-099388-1.00003-0

[3] Downey, A. (2016). 声音与信号。Think DSP: Python 中的数字信号处理(第 1–11 页)。(第一版)。O’Reilly Media, Inc.

[4] Thakur, B., & Mehra, R. (2016). 使用不同窗口技术算法的离散傅里叶变换分析。国际计算机应用期刊, 975, 8887。

[5] Kong, Q., Siauw, T., & Bayen, A. (2020)。傅里叶变换。Python 编程与数值方法:工程师和科学家的指南(第 415–444 页)。学术出版社。

[6] Numpy 文档,API 参考,离散傅里叶变换 (numpy.fft)。[访问日期:2023 年 2 月 2 日]

[7] Scipy 文档,API 参考,遗留的离散傅里叶变换 (scipy.fftpack)。[访问日期:2023 年 2 月 2 日]

学习 RabbitMQ 用于事件驱动架构(EDA)

原文:towardsdatascience.com/learn-rabbitmq-for-event-driven-architecture-eda-e1e7377db2b

一份适合初学者的教程,介绍 RabbitMQ 的工作原理以及如何在 Go 中使用 RabbitMQ,这是学习 EDA 的第一步。

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

·发表于 Towards Data Science ·阅读时长 39 分钟·2023 年 4 月 5 日

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

照片由 Bradyn Trollip 提供,来自 Unsplash

事件驱动架构(EDA)是我在编程中最喜欢的东西之一。这种架构允许我们构建微服务并轻松地在它们之间共享信息。

在常规的顺序软件中,你会有一个函数触发另一个函数,或者一个定期脚本来检查某些任务。

在事件驱动架构中,我们利用队列或发布/订阅模式。允许不同的服务之间通知或传送信息,以触发代码执行。

事件驱动架构通常用于构建高度灵活和可扩展的软件。这是因为可以通过简单地监听事件来轻松添加或移除功能。

这使得影子部署和测试新服务与生产环境并行变得非常容易,因为你可以让新服务对相同事件做出反应,而不会干扰正在运行的系统。

然而,并非所有的情况都是一帆风顺的,一些人认为事件驱动架构(EDA)系统可能略显复杂,而且在考虑到完整的服务流程时,测试有时会更困难。我认为测试其实更简单,因为我们可以轻松触发事件并查看相关服务或单个服务的反应。但如果没有适当的架构文档,也可能很难理解是什么触发了什么以及原因。

本教程将探讨如何使用 RabbitMQ 构建两个通过事件进行通信的微服务。我们将研究 RabbitMQ 中使用的不同范式,虽然我们将学习如何在 Go 中使用 RabbitMQ,但我们主要集中于学习 RabbitMQ 的概念。涵盖一些常见错误和一些最佳实践。

RabbitMQ 支持多种协议来发送数据,但在本教程中,我们将专注于使用AMQP

在本教程中,我们将学习以下内容

  • 使用 Docker 设置 RabbitMQ

  • 虚拟主机、用户和权限

  • 使用 CLI 管理 RabbitMQ,通过rabbitmqctlrabbitmqadmin

  • 了解生产者、消费者以及如何编写它们。

  • 了解队列、交换机和绑定

  • 使用工作队列(先进先出)

  • 使用 RabbitMQ 进行发布/订阅

  • 使用基于 RPC 的模式和回调。

  • 使用 TLS 加密流量

  • 使用配置来声明 RabbitMQ 中的资源

该教程的视频录制,适合喜欢视频的人。

本文中使用的所有代码可以在这里找到。

安装 RabbitMQ — 设置用户、虚拟主机和权限

使 RabbitMQ 运行可以通过下载和安装中的示例完成。我建议在生产环境中遵循该指南,但为了本教程和实验,我们可以使用更简单的方法。

像往常一样,最简单的方法是运行 Docker!

此命令将下载最新的 RabbitMQ 并将其作为后台进程启动,暴露端口567215672

docker run -d --name rabbitmq -p 5672:5672 -p 15672:15672 rabbitmq:3.11-management

端口 5672 用于启用 AMQP 连接。AMQP 是 RabbitMQ 和许多其他消息中间件使用的网络协议。

端口 15672 被开启,因为管理 UI 和管理界面托管在该端口,管理 RabbitMQ 的 API 也在该端口上。

有关端口的更多细节,请参阅 RabbitMQ 的网络指南。

一旦 Docker 启动,让我们开始访问托管在localhost:15672上的管理 UI。

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

RabbitMQ 管理 UI — 图片由 Percy Bolmer 提供

哎呀,我们需要一个用户!让我们使用RabbitMQCLI创建一个。别担心安装问题,它已经存在于我们运行的 Docker 容器中。

我们可以使用命令add_user来创建新用户,后跟用户名和密码。我们使用docker exec rabbitmq在 docker 内部执行命令,将rabbitmq替换为你为 docker 容器指定的名称。

docker exec rabbitmq rabbitmqctl add_user percy secret

我建议在探索期间也授予管理员权限。我们可以通过给新用户添加管理员标签来实现这一点。

docker exec rabbitmq rabbitmqctl set_user_tags percy administrator

哦,最后一件事,默认情况下有一个 guest 用户,我强烈建议删除此用户!此用户仅对使用本地主机的用户可用,但还是安全起见比较好。

docker exec rabbitmq rabbitmqctl delete_user guest

就这样,回到管理 UI 并登录。

登录后你会看到一个看起来相当老旧的用户界面,但这非常好,因为我们可以真正从这里监控 RabbitMQ,并查看发生了什么。我们还不会玩弄这个界面,我们需要先有一个实际连接并发送数据的服务。

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

管理 UI 显示正在运行的实例指标 — 图片由 Percy Bolmer 提供

在我们开始操作之前,我们需要再修复两个问题。

RabbitMQ 中的资源,如队列以及我们将很快学习的其他内容,按照逻辑层进行分隔,这个逻辑层称为虚拟主机(Vhost)。

解释虚拟主机最简单的方法可能是将其与命名空间进行比较,但这可能在某些方面不完全正确。

我们可以使用这些虚拟主机将某些资源分组在一起,并通过添加允许使用虚拟主机的用户来限制访问。

让我们开始使用 add_vhost 命令创建虚拟主机,它接受一个输入,即虚拟主机的名称。

docker exec rabbitmq rabbitmqctl add_vhost customers

现在我们有了一个虚拟主机,我们可以为之前创建的用户添加权限,以便它可以连接。

添加权限是通过 set_permissions 命令完成的,我们使用 -p 标志来指定要添加权限的虚拟主机。语法中的下一个项是要添加权限的用户。

命令的最后部分是令人害怕的部分,它是一个定义要添加权限的正则表达式,添加所有权限的示例如下,或者对所有以 customer- 开头的资源的权限为 "^customer-*".

会有 3 个正则表达式槽位,按顺序配置以下权限。

  • 配置 — 对匹配正则表达式的资源的配置权限

  • — 对匹配正则表达式的资源的写权限

  • 读取 — 对匹配正则表达式的资源的读取权限

为我的用户 percy 添加对 customer 虚拟主机的全面配置、写入和读取权限的完整命令如下。注意,我给予了 .* 的访问权限,即所有权限。

docker exec rabbitmq rabbitmqctl set_permissions -p customers percy ".*" ".*" ".*"

创建完成后,你应该能在管理 UI 的右上角看到新的虚拟主机。

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

选择新的虚拟主机。 — 图片由 Percy Bolmer 提供

RabbitMQ 基础知识 — 生产者、消费者、交换机和队列

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

展示生产者、交换机、队列和消费者如何协同工作 — 图片由 Percy Bolmer 提供

当我们构建事件驱动架构时,有一些术语需要理解。

  • 生产者 — 任何发送消息的软件。

  • 消费者 — 任何接收消息的软件。

  • 队列 — 一个队列接受一个消息,输出这个消息,可以将其视为一个大型缓冲区。队列是 FIFO(先进先出)的,这意味着消息按插入队列的顺序输出。

  • 交换机 — 一种路由器,是生产者发送消息的地方。交换机接受消息,并根据交换机的类型和应用的绑定(规则)将其发送到正确的队列。

一般来说,我们可以使用这个来在服务之间发送和接收消息。值得一提的是,生产者和消费者不需要在同一主机上运行,这使得系统具有很好的扩展性。

首先创建一个新的 Go 项目,如果你还没有安装 Go,请从这里进行安装。

在实际的 Go 项目设置中,我可能会使用 Cobra,但为了避免新用户感到困惑,我将简单地创建两个主要包。

让我们在 Go 中构建一个生产者,能够开始在队列上发送消息。

开始为生产者创建一个新的项目,并获取由 RabbitMQ 团队官方维护的 AMQP 库。

该项目将有一个cmd文件夹,包含所有不同的服务,每个服务都是一个独立的可运行程序。

我们还将有一个internal文件夹,用于存储共享库等。

mkdir eventdriven
cd eventdriven
mkdir -p cmd/producer
mkdir internal
touch cmd/producer/main.go
touch internal/rabbitmq.go
go mod init programmingpercy.tech/eventdrivenrabbit
go get github.com/rabbitmq/amqp091-go

你的文件夹结构应该如下所示。

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

cmd文件夹和internal文件夹已准备好 — 图片来自 Percy Bolmer

首先在internal/rabbitmq.go中添加一个与 RabbitMQ 实例的连接。

我们将创建一个小的助手函数,用于通过amqp协议连接到 RabbitMQ。我们将允许用户指定凭证、主机以及要连接的 vhost。

我将简单地返回指向连接的指针,即网络连接和用于并发发送消息的amqp.Channel。将连接的管理留给用户。

package internal

import (
 "context"
 "fmt"

 amqp "github.com/rabbitmq/amqp091-go"
)

// RabbitClient is used to keep track of the RabbitMQ connection
type RabbitClient struct {
 // The connection that is used
 conn *amqp.Connection
 // The channel that processes/sends Messages
 ch *amqp.Channel
}

// ConnectRabbitMQ will spawn a Connection
func ConnectRabbitMQ(username, password, host, vhost string) (*amqp.Connection, error) {
 // Setup the Connection to RabbitMQ host using AMQP
 conn, err := amqp.Dial(fmt.Sprintf("amqp://%s:%s@%s/%s", username, password, host, vhost))
 if err != nil {
  return nil, err
 }
 return conn, nil
}

// NewRabbitMQClient will connect and return a Rabbitclient with an open connection
// Accepts a amqp Connection to be reused, to avoid spawning one TCP connection per concurrent client
func NewRabbitMQClient(conn *amqp.Connection) (RabbitClient, error) {
 // Unique, Conncurrent Server Channel to process/send messages
 // A good rule of thumb is to always REUSE Conn across applications
 // But spawn a new Channel per routine
 ch, err := conn.Channel()
 if err != nil {
  return RabbitClient{}, err
 }

 return RabbitClient{
  conn: conn,
  ch:   ch,
 }, nil
}

// Close will close the channel
func (rc RabbitClient) Close() error {
 return rc.ch.Close()
}

一个很好的经验法则是,在整个应用程序中重用一个连接,并为并发任务创建新的通道。原因是连接是 TCP 连接,而通道是在分配的 TCP 连接中的多路复用连接。遵循这个经验法则可以实现更具可扩展性的解决方案。

让我们将这个简单的客户端导入到cmd/producer/main.go中并尝试连接,看看会发生什么。

现在,我们将简单地连接并在关闭连接前休眠 30 秒。

package main

import (
 "log"
 "programmingpercy/eventdrivenrabbit/internal"
 "time"
)

func main() {
 conn, err := internal.ConnectRabbitMQ("percy", "secret", "localhost:5672", "customers")

 if err != nil {
  panic(err)
 }
 defer conn.Close()
 client, err := internal.NewRabbitMQClient(conn)
 if err != nil {
  panic(err)
 }
 defer client.Close()

 time.Sleep(30 * time.Second)

 log.Println(client)
}

一旦完成这些设置,就运行生产者。

go run cmd/producer/main.go

一旦运行,返回管理 UI 并查看我们是否可以看到现在有一个连接和一个通道。

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

我们现在有一个连接和一个通道,而不是零个 — 图片来自 Percy Bolmer

通道是一种非常聪明的处理 TCP 层的方法,你可以在文档中阅读更多内容。它允许用户在多个通道之间重用一个打开的 TCP 连接,而不是打开多个 TCP 连接。这是一种复用技术。

现在是开始发送数据的时候了,这是在上述通道上完成的。通道的功能远超过你可能想象的,它不仅仅是一个简单的管道,还可以在创建时配置一些巧妙的选项。

我们可以从 UI 创建队列,但我喜欢在测试时通过代码创建队列。在生产环境中,我喜欢使用配置文件来声明一些基本设置,我们稍后会讨论这个问题。

我们可以通过调用 amqp.QueueDeclare 来创建队列,这个函数有许多输入参数,我们需要理解这些参数以获得想要的队列行为。函数签名如下所示。

func (*amqp.Channel).QueueDeclare(name string, durable bool, autoDelete bool, exclusive bool, noWait bool, args amqp.Table) (amqp.Queue, error)
  • 名称 — 用于引用队列的名称。此项可以为空,在这种情况下,服务器将生成一个名称。

  • 持久化 — 如果队列在 Broker 重启(RabbitMQ 重启)时应该被保留

  • 自动删除 — 如果队列在最后一个消费者离开时应自动删除

  • 独占 — 仅适用于创建队列的相同连接。

  • 无等待 — 假定队列在服务器上创建

  • 参数 — 提供用户提供的参数的选项。

为了让这件事更简单,我会创建一个接受 namedurableautodelete 参数的包装函数。我将默认禁用其他参数。

// CreateQueue will create a new queue based on given cfgs
func (rc RabbitClient) CreateQueue(queueName string, durable, autodelete bool) error {
 _, err := rc.ch.QueueDeclare(queueName, durable, autodelete, false, false, nil)
 return err
}

让我们更新 producer/main.go 以执行新的 CreateQueue 函数,我将创建一个持久化队列,因为我希望处理新客户的队列保持活跃和持久,我还会将自动删除设置为 false

我还会创建一个名为 customers_test 的非持久化队列,以展示区别。

func main() {
 conn, err := internal.ConnectRabbitMQ("percy", "secret", "localhost:5672", "customers")

 if err != nil {
  panic(err)
 }
 defer conn.Close()

 client, err := internal.NewRabbitMQClient(conn)
 if err != nil {
  panic(err)
 }
 defer client.Close()

 if err := client.CreateQueue("customers_created", true, false); err != nil {
  panic(err)
 }
 if err := client.CreateQueue("customers_test", false, true); err != nil {
  panic(err)
 }

 time.Sleep(10 *time.Second)

 log.Println(client)

}

添加完后,请确保执行生产者。

go run cmd/producer/main.go

你可以访问 UI 并查看应该都可用的队列。请注意,一旦程序退出,customers_test 队列没有被删除,这是因为我们还没有消费者连接。只有连接了消费者的队列才会被删除。

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

customers-test 使用自动删除创建,一旦程序退出,它将被删除。— 图片来源:Percy Bolmer

为了好玩,你可以尝试现在重启 RabbitMQ,看看 customers_test 是如何消失的,因为它没有被标记为持久化。

docker restart rabbitmq

探索交换机和绑定

在我们开始在队列上发送消息之前,我们需要创建一个交换机。已经创建了一些默认的交换机,但我们将创建自己的交换机以了解更多信息。

交换机是 RabbitMQ 的一个重要部分,它是我们发送消息的资源。交换机的工作是将消息发送到正确的队列。

要开始接收队列上的消息,该队列需要绑定到一个交换上,这被称为绑定。绑定基本上是一个路由规则。一个重要的点是,队列可以绑定到多个交换,这也使得不同交换类型的意义更加明确。

有几种不同类型的交换,每种交换在消息发送方式上有不同的行为。

首先,我们有最基本的Direct交换。这种交换非常简单,消息是基于其确切的路由键进行路由的。在示例图中,我们看到发送到customer_created的消息仅通过交换customer_events路由到那个特定的队列。直接交换在需要将工作分配给一组工作者时非常有用。

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

直接交换(Direct Exchange)—— 只有直接匹配customer_created的消息才会收到匹配 —— 图片由 Percy Bolmer 提供

第二种类型是Fanout交换,它用于将消息发送到所有绑定的队列。任何绑定到交换的队列都会接收到消息,路由键会被简单地忽略!这通常用于将消息广播给任何感兴趣的方。

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

扩展交换(Fanout Exchange)—— 任何绑定的队列都接收消息 —— 图片由 Percy Bolmer 提供

然后我们有Topic交换,这种交换非常酷。它们允许绑定指定规则以根据路由键选择消息的子集。

路由键在每个词之间用.分隔,例如customers.eu.stockholm。这可能是来自瑞典斯德哥尔摩的客户的路由键,然后我们可以有一个绑定来告诉交换机某个队列想要这些消息,但不包括customers.us.florida

有几个特殊字符,#表示零个或多个匹配,因此例如customers.#将匹配任何以customers.开头的路由键。

还有*,它是特定位置的特定词,例如customers.*.stockholm将仅匹配具有第一个词customers和第三个词stockholm的路由键。

当然,这对于某些服务只接收与特定主题子集相关的消息是非常有用的。下面的例子展示了如何在二月份创建一个新客户,队列customer_created会接收到消息,因为绑定规则是customers.created.#,而队列customer_emailed则不会接收到消息,因为它不匹配绑定customers.created.march

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

主题交换(Topic Exchange)—— 允许使用简单的正则表达式根据路由键选择子集 —— 图片由 Percy Bolmer 提供

最终的交换是Header交换,每条我们在 RabbitMQ 上发送的消息都有可能添加头信息,这是一个键值字段。当我们需要基于更高级别的内容进行路由时,这非常有用。

比如说我们添加一个browser头信息,指示用户在注册时使用了什么网页浏览器。例如,我们可以将所有 Linux 用户路由到某个特定的队列。

你可以指定多个头信息,并且它们都必须匹配,或者只需一个匹配。这通过将x-match设置为allany来完成。

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

Header Exchange — 允许基于消息中可以提供的额外头信息进行路由 — 图片由 Percy Bolmer 提供

让我们停止讨论,创建一个我们可以使用的交换机。

要添加一个交换机,我们将使用与之前使用的rabbitmqcli非常类似的rabbitmqadmin CLI 工具。

我们使用declare exchange命令,后跟交换机的名称和类型。对于这个教程,我将使用Topic交换机。

我们将创建一个名为customer-events的交换机。我们还需要指定虚拟主机以及管理员的用户名和密码。如果你希望交换机在重启时保持存在,记得将 durable 设置为 true。

docker exec rabbitmq rabbitmqadmin declare exchange --vhost=customers name=customer_events type=topic -u percy -p secret durable=true

我们还需要授权用户在这个交换机上发送消息。我们使用set_topic_permissions命令设置特定主题上的权限。以下命令将用户percy设置为允许在交换机customer_events上的虚拟主机customers上发布,路由键以customers开头。

docker exec rabbitmq rabbitmqctl set_topic_permissions -p customers percy customer_events "^customers.*" "^customers.*" 

现在在这个交换机上发布将不会有任何反应,因为队列和交换机之间没有绑定。

任何发送的消息都会被丢弃。

发布消息到交换机

要开始发布消息,我们首先需要在customers_createdcustomers_test队列与customers_events交换机之间创建绑定。

打开rabbitmq.go并添加一个添加绑定的CreateBinding函数。

// CreateBinding is used to connect a queue to an Exchange using the binding rule
func (rc RabbitClient) CreateBinding(name, binding, exchange string) error {
 // leaveing nowait false, having nowait set to false wctxill cause the channel to return an error and close if it cannot bind
 // the final argument is the extra headers, but we wont be doing that now
 return rc.ch.QueueBind(name, binding, exchange, false, nil)
}

然后在producer/main.go中添加绑定,以便连接所有内容。我们预计客户会在主题customers.created上发布,后面跟着他们来自的国家。但是绑定不会关心国家,只要它匹配模式即可。

 ...
 // Create binding between the customer_events exchange and the customers-created queue
 if err := client.CreateBinding("customers-created", "customers.created.*", "customer_events"); err != nil {
  panic(err)
 }
 // Create binding between the customer_events exchange and the customers-test queue
 if err := client.CreateBinding("customers-test", "customers.*", "customer_events"); err != nil {
  panic(err)
 }

如果你执行生产者,我们可以访问管理用户界面并查看可用的绑定。

go run cmd/producer/main.go

然后进入用户界面并访问你的交换机。

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

交换机显示当前的绑定和它们的路由键 — 图片由 Percy Bolmer 提供

现在我们已经有了绑定,可以开始查看发布消息的内容。我们从最简单的开始。

我们创建一个名为Send的包装函数,该函数接受关于要发布到哪个交换机和路由键的参数。该函数还会接受一个上下文和一个amqp.Publishing 结构体。

amqp.Publishing 结构体非常重要,因为它允许我们自定义我们发送的消息的功能和行为。

我们将一步步地探索它们,因为它们有很多。

// Send is used to publish a payload onto an exchange with a given routingkey
func (rc RabbitClient) Send(ctx context.Context, exchange, routingKey string, options amqp.Publishing) error {
 return rc.ch.PublishWithContext(ctx,
  exchange,   // exchange
  routingKey, // routing key
  // Mandatory is used when we HAVE to have the message return an error, if there is no route or queue then
  // setting this to true will make the message bounce back
  // If this is False, and the message fails to deliver, it will be dropped
  true, // mandatory
  // immediate Removed in MQ 3 or up https://blog.rabbitmq.com/posts/2012/11/breaking-things-with-rabbitmq-3-0§
  false,   // immediate
  options, // amqp publishing struct
 )
}

返回到producer/main.go,我们将创建一条消息进行发送。我们将发送两条消息,每个队列一条。这是为了展示deliveryMode参数,这个参数非常重要。如果将其设置为持久性,消息将被保存直到某个消费者获取它,但这会带来开销和更长的延迟。

如果有些东西不需要持久化,则将其设置为Transient以提高性能。

记住,如果你发送的是持久消息,你的队列也需要是持久的;如果队列本身不存在,那么保存消息也没有意义。

... 
// Create context to manage timeout
 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 defer cancel()
 // Create customer from sweden
 if err := client.Send(ctx, "customer_events", "customers.created.se", amqp091.Publishing{
  ContentType:  "text/plain",       // The payload we send is plaintext, could be JSON or others..
  DeliveryMode: amqp091.Persistent, // This tells rabbitMQ that this message should be Saved if no resources accepts it before a restart (durable)
  Body:         []byte("An cool message between services"),
 }); err != nil {
  panic(err)
 }
 if err := client.Send(ctx, "customer_events", "customers.test", amqp091.Publishing{
  ContentType:  "text/plain",
  DeliveryMode: amqp091.Transient, // This tells rabbitMQ that this message can be deleted if no resources accepts it before a restart (non durable)
  Body:         []byte("A second cool message"),
 }); err != nil {
  panic(err)
 }

 log.Println(client)
}

现在是执行生产者的时候了。

go run cmd/producer/main.go

你现在应该在 UI 的Queue页面下看到每个队列的一条消息。

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

每个队列都有消息发送到它们 — 图片来源:Percy Bolmer

如果你愿意,可以进入每个队列并消费消息进行查看,但我建议重新启动 RabbitMQ,以显示 Transient 和 Persistent 之间的区别。

docker restart rabbitmq

重新启动后尝试重新加载 UI,你应该会看到整个customers-test队列被删除了,但customers-created队列实际上保留了旧消息。

这是因为持久消息会被写入磁盘,以便在崩溃等情况发生时能够保存。

我们将很快介绍更高级的发布技术。

消费消息、确认、拒绝和重新入队

我们知道如何发布消息,但如果我们不能在另一个服务中消费这些消息,那是没有用的。

消费是从队列中获取消息的过程。

让我们创建一个新的二进制文件,以便我们可以用来消费消息。

mkdir cmd/consumer
touch cmd/consumer/main.go

在我们开始消费之前,我们将在Rabbitmq.go中添加一个Consume函数,它将封装通道消费函数。

消费时有几个选项需要考虑。

  • Exclusive — 如果设置为 true,将确保这是该队列上的唯一消费者;如果设置为 false,服务器将公平地将消息分配给多个消费者。

  • AutoAck — 当设置为 true 时,将自动确认交付;当设置为 false 时,将期望消费者在完成时调用确认。AutoAck 可能听起来很棒,但它很棘手,如果你的消费者在确认耗时的过程后失败,消息会丢失,因为服务器认为它已经完成。

  • NoLocal — 在 RabbitMQ 中不支持,这是 AMQP 字段,用于避免在同一领域中发布和消费。

  • NoWait — 不会等待服务器确认。

让我们将Consume函数添加到Rabbitmq.go中。

// Consume is a wrapper around consume, it will return a Channel that can be used to digest messages
// Queue is the name of the queue to Consume
// Consumer is a unique identifier for the service instance that is consuming, can be used to cancel etc
// autoAck is important to understand, if set to true, it will automatically Acknowledge that processing is done
// This is good, but remember that if the Process fails before completion, then an ACK is already sent, making a message lost
// if not handled properly
func (rc RabbitClient) Consume(queue, consumer string, autoAck bool) (<-chan amqp.Delivery, error) {
 return rc.ch.Consume(queue, consumer, autoAck, false, false, false, nil)
}

现在我们可以进行消费了,让我们填写consumer/main.go,使其连接到 RabbitMQ 并开始从队列中获取消息。

package main

import (
 "log"
 "programmingpercy/eventdrivenrabbit/internal"
)

func main() {

 conn, err := internal.ConnectRabbitMQ("percy", "secret", "localhost:5672", "customers")
 if err != nil {
  panic(err)
 }

 mqClient, err := internal.NewRabbitMQClient(conn)
 if err != nil {
  panic(err)
 }

 messageBus, err := mqClient.Consume("customers_created", "email-service", false)
 if err != nil {
  panic(err)
 }

 // blocking is used to block forever
 var blocking chan struct{}

 go func() {
  for message := range messageBus {
   // breakpoint here
   log.Printf("New Message: %v", message)
  }
 }()

 log.Println("Consuming, to close the program press CTRL+C")
 // This will block forever
 <-blocking

}

运行该消费者时,一旦发布者有消息发送,它应该会打印出一条消息。

记住,复用连接,但为每个并行处理创建一个新的通道,在我们的例子中,将创建一个第二个 RabbitMQ 客户端来管理customers-test队列。

go run cmd/consumer/main.go

如果你没有看到任何消息,可能是因为你需要先运行生产者。

2023/02/12 22:17:24 New Message: {0xc0000b0000 map[] text/plain  2 0     0001-01-01 00:00:00 +0000 UTC    ema
il-service 0 1 false customer_events customers.created.se [65 110 32 99 111 111 108 32 109 101 115 115 97 103
 101 32 98 101 116 119 101 101 110 32 115 101 114 118 105 99 101 115]}

可能值得探索通过通道传递的结构体,即 amqp.Delivery 结构体,它提供了所有字段的良好视图。

// Delivery captures the fields for a previously delivered message resident in
// a queue to be delivered by the server to a consumer from Channel.Consume or
// Channel.Get.
type Delivery struct {
 Acknowledger Acknowledger // the channel from which this delivery arrived

 Headers Table // Application or header exchange table

 // Properties
 ContentType     string    // MIME content type
 ContentEncoding string    // MIME content encoding
 DeliveryMode    uint8     // queue implementation use - non-persistent (1) or persistent (2)
 Priority        uint8     // queue implementation use - 0 to 9
 CorrelationId   string    // application use - correlation identifier
 ReplyTo         string    // application use - address to reply to (ex: RPC)
 Expiration      string    // implementation use - message expiration spec
 MessageId       string    // application use - message identifier
 Timestamp       time.Time // application use - message timestamp
 Type            string    // application use - message type name
 UserId          string    // application use - creating user - should be authenticated user
 AppId           string    // application use - creating application id

 // Valid only with Channel.Consume
 ConsumerTag string

 // Valid only with Channel.Get
 MessageCount uint32

 DeliveryTag uint64
 Redelivered bool
 Exchange    string // basic.publish exchange
 RoutingKey  string // basic.publish routing key

 Body []byte
}

如果你重新运行当前的消费者,你会看到相同的消息再次出现。这是因为我们从未确认消费者已经使用了这条消息。这必须在迭代消息或使用自动确认标志时手动完成。

在确认时,我们可以传递一个 multiple 标志,指示是否一次确认多条消息,我们可以将其设为 false。

我们可以确认或 NACK 消息,确认表示一切正常,NACK 表示我们处理失败,然后消息会被重新放回队列中。

让我们更新处理消息的代码,以便确认它们。

 go func() {
  for message := range messageBus {
   // breakpoint here
   log.Printf("New Message: %v", message)
   // Multiple means that we acknowledge a batch of messages, leave false for now
   if err := message.Ack(false); err != nil {
    log.Printf("Acknowledged message failed: Retry ? Handle manually %s\n", message.MessageId)
    continue
   }
   log.Printf("Acknowledged message %s\n", message.MessageId)
  }
 }()

现在重新运行代码,你应该会看到消息再次打印,但在重新启动后消息就消失了。

这非常有用,以避免消费者接收一条消息,在处理时失败,然后消息就会丢失。

为了展示自动确认可能是危险的,这里是一个修改后的例子,我们将自动确认设置为 true,但在处理过程中失败了。

 // Auto Ack is now True
 messageBus, err := mqClient.Consume("customers-created", "email-service", true)
 if err != nil {
  panic(err)
 }

 // blocking is used to block forever
 var blocking chan struct{}

 go func() {
  for message := range messageBus {
   log.Printf("New Message: %v", message)
   panic("Whops I failed here for some reason")

  }
 }()

运行消费者两次,你会看到它实际上只在第一次执行时接受了消息。如果你没有妥善管理,这可能是危险的行为。这就是我不断提到它的原因!

要处理失败,你可以使用 Nack 告诉 RabbitMQ 失败了,你可以使用 redelivered 字段来避免过多重试。

Nack 接受一个重新排队的参数,这非常方便!

这是一个例子,我们在消息到达第一次时失败,重新排队,然后在下一次到达时确认它。

 messageBus, err := mqClient.Consume("customers-created", "email-service", false)
 if err != nil {
  panic(err)
 }

 // blocking is used to block forever
 var blocking chan struct{}

 go func() {
  for message := range messageBus {
   log.Printf("New Message: %v", message)

   if !message.Redelivered {
    // Nack multiple, Set Requeue to true
    message.Nack(false, true)
    continue
   }

   // Multiple means that we acknowledge a batch of messages, leave false for now
   if err := message.Ack(false); err != nil {
    log.Printf("Acknowledged message failed: Retry ? Handle manually %s\n", message.MessageId)
    continue
   }
   log.Printf("Acknowledged message %s\n", message.MessageId)
  }
 }()

这里还有更多需要考虑的,目前我们使用的处理程序是单线程的,这意味着我们一次只能处理一条消息。我们可以通过实现一个工作组来修复这一点,该工作组允许一定数量的并发任务。

我将添加一个 errgroup,因此这种方法需要 Go 1.2。使用 ErrGroup 非常简单,我们可以将其限制为每个消费者处理 10 条消息。

errgroup 来自 golang.org/x/sync/errgroup 包。

..... 
// Set a timeout for 15 secs
 ctx := context.Background()
 ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
 defer cancel()
 // Create an Errgroup to manage concurrecy
 g, ctx := errgroup.WithContext(ctx)
 // Set amount of concurrent tasks
 g.SetLimit(10)
 go func() {
  for message := range messageBus {
   // Spawn a worker
   msg := message
   g.Go(func() error {
    log.Printf("New Message: %v", msg)

    time.Sleep(10 * time.Second)
    // Multiple means that we acknowledge a batch of messages, leave false for now
    if err := msg.Ack(false); err != nil {
     log.Printf("Acknowledged message failed: Retry ? Handle manually %s\n", msg.MessageId)
     return err
    }
    log.Printf("Acknowledged message %s\n", msg.MessageId)
    return nil
   })
  }
 }() 

添加这个使得消费者变得稍微更好一些。

SetLimit 目前仅用于此,还有另一种管理消费消息数量的方法,即使用 RabbitMQ,我推荐使用的叫做 Prefetch,我们稍后会讲到。

我们可以通过将 Send 函数包裹在 for 循环中来更新发布者以发送更多的消息。

 // Create context to manage timeout
 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 defer cancel()
 // Create customer from sweden
 for i := 0; i < 10; i++ {
  if err := client.Send(ctx, "customer_events", "customers.created.se", amqp091.Publishing{
   ContentType:  "text/plain",       // The payload we send is plaintext, could be JSON or others..
   DeliveryMode: amqp091.Persistent, // This tells rabbitMQ that this message should be Saved if no resources accepts it before a restart (durable)
   Body:         []byte("An cool message between services"),
  }); err != nil {
   panic(err)
  }
 }

 if err := client.Send(ctx, "customer_events", "customers.test", amqp091.Publishing{
  ContentType:  "text/plain",
  DeliveryMode: amqp091.Transient, // This tells rabbitMQ that this message can be deleted if no resources accepts it before a restart (non durable)
  Body:         []byte("A second cool message"),
 }); err != nil {
  panic(err)
 }

 log.Println(client)
}

尝试一下,看看消费者现在是否接受多条消息,或者尝试启动多个消费者来进行一些测试。

注意到生产者在发送消息后立即退出了吗?目前,Send 函数不会等待来自服务器的任何确认。有时,我们可能希望阻塞,直到服务器确认它已接收到消息。

高兴的是,我们可以!我们需要将 RabbitMQ 中使用的Publish函数更改为PublishWithDeferredConfirmWithContext,这将返回一个可以用来Wait等待服务器确认的对象。

这个对象将始终是 NIL,除非将通道设置为Confirm模式,将其设置为 Confirm 模式将使服务器在收到发布的消息时发送确认。

Rabbitmq.go中,让我们修改发布方法并添加一个等待。

// Send is used to publish a payload onto an exchange with a given routingkey
func (rc RabbitClient) Send(ctx context.Context, exchange, routingKey string, options amqp.Publishing) error {
 // PublishWithDeferredConfirmWithContext will wait for server to ACK the message
 confirmation, err := rc.ch.PublishWithDeferredConfirmWithContext(ctx,
  exchange,   // exchange
  routingKey, // routing key
  // Mandatory is used when we HAVE to have the message return an error, if there is no route or queue then
  // setting this to true will make the message bounce back
  // If this is False, and the message fails to deliver, it will be dropped
  true, // mandatory
  // immediate Removed in MQ 3 or up https://blog.rabbitmq.com/posts/2012/11/breaking-things-with-rabbitmq-3-0§
  false,   // immediate
  options, // amqp publishing struct
 )
 if err != nil {
  return err
 }
 // Blocks until ACK from Server is receieved
 log.Println(confirmation.Wait())
 return nil
}

让我们也更新NewRabbitMQClient以始终将通道设置为Confirm模式。

// NewRabbitMQClient will connect and return a Rabbitclient with an open connection
// Accepts a amqp Connection to be reused, to avoid spawning one TCP connection per concurrent client
func NewRabbitMQClient(conn *amqp.Connection) (RabbitClient, error) {
 // Unique, Conncurrent Server Channel to process/send messages
 // A good rule of thumb is to always REUSE Conn across applications
 // But spawn a new Channel per routine
 ch, err := conn.Channel()
 if err != nil {
  return RabbitClient{}, err
 }
 // Puts the Channel in confirm mode, which will allow waiting for ACK or NACK from the receiver
 if err := ch.Confirm(false); err != nil {
  return RabbitClient{}, err
 }

 return RabbitClient{
  conn: conn,
  ch:   ch,
 }, nil
}

Rabbitmq.go的一个更好方法可能是添加一个NewChannel函数,然后让每个函数接受一个 Channel 作为输入参数。

现在运行程序,你应该会看到publisher.go在每次服务器确认消息时打印 TRUE,注意这与 Consumer 的ACK不同。我们只是等待服务器确认发布的消息已被接受。

发布和订阅(PubSub)

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

RabbitMQ 中的 Pub/Sub 模式使用 Fanout 交换机 — 图片由 Percy Bolmer 提供

直到目前为止,我们一直在使用 FIFO 队列(先进先出)。这意味着每条消息只发送给一个消费者。

在发布和订阅模式中,你会希望每个消费者接收到相同的消息。

我们关于绑定等的所有知识仍然适用,使用方式相同。我们可以使用 Fanout 交换机(将消息推送到所有绑定的队列)而不管队列名称。

这个想法是让每个消费者创建一个未命名的队列,未命名的队列将由 RabbitMQ 服务器生成一个随机的唯一名称。

这是在代码中创建队列非常适合的一个好例子。

我们可能希望将customers_event发送到多个服务。例如,我们可能希望有一个电子邮件服务和一个日志记录服务来记录每个客户事件。

让我们来构建它。(由于这是一个学习 RabbitMQ 的教程,我们将简单地启动两个 Consumer 实例。)

我们首先删除现有的交换机,因为它的类型不正确。我们还创建一个新的交换机,但类型为Fanout。这一次我们没有为权限指定特定的前缀,而是给予了完全访问权限。

docker exec rabbitmq rabbitmqadmin delete exchange name=customer_events --vhost=customers -u percy -p secret
docker exec rabbitmq rabbitmqadmin declare exchange --vhost=customers name=customer_events type=fanout -u percy -p secret durable=true
docker exec rabbitmq rabbitmqctl set_topic_permissions -p customers percy customer_events ".*" ".*"

由于我们在当前代码中创建一个未命名的队列时无法知道队列名称,因此需要进行修改。让我们返回来自CreateQueue的 RabbitMQ 包中的队列信息。该对象将包含随机创建的名称。

// CreateQueue will create a new queue based on given cfgs
func (rc RabbitClient) CreateQueue(queueName string, durable, autodelete bool) (amqp.Queue, error) {
 q, err := rc.ch.QueueDeclare(queueName, durable, autodelete, false, false, nil)
 if err != nil {
  return amqp.Queue{}, nil
 }

 return q, nil
}

现在是更新Publisher的时候了,在教程早些时候我们在 Publisher 中创建了 Channel 绑定。依我看这样做并不完全合理,这只是为了不走得太快,同时展示功能。

Consumer 声明绑定更有意义,因为它与消费者相关。在发布和订阅中,消费者的数量和路径可能未知,现在这更没有意义。让我们更新 publisher.go 使其变得更小。

package main

import (
 "context"
 "log"
 "programmingpercy/eventdrivenrabbit/internal"
 "time"

 "github.com/rabbitmq/amqp091-go"
)

func main() {
 conn, err := internal.ConnectRabbitMQ("percy", "secret", "localhost:5672", "customers")

 if err != nil {
  panic(err)
 }
 defer conn.Close()
 client, err := internal.NewRabbitMQClient(conn)
 if err != nil {
  panic(err)
 }
 defer client.Close()

 // Create context to manage timeout
 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 defer cancel()
 // Create customer from sweden
 for i := 0; i < 10; i++ {
  if err := client.Send(ctx, "customer_events", "customers.created.se", amqp091.Publishing{
   ContentType:  "text/plain",       // The payload we send is plaintext, could be JSON or others..
   DeliveryMode: amqp091.Persistent, // This tells rabbitMQ that this message should be Saved if no resources accepts it before a restart (durable)
   Body:         []byte("An cool message between services"),
  }); err != nil {
   panic(err)
  }
 }

 log.Println(client)
}

我们将更新 consumer.go 以创建一个未命名的队列,创建绑定,然后开始消费该队列。

package main

import (
 "context"
 "log"
 "programmingpercy/eventdrivenrabbit/internal"
 "time"

 "golang.org/x/sync/errgroup"
)

func main() {

 conn, err := internal.ConnectRabbitMQ("percy", "secret", "localhost:5672", "customers")
 if err != nil {
  panic(err)
 }

 mqClient, err := internal.NewRabbitMQClient(conn)
 if err != nil {
  panic(err)
 }

 // Create Unnamed Queue which will generate a random name, set AutoDelete to True
 queue, err := mqClient.CreateQueue("", true, true)
 if err != nil {
  panic(err)
 }
 // Create binding between the customer_events exchange and the new Random Queue
 // Can skip Binding key since fanout will skip that rule
 if err := mqClient.CreateBinding(queue.Name, "", "customer_events"); err != nil {
  panic(err)
 }

 messageBus, err := mqClient.Consume(queue.Name, "email-service", false)
 if err != nil {
  panic(err)
 }
 // blocking is used to block forever
 var blocking chan struct{}
 // Set a timeout for 15 secs
 ctx := context.Background()
 ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
 defer cancel()
 // Create an Errgroup to manage concurrecy
 g, ctx := errgroup.WithContext(ctx)
 // Set amount of concurrent tasks
 g.SetLimit(10)
 go func() {
  for message := range messageBus {
   // Spawn a worker
   msg := message
   g.Go(func() error {
    log.Printf("New Message: %v", msg)

    time.Sleep(10 * time.Second)
    // Multiple means that we acknowledge a batch of messages, leave false for now
    if err := msg.Ack(false); err != nil {
     log.Printf("Acknowledged message failed: Retry ? Handle manually %s\n", msg.MessageId)
     return err
    }
    log.Printf("Acknowledged message %s\n", msg.MessageId)
    return nil
   })
  }
 }()

 log.Println("Consuming, to close the program press CTRL+C")
 // This will block forever
 <-blocking

}

这个设置可以用来正确展示 Pub/Sub,我们可以先启动两个消费者,然后是发布者。它将展示所有消费者如何看到所有消息。

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

所有消费者都会接收到消息。

我们现在知道如何使用常规队列和 PubSub。

还有一件事,第三种非常常见的场景是基于 RPC 的范式。

使用 RabbitMQ 的远程过程调用(RPC)

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

RabbitMQ 中的 RPC 使用消息中的 ReplyTo 头部。— 图片由 Percy Bolmer 提供

有时,我们希望在消息上进行一些回调。比如说,生产者希望知道客户何时发送了电子邮件。

这是常见且容易解决的问题。我们可以在消息中设置一个名为 ReplyTo 的字段,这可以用于告诉消费者在特定队列上回复响应。

我们可能需要知道回调与哪个消息相关,因此我们还可以添加 correlationID,以便了解响应与哪个请求相关。

开始创建一个 Direct 类型的新交换机。我会将其命名为 customer_callbacks。Direct 类型在这里效果很好。

docker exec rabbitmq rabbitmqadmin declare exchange --vhost=customers name=customer_callbacks type=direct -u percy -p secret durable=true
docker exec rabbitmq rabbitmqctl set_topic_permissions -p customers percy customer_callbacks ".*" ".*"

我们需要了解的第一件事是目前的一个重要最佳实践。

拥有回调将要求相同的服务既进行发布又进行消费,这没什么问题。

一个著名的规则是,但绝不要在同一连接上进行发布和消费

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

压力回溯可能会阻止 ACK 消息的发送 — 图片由 Percy Bolmer 提供

想象一下,如果你有一个同时进行生产和消费的服务,并且在同一个连接上进行,那么假设服务正在消费大量消息。如果消息数量超过了服务能够处理的范围,消息开始堆积。RabbitMQ 可能会施加回压,开始阻塞 TCP 连接的发送,结果,ACK 消息必须被发送来处理消息。突然间,由于连接被阻塞,你的代码无法发送 ACK 消息。这可能会导致延迟。

黄金规则是

  • 在应用程序中重用连接

  • 一个用于消费,一个用于发布

  • 为每个 Goroutine 创建新的通道

让我们更新 producer.go 来启动两个连接,一个用于发布,一个用于消费。我们还将创建一个未命名的队列并将其绑定到交换机,然后我们将消费这些响应。

我们还将在消息中添加replyTo,这告诉消费者回复的地址,以及correlationId,它解释了消息关联的唯一事件。

package main

import (
 "context"
 "fmt"
 "log"
 "programmingpercy/eventdrivenrabbit/internal"
 "time"

 "github.com/rabbitmq/amqp091-go"
)

func main() {
 conn, err := internal.ConnectRabbitMQ("percy", "secret", "localhost:5672", "customers")
 if err != nil {
  panic(err)
 }
 defer conn.Close()
 // Never use the same Connection for Consume and Publish
 consumeConn, err := internal.ConnectRabbitMQ("percy", "secret", "localhost:5672", "customers")
 if err != nil {
  panic(err)
 }
 defer consumeConn.Close()

 client, err := internal.NewRabbitMQClient(conn)
 if err != nil {
  panic(err)
 }
 defer client.Close()

 consumeClient, err := internal.NewRabbitMQClient(consumeConn)
 if err != nil {
  panic(err)
 }
 defer consumeClient.Close()

 // Create Unnamed Queue which will generate a random name, set AutoDelete to True
 queue, err := consumeClient.CreateQueue("", true, true)
 if err != nil {
  panic(err)
 }

 if err := consumeClient.CreateBinding(queue.Name, queue.Name, "customer_callbacks"); err != nil {
  panic(err)
 }

 messageBus, err := consumeClient.Consume(queue.Name, "customer-api", true)
 if err != nil {
  panic(err)
 }
 go func() {
  for message := range messageBus {
   log.Printf("Message Callback %s\n", message.CorrelationId)
  }
 }()
 // Create context to manage timeout
 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 defer cancel()
 // Create customer from sweden
 for i := 0; i < 10; i++ {
  if err := client.Send(ctx, "customer_events", "customers.created.se", amqp091.Publishing{
   ContentType:  "text/plain",       // The payload we send is plaintext, could be JSON or others..
   DeliveryMode: amqp091.Persistent, // This tells rabbitMQ that this message should be Saved if no resources accepts it before a restart (durable)
   Body:         []byte("An cool message between services"),
   // We add a REPLYTO which defines the
   ReplyTo: queue.Name,
   // CorrelationId can be used to know which Event this relates to
   CorrelationId: fmt.Sprintf("customer_created_%d", i),
  }); err != nil {
   panic(err)
  }
 }
 var blocking chan struct{}

 log.Println("Waiting on Callbacks, to close the program press CTRL+C")
 // This will block forever
 <-blocking
}

消费者需要更新,以便它也使用两个连接。当我们完成处理消息时,我们将其添加到replyTo队列,以便发送响应。再次,我们必须使用两个不同的连接,一个用于消费,另一个用于发布。

package main

import (
 "context"
 "log"
 "programmingpercy/eventdrivenrabbit/internal"
 "time"

 "github.com/rabbitmq/amqp091-go"
 "golang.org/x/sync/errgroup"
)

func main() {

 conn, err := internal.ConnectRabbitMQ("percy", "secret", "localhost:5672", "customers")
 if err != nil {
  panic(err)
 }
 defer conn.Close()

 publishConn, err := internal.ConnectRabbitMQ("percy", "secret", "localhost:5672", "customers")
 if err != nil {
  panic(err)
 }
 defer publishConn.Close()

 mqClient, err := internal.NewRabbitMQClient(conn)
 if err != nil {
  panic(err)
 }

 publishClient, err := internal.NewRabbitMQClient(publishConn)
 if err != nil {
  panic(err)
 }
 // Create Unnamed Queue which will generate a random name, set AutoDelete to True
 queue, err := mqClient.CreateQueue("", true, true)
 if err != nil {
  panic(err)
 }
 // Create binding between the customer_events exchange and the new Random Queue
 // Can skip Binding key since fanout will skip that rule
 if err := mqClient.CreateBinding(queue.Name, "", "customer_events"); err != nil {
  panic(err)
 }

 messageBus, err := mqClient.Consume(queue.Name, "email-service", false)
 if err != nil {
  panic(err)
 }
 // blocking is used to block forever
 var blocking chan struct{}
 // Set a timeout for 15 secs
 ctx := context.Background()
 ctx, cancel := context.WithTimeout(ctx, 15*time.Second)
 defer cancel()
 // Create an Errgroup to manage concurrecy
 g, ctx := errgroup.WithContext(ctx)
 // Set amount of concurrent tasks
 g.SetLimit(10)
 go func() {
  for message := range messageBus {
   // Spawn a worker
   msg := message
   g.Go(func() error {
    // Multiple means that we acknowledge a batch of messages, leave false for now
    if err := msg.Ack(false); err != nil {
     log.Printf("Acknowledged message failed: Retry ? Handle manually %s\n", msg.MessageId)
     return err
    }

    log.Printf("Acknowledged message, replying to %s\n", msg.ReplyTo)

    // Use the msg.ReplyTo to send the message to the proper Queue
    if err := publishClient.Send(ctx, "customer_callbacks", msg.ReplyTo, amqp091.Publishing{
     ContentType:   "text/plain",      // The payload we send is plaintext, could be JSON or others..
     DeliveryMode:  amqp091.Transient, // This tells rabbitMQ to drop messages if restarted
     Body:          []byte("RPC Complete"),
     CorrelationId: msg.CorrelationId,
    }); err != nil {
     panic(err)
    }
    return nil
   })
  }
 }()

 log.Println("Consuming, to close the program press CTRL+C")
 // This will block forever
 <-blocking

}

尝试一下代码,你应该看到生产者接收到 RPC 响应并将其打印出来。

注意这段代码可以进行清理,但本教程重点在于 RabbitMQ 的工作原理,而不是清洁代码。

预取限制——限制发送的消息数量。

记得我们之前通过使用errgroup限制了消费者的工作量吗?这只是一个软限制,由代码施加,但 RabbitMQ 仍然可以向消费者发送更多消息。

还有更好的解决方案,实际上,如果你希望消费者并发处理消息,应该使用组合方案。

AMQP 协议允许我们应用预取限制。这告诉 RabbitMQ 服务器每次可以发送到频道的未确认消息数量。这样我们可以添加一个硬限制。

通过应用一组服务质量规则(QOS)来完成这一点。让我们在rabbitmq.go中添加一个方法,应用这三条可用的规则。

以下是参数:

  • 预取计数——服务器可以发送多少未确认的消息。

  • 预取大小——服务器可以发送多少字节的未确认消息。

  • 全局——一个标志,用于确定规则是否应用于连接或全局。

// ApplyQos is used to apply qouality of service to the channel
// Prefetch count - How many messages the server will try to keep on the Channel
// prefetch Size - How many Bytes the server will try to keep on the channel
// global -- Any other Consumers on the connection in the future will apply the same rules if TRUE
func (rc RabbitClient) ApplyQos(count, size int, global bool) error {
 // Apply Quality of Serivce
 return rc.ch.Qos(
  count,
  size,
  global,
 )
}

然后在consumer.go中,我们可以简单地调用它并应用我们想要允许的消息数量。

 // Create an Errgroup to manage concurrecy
 g, ctx := errgroup.WithContext(ctx)
 // Set amount of concurrent tasks
 g.SetLimit(10)

 // Apply Qos to limit amount of messages to consume
 if err := mqClient.ApplyQos(10, 0, true); err != nil {
  panic(err)
 }
 go func() {
  for message := range messageBus {

使用 TLS 保护连接。

现在是 2023 年,在投入生产之前,我认为我们应该加密流量是非常安全的。

RabbitMQ 有一个 GitHub repository来帮助我们创建 rootCA 和所需的证书,这是加密流量的第一步。

我们需要克隆此存储库并执行内部的 make 文件,以生成所需的文件。

git clone https://github.com/rabbitmq/tls-gen tls-gen
cd tls-gen/basic
make PASSWORD=
make verify

所有生成的文件将出现在一个名为result的新文件夹中。为使其在 Docker 中正常工作,我们需要更改它们的权限。

 sudo chmod 644 tls-gen/basic/result/*

我们需要删除正在运行的 RabbitMQ 容器,我们需要用配置文件创建一个新的容器。

sudo docker container rm -f rabbitmq 

配置文件名为rabbitmq.conf,应放置在容器中的/etc/rabbitmq/rabbitmq.conf内。

这个配置文件不仅可以配置 TLS,但我们现在只讨论 TLS。在项目根目录下创建一个具有正确名称的新文件。

cd ../../ # Go to root of Project
touch rabbitmq.conf

我们需要在启动容器时将配置文件挂载到 Docker 中。我们还将把 TLS-Gen 工具生成的证书挂载到/certs,以便容器可以找到它们。请注意,这两个端口都减少了一,以符合 RabbitMQ 协议的标准。

docker run -d --name rabbitmq -v "$(pwd)"/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro -v "$(pwd)"/tls-gen/basic/result:/certs -p 5671:5671 -p 15671:15671 rabbitmq:3.11-management

一旦完成,我们可以开始将 TLS 配置添加到这个容器中。

rabbitmq.conf中添加证书和根 CA 的路径。我的计算机名为blackbox,你需要将证书名称替换为你计算机生成的名称。

# Disable NON TCP
listeners.tcp = none
# TCP port
listeners.ssl.default = 5671
# SSL Certs
ssl_options.cacertfile = /certs/ca_certificate.pem
ssl_options.certfile   = /certs/server_blackbox_certificate.pem
ssl_options.keyfile    = /certs/server_blackbox_key.pem
# Peer verification
ssl_options.verify     = verify_peer
ssl_options.fail_if_no_peer_cert = true

然后重新启动 RabbitMQ。

docker restart rabbitmq

为了验证一切是否正常工作,你可以使用docker logs rabbitmq查看 Docker 日志。搜索有关监听器的日志。

2023-02-19 07:35:15.566316+00:00 [info] <0.738.0> Ready to start client connection listeners
2023-02-19 07:35:15.567418+00:00 [info] <0.885.0> started TLS (SSL) listener on [::]:5671

现在,旧程序将无法再工作。它尝试在没有 TLS 的情况下进行连接,所以我们来修复一下。

程序需要更新以使用客户端证书。我们将其作为输入添加到ConnectRabbitMQ函数中。

// ConnectRabbitMQ will spawn a Connection
func ConnectRabbitMQ(username, password, host, vhost, caCert, clientCert, clientKey string) (*amqp.Connection, error) {
 ca, err := os.ReadFile(caCert)
 if err != nil {
  return nil, err
 }
 // Load the key pair
 cert, err := tls.LoadX509KeyPair(clientCert, clientKey)
 if err != nil {
  return nil, err
 }
 // Add the CA to the cert pool
 rootCAs := x509.NewCertPool()
 rootCAs.AppendCertsFromPEM(ca)

 tlsConf := &tls.Config{
  RootCAs:      rootCAs,
  Certificates: []tls.Certificate{cert},
 }
 // Setup the Connection to RabbitMQ host using AMQPs and Apply TLS config
 conn, err := amqp.DialTLS(fmt.Sprintf("amqps://%s:%s@%s/%s", username, password, host, vhost), tlsConf)
 if err != nil {
  return nil, err
 }
 return conn, nil
}

请注意,我们现在使用的是amqps协议。证书路径是绝对路径,我们需要更新consumerproducer以插入这些路径,我现在会使用硬编码的值,但在实际应用中你不应该这样做。

 conn, err := internal.ConnectRabbitMQ("percy", "secret", "localhost:5671", "customers",
  "/home/pp/development/blog/event-driven-rabbitmq/tls-gen/basic/result/ca_certificate.pem",
  "/home/pp/development/blog/event-driven-rabbitmq/tls-gen/basic/result/client_blackbox_certificate.pem",
  "/home/pp/development/blog/event-driven-rabbitmq/tls-gen/basic/result/client_blackbox_key.pem",
 )
 if err != nil {
  panic(err)
 }
 defer conn.Close()
 // Never use the same Connection for Consume and Publish
 consumeConn, err := internal.ConnectRabbitMQ("percy", "secret", "localhost:5671", "customers",
  "/home/pp/development/blog/event-driven-rabbitmq/tls-gen/basic/result/ca_certificate.pem",
  "/home/pp/development/blog/event-driven-rabbitmq/tls-gen/basic/result/client_blackbox_certificate.pem",
  "/home/pp/development/blog/event-driven-rabbitmq/tls-gen/basic/result/client_blackbox_key.pem",
 )
 defer consumeConn.Close()

BAM!太棒了,我们有了 TLS。

尝试运行生产者或消费者,然后使用docker logs rabbitmq查看 Docker 日志。

2023-02-19 07:49:53.015732+00:00 [error] <0.948.0> Error on AMQP connection <0.948.0> (172.17.0.1:49066 -> 172.17.0.2:5671, state: starting):
2023-02-19 07:49:53.015732+00:00 [error] <0.948.0> PLAIN login refused: user 'percy' - invalid credentials

对,删除 Docker 时我们删除了虚拟主机、用户、交换机以及所有内容,因为我们没有持久化存储。

这很好,因为这将引导我们进入本教程的下一步也是最后一步,默认配置。

RabbitMQ 配置与管理

相信我,你不想使用 AdminCLI 来管理多个用户的 RabbitMQ,因为如果你因为某些原因重置集群,这将是重复的繁重工作。

支持插入定义文件、定义用户、虚拟主机、权限、队列和交换机的 JSON 文件,甚至是绑定。

它们真的很容易使用,让我们添加我的旧用户,并赋予其在customers虚拟主机上读写权限,添加一个基本交换机。

在此之前,我们需要一个密码哈希,这可能比想象的要复杂。它取决于你拥有的 RabbitMQ 设置以及你配置的算法。默认的是 SHA256。

我在stackoverflow上找到了一个很棒的 bash 脚本来为我生成它。创建一个名为encodepassword.sh的文件,并将secret替换为你要编码的密码。

#!/bin/bash

function encode_password()
{
    SALT=$(od -A n -t x -N 4 /dev/urandom)
    PASS=$SALT$(echo -n $1 | xxd -ps | tr -d '\n' | tr -d ' ')
    PASS=$(echo -n $PASS | xxd -r -p | sha256sum | head -c 128)
    PASS=$(echo -n $SALT$PASS | xxd -r -p | base64 | tr -d '\n')
    echo $PASS
}

encode_password "secret"

运行脚本bash encodepassword.sh并存储 Hash。

更新rabbitmq.conf以包含load_definitions字段,这个字段可以在启动时加载定义文件。

log.console = true
# Disable NON TCP
listeners.tcp = none
# TCP port
listeners.ssl.default = 5671
# SSL Certs
ssl_options.cacertfile = /certs/ca_certificate.pem
ssl_options.certfile   = /certs/server_blackbox_certificate.pem
ssl_options.keyfile    = /certs/server_blackbox_key.pem
# Peer verification
ssl_options.verify     = verify_peer
ssl_options.fail_if_no_peer_cert = true
# Load definitions file
load_definitions = /etc/rabbitmq/rabbitmq_definitions.json

我会指向一个名为/etc/rabbitmq/rabbitmq_definitions.json的文件。

在项目根目录下创建一个名为 rabbitmq_definitions.json 的文件,并用以下 JSON 填充它。目前,我认为我们不需要详细讲解 JSON 字段,一切应该是可以理解的。它与我们之前运行的 CLI 命令非常相似。

以下定义文件创建了两个交换机:customer_eventscustomer_callbacks。当前代码会生成自己的队列,因此我们仅在示例中定义一个以便于理解。

{
    "users": [
        {
            "name": "percy",
            "password_hash": "dPOoDgfw31kjUy41HSmqQR+X2Q9PCA5fD++fbxQCgPvKZmnX",
            "tags": "administrator"
        }
    ],
    "vhosts": [
        {
            "name": "/"
        },{
            "name": "customers"
        }
    ],
    "permissions": [
        {
            "user": "percy",
            "vhost": "customers",
            "configure": ".*",
            "write": ".*",
            "read": ".*"
        }
    ],
    "exchanges": [
        {
            "name": "customer_events",
            "vhost": "customers",
            "type": "fanout",
            "durable": true,
            "auto_delete": false,
            "internal": false,
            "arguments": {}
        },
        {
            "name": "customer_callbacks",
            "vhost": "customers",
            "type": "direct",
            "durable": true,
            "auto_delete": false,
            "internal": false,
            "arguments": {}
        }
    ],
    "queues": [
        {
            "name": "customers_created",
            "vhost": "customers",
            "durable": true,
            "auto_delete": false,
            "arguments": {}
        }
    ],
    "bindings": [
        {
            "source": "customers_events",
            "vhost": "customers",
            "destination": "customers_created",
            "destination_type": "queue",
            "routing_key": "customers.created.*",
            "arguments": {}
        }
    ]
}

一旦两个文件都到位,删除旧的 Docker,并重启一个新的,但这次我们为定义添加了第三个挂载点。

docker run -d --name rabbitmq -v "$(pwd)"/rabbitmq_definitions.json:/etc/rabbitmq/rabbitmq_definitions.json:ro -v "$(pwd)"/rabbitmq.conf:/etc/rabbitmq/rabbitmq.conf:ro -v "$(pwd)"/tls-gen/basic/result:/certs -p 5671:5671 -p 15672:15672 rabbitmq:3.11-management

运行后,检查日志,确认它们打印了创建用户的相关信息。

2023-02-19 08:17:53.467218+00:00 [info] <0.867.0> Started message store of type persistent for vhost 'customers'
2023-02-19 08:17:53.467310+00:00 [info] <0.867.0> Recovering 0 queues of type rabbit_classic_queue took 3ms
2023-02-19 08:17:53.467348+00:00 [info] <0.867.0> Recovering 0 queues of type rabbit_quorum_queue took 0ms
2023-02-19 08:17:53.467371+00:00 [info] <0.867.0> Recovering 0 queues of type rabbit_stream_queue took 0ms
2023-02-19 08:17:53.468487+00:00 [info] <0.698.0> Importing concurrently 1 permissions...
2023-02-19 08:17:53.469946+00:00 [info] <0.680.0> Successfully set permissions for 'percy' in virtual host 'customers' to '.*', '.*', '.*'

完成这些后,尝试运行消费者和生产者,你应该会看到一切按预期工作。唯一的不同是,我们现在使用配置在 RabbitMQ 中创建基础设施,而不是使用 CLI,并且流量是加密的。

结论

很遗憾,这个漫长但令人兴奋的 RabbitMQ 冒险到此结束。

让我们回顾一下我们学到的内容。

我们已经学习了如何用虚拟主机配置 RabbitMQ,以及如何在这些虚拟主机上创建具有权限的用户。

我们还学习了如何在队列和交换机上生产和消费消息。

你应该对所有资源,如队列、交换机和绑定有一定了解。

我们还涵盖了如何创建发布和订阅模式、RPC 模式以及常规工作队列。

希望你已经清楚如何使用连接和通道以及它们之间的区别。连接是一个 TCP 连接,而通道是在连接上的复用虚拟通道。在同一个软件中重用连接,但为每个并行进程创建新的通道。

我们了解到永远不要在同一个连接上进行生产和消费。

我们还涵盖了如何设置 TLS 以及如何为 RabbitMQ 添加预定义配置的定义。

我真的希望你喜欢这个教程,你可以在 GitHub 上找到所有使用的代码。

随时向我提问!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值