自然语言处理实战第二版(MEAP)(五)(3)https://developer.aliyun.com/article/1519664
注意
因果语言模型的设计是为了模拟人类大脑模型在阅读和书写文本时的工作方式。在你对英语的心理模型中,每个词都与你左到右移动时说或打的下一个词有因果关系。你不能回去修改你已经说过的词……除非你在用键盘说话。而我们经常使用键盘。这使我们形成了跳跃阅读或撰写句子时可以左右跳跃的心理模型。也许如果我们所有人都被训练成像 BERT 那样预测被屏蔽的单词,我们会有一个不同(可能更有效)的阅读和书写文本的心理模型。速读训练会使一些人在尽可能快地阅读和理解几个词的文本时,学会一次性读懂几个单词。那些将内部语言模型学习方式与典型人不同的人可能会在阅读或书写文本时开发出在心里从一个词跳到另一个词的能力。也许有阅读困难或自闭症症状的人的语言模型与他们学习语言的方式有关。也许神经非常规脑中的语言模型(以及速读者)更类似于 BERT(双向),而不是 GPT(从左到右)。
现在你已经准备好开始训练了!你可以使用你的整理器和训练参数来配置训练,并将其应用于你的数据。
列表 10.12 使用 HuggingFace 的 Trainer 类微调 GPT-2
>>> from transformers import Trainer >>> ft_model = GPT2LMHeadModel.from_pretrained("gpt2") # #1 >>> trainer = Trainer( ... ft_model, ... training_args, ... data_collator=collator, # #2 ... train_dataset=train_dataset, # #3 ... eval_dataset=eval_dataset) >>> trainer.train()
这次训练运行在 CPU 上可能需要几个小时。所以如果你可以访问 GPU,你可能想在那里训练你的模型。在 GPU 上训练应该会快大约 100 倍。
当然,在使用现成的类和预设时存在一种权衡——它会使你在训练方式上的可见性降低,并且使得调整参数以提高性能更加困难。作为一个可带回家的任务,看看你是否可以用 PyTorch 例程以老方法训练模型。
现在让我们看看我们的模型表现如何!
>>> generate(model=ft_model, tokenizer=tokenizer, ... prompt='NLP is') NLP is not the only way to express ideas and understand ideas.
好的,那看起来像是这本书中可能会出现的句子。一起看看两种不同模型的结果,看看你的微调对 LLM 生成的文本有多大影响。
>>> print(generate(prompt="Neural networks", model=vanilla_gpt2, tokenizer=tokenizer, **nucleus_sampling_args)) Neural networks in our species rely heavily on these networks to understand their role in their environments, including the biological evolution of language and communication... >>> print(generate(prompt="Neural networks", model=ft_model, tokenizer=tokenizer, **nucleus_sampling_args)) Neural networks are often referred to as "neuromorphic" computing because they mimic or simulate the behavior of other human brains. footnote:...
看起来差别还是挺大的!普通模型将术语“神经网络”解释为其生物学内涵,而经过微调的模型意识到我们更有可能在询问人工神经网络。实际上,经过微调的模型生成的句子与第七章的一句话非常相似:
神经网络通常被称为“神经形态”计算,因为它们模仿或模拟我们大脑中发生的事情。
然而,有一点细微的差别。注意“其他人类大脑”的结束。看起来我们的模型并没有完全意识到它在谈论人工神经网络,而不是人类神经网络,所以结尾没有意义。这再次表明,生成模型实际上并没有对世界建模,或者说“理解”它所说的话。它所做的只是预测序列中的下一个词。也许现在你可以看到为什么即使像 GPT-2 这样相当大的语言模型也不是很聪明,并且经常会生成无意义的内容。
10.1.8 无意义(幻觉)
随着语言模型的规模越来越大,它们听起来越来越好。但即使是最大的 LLMs 也会生成大量无意义的内容。对于训练它们的专家来说,缺乏“常识”应该不足为奇。LLMs 没有被训练利用传感器(如摄像头和麦克风)来将它们的语言模型扎根于物理世界的现实之中。一个具有身体感知的机器人可能能够通过检查周围真实世界中的感知来将自己扎根于现实之中。每当现实世界与那些错误规则相矛盾时,它都可以更正自己的常识逻辑规则。甚至看似抽象的逻辑概念,如加法,在现实世界中也有影响。一个扎根的语言模型应该能够更好地进行计数和加法。
就像一个学习行走和说话的婴儿一样,LLMs 可以通过让它们感觉到自己的假设不正确来从错误中学习。如果一个具有身体感知的人工智能犯了 LLMs 那样的常识性错误,它将无法存活很长时间。一个只在互联网上消费和产生文本的 LLM 没有机会从现实世界中的错误中学习。LLM“生活”在社交媒体的世界中,事实和幻想常常难以分辨。
即使是规模最大的万亿参数变压器也会生成无意义的响应。扩大无意义的训练数据也无济于事。最大且最著名的大型语言模型(LLMs)基本上是在整个互联网上进行训练的,这只会改善它们的语法和词汇量,而不是它们的推理能力。一些工程师和研究人员将这些无意义的文本描述为幻觉。但这是一个误称,会使你在试图从 LLMs 中得到一些一贯有用的东西时误入歧途。LLM 甚至不能幻想,因为它不能思考,更不用说推理或拥有现实的心智模型了。
幻觉发生在一个人无法将想象中的图像或文字与他们所生活的世界的现实分开时。但 LLM 没有现实感,从来没有生活在现实世界中。你在互联网上使用的 LLM 从未被体现在机器人中。它从未因错误而遭受后果。它不能思考,也不能推理。因此,它不能产生幻觉。
LLMs 对真相、事实、正确性或现实没有概念。你在网上与之交互的 LLMs“生活”在互联网虚幻的世界中。工程师们为它们提供了来自小说和非小说来源的文本。如果你花费大量时间探索 LLMs 知道的内容,你很快就会感受到像 ChatGPT 这样的模型是多么不踏实。起初,你可能会对它对你问题的回答有多么令人信服和合理感到惊讶。这可能会导致你赋予它人格化。你可能会声称它的推理能力是研究人员没有预料到的“ emergent ”属性。而你说得对。BigTech 的研究人员甚至没有开始尝试训练 LLMs 进行推理。他们希望,如果他们为 LLMs 提供足够的计算能力和阅读的文本,推理能力将会神奇地出现。研究人员希望通过为 LLMs 提供足够的对真实世界的描述来抄近道,从而避免 AI 与物理世界互动的必要性。不幸的是,他们也让 LLMs 接触到了同等或更多的幻想。在线找到的大部分文本要么是小说,要么是有意误导的。
因此,研究人员对于捷径的希望是错误的。LLMs 只学到了它们所教的东西——预测序列中最合理的下一个词。通过使用点赞按钮通过强化学习来引导 LLMs,BigTech 创建了一个 BS 艺术家,而不是他们声称要构建的诚实透明的虚拟助手。就像社交媒体上的点赞按钮把许多人变成了轰动的吹牛者一样,它们把 LLMs 变成了“影响者”,吸引了超过 1 亿用户的注意力。然而,LLMs 没有能力或动机(目标函数)来帮助它们区分事实和虚构。为了提高机器回答的相关性和准确性,你需要提高grounding模型的能力——让它们的回答基于相关的事实和知识。
幸运的是,有一些经过时间考验的技术可以激励生成模型达到正确性。知识图谱上的信息提取和逻辑推理是非常成熟的技术。而且大部分最大、最好的事实知识库都是完全开放源代码的。BigTech 无法吸收并摧毁它们所有。尽管开源知识库 FreeBase 已经被摧毁,但 Wikipedia、Wikidata 和 OpenCyc 仍然存在。在下一章中,你将学习如何使用这些知识图谱来让你的 LLMs 接触现实,这样至少它们就不会像大多数 BigTech 的 LLMs 那样有欺骗性。
在下一节中,你将学习另一种让你的 LLM 接触现实的方法。而这个新工具不需要你手动构建和验证知识图谱。即使你每天都在使用它,你可能已经忘记了这个工具。它被称为信息检索,或者只是搜索。你可以在实时搜索非结构化文本文档中的事实,而不是给模型提供关于世界的事实知识库。
10.2 使用搜索功能来提升 LLMs 的智商
大型语言模型最强大的特点之一是它会回答你提出的任何问题。但这也是它最危险的特点。如果你将 LLM 用于信息检索(搜索),你无法判断它的答案是否正确。LLMs 并不是为信息检索而设计的。即使你想让它们记住读过的所有内容,你也无法构建一个足够大的神经网络来存储所有的信息。LLMs 将它们读到的所有内容进行压缩,并将其存储在深度学习神经网络的权重中。而且就像常规的压缩算法(例如“zip”)一样,这个压缩过程会迫使 LLM 对它在训练时看到的单词模式进行概括。
解决这个古老的压缩和概括问题的答案就是信息检索的古老概念。如果将 LLMs 的词语处理能力与一个搜索引擎的传统信息检索能力相结合,那么你可以构建更快、更好、更便宜的 LLMs。在下一节中,你将看到如何使用你在第三章学到的 TF-IDF 向量来构建一个搜索引擎。你将学习如何将全文搜索方法扩展到数百万个文档。之后,你还将看到如何利用 LLMs 来提高搜索引擎的准确性,通过基于语义向量(嵌入)帮助你找到更相关的文档。在本章结束时,你将知道如何结合这三个必需的算法来创建一个能够智能回答问题的自然语言处理流水线:文本搜索、语义搜索和 LLM。你需要文本搜索的规模和速度,结合语义搜索的准确性和召回率,才能构建一个有用的问答流水线。
10.2.1 搜索词语:全文搜索
导航到互联网浩瀚的世界中寻找准确的信息常常感觉就像是一次费力的探险。这也是因为,越来越多的互联网文本并非由人类撰写,而是由机器生成的。由于机器在创建新的信息所需要的人力资源的限制,互联网上的文本数量呈指数级增长。生成误导性或无意义文本并不需要恶意行为。正如你在之前的章节中所看到的,机器的目标函数与你最佳利益并不一致。机器生成的大部分文本都包含误导性信息,旨在吸引你点击,而不是帮助你发现新知识或完善自己的思考。
幸运的是,就像机器用来创建误导性文本一样,它们也可以成为你寻找准确信息的盟友。使用你们学到的工具,你可以通过使用开源模型和从互联网高质量来源或自己的图书馆检索的人工撰写文本,在所使用的 LLMs 中掌控。使用机器辅助搜索的想法几乎与万维网本身一样古老。虽然在它的开端,WWW 是由它的创建者 Tim Berners-Lee 手动索引的,^([[27]) 但在 HTTP 协议向公众发布后,这再也不可行了。
由于人们需要查找与关键词相关的信息,全文搜索 很快就开始出现。索引,尤其是反向索引,是帮助这种搜索变得快速和高效的关键。反向索引的工作方式类似于你在教科书中查找主题的方式——查看书末的索引并找到提到该主题的页码。
第一个全文搜索索引只是编目了每个网页上的单词以及它们在页面上的位置,以帮助查找确切匹配所查关键词的页面。然而,你可以想象,这种索引方法非常有限。例如,如果你正在查找单词“猫”,但页面只提到了“猫咪”,则不会在搜索结果中出现。这就是为什么现代的全文搜索引擎使用基于字符的三元组索引,以帮助你找到不管你输入搜索栏中的任何内容或 LLM 聊天机器人提示都能搜到的“猫”和“猫咪”。
Web 规模的反向索引
随着互联网的发展,越来越多的组织开始拥有自己的内部网络,并寻找在其中高效地查找信息的方法。这催生了企业搜索领域,以及像 Apache Lucene ^([28]),Solr ^([29]) 和 OpenSearch 等搜索引擎库。
在该领域中的一个(相对)新的参与者,Meilisearch ^([30]) 提供了一款易于使用和部署的搜索引擎。因此,它可能比其他更复杂的引擎成为你在全文搜索世界中开始旅程的更好起点。
Apache Solr、Typesense、Meilisearch 等全文搜索引擎快速且能很好地扩展到大量文档。Apache Solr 可以扩展到整个互联网。它是 DuckDuckGo 和 Netflix 搜索栏背后的引擎。传统搜索引擎甚至可以随输入实时返回结果。随输入实时功能比您可能在网络浏览器中看到的自动完成或搜索建议更令人印象深刻。Meilisearch 和 Typesense 如此快速,它们可以在毫秒内为您提供前 10 个搜索结果,每次键入新字符时对列表进行排序和重新填充。但全文搜索有一个弱点 - 它搜索文本匹配而不是语义匹配。因此,传统搜索引擎在您的查询中的单词不出现在您要查找的文档中时会返回很多"假阴性"。
使用三元组索引改进您的全文搜索
我们在前一节介绍的逆向索引对于找到单词的精确匹配非常有用,但并不适合找到近似匹配。词干处理和词形还原可以帮助增加同一个词不同形式的匹配;然而,当您的搜索包含拼写错误或拼写错误时会发生什么?
举个例子 - 玛丽亚可能在网上搜索著名作家斯蒂芬·金的传记。如果她使用的搜索引擎使用常规的逆向索引,她可能永远找不到她要找的东西 - 因为金的名字拼写为斯蒂芬。这就是三元组索引派上用场的地方。
三元组是单词中三个连续字符的组合。例如,单词"trigram"包含三元组"tri"、“rig”、“igr”、“gra"和"ram”。事实证明,三元组相似性 - 基于它们共有的三元组数量比较两个单词 - 是一种寻找单词近似匹配的好方法。从 Elasticsearch 到 PostgreSQL,多个数据库和搜索引擎都支持三元组索引。这些三元组索引比词干处理和词形还原更有效地处理拼写错误和不同的单词形式。三元组索引将提高你的搜索结果的召回率和精确度。
语义搜索允许您在您无法想起作者写文本时使用的确切单词时找到您要找的内容。例如,想象一下,您正在搜索关于"大猫"的文章。如果语料库包含关于狮子、老虎(还有熊),但从未提到"猫"这个词,您的搜索查询将不返回任何文档。这会在搜索算法中产生一个假阴性错误,并降低您的搜索引擎的总召回率,这是搜索引擎性能的一个关键指标。如果您正在寻找需要用很多词语描述的微妙信息,比如查询"I want a search algorithm with high precision, recall, and it needs to be fast.",问题会变得更加严重。
下面是另一个全文搜索无法帮助的场景——假设你有一个电影情节数据库,你试图找到一个你模糊记得情节的电影。如果你记得演员的名字,你可能会有些幸运——但是如果你输入类似于“不同的团体花了 9 小时返回珠宝”的内容,你不太可能收到“指环王”作为搜索结果的一部分。
最后,全文搜索算法没有利用 LLM 提供的新的更好的嵌入单词和句子的方法。BERT 嵌入在反映处理文本意义方面要好得多。即使文档使用不同的词来描述类似的事物,谈论相同事物的文本段落的语义相似性也会在这些密集嵌入中显示出来。
要使你的 LLM 真正有用,你确实需要这些语义能力。在 ChatGPT、You.com 或 Phind 等热门应用中,大型语言模型在幕后使用语义搜索。原始 LLM 对你以前说过的任何事情都没有记忆。它完全是无状态的。每次问它问题时,你都必须给它一个问题的前提。例如,当你向 LLM 问一个关于你先前在对话中提到的内容的问题时,除非它以某种方式保存了对话,否则 LLM 无法回答你。
10.2.2 搜索含义:语义搜索
帮助你的 LLM 的关键是找到一些相关的文本段落包含在你的提示中。这就是语义搜索的用武之地。
不幸的是,语义搜索比文本搜索要复杂得多。
你在第三章学习了如何比较稀疏二进制(0 或 1)向量,这些向量告诉你每个单词是否在特定文档中。在前一节中,你了解了几种可以非常有效地搜索这些稀疏二进制向量的数据库,即使对于数百万个文档也是如此。你总是能够找到包含你要查找的单词的确切文档。PostgreSQL 和传统搜索引擎从一开始就具有这个功能。在内部,它们甚至可以使用像Bloom 过滤器这样的花哨的数学方法来最小化你的搜索引擎需要进行的二进制比较的数量。不幸的是,对于文本搜索所使用的稀疏离散向量来说看似神奇的算法不适用于 LLM 的密集嵌入向量。
要实现可扩展的语义搜索引擎,你可以采用什么方法?你可以使用蛮力法,对数据库中的所有向量进行点积计算。尽管这样可以给出最准确的答案,但会花费大量时间(计算)。更糟糕的是,随着添加更多文档,你的搜索引擎会变得越来越慢。蛮力方法随着数据库中文档数量的增加呈线性扩展。
不幸的是,如果你希望你的 LLM 运作良好,你将需要向数据库中添加大量文档。当你将 LLMs 用于问答和语义搜索时,它们一次只能处理几个句子。因此,如果你希望通过 LLM 管道获得良好的结果,你将需要将数据库中的所有文档拆分成段落,甚至句子。这会导致你需要搜索的向量数量激增。蛮力方法行不通,也没有任何神奇的数学方法可以应用于密集连续向量。
这就是为什么你需要在武器库中拥有强大的搜索工具。向量数据库是解决这一具有挑战性的语义搜索问题的答案。向量数据库正在推动新一代搜索引擎的发展,即使你需要搜索整个互联网,也能快速找到你正在寻找的信息。但在此之前,让我们先来了解搜索的基础知识。
现在让我们将问题从全文搜索重新构想为语义搜索。你有一个搜索查询,可以使用 LLM 嵌入。你还有你的文本文档数据库,其中你已经使用相同的 LLM 将每个文档嵌入到一个向量空间中。在这些向量中,你想找到最接近查询向量的向量 — 也就是,余弦相似度(点积)最大化。
10.2.3 近似最近邻搜索
找到我们查询的 精确 最近邻的唯一方法是什么?还记得我们在第四章讨论过穷举搜索吗?当时,我们通过计算搜索查询与数据库中的每个向量的点积来找到搜索查询的最近邻。那时还可以,因为当时你的数据库只包含几十个向量。这种方法不适用于包含数千或数百万个文档的数据库。而且你的向量是高维的 — BERT 的句子嵌入有 768 个维度。这意味着你想对向量进行的任何数学运算都会受到维度诅咒的影响。而 LLM 的嵌入甚至更大,所以如果你使用比 BERT 更大的模型,这个诅咒会变得更糟。你不会希望维基百科的用户在你对 600 万篇文章进行点积运算时等待!
就像在现实世界中经常发生的那样,你需要付出一些东西才能得到一些东西。如果你想优化算法的检索速度,你就需要在精度上做出妥协。就像你在第四章看到的那样,你不需要做太多妥协,而且找到几个近似的邻居实际上对你的用户可能有用,并增加他们找到他们想要的东西的机会。
在第四章中,你已经看到了一种名为局部敏感哈希(LSH)的算法,它通过为高维空间(超空间)中你的嵌入所在的区域分配哈希来帮助你寻找近似最近邻的向量。LSH 是一个近似 k-最近邻(ANN)算法,既负责索引你的向量,也负责检索你正在寻找的邻居。但你将遇到的还有许多其他算法,每种算法都有其优势和劣势。
要创建你的语义搜索管道,你需要做出两个关键选择——使用哪个模型来创建你的嵌入,并选择使用哪个 ANN 索引算法。你已经在本章中看到了 LLM 如何帮助你提高向量嵌入的准确性。因此,主要剩下的决定是如何索引你的向量。
如果你正在构建一个需要扩展到数千或数百万用户的生产级应用程序,你可能会寻找托管的向量数据库实现,如 Pinecone、Milvus 或 OpenSearch。托管方案将使你能够快速准确地存储和检索语义向量,从而为用户提供愉悦的用户体验。而提供商将管理扩展你的向量数据库的复杂性,随着你的应用程序越来越受欢迎。
但你可能更感兴趣的是如何启动自己的向量搜索管道。事实证明,即使对于拥有数百万个向量(文档)的数据库,你自己也可以轻松完成这项任务。
10.2.4 选择索引
随着在越来越大的数据集中查找信息的需求不断增加,ANN 算法的领域也迅速发展。近期几乎每个月都有向量数据库产品推出。而且你可能很幸运,你的关系型或文档型数据库已经开始发布内置的向量搜索算法早期版本。
如果你在生产数据库中使用 PostgreSQL,你很幸运。他们在 2023 年 7 月发布了 pgvector
插件,为你提供了一种无缝的方式来在数据库中存储和索引向量。他们提供精确和近似相似性搜索索引,因此你可以在应用中尝试适合你的准确性和速度之间的权衡。如果你将此与 PostgreSQL 的高效和可靠的全文搜索索引相结合,很可能可以将你的 NLP 管道扩展到数百万用户和文档。^([31])
不幸的是,在撰写本文时,pgvector
软件尚处于早期阶段。在 2023 年 9 月,pgvector
中的 ANN 向量搜索功能在速度排名中处于最低四分之一。而且你将被限制在两千维的嵌入向量上。因此,如果你要对几个嵌入的序列进行索引,或者你正在使用来自大型语言模型的高维向量,你将需要在流水线中添加一个降维步骤(例如 PCA)。
LSH 是在 2000 年代初开发的;从那时起,数十种算法加入了近似最近邻(ANN)家族。ANN 算法有几个较大的家族。我们将看看其中的三个 - 基于哈希、基于树和基于图。
基于哈希的算法最好的代表是 LSH 本身。你已经在第四章看到了 LSH 中索引的工作原理,所以我们在这里不会花时间解释它。尽管其简单性,LSH 算法仍然被广泛应用于流行的库中,例如 Faiss(Facebook AI 相似搜索),我们稍后将使用它。[³²] 它还衍生出了针对特定目标的修改版本,例如用于搜索生物数据集的 DenseFly 算法。[³³]
要理解基于树的算法如何工作,让我们看看 Annoy,这是 Spotify 为其音乐推荐创建的一个包。Annoy 算法使用二叉树结构将输入空间递归地划分为越来越小的子空间。在树的每个级别,算法选择一个超平面,将剩余的点划分为两组。最终,每个数据点都被分配到树的叶节点上。
要搜索查询点的最近邻居,算法从树的根部开始,并通过比较查询点到每个节点的超平面的距离和迄今为止找到的最近点的距离之间的距离来下降。算法越深入,搜索越精确。因此,你可以使搜索更短但不太准确。你可以在图 10.3 中看到算法的简化可视化。
图 10.3 Annoy 算法的简化可视化
接下来,让我们看看基于图的算法。图算法的良好代表,分层可导航小世界(HNSW)4 算法,是自下而上地解决问题。它首先构建可导航小世界图,这是一种图,其中每个向量都通过一个顶点与它最接近的邻居相连。要理解它的直觉,想想 Facebook 的连接图 - 每个人只与他们的朋友直接连接,但如果您计算任意两人之间的“分离度”,实际上相当小。(Stanley Milgram 在 1960 年代的一项实验中发现,平均每两个人之间相隔 5 个连接。5 如今,对于 Twitter 用户,这个数字低至 3.5。)
然后,HNSW 将 NSW 图分成层,每一层包含比它更远的少量点。要找到最近的邻居,您将从顶部开始遍历图,每一层都让您接近您要寻找的点。这有点像国际旅行。您首先乘飞机到您要去的国家首都。然后您乘火车去更接近目的地的小城市。您甚至可以骑自行车到达那里!在每一层,您都在接近您的最近邻居 - 根据您的用例需求,您可以在任何层停止检索。
10.2.5 数字化数学
您可能会听说 量化 与其他索引技术结合使用。本质上,量化基本上是将向量中的值转换为具有离散值(整数)的低精度向量。这样,您的查询可以寻找整数值的精确匹配,这比搜索浮点数范围的值要快得多。
想象一下,你有一个以 64 位 float
数组存储的 5D 嵌入向量。下面是一个将 numpy
浮点数进行量化的简单方法。
列表 10.13 数值化 numpy 浮点数
>>> import numpy as np >>> v = np.array([1.1, 2.22, 3.333, 4.4444, 5.55555]) >>> type(v[0]) numpy.float64 >>> (v * 1_000_000).astype(np.int32) array([1100000, 2220000, 3333000, 4444400, 5555550], dtype=int32) >>> v = (v * 1_000_000).astype(np.int32) # #1 >>> v = (v + v) // 2 >>> v / 1_000_000 array([1.1 , 2.22 , 3.333 , 4.4444 , 5.55555]) # #2
如果您的索引器正确进行了缩放和整数运算,您可以只用一半的空间保留所有原始向量的精度。通过将您的向量量化(取整),您将搜索空间减少了一半,创建了 32 位整数桶。更重要的是,如果您的索引和查询算法通过整数而不是浮点数进行艰苦工作,它们运行得快得多,通常快 100 倍。如果您再量化一点,只保留 16 位信息,您可以再获得一个数量级的计算和内存需求。
>>> v = np.array([1.1, 2.22, 3.333, 4.4444, 5.55555]) >>> v = (v * 10_000).astype(np.int16) # #1 >>> v = (v + v) // 2 >>> v / 10_000 array([ 1.1 , -1.0568, 0.0562, 1.1676, -0.9981]) # #2 >>> v = np.array([1.1, 2.22, 3.333, 4.4444, 5.55555]) >>> v = (v * 1_000).astype(np.int16) # #3 >>> v = (v + v) // 2 >>> v / 1_000 array([1.1 , 2.22 , 3.333, 4.444, 5.555]) # #4
用于实现语义搜索的产品量化需要比这更加复杂。您需要压缩的向量更长(具有更多维度),压缩算法需要更好地保留向量中的所有微妙信息。这对于抄袭检测和 LLM 检测器尤其重要。事实证明,如果将文档向量分成多个较小的向量,并且每个向量都使用聚类算法进行量化,则可以更多地了解量化过程。^([36])
如果您继续探索最近邻算法的领域,可能会遇到 IVFPQ(带有产品量化的反向文件索引)的缩写。Faiss 库使用 IVFPQ 来处理高维向量。^([37])并且直到 2023 年,HNSW+PQ 的组合方式被像 Weaviate 这样的框架采用。^([38])因此,对于许多面向网络规模的应用程序而言,这绝对是最先进的技术。
融合了许多不同算法的索引被称为组合索引。组合索引在实现和使用上稍微复杂一些。搜索和索引的性能(响应时间、吞吐量和资源限制)对索引流程的各个阶段配置非常敏感。如果配置不正确,它们的性能可能比简单的矢量搜索和索引流程差得多。为什么要增加这么多复杂性呢?
主要原因是内存(RAM 和 GPU 内存大小)。如果您的向量是高维的,那么计算点积不仅是一个非常昂贵的操作,而且您的向量在内存中占用更多的空间(在 GPU 或 RAM 上)。即使您只将数据库的一小部分加载到 RAM 中,也可能发生内存溢出。这就是为什么常常使用诸如 PQ 之类的技术,在将向量馈送到 IVF 或 HNSW 等其他索引算法之前对其进行压缩的原因。
对于大多数实际应用程序,当您没有尝试对整个互联网进行索引时,您可以使用更简单的索引算法。您还可以始终使用内存映射库高效地处理存储在磁盘上的数据表,特别是闪存驱动器(固态硬盘)。
选择您的实现库
现在,您对不同算法有了更好的了解,就该看看现有的实现库的丰富性了。虽然算法只是索引和检索机制的数学表示,但它们的实现方式可以决定算法的准确性和速度。大多数库都是用内存高效的语言(如 C++)实现的,并且具有 Python 绑定,以便可以在 Python 编程中使用它们。
一些库实现了单一算法,例如 Spotify 的 annoy 库。^([39]) 其他一些库,例如 Faiss ^([40]) 和 nmslib
^([41]) 则提供了多种您可以选择的算法。
图 10.4 展示了不同算法库在文本数据集上的比较。您可以在 Erik Bern 的 ANN 基准库中发现更多比较和链接到数十个 ANN 软件库。^([42])
图 10.4 ANN 算法在纽约时报数据集上的性能比较
如果您感到决策疲劳并对所有选择感到不知所措,一些一站式解决方案可以帮助您。OpenSearch 是 2021 年 ElasticSearch 项目的一个分支,它是全文搜索领域的一个可靠的工作马,它内置了矢量数据库和最近邻搜索算法。而且 OpenSearch 项目还通过诸如语义搜索矢量数据库和 ANN 矢量搜索等前沿插件胜过了它的商业源代码竞争对手 ElasticSearch。^([43]) 开源社区通常能够比在专有软件上工作的较小内部企业团队更快地实现最先进的算法。
小贴士
要注意可能随时更改软件许可证的开源项目。ElasticSearch、TensorFlow、Keras、Terraform,甚至 Redhat Linux 的开发者社区都不得不在公司赞助商决定将软件许可证更改为商业源代码后对这些项目进行分叉。商业源代码是开发人员用来指称由公司宣传为开源的专有软件的术语。该软件附带商业使用限制。并且赞助公司可以在项目变得流行并且他们想要变现开源贡献者为项目付出的辛勤工作时更改这些条款。
如果您对在 Docker 容器上部署 Java OpenSearch 包感到有些害怕,您可以尝试一下 Haystack。这是一个很好的方式来尝试索引和搜索文档的自己的想法。您可能是因为想要理解所有这些是如何工作的才来到这里。为此,您需要一个 Python 包。Haystack 是用于构建问题回答和语义搜索管道的最新最好的 Python 包。
10.2.6 使用 haystack 将所有内容汇总
现在你几乎已经看到了问题回答管道的所有组件,可能会感到有些不知所措。不要担心。以下是您管道所需的组件:
- 一个模型来创建您文本的有意义的嵌入
- 一个 ANN 库来索引您的文档并为您的搜索查询检索排名匹配项
- 一个模型,给定相关文档,将能够找到您的问题的答案 - 或者生成它。
对于生产应用程序,您还需要一个向量存储(数据库)。 向量数据库保存您的嵌入向量并对其进行索引,以便您可以快速搜索它们。 并且您可以在文档文本更改时更新您的向量。 一些开源向量数据库的示例包括 Milvus,Weaviate 和 Qdrant。 您还可以使用一些通用数据存储,如 ElasticSearch。
如何将所有这些组合在一起? 嗯,就在几年前,你要花费相当长的时间才能弄清楚如何将所有这些拼接在一起。 如今,一个完整的 NLP 框架系列为您提供了一个简单的接口,用于构建、评估和扩展您的 NLP 应用程序,包括语义搜索。 领先的开源语义搜索框架包括 Jina,^([44]) Haystack,^([45]) 和 txtai。^([46])
在下一节中,我们将利用其中一个框架,Haystack,将您最近章节中学到的所有内容结合起来,变成您可以使用的东西。
10.2.7 变得真实
现在您已经了解了问答流水线的不同组件,是时候将它们全部整合起来创建一个有用的应用程序了。
你将要创建一个基于…… 这本书的问答应用程序! 你将使用我们之前看过的相同数据集 - 这本书前 8 章的句子。 你的应用程序将找到包含答案的句子。
让我们深入研究一下! 首先,我们将加载我们的数据集,并仅取其中的文本句子,就像我们之前所做的那样。
列表 10.14 加载 NLPiA2 行数据集
>>> import pandas as pd >>> DATASET_URL = ('https://gitlab.com/tangibleai/nlpia2/' ... '-/raw/main/src/nlpia2/data/nlpia_lines.csv') >>> df = pd.read_csv(DATASET_URL) >>> df = df[df['is_text']]
10.2.8 知识的一堆草垛
一旦您加载了自然语言文本文档,您就希望将它们全部转换为 Haystack 文档。 在 Haystack 中,一个 Document 对象包含两个文本字段:标题和文档内容(文本)。 您将要处理的大多数文档与维基百科文章相似,其中标题将是文档主题的唯一可读标识符。 在您的情况下,本书的行太短,以至于标题与内容不同。 所以你可以稍微作弊,将句子的内容放在 Document
对象的标题和内容中。
列表 10.15 将 NLPiA2 行转换为 Haystack 文档
>>> from haystack import Document >>> >>> titles = list(df["line_text"].values) >>> texts = list(df["line_text"].values) >>> documents = [] >>> for title, text in zip(titles, texts): ... documents.append(Document(content=text, meta={"name": title or ""})) >>> documents[0] <Document: {'content': 'This chapter covers', 'content_type': 'text', 'score': None, 'meta': {'name': 'This chapter covers'}, 'id_hash_keys': ['content'], 'embedding': None, ...
现在你想要将你的文档放入数据库,并设置一个索引,这样你就可以找到你正在寻找的“知识针”。Haystack 提供了几种快速的矢量存储索引,非常适合存储文档。下面的示例使用了 Faiss 算法来查找文档存储中的向量。为了使 Faiss 文档索引在 Windows 上正常工作,你需要从二进制文件安装 haystack,并在git-bash
或 WSL(Windows Subsystem for Linux)中运行你的 Python 代码。^([47])
列表 10.16 仅适用于 Windows
$ pip install farm-haystack -f \ https://download.pytorch.org/whl/torch_stable.html
在 Haystack 中,你的文档存储数据库包装在一个DocumentStore
对象中。DocumentStore
类为包含刚从 CSV 文件中下载的文档的数据库提供了一致的接口。暂时,“文档”只是本书的一份早期版本 ASCIIDoc 手稿的文本行,非常非常短的文档。haystack 的DocumentStore
类使你能够连接到不同的开源和商业向量数据库,你可以在本地主机上运行,如 Faiss、PineCone、Milvus、ElasticSearch,甚至只是 SQLLite。暂时使用FAISSDocumentStore
及其默认的索引算法('Flat'
)。
>>> from haystack.document_stores import FAISSDocumentStore >>> document_store = FAISSDocumentStore( ... return_embedding=True) # #1 >>> document_store.write_documents(documents)
haystack 的 FAISSDocumentStore 提供了三种选择这些索引方法的选项。默认的'Flat'
索引会给你最准确的结果(最高召回率),但会占用大量的 RAM 和 CPU。
如果你的 RAM 或 CPU 非常有限,比如当你在 Hugging Face 上托管应用程序时,你可以尝试使用另外两种 FAISS 选项:'HNSW'
或f’IVF{num_clusters},Flat'
。你将在本节末尾看到的问答应用程序使用了'HNSW'
索引方法,在 Hugging Face 的“免费套餐”服务器上适用。有关如何调整向量搜索索引的详细信息,请参阅 Haystack 文档。你需要在速度、RAM 和召回率之间进行平衡。像许多 NLP 问题一样,“最佳”向量数据库索引的问题没有正确答案。希望当你向你的问答应用程序提问这个问题时,它会回答“要看情况……”。
现在进入你运行此 Python 代码的工作目录。你应该能看到一个名为'faiss_document_store.db'
的文件。这是因为 FAISS 自动创建了一个 SQLite 数据库来包含所有文档的文本内容。每当你使用向量索引进行语义搜索时,你的应用程序都需要这个文件。它将为你提供与每个文档的嵌入向量相关联的实际文本。然而,仅凭该文件还不足以将数据存储加载到另一段代码中,为此,你需要使用DocumentStore
类的save
方法。在我们填充文档存储与嵌入向量之后,我们将在代码中进行这样的操作。
现在,是时候设置我们的索引模型了!语义搜索过程包括两个主要步骤 - 检索可能与查询相关的文档(语义搜索),以及处理这些文档以创建答案。因此,您将需要一个 EmbeddingRetriever 语义向量索引和一个生成式变压器模型。
在第九章中,您已经了解了 BERT 并学会了如何使用它来创建代表文本含义的通用嵌入。现在,您将学习如何使用基于嵌入的检索器来克服维度诅咒,并找到最有可能回答用户问题的文本嵌入。您可能会猜到,如果您的检索器和阅读器都针对问答任务进行了微调,那么您将获得更好的结果。幸运的是,有很多基于 BERT 的模型已经在像 SQuAD 这样的问答数据集上进行了预训练。
列表 10.17 配置问答流水线的 reader
和 retriever
组件
>>> from haystack.nodes import TransformersReader, EmbeddingRetriever >>> reader = TransformersReader(model_name_or_path ... ="deepset/roberta-base-squad2") # #1 >>> retriever = EmbeddingRetriever( ... document_store=document_store, ... embedding_model="sentence-transformers/multi-qa-mpnet-base-dot-v1") >>> document_store.update_embeddings(retriever=retriever) >>> document_store.save('nlpia_index_faiss') # #2
请注意,阅读器和检索器不必基于相同的模型 - 因为它们不执行相同的工作。multi-qa-mpnet-base-dot-v1
被优化用于语义搜索 - 也就是说,找到与特定查询匹配的正确文档。另一方面,roberta-base-squad2
在一组问题和简短答案上进行了训练,因此更擅长找到回答问题的上下文相关部分。
我们还终于保存了我们的数据存储以供以后重用。如果您转到脚本的运行目录,您会注意到有两个新文件:nlpia_faiss_index.faiss
和nlpia_faiss_index.json
。剧透 - 您很快就会需要它们!
现在,您已经准备好将各部分组合成一个由语义搜索驱动的问答流水线了!您只需要将您的"Query"
输出连接到Retriever
输出,然后连接到 Reader 输入:
列表 10.18 从组件创建一个 Haystack 流水线
>>> from haystack.pipelines import Pipeline ... >>> pipe = Pipeline() >>> pipe.add_node(component=retriever, name="Retriever", inputs=["Query"]) >>> pipe.add_node(component=reader, name="Reader", inputs=["Retriever"])
您还可以使用 Haystack 的一些现成的管道在一行中完成它:
>>> from haystack.pipelines import ExtractiveQAPipeline >>> pipe= ExtractiveQAPipeline(reader, retriever)
10.2.9 回答问题
让我们试试我们的问答机器吧!我们可以从一个基本问题开始,看看它的表现如何:
>>> question = "What is an embedding?" >>> result = pipe.run(query=question, ... params={"Generator": { ... "top_k": 1}, "Retriever": {"top_k": 5}}) >>> print_answers(result, details='minimum') 'Query: what is an embedding' 'Answers:' [ { 'answer': 'vectors that represent the meaning (semantics) of words', 'context': 'Word embeddings are vectors that represent the meaning ' '(semantics) of words.'}]
不错!请注意“context”字段,它为您提供包含答案的完整句子。
10.2.10 将语义搜索与文本生成相结合
因此,你的抽取式问答管道非常擅长找到在你给定的文本中清晰陈述的简单答案。然而,它并不擅长扩展和解释对更复杂问题的回答。抽取式摘要和问答在为“为什么”和“如何”问题生成冗长复杂文本方面遇到困难。对于需要推理的复杂问题,你需要将最佳的 NLU 模型与最佳的生成式 LLMs 结合起来。BERT 是一个专门用于理解和将自然语言编码为语义搜索向量的双向 LLM。但是对于生成复杂句子,BERT 并不那么出色,你需要一个单向(因果关系)模型,比如 GPT-2。这样你的管道就可以处理复杂的逻辑和推理,回答你的“为什么”和“如何”问题。
幸运的是,你不必自己拼凑这些不同的模型。开源开发者们已经领先于你。BART 模型是这样的。[49] BART 具有与其他 transformer 相同的编码器-解码器架构。尽管其编码器是双向的,使用基于 BERT 的架构,但其解码器是单向的(对于英语是从左到右的),就像 GPT-2 一样。在技术上,直接使用原始的双向 BERT 模型生成句子是可能的,如果你在末尾添加 令牌并多次重新运行模型。但 BART 通过其单向解码器为你处理了文本生成的“递归”部分。
具体来说,你将使用一个为长篇问答(LFQA)预训练的 BART 模型。在这个任务中,需要一个机器根据检索到的文档生成一个段落长的答案,以逻辑方式结合其上下文中的信息。LFQA 数据集包括 25 万对问题和长篇答案。让我们看看在此训练的模型的表现如何。
我们可以继续使用相同的检索器,但这次,我们将使用 Haystack 预先制作的管道之一,GenerativeQAPipeline。与之前的示例中的 Reader 不同,它包含一个 Generator,根据检索器找到的答案生成文本。因此,我们只需要更改几行代码。
列表 10.19 使用 Haystack 创建一个长篇问答管道
>>> from haystack.nodes import Seq2SeqGenerator >>> from haystack.pipelines import GenerativeQAPipeline >>> generator = Seq2SeqGenerator( ... model_name_or_path="vblagoje/bart_lfqa", ... max_length=200) >>> pipe = GenerativeQAPipeline(generator, retriever)
就是这样!让我们看看我们的模型在一些问题上的表现如何。
>>> question = "How CNNs are different from RNNs" >>> result = pipe.run( query=question, ... params={"Retriever": {"top_k": 10}}) # #1 >>> print_answers(result, details='medium') 'Query: How CNNs are different from RNNs' 'Answers:' [{ 'answer': 'An RNN is just a normal feedforward neural network "rolled up" so that the weights are multiplied again and again for each token in your text. A CNN is a neural network that is trained in a different way.' }]
嗯,那有点模糊但正确!让我们看看我们的模型如何处理书中没有答案的问题:
>>> question = "How can artificial intelligence save the world" >>> result = pipe.run( ... query="How can artificial intelligence save the world", ... params={"Retriever": {"top_k": 10}}) >>> result 'Query: How can artificial intelligence save the world' 'Answers:' [{'answer': "I don't think it will save the world, but it will make the world a better place."}]
说得好,对于一个随机的变色龙来说!