生产中的大语言模型(MEAP)(全)

生产中的大语言模型(MEAP)(全)

原文:zh.annas-archive.org/md5/693c44e001dd1f15db0e9297cac87f87

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:词语的觉醒:大型语言模型为何引起关注

本章节涵盖

  • 什么是大型语言模型,以及它们可以做什么和不能做什么

  • 什么时候你应该部署自己的大型语言模型,以及什么时候不应该

  • 大型语言模型的神话及其背后的真相

"任何足够先进的技术都与魔法无异。"

  • 阿瑟·克拉克

现在是 1450 年。德国美因茨的一个寂静角落,在不知不觉中站在一个伟大时代的边缘。在 Humbrechthof,这个隐藏在城镇阴影中的不起眼作坊里,空气中充满着油、金属和坚定决心的气味,约翰内斯·古腾堡,一位金匠和创新者,在这里辛苦劳作,无声地孕育着一场革命。在深夜的时刻,平静被金属与金属的节奏性敲打声不时打破。在作坊灯光明亮的中心,站立着古腾堡十年来的心血之作——一台设计和用途都独一无二的装置。

这不是一件普通的发明。工艺和创造力将一组可移动的金属类型、单个铸造的字符精心制造在模具中。跳跃的灯光在金属标志上闪烁。空气中充满了突破的期待和来自古腾堡自己的基于油的墨水的香甜。在这个瞬间的静寂中,主印刷师挺直了肩膀,用无与伦比的技艺,将一张干净的羊皮纸放在装满墨水的模具下面,让他的发明牢固地印在页面上。房间里调整到一片宁静的交响乐,屏住呼吸的空气显得沉重。当印刷机抬起时,它在自身重量下发出吱吱声,每个声响都像是一声战吼,宣告一个激动人心的新世界的到来。

古腾堡在一片忙碌中从印刷机上取下第一页印刷好的纸,并将其平放在木桌上,仔细检查每个字符,这些字符如他的愿景一样粗体且宏伟。整个房间都被这幅景象深深吸引。一张普通的羊皮纸已经成为了转变的见证。随着夜幕渐渐消散,他带着充满活力的自豪感审视他的作坊。他的遗产已经诞生,在历史的记载中回响,永远改变了信息传播的方式。约翰内斯·古腾堡,这个新的千年英雄,从阴影中走出来,这个敢于梦想的发明家。他的名字与印刷机紧密相连,这不仅是一个开创性的发明,更是现代世界的催化剂。

随着关于古腾堡成就的消息开始在欧洲大陆传播,来自各个学科的学者们尚未意识到他们手中掌握的这个非凡工具。知识和学习,曾经被珍视的宝藏,现在平民也可以轻松获得。而对于这种新发现的便利,意见却众说纷纭。

“在我们这个时代,由于那些来自莱茵河的人才和工业,书籍数量激增。曾经只属于富人 - 不,属于国王的书籍现在可以在一个朴素的屋顶下见到。[…] 如今,我们的孩子们[…]无一不知。”

  • 塞巴斯蒂安·布兰特

“学术努力从未像现在一样普遍衰退。事实上,聪明才智在国内外都受到厌恶。除了眼泪,阅读对学生有什么好处呢?很罕见,当它被出售时毫无价值,也没有智慧。”

  • 列日的埃格伯特

人们对书籍有着各种各样的看法。有一点我们可以达成共识,在虚拟印刷厂存在且书籍无处不在的时代:印刷机改变了历史。尽管我们并不是真的在古腾堡使用印刷机打印第一页的时候在场,但我们已经看到很多人第一次尝试与大型语言模型(LLMs)互动。当他们看到它对他们的第一个提示做出回应时,他们脸上的惊讶。当他们挑战它的困难问题时,看到它像领域专家一样回应时,他们的兴奋。当他们意识到他们可以利用这一点来简化他们的生活或使自己富有时,他们的灵光一现。我想象这些情绪波动只是约翰内斯·古腾堡所感受到的一小部分。快速生成文本和加速沟通一直都是有价值的。

1.1 大型语言模型加速沟通

每一份工作都有一定程度的沟通。通常这种沟通是单调乏味的、官僚主义的和政治性的。我经常警告学生和门徒,每份工作都有它的文书工作。曾经是激情的东西很容易被日复一日的乏味和琐碎的工作所扼杀,当它变成一份工作时就会出现这种情况。事实上,当我们谈论我们的职业时,我们经常吹嘘它们,试图提高我们的社会地位,所以你很少会听到全部真相。你不会听到有关无聊的部分,日常的苦差事会方便地被遗忘。

然而,想象一下一个减轻单调工作负担的世界。一个警察不再每天浪费数小时填写报告,而是可以把那些时间用在社区外展项目上的地方。或者一个世界,教师不再彻夜忙于批改作业和准备课程计划,而是可以考虑和准备针对个别学生的定制课程。甚至一个世界,律师们不再陷入为期数日的法律文件梳理之中,而是有自由去接受激励他们的慈善案件?当沟通负担、文书负担和会计负担消失时,工作更像是我们所说的那样。

对于这一点,LLMs 是自印刷术以来最有前途的技术。首先,它们彻底颠覆了人类和计算机之间的角色和关系,改变了我们认为它们有能力的东西。它们已经通过了医学考试[1],律师资格考试,以及多次心灵理论测试。它们已经通过了谷歌和亚马逊的编程面试。它们在 SAT 考试中至少获得了 1600 分中的 1410 分。其中最令作者印象深刻的是,GPT-4 甚至通过了高级侍酒师考试-这让我们想知道它们是如何通过实际的品酒部分的。事实上,它们前所未有的成就以惊人的速度发展,往往让我们这些凡人感到有点不安和不安。对于一种似乎能做任何事情的技术,你会做什么呢?

虽然通过考试很有趣,但除非我们的目标是建造史上最昂贵的作弊机器,否则并不是特别有帮助,我们承诺我们的时间有更好的利用。LLMs 擅长的是语言,特别是帮助我们改善和自动化沟通。这使我们能够将常见的苦涩经验转化为轻松愉快的经验。首先,想象一下走进你的家门,你有自己的个人 JARVIS,就好像踏入了钢铁侠的鞋子,这是一个为你的日常生活增添了无与伦比的活力的 AI 助手。虽然 LLMs 还没有达到漫威电影中 JARVIS 所表现出的人工智能(AGI)水平,但它们正在为改善客户支持到帮助你购物挑选爱人的生日礼物等新的用户体验提供动力。它懂得询问你关于这个人,了解他们的兴趣和身份,找出你的预算,然后给出专业的建议。尽管许多这些助手正在被很好地利用,但还有许多只是用户可以与之对话和娱乐的聊天机器人-这很重要,因为即使我们的想象朋友这些天也太忙了。开玩笑的,这些可以创造出惊人的体验,让你见到你最喜爱的虚构角色,比如哈利·波特,福尔摩斯,阿纳金·天行者或者钢铁侠。

我们确信许多读者对编程助手感兴趣,因为我们都知道无所不 Google 实际上是最糟糕的用户体验之一。能够用简单的英语写下几个目标,然后看到助手帮你写代码是一种令人振奋的体验。我个人曾经使用这些工具来帮助我记住语法,简化和清理代码,编写测试,并学习一种新的编程语言。

视频游戏是另一个有趣的领域,我们可以期待大型语言模型(LLM)带来许多创新。它们不仅帮助程序员创造游戏,还允许设计者创造更沉浸式的体验。例如,与 NPC 对话会更加深入和引人入胜。想象一下,像《动物之森》或《星露谷物语》这样的游戏会有几乎无穷无尽的任务和对话。

考虑其他行业,比如教育,似乎没有足够的老师满足需求,意味着我们的孩子没有得到他们需要的一对一关注。LLM 助手可以帮助老师节省做手工事务的时间,同时为那些困难的孩子提供私人辅导。企业正在研究将 LLM 用于对话式数据工作,协助员工理解季度报告和数据表,基本上为每个人提供他们自己的个人分析师。销售和营销部门肯定会利用这一神奇的创新,无论好坏。搜索引擎优化(SEO)的状况也将发生很大变化,因为目前它主要是通过产生内容来希望使网站更受欢迎,而现在这变得非常容易。

那个清单只是我看到的一些常见例子,公司对使用它们感兴趣。人们也出于个人原因使用它们。创作音乐、诗歌,甚至写书,翻译语言,总结法律文件或电子邮件,甚至免费咨询——是的,这个主意很糟糕,因为它们在这方面还是很糟糕。个人偏好,但当我们的心智受到威胁时,我们不应该节省一分钱。当然,这导致了人们已经开始将它们用于欺骗、诈骗和伪造新闻以扭曲选举的黑暗目的。此时,清单已经变得相当大而且多样化,但我们只是开始挖掘其潜力的一角。实际上,由于大型语言模型(LLM)通常帮助我们进行沟通,所以更好的思考方式应该是,“它们做不到什么?”而不是“它们能做什么?”。

或者更好的是,“它们不应该做什么?”嗯,作为一种技术,有一些限制和约束,例如,LLM 速度有点慢。当然,慢是一个相对的词,但响应时间通常是以秒为单位而不是毫秒。我们将在第三章中更深入地探讨这个问题,但举个例子,我们可能不会很快看到它们用于自动完成任务,因为那需要非常快的推理才能有用。毕竟,自动完成需要能够比某人的打字速度更快地预测单词或短语。类似地,LLM 是大型复杂系统,我们不需要它们来解决这样一个简单的问题。用 LLM 解决自动完成问题不仅仅是用锤子打钉子,而是用整个拆迁球撞击它。就像租用一颗拆迁球比买一把锤子更贵一样,运行 LLM 会花费你更多。有很多类似的任务,我们应该考虑解决的问题的复杂性。

还有许多复杂的问题通常很难用 LLM 解决,比如预测未来。不,我们不是指神秘的艺术,而是预测问题,比如预测天气或海滩上涨潮的时间。这些实际上是我们解决了的问题,但我们并没有一定好的方法来传达这些问题是如何解决的。它们通过数学解决方案的组合来表达,比如傅里叶变换和谐波分析,或者通过黑盒子 ML 模型。有很多问题符合这个类别,比如异常值预测,微积分,或者找到胶带卷的结尾。

你也可能想要避免在高风险项目中使用它们。LLM 并不是绝对可靠,经常会犯错误。为了增加创造力,我们通常允许 LLM 中有一点随机性,这意味着你可以问 LLM 同样的问题,得到不同的答案。这是有风险的。你可以通过降低温度来消除这种随机性,但根据你的需求,这可能会使它变得无用。例如,你可能决定使用 LLM 将投资选项分类为好或坏,但你是否希望它根据其输出做出实际的投资决策?除非你的目标是制作一个迷因视频,否则不要没有监督地这样做。

最终,LLM 只是一个模型,它不能对你的损失负责,而实际上是你选择使用它造成了损失。类似的风险问题可能包括填写税表或寻求医疗建议。虽然 LLM 可以做这些事情,但它不会像雇用经过认证的注册会计师那样保护你免受 IRS 审计的严重处罚。如果你从 LLM 那里得到错误的医疗建议,就没有医生可以告你医疗事故。然而,在所有这些例子中,LLM 都有可能大大帮助从业者更好地执行他们的工作角色,减少错误并提高速度。

何时使用 LLM

用途包括:

  • 生成内容

  • 问答服务

  • 聊天机器人和 AI 助手

  • 扩散(txt2img、txt23d、txt2vid 等)

  • 与数据交互的应用

  • 任何涉及沟通的事情

避免用于以下场景:

  • 对延迟敏感的工作负载

  • 简单的项目

  • 不是用文字解决而是用数学或算法解决的问题 - 预测、异常值预测、微积分等

  • 严格的评估

  • 高风险项目

语言不仅仅是人们用来沟通的媒介。它是使人类成为顶级捕食者并为每个个体赋予社区自我定义的工具。人类生活的每个方面,从与父母争论到大学毕业再到阅读这本书,都被我们的语言所渗透。语言模型正在学习利用人类存在的基本方面之一,并且在使用时负责任地帮助我们完成每一个任务。它们有潜力解锁关于我们自己和他人的理解维度,如果我们负责任地教给它们如何。

自 LLMs 发布以来,它们就吸引了全球的注意力,因为它们的潜力让想象力勃发。LLMs 承诺了这么多,但是……这些解决方案都在哪里?哪里有给我们带来沉浸式体验的视频游戏?为什么我们的孩子还没有个人 AI 导师?为什么我还没有自己的个人助手像钢铁侠一样?这些是激发我们写这本书的深刻而深奥的问题。特别是最后一个问题,它让我夜不能寐。因此,虽然 LLMs 可以做出惊人的事情,但真正知道如何将它们转化为产品的人不够多,这正是我们在本书中的目标。

这不仅仅是一本机器学习运营的书。制作 LLM 在生产中工作涉及许多陷阱和问题,因为 LLM 不像传统软件解决方案那样工作。要将 LLM 转变为能够与用户连贯交互的产品,将需要整个团队和多种技能。根据您的用例,您可能需要训练或微调然后部署自己的模型,或者您可能只需要通过 API 从供应商那里访问模型。

无论你使用哪个 LLM,如果你想充分利用这项技术并构建最佳用户体验,你需要了解它们的工作原理。这不仅仅是数学/技术方面,还包括软技能,如如何为用户打造良好体验。在本书中,我们将涵盖使 LLMs 在生产中运行所需的一切。我们将讨论最佳工具和基础设施,如何通过提示工程和其他最佳实践来最大化它们的效用,比如控制成本。LLMs 可能是迈向更大平等的一步,所以如果你认为,“我不觉得这本书是为我而写的,”请重新考虑。这本书是为整个团队以及未来将与 LLMs 互动的任何人而写的。

我们将在实践层面上涵盖您在收集和创建数据集、在消费者或工业硬件上训练或微调 LLM,并以各种方式部署该模型供客户与之交互所需的一切。虽然我们不打算涉及太多理论,但我们将用真实的例子来覆盖从头到尾的过程。在本书结束时,您将知道如何部署 LLM,并具有一些可行的经验来支持它。

1.2 在大型语言模型的构建与购买决策之间导航

如果您购买了本书,那么您很可能已经相信 LLM 在您的生活和组织中具有巨大的潜力。那么购买本书就是将您的梦想变成现实的第一步,因为在我们知道如何将这些模型投入生产之前,这一切都是不可能的。毕竟,如果您与任何企业家或投资者交谈,他们都会告诉您,好的想法是大把的,重要的是执行和实现这些想法。我们需要做的是将这些模型投入生产,让它们随时可用于为您执行实际工作。

无论如何都无法避免,也没有必要美化,将 LLM 部署到生产环境中都很困难。通常,任何值得追求的事情都是如此。在本书中,我们旨在教您一切所需的知识,让您能够做到这一点,并提供一些实际的实践经验。但是因为它很困难,所以很容易想要走捷径。像 OpenAI 和 Google 这样的大型公司有一些很棒的模型选择,为什么不直接购买呢?让我们首先考虑一下它们提供了什么,以及这可能是一个好选择,然后我们将看看另一方面,这些提供通常存在的问题。

1.2.1 购买:被打的道路

简单地购买 LLM 访问权限有许多很好的理由。首先也是最重要的是,访问 API 提供的速度和灵活性。使用 API 是建立原型并迅速入手的一种非常简单和便宜的方法。事实上,如您在列表 1.1 中所见,只需几行代码即可开始连接到 OpenAI 的 API 并开始使用 LLM。当然,有很多可能性,但是过度投资于 LLM,只是发现它们恰好在您特定的领域失败,这将是一个糟糕的主意。使用 API 可以让您快速失败。构建原型应用程序以证明概念,并通过 API 启动它是一个很好的起点。

列表 1.1 调用 OpenAI API 的简单应用程序
import os
import openai

# Load your API key from an environment variable
openai.api_key = os.getenv("OPENAI_API_KEY")

chat_completion = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=[{"role": "user", "content": "Hello world"}],
)

往往,购买对模型的访问权可以为您带来竞争优势。在许多情况下,市场上最好的模型很可能是由专门从事该领域的公司构建的,他们使用了他们花费了大量金钱来策划的专业数据集。虽然你可以尝试竞争并构建自己的模型,但也许直接购买他们的模型访问权更符合你的目的。最终,谁拥有更好的特定领域数据来进行微调,很可能会获胜,而如果这只是贵公司的一个副产品,那可能不是你。毕竟,策划数据可能是昂贵的。提前购买可以节省很多工作量。

这导致了下一个观点,购买是快速获得专业知识和支持的方法。例如,OpenAI 花了大量时间使他们的模型安全,并配备了大量的过滤器和控件,以防止滥用他们的 LLMs。他们已经遇到并覆盖了很多边缘情况,因此您无需担心。购买他们模型的访问权还可以让您访问他们围绕其构建的系统。

更不用说,部署 LLM 本身只是问题的一半。你还需要构建一个完整的应用程序在其之上。有时候,购买 OpenAI 的模型成功超过竞争对手,这在很大程度上要归功于他们的用户体验和一些技巧,比如让标记看起来像是正在被输入。我们将带领您了解如何开始解决您的用例中的用户体验问题,以及您可以采用一些原型设计方法,为此领域提供主要的起步。

1.2.2 建设:少有人走的道路

使用 API 很容易,在大多数情况下,可能是最佳选择。但是,有很多理由可以让你应该努力拥有这项技术,并学会自己部署它。虽然这条路可能更难走,但我们会教你如何做到。让我们从最明显的原因开始探讨其中的几个原因:控制。

控制

第一批真正采用 LLMs 作为核心技术的公司之一是一家名为 Latitude 的小型游戏公司。Latitude 专注于使用 LLM 聊天机器人的 Dungeon and Dragons 类型角色扮演游戏,并在与它们合作时遇到了挑战。这并不是要批评这家公司的失误,因为他们为我们的集体学习经验做出了贡献,并且是开辟新道路的先驱。尽管如此,他们的故事令人着迷而有趣,就像一场我们个人无法停止观看的火车失事。

Latitude 的第一个发布是一个名为 AI Dungeon 的游戏。在初始阶段,它利用了 OpenAI 的 GPT2 来创建一个交互式和动态的故事体验。它很快就吸引了大量玩家,当然,他们开始不适当地使用它。当 OpenAI 向 Latitude 提供了 GPT3 的访问权限时,它承诺升级游戏体验,但实际上得到的是一场噩梦。

告诉你一个事,GPT3 加入了人类反馈加强学习(RLHF)这个功能,这大大有助于提高功能,但这也意味着 OpenAI 的合同工现在正在查看提示。这就是人类反馈的部分。这些工作人员并不是太喜欢阅读游戏创建的淫秽内容。OpenAI 的代表迅速向 Latitude 提出了最后通牒。要么他们需要开始审查玩家,否则他们将撤销他们对模型的访问——这将基本上扼杀了游戏和公司。由于没有其他选择,他们迅速添加了一些过滤器,但过滤系统却太过于是一个应急措施,一个漏洞和故障的混乱。玩家们对系统的糟糕程度感到不满,并且意识到 Latitude 的开发人员正在阅读他们的故事,完全不知道 OpenAI 已经在这方面做出了贡献。这是一场公关灾难。但这还没有结束。

OpenAI 认为游戏工作室不够努力,一直在制造障碍,他们被迫增加保障措施,开始禁止玩家进入游戏。这里是因果转折,很多故事变得淫秽是因为这个模型有偏好于情色文学。它经常会将无害的情节意外地转化为不当的情挑状况,导致玩家被驱逐和禁止进入游戏。OpenAI 又扮演起真理的楷模,但问题在于他们的模型。这让玩家们面临了游戏史上最具讽刺意味和不公正的问题:他们因游戏所传达的内容而被禁止进入游戏。

因此,这是一个年轻的游戏工作室,只是想制作一个有趣的游戏却陷入了挫败的客户和一个把所有责任都推到他们身上的技术巨头之间的困境。如果公司对技术拥有更多的控制权,他们可以采取真正的解决方案,比如修复模型,而不是只能把化妆品涂在猪身上。

在这个例子中,控制可能表现为你微调模型的能力,而 OpenAI 现在提供了微调功能,但仍有许多细节的决策在使用服务而不是自己的解决方案时会丢失。例如,使用哪些训练方法,将模型部署到哪些区域,或者在哪种基础设施上运行。控制对于任何面向客户或内部的工具也很重要。你不希望代码生成器意外输出侵犯版权或为你的公司创建法律问题的代码。你也不希望你面向客户的 LLM 输出关于你的公司或其过程的事实不正确的信息。

控制是你管理操作、过程、资源的能力,以便与你的目标、目的和价值观保持一致。如果一个模型最终成为你产品提供的核心,并且供应商意外提高了价格,你能做的很少。如果供应商决定他们的模型应该给出更自由或更保守的答案,而这不再与你的价值

技术对你的业务计划越核心,控制它的重要性就越大。这就是为什么麦当劳拥有其特许经营的房地产,以及为什么谷歌、微软和亚马逊都拥有自己的云网络。甚至为什么那么多企业家通过 Shopify 建立在线商店,而不只是使用其他平台,比如 Etsy 或亚马逊市场。最终,当你购买别人的产品时,第一件失去的就是控制权。保持控制权将为你提供更多解决未来问题的选择,并且也将为你带来竞争优势。

竞争优势

部署自己模型最有价值的一个方面是它给你带来了竞争优势。定制化——训练模型成为最擅长的一件事。例如,在 2017 年发布了双向编码器表示来自变压器(BERT),这是一种你可以用来训练自己模型的变压器模型架构后,有一大批研究人员和企业开始测试这项新技术在他们自己的数据上取得全球成功。在撰写本文时,如果你在 Hugging Face Hub 搜索“BERT”,会返回超过 13.7k 个模型,所有这些模型都是人们为了自己的目的个别训练的,以成为他们任务的最佳模型。

在这个领域的一个我个人的经验是训练斯洛文尼亚 BERTcina。在征得许可的情况下,通过爬取斯洛伐克国家语料库等资源,我汇总了当时最大的(单语种斯洛伐克语)数据集,还包括像 OSCAR 项目和欧洲议会语料库等一大堆其他资源。它从未创造过任何计算记录,也从未出现在任何模型评论中或为我工作的公司产生合作伙伴关系。然而,在它训练的任务上,它确实胜过了市场上的其他任何模型。

很可能,你和你的公司都不需要 AGI(人工通用智能)来从你的数据中生成相关见解,实际上,如果你发明了一个真正自我意识的 AGI,并计划仅仅将其用于每周一次为幻灯片制作一些数字、分析数据和生成可视化内容,那肯定足以成为 AGI 消灭人类的理由。更有可能的情况是,你需要的正是我制作斯洛文尼亚 BERTcina 时所需要的,一个大型语言模型,它在市场上执行的两到三个任务比其他任何模型都要好,并且不会将你的数据与微软或其他潜在竞争对手共享。虽然一些数据需要因安全或法律原因保密,但很多数据只是因为它们是商业机密而需要保护。

有数百种针对通用智能和特定任务基础知识的开源 LLM。我们将在第四章中介绍一些我们最喜欢的。采用这些开源替代方案之一,并对其进行训练以创建世界上最好的该任务模型,将确保您在市场上具有竞争优势。它还将允许您以自己的方式部署模型并将其集成到系统中以产生最大影响。

集成到任何地方

假设您想要部署 LLM 作为选择自己冒险风格游戏的一部分,该游戏使用设备的 GPS 位置来确定故事情节。您知道您的用户经常会冒险进入山区、海上等地,通常会遇到网络服务不佳和缺乏互联网访问的情况。调用 API 是行不通的。现在,别误会,像在这种情况下将 LLM 部署到边缘设备上仍然是一个探索性的课题,但是这是可能的,我们将在第九章中向您展示如何做到这一点。依靠 API 服务对于沉浸式体验来说是不可行的。

类似地,使用第三方 LLM 并调用 API 会增加集成和延迟问题,需要您将数据发送到网络并等待响应。API 很棒,但它们总是很慢,而且不总是可靠的。当延迟对项目很重要时,最好在内部提供服务。前一节关于竞争优势的讨论提到了两个以边缘计算为优先的项目,但还有许多其他项目存在。LLAMA.cpp 和 ALPACA.cpp 是最早的这类项目之一,而且这个领域的创新速度比其他任何领域都要快。4 位量化、低秩适应和参数高效微调都是最近为满足这些需求而创建的方法,我们将从第三章开始逐一介绍这些方法。

当我们团队首次开始与 ChatGPT 的 API 集成时,这是一次令人敬畏和令人谦卑的经历。令人敬畏的是,它使我们能够快速构建一些有价值的工具。令人谦卑的是,正如一位工程师对我开玩笑说的那样:“当你触及端点时,你会收到 503 错误,有时你会收到一条文本响应,就好像模型正在生成文本一样,但我认为那是一个 bug。”在生产环境中提供 LLM,试图满足这么多客户的需求,这并不是一件容易的事情。然而,部署一个集成到您的系统中的模型会让您对过程有更多的控制,提供比目前市场上找到的更高的可用性和可维护性。

成本

考虑成本始终很重要,因为它在做出明智决策并确保项目或组织的财务健康方面起着关键作用。它可以帮助您有效地管理预算,并确保资源得到适当的分配。控制成本可以让您在长期内维持项目的可行性和可持续性。

此外,考虑成本对于风险管理至关重要。当你了解不同的成本方面时,你可以识别潜在风险并更好地对其进行控制。这样,你可以避免不必要的支出,并确保你的项目对市场或行业的意外变化更具弹性。

最后,成本考虑对于保持透明度和问责制是重要的。通过监控和披露成本,组织向利益相关者、客户和员工展示了他们对道德和高效运营的承诺。这种透明度可以提高组织的声誉并有助于建立信任。

在考虑构建和购买 LLM 时,所有这些都适用。购买可能似乎立即较为廉价,因为目前市场上使用最多的服务每月只需 20 美元。但与在 AWS 上运行的 EC2 实例相比,仅仅运行同样的模型进行推理(甚至不是训练)每年可能会花费约 25 万美元。然而,这正是构建方面最快的创新所在。如果你只需要一个 LLM 来证明概念,文中竞争优势部分提到的任何项目都可以让你以运行演示计算机所需的电费为代价创建演示,并且中文文本处理后能很容易地解释出训练相关的内容,从而能够大大降低自己数据训练模型的成本,最低为 100 美元(是的,这个数字是真实的),可以训练包含 200 亿个参数的模型。另一个好处是,如果你自己构建,你的成本将永远不会增加,而支付服务的话,成本却很可能会大幅上涨。

安全性和隐私

考虑下面的情况。你是负责军事核弹头维护的军事人员。所有的文档都放在一本庞大的手册里。为了概述所有的安全要求和维护协议,学员们会因为遗忘重要信息而经常先剪掉保险丝再剪断导线。你决定调整一个 LLM 模型成为个人助手,给出指示,帮助压缩所有这些信息,为士兵在需要时提供精确所需的信息。将这些手册上传到另外一家公司显然不是个好主意——这是本世纪的轻描淡写——你需要在本地训练一些保持安全和隐私的东西。

这种情况听起来可能很牵强,但当与一家警察局的分析专家交谈时,他们同样表达了这个问题。和他们交谈时,他们表达了 ChatGPT 有多酷,甚至让他们整个团队参加了一门提示工程课程,以更好地利用它,但他们抱怨说,没有办法让他们的团队在不暴露敏感数据和对话的情况下使用该模型进行最有价值的工作-那种可以拯救生命的工作。任何处于类似境地的人都应该热切学习如何安全、可靠地部署模型。

不用在军队或警察部门工作,也能处理敏感数据。每家公司都有重要的知识产权和商业机密,最好保守秘密。我们在半导体、医疗保健和金融行业工作过,可以亲身告诉你,偏执和企业间谍是这些行业文化的一部分。因此,三星和其他行业参与者最初锁定了 ChatGPT,防止员工使用它,后来才开放了它。当然,不久之后,几名三星员工泄露了机密源代码[4]。由于 OpenAI 使用了 RLHF,这些代码被保留并用于以后进一步训练模型。这意味着只要有正确的提示注入,任何人都有可能从模型中提取代码。

不仅代码容易丢失。商业计划、会议记录、机密电子邮件甚至潜在专利想法都有风险。我们很不幸地知道一些公司已经开始向 ChatGPT 发送机密数据,使用该模型清理和提取 PII。如果您认为这可能是潜在的疏忽滥用,你是对的。这种方法直接暴露客户数据,不仅仅是给 Microsoft/OpnAI,而是给他们使用的任何第三方服务(包括 AWS Mechanical Turk、Fiverr 和自由职业者)执行 RLHF 中的人类反馈部分。

总结

如你所见,有很多原因使得公司想要拥有和构建自己的 LLMs,包括更大的控制力、降低成本以及满足安全和监管要求。尽管如此,我们了解到购买很容易,建设却更加困难,因此对于许多项目来说,仅购买是有意义的,但在你这样做之前,在图 1.1 中,我们分享了一些你应该自问的问题的流程图。尽管这是更艰难的道路,但建设可能会更加有回报。

图 1.1 在做出构建与购买决策之前你应该问自己的问题。

我们认为这些关于建立与购买的对话永远没有充分讨论的一个最后一个观点是,“为什么不两者兼而有之?”购买能让你得到建立所不擅长的所有东西:上市时间短,成本相对较低,易用性。建立则能让你得到购买所苦于的所有东西:隐私、控制和灵活性。研究和原型阶段可能非常受益于购买 GPT4 或 Databricks 的订阅,以快速构建一些东西来帮助筹集资金或获得利益相关者的支持。然而,生产往往不是一个适合第三方解决方案的环境。

最终,无论你打算建立还是购买,我们都为你写了这本书。显然,如果你打算建立它,你需要了解更多的东西,所以这本书的大部分内容将面向这些人群。事实上,我们不需要再多谈这个观点了,我们会在这本书中教你如何建立,但这并不妨碍你为你的公司做正确的事情。

1.2.3 警告一词:现在就拥抱未来

新技术就像火一样,它们可以在寒冷的夜晚为我们提供温暖,帮助我们烹饪食物,但它们也可能烧毁我们的家园,伤害我们。在商业上,有很多公司因为没有适应新技术而失败的故事。我们可以从他们的失败中学到很多。

Borders Books 在 1971 年首次开业。在开发了包括先进分析能力在内的全面库存管理系统后,它飙升成为全球第二大图书零售商,仅次于 Barnes & Noble。利用这项新技术,它颠覆了行业,使其能够轻松跟踪数万本书籍,开设大型商店,顾客可以在那里浏览比小型商店更多的书籍。分析能力帮助它跟踪哪些书籍正在流行,并更好地了解他们的顾客,从而做出更好的业务决策。它在行业中占据主导地位超过两十年。

然而,Borders 没有从自己的历史中吸取教训,在 2011 年破产,未能适应并被技术(电子商务)打乱。2001 年,他们没有建立自己的平台和在线商店,而是决定将在线销售外包给亚马逊。[5] 这个决定被许多评论家认为类似于把竞争对手的关键交给了他们的业务。虽然并不是直接交出他们的秘密配方,但这个决定放弃了他们的竞争优势。

在接下来的七年里,他们对不断增长的在线部门视而不见,而是专注于扩大他们的实体店铺存在,收购竞争对手并获得令人垂涎的星巴克交易。当亚马逊在 2007 年发布 Kindle 时,图书零售业的格局完全改变了。Barnes & Noble 运营他们自己的在线商店,迅速转变并发布 Nook 来竞争,然而,Borders 则无所作为,或者事实上,无能为力。

通过借助第三方来拥抱电子商务,他们未能发展内部所需的专业知识,无法制定出成功的在线销售策略,导致市场份额大幅下降。他们最终在 2010 年底推出了自己的电子阅读器 Kobo,但为时已晚。由于无法充分理解和有效实施电子商务技术,导致了巨额财务损失、门店关闭,最终于 2011 年破产。

Borders Books 是一个警示性的故事,但还有许多类似的公司因未能采用新技术而自食其果。对于如此具有影响力的新技术 LLMs,每家公司都必须决定他们希望站在哪一方。他们将执行和部署委托给大型的 FAANG 公司,只限于调用 API,还是他们愿意主导技术并自行部署?

我们希望从这个故事中传达的最重要的一点是技术是相互依赖的。电子商务是建立在互联网之上的。没能建立自己的在线商店意味着 Borders 在景观转变时未能建立起所需的内部技术专长,导致了失败。如今,我们也能看到同样的情况出现在 LLM 领域,因为最好准备利用 LLM 的公司已经积累了机器学习和数据科学方面的专业知识,并且对如何使用这些知识有一定的了解。

我们没有能预知未来的水晶球,但许多人相信 LLMs 是一项像互联网或电力一样具有革命性的新技术。了解如何使用这些模型,或者未能这样做,可能会成为许多公司的决定性时刻。不是因为这样做现在会让他们的公司起死回生,而是因为未来会有更有价值的建立在 LLMs 之上的东西。

进军部署 LLMs 的新世界可能具有挑战性,但它将帮助您的公司建立起保持领先地位所需的技术专长。没有人真正知道这项技术将引领我们走向何方,但了解这项技术很可能是避免类似 Borders Books 的错误所必需的。

有很多很好的原因可以通过购买成功,但至少有一个普遍的想法是完全错误的。这就是只有大公司才能在这个领域工作,因为训练这些模型需要数百万美元和数千个 GPU。通过创造这个金钱和资源无法逾越的围墙,小人物无法希望跨越。我们将在下一节中更详细地讨论这个问题,但任何规模的公司都可以开始,现在做的时机再好不过了。

1.3 揭穿神话

我们都听说过大公司和 LLM 领域的领先者训练 LLM 从头开始有多么困难,以及尝试对其进行微调有多么剧烈。无论是来自 OpenAI、BigScience 还是 Google,他们都谈到了巨额投资以及对强大的数据和工程人才的需求。但这些都有多少真实性,又有多少只是公司试图创建技术壕沟的企图呢?

大多数这些障碍都是基于这样的假设:如果您希望解决自己的问题,您将需要从头开始训练一个 LLM。简单来说,不需要!不断发布涵盖语言模型许多维度的开源模型,所以您很可能不需要从头开始。尽管他们所说的训练 LLM 从头开始确实非常困难,但我们仍然在不断学习如何做到,并能够越来越多地自动化可重复部分。此外,由于这是一个活跃的研究领域,框架和库每天都在发布或更新,并将帮助您从当前位置开始。像 oobabooga 的 Gradio 这样的框架将帮助您运行 LLM,而像 Falcon 40B 这样的基础模型将成为您的起点。所有这些都覆盖在内。除此之外,大公司内部已经传达了关于当前任何组织在开源社区上缺乏竞争优势的备忘录。

有个朋友曾经向我透露:“我真的很想参与到所有这些机器学习和数据科学的东西中。每次我眨眼的时候,它似乎变得更酷了。然而,感觉唯一参与的方式就是经历一次漫长的职业转变,去为一家 FAANG 公司工作。不,谢谢。我在大公司待过了一段时间,它们不适合我。但是我讨厌感觉自己被困在外面。”这就是启发本书的神话。我们在这里为您提供工具和示例,帮助您摆脱被困在外面的感觉。我们将帮助您解决我们正试图用 LLM 解决的语言问题,以及针对模型规模的机器学习操作策略。

奇怪的是,很多人认为他们被困在外面,而另一些人则相信他们可以在一个周末内成为专家。只需要获得一个 GPT API 密钥,这就完成了。这导致了很多狂热和炒作,每天都会在社交媒体上出现一个酷炫的新演示。但是,大多数这些演示从未成为实际产品,而并不是因为人们不想要它们。

要理解这一点,让我们讨论一下 IBM 的沃森,这是在 GPT 之前世界上最先进的语言模型。沃森是一个问答机器,在 2011 年击败了一些曾经出现在节目中的最优秀的人类选手,布拉德·拉特和肯·詹宁斯,赢得了Jeopardy比赛。拉特是游戏节目中赚钱最多的选手,詹宁斯是一位如此出色的选手,他连续赢得了令人瞠目结舌的 74 次。尽管面对这些传奇人物,比赛结果一点也不悬念。沃森以压倒性的优势获胜。詹宁斯对于失利的回应是著名的一句话:“我,作为一个人类,欢迎我们的新电脑统治者。”

沃森是对语言建模的一次令人印象深刻的尝试,许多公司都渴望利用其能力。从 2013 年开始,沃森开始被商业化利用。其中一个最大的应用是尝试将其整合到医疗保健领域,以解决各种问题。然而,这些解决方案从未真正发挥出应有的作用,业务也从未盈利。到 2022 年,沃森健康业务被出售。

当我们解决与语言相关的问题时,我们发现制作原型很容易,而制作一个真正的产品则非常非常困难。语言中有太多微妙之处。许多人想知道为什么 ChatGPT 如此火爆?仅仅在五天内就获得了超过一百万的客户。我听到的大多数答案都无法满足专家,因为 ChatGPT 并不比 GPT3 或其他已经存在几年的 LLMs 更令人印象深刻。我听说 OpenAI 的山姆·奥尔特曼在一次采访中亲自说过,他们并没有想到 ChatGPT 会引起这么大的关注,他们认为这将随着 GPT4 的发布而到来。那么为什么它会如此火爆呢?在我们看来,其中的魔法在于它是第一个真正将 LLMs 投入生产的产品。将它从演示变成实际产品。任何人都可以与之互动并提出棘手的问题,只会被其回答的出色而惊讶。演示只需要工作一次,但产品必须每次都工作,即使成千上万的用户向朋友展示它时说:“看看这个!”这种魔法正是您可以希望从阅读本书中学到的。

我们对撰写这本书感到兴奋。我们对将这种魔法带给您并让您将其带给世界的可能性感到兴奋。LLMs 处于许多领域的交汇处,如语言学、数学、计算机科学等。虽然知识越多越有帮助,但并不需要成为专家。对任何一个单独领域的专业知识只会提高技能的上限,而不会提高入门门槛。考虑一位物理学或音乐理论专家,他们不会自动具备音乐制作的技能,但他们将更有准备快速学习。LLMs 是一种沟通工具,几乎每个人都需要沟通技能。

像所有其他技能一样,你的接近度和参与意愿是知识的两个主要障碍,而不是学位或能力注释——这些只会缩短你被听到和理解的旅程。如果你在这个领域没有任何经验,也许最好是先通过参与像 OpenAssistant 这样的项目来培养对 LLM 是什么以及需要什么的直觉。如果你是一个人,那正是他们所需要的。通过志愿服务,您可以开始了解这些模型训练的内容以及原因。无论您从零知识到成为专业机器学习工程师的程度如何,我们都将传授必要的知识,以大大缩短您的对话时间和理解时间。如果您不感兴趣学习该主题的理论基础,我们有大量的实际例子和项目可以让您动手实践。

我们现在都听过有关 LLM 幻觉的故事,但 LLMs 不需要是难以预测的。像 Lakera 这样的公司每天都在努力提高安全性,而像 LangChain 这样的公司则正在使模型更易于提供实用的上下文,从而使它们更一致,更不易偏离。诸如 RLHF 和 Chain of Thought 之类的技术进一步使我们的模型能够与我们已经接受的谈判保持一致,人们和模型从一开始就应该理解这些谈判,比如基本的加法或当前日期,这两者在概念上都是任意的。我们将帮助您从语言角度增强模型的稳定性,因此它们不仅能找出最有可能的输出,还能找出最有用的输出。

在您进一步深入这条道路时,需要考虑的不仅是进入模型/代码的安全性,还有出来的内容。LLMs 有时可能会生成过时的、事实不正确的,甚至是版权或许可的材料,这取决于其训练数据的内容。LLMs 不知道人们对什么是商业秘密和什么可以公开共享所做的任何协议。也就是说,除非您在训练期间告诉它这些协议,或者在推理期间通过仔细的提示机制告诉它。事实上,围绕提示注入产生不准确信息的挑战主要是由两个因素引起的:首先,用户请求超出了模型的理解范围;其次,模型开发者未能完全预测用户将如何与模型互动或其询问的性质。如果您有一个资源可以帮助您提前解决第二个问题,那么它将非常接近无价,不是吗?

最后,我们不想通过 LLMs 人为或不真实地增加你的希望感。它们需要大量资源来训练和运行。它们很难理解,而且很难按照你的意愿使其正常运行。它们是新的,且尚未被充分理解。好消息是,这些问题正在积极解决中,我们已经付出了大量工作,找到了与写作并行的实现方法,并积极减轻了你对整个深度学习架构了解的负担。从量化到 Kubernetes,我们将帮助你弄清楚你现有的一切知识。也许我们会无意中让你相信这太难了,你应该只是从供应商那里购买。无论哪种方式,我们都将在每一步为你提供帮助,以帮助你从这种神奇的技术中获得你所需要的结果。

1.4 总结

  • LLMs 之所以令人兴奋,是因为它们与人类一起工作,而不是反对他们

  • 社会是建立在语言之上的,因此有效的语言模型具有无限的应用,如聊天机器人、编程助手、视频游戏和人工智能助手。

  • LLMs 在许多任务上表现出色,甚至可以通过高级医学和法律考试

  • LLMs 是破坏性的,而不是锤子,应避免用于简单问题、需要低延迟的问题和高风险的问题。

  • 购买原因包括:

  • 快速启动并进行研究和原型用例

  • 轻松访问高度优化的生产模型

  • 访问供应商的技术支持和系统

  • 构建原因包括:

  • 为您的业务用例获得竞争优势

  • 保持低成本和透明度

  • 确保模型的可靠性

  • 保护您的数据安全

  • 在敏感或私密话题上控制模型输出

  • 没有技术壁垒阻止你与更大的公司竞争,因为开源框架和模型提供了打造自己道路的基石。

[1] Med-PaLM 2 在 MedQA 考试中取得了 86.5%的分数。

[2] WIRED,“It began as an AI-fueled dungeon game. Then it got much darker,”Ars Technica,2021 年 5 月 8 日。arstechnica.com/gaming/2021/05/it-began-as-an-ai-fueled-dungeon-game-then-it-got-much-darker/

[3] 对于那些想知道的人来说,这是 MAS*H 的参考:youtu.be/UcaWQZlPXgQ

[4] 이코노미스트,“[단독] 우려가 현실로…삼성전자, 챗GPT 빗장 풀자마자 ‘오남용’ 속출,” 이코노미스트,2023 年 3 月 30 日。economist.co.kr/article/view/ecn202303300057?s=31

[5] A. Lowrey,“Borders bankruptcy: Done in by its own stupidity, not the Internet.,”Slate Magazine,2011 年 7 月 20 日。slate.com/business/2011/07/borders-bankruptcy-done-in-by-its-own-stupidity-not-the-internet.html

[6] J. Best,“IBM 沃森:制霸“危险边缘”的全过程以及接下来要做的事情”,TechRepublic,2013 年 9 月 9 日。www.techrepublic.com/article/ibm-watson-the-inside-story-of-how-the-jeopardy-winning-supercomputer-was-born-and-what-it-wants-to-do-next/

[7] “与 OpenAI CEO Sam Altman 的谈话|由 Elevate 主办”,2023 年 5 月 18 日。youtu.be/uRIWgbvouEw

第二章:大型语言模型:深入探究语言建模

本章内容包括:

  • 理解含义和解释的语言学背景

  • 语言建模技术的比较研究

  • 注意力和 Transformer 架构

  • 大型语言模型如何融入和建立在这些历史之上

所有好故事都以“从前有一位”为开头,但不幸的是,本书不是故事,而是关于创建和生产化 LLMs 的书籍。因此,本章将深入探讨与大型语言模型(LLMs)的开发相关的语言学知识,探索形式语言学的基础、语言特征和塑造自然语言处理(NLP)领域的语言建模技术的演变。我们将从研究语言学的基础和其与 LLMs 的关联性开始,重点介绍句法、语义和语用等关键概念,这些概念构成了自然语言的基础,并在 LLMs 的运行中起着至关重要的作用。我们将深入探讨符号学,即符号和符号的研究,并探讨其原则如何影响 LLMs 的设计和解释。

接下来我们将在 2.2 节中追溯语言建模技术的演变,概述了早期方法,包括 N-Gram、朴素贝叶斯分类器以及基于神经网络的方法,如多层感知机(MLP)、循环神经网络(RNN)和长短期记忆(LSTM)网络。然后在 2.3 节中讨论了基于 Transformer 模型的革命性转变,这为大型语言模型(LLMs)的出现奠定了基础,而 LLMs 只是非常大型的基于 Transformer 模型。最后,在 2.4 节中介绍了 LLMs 及其独特的特点,讨论了它们如何在建立在并超过早期的语言建模技术基础上,从而彻底改变了自然语言处理(NLP)领域。

这是一本关于 LLMs 在生产中的书籍。我们坚决认为,如果你想将 LLMs 变成实际产品,更好地理解这项技术将提高你的成果,避免犯下代价高昂且耗时的错误。任何工程师都可以弄清楚如何将大型模型引入生产环境并投入大量资源使其运行,但这种愚蠢的策略完全忽略了人们在之前尝试做同样事情时已经学到的教训,而这正是我们首先要解决的问题。掌握这些基础知识将更好地为您准备处理 LLMs 时可能遇到的棘手部分、陷阱和边界情况。通过了解 LLMs 出现的背景,我们可以欣赏它们对 NLP 的变革性影响,以及如何使其能够创建各种应用程序。

2.1 语言建模

在深入讨论 LLM 之前,如果不首先讨论语言,那么就对 LLM 做了一个极大的伤害,首先我们将从简短但全面的语言模型概述开始,重点是可以帮助我们理解现代 LLM 的教训。让我们首先讨论抽象层级,因为这将帮助我们欣赏语言建模。

语言作为一个概念,是我们头脑中产生的感觉和思维的抽象。同样,数学是语言的一种抽象,着重于逻辑和可证明性,但正如任何数学家所说的那样,它是一种用于有组织和“逻辑”方式描述和定义的语言的子集。二进制语言就源于数学,它是一种由组成的二进制数字表示系统。

二进制是数学的非常有用的抽象,它又是语言的抽象,而语言又是我们感觉的抽象,因为它是软件、模型和计算机中使用的底层工具。当计算机首次制造出来时,我们通过打孔卡片或直接使用二进制与它们交流。不幸的是,人类用这种方式沟通重要事物需要太长时间,因此二进制也被抽象为汇编语言,这是一种更符合人类理解的与计算机交流的语言。这又进一步抽象为高级汇编语言 C,甚至进一步抽象为面向对象的编程语言如 Python。我们刚才讨论的流程如图 2.1 所示。

图 2.1 比较了认知抽象层级与编程抽象层级,直到逻辑二进制抽象。Python 并不来自 C,也不编译为 C。然而,Python 是另一种与二进制相距一定抽象层级的语言。类似地,语言也经过类似的路径。每一层的抽象都会造成潜在的错误点。创建模型也有多层次的抽象,每一层都很重要。

显然,这是一种简化,但了解一下,你在头脑中所感受到的情感与计算机实际读取的二进制相隔着相同数量的抽象层级,就像大多数人用来编程的语言一样。有些人可能会认为 Python 和二进制之间存在更多的步骤,例如编译器或使用汇编语言支持 C 语言,这是正确的,但在语言方面也有更多的步骤,例如形态学、语法、逻辑和一致性。

这可以帮助我们理解让人工智能语言模型(LLM)理解我们想要表达的内容的过程有多困难,甚至可以帮助我们更好地理解语言建模技术。我们在这里专注于二进制的原因仅仅是为了说明从你有的想法或我们的代码示例之一到一个工作模型的抽象层次是相似的数量。就像儿童的电话游戏,参与者互相耳语一样,每一个抽象层次都会产生一个断开点或障碍,错误可能在这些地方发生。

图 2.1 也旨在说明不仅创建可靠的代码和语言输入的困难,而且要引起注意的是中间的抽象步骤(如标记化和嵌入)对于模型本身是多么重要。即使你有完全可靠的代码和完美表达的想法,含义在到达 LLM 之前可能会在这些过程中被错误地处理。

在本章中,我们将尝试帮助您理解如何减少这些失败点的风险,无论是在语言、编码还是建模方面。不幸的是,在为当前任务不立即重要的语言学知识和为您提供太多的技术知识之间取得平衡有点棘手,尽管它们很有用,但并不能帮助您培养对语言建模作为一种实践的直觉。在此基础上,您应该知道语言学可以追溯到我们历史上数千年前,并且有很多可以从中学到的东西。我们在附录 A 中提供了一个简要的概述,供有兴趣的读者了解语言建模是如何随着时间的推移发展的,并鼓励您查阅。

让我们从我们专注于构成语言本身的基本组成部分开始。我们期望我们的读者至少在尝试过语言建模之前,并可能听说过像 PyTorch 和 Tensorflow 这样的库,但我们不指望大多数读者之前曾考虑过语言方面的问题。通过理解构成语言的基本特征,我们可以更好地欣赏创建有效语言模型所涉及的复杂性,以及这些特征如何相互作用形成我们所有人之间相互连接的复杂的交流网络。在接下来的章节中,我们将研究语言的各个组成部分,如语音学、语用学、形态学、语法和语义,以及它们在塑造我们对世界各地语言的理解和使用方面所起的作用。让我们花一点时间来探索我们目前如何理解语言以及我们面临的 LLM 意图解决的挑战。

2.1.1 语言特征

我们目前对语言的理解是,语言由至少 5 个部分组成:语音学、句法学、语义学、语用学和形态学。这些部分中的每一个都对任何对话中听者所接受的整体体验和意义做出了重大贡献。并非所有的交流都使用所有这些形式,例如,你正在阅读的书籍中没有语音学,这就是为什么很多人认为短信不适合进行更严肃或更复杂的对话的原因之一。让我们仔细分析一下这些内容,以确定如何向语言模型呈现它们,以实现全面的沟通能力。

语音学

对于语言模型来说,语音学可能是最容易吸收的,它涉及到语言的实际发音。这就是口音的表现形式,涉及到语音的产生和感知,而音韵学则侧重于声音在特定语言系统内的组织方式。类似于计算机视觉,虽然作为一个整体来处理声音并不一定容易,但如何解析、向量化或标记实际的声波并不模糊。它们在每个部分都附有一个数字值,包括波峰、波谷以及每个频率周期内的斜率。相比文本,它更容易被计算机标记和处理,但同样复杂。声音本质上包含的编码意义比文本更多,例如,想象一下有人对你说“是的,对啊”。可能是讽刺的,也可能是祝贺的,这取决于语调,而英语甚至不是语调语言!不幸的是,语音学通常没有与之相关的千兆字节数据集,并且在语音数据的数据采集和清理方面,尤其是在需要训练 LLM 的规模上,这是非常困难的。在一个声音数据比文本数据更普遍且占用更小内存空间的替代世界中,基于语音或具有语音感知能力的 LLM 将会更加复杂,创造这样一个世界是一个可靠的目标。

为了应对这个问题,创建于 1888 年的国际音标(IPA)在 20 世纪和 21 世纪都进行了修订,使其更加简洁、一致和清晰,这可能是将语音认知融入文本数据的一种方式。 IPA 作为每种语言的音韵特征的国际标准化版本。音韵特征是语言使用的一套音素,例如在英语中,我们绝不会把音素/ʃ/(she, shirt, sh)与/v/音放在一起。 IPA 用来书写音素,而不是像大多数语言那样书写字母或表意文字。例如,你可以用这些符号简单地描述如何发音单词“cat”:/k/、/æ/和/t/。当然这是一个非常简化的版本,但对于模型来说,不必太复杂。你还可以描述语调和送气。这可能是文本和语音之间的一种折中,捕捉到一些音韵信息。想想短语“what’s up?” 你的发音和语调可以极大地改变你理解这个短语的方式,有时听起来像友好的“wazuuuuup”,有时像一种几乎威胁的“‘sup”,而这些都会被 IPA 完全捕捉到。然而,IPA 并不是一个完美的解决方案,例如,它并不能很好地解决模拟语调的问题。

音韵学被列为首位,因为在所有特点中,LLMs 对其应用最少,因此在改进空间方面有最大的可能性。即使是现代的 TTS 和语音合成模型大部分时间最终会将声音转换为频谱图,并分析该图像,而不是将任何类型的音韵语言建模纳入其中。这是未来几个月甚至几年研究的一个方向。

语法

这是当前 LLM 性能最好的领域,既能从用户那里解析语法,又能生成自己的语法。语法通常是我们所说的语法和词序,研究单词如何组合形成短语、从句和句子。语法也是语言学习程序开始帮助人们习得新语言的第一个地方,特别是根据你的母语出发。例如,对于一个以英语为母语的人学习土耳其语来说,知道语法完全不同很重要,你可以经常构建完全由一个长复合词组成的土耳其语句子,而在英语中,我们从来不把主语和动词合为一个词。

语法与语言意义大体上是分开的,正如语法之父诺姆·乔姆斯基(Noam Chomsky)所说的那句名言:“无色的绿色思想在狂暴地睡眠。” 这句话的一切都在语法上是正确的,并且在语义上是可以理解的。问题不在于它毫无意义,而在于它确实有意义,而且这些词的编码意义相互冲突。然而,这只是一种简化,你可以把所有时候大型语言模型给出荒谬答案看作是这种现象的表现。不幸的是,语法也是歧义最常见的地方。考虑一下这个句子,“我看见一个老人和一个女人。”现在回答这个问题:这个女人也老吗?这是语法歧义,我们不确定修饰语“老”的范围是适用于后续短语中的所有人还是仅仅适用于紧接着的那个人。这比语义和语用歧义出现在语法中更不重要。现在考虑这个句子,“我在一个山上看见一个人,他手里拿着望远镜”,然后回答这个问题:说话者在哪里,正在做什么?说话者在山上用望远镜将一个人割开吗?可能你在读这个句子时甚至没有考虑到这个选项,因为当我们解释语法时,我们所有的解释至少在语义和语用上都是得到了启发的。我们从生活经验中知道,那种解释根本不可能发生,所以我们立即将其排除,通常甚至没有时间去处理我们将其从可能含义的范围中消除的事实。当我们与大型语言模型进行项目时,请稍后考虑这一点。

对于为什么大型语言模型需要具备语法意识以实现高性能,不应该需要任何逻辑推理。那些不能正确排序单词或生成无意义内容的大型语言模型通常不被描述为“优秀”。大型语言模型对语法的依赖性是导致乔姆斯基甚至称大型语言模型为“随机鹦鹉”的原因之一。在作者看来,2018 年的 GPT2 是语言建模将语法解决为完全独立于意义的演示,我们很高兴看到最近的尝试将 GPT2 如此出色的语法与编码和暗示的意义结合起来,我们将在接下来详细讨论。

语义

语义是话语中词语的字面编码含义。人们会自动优化语义含义,只使用他们认为在当前语言时期有意义的词语。如果你曾经创建或使用过语言模型的嵌入(如 word2vec、ELMO、BERT【E 代表嵌入】、MUSE 等),你就使用了语义近似。词语经常经历语义转变,虽然我们不会涵盖这个话题的全部内容,也不会深入讨论,但下面是一些你可能已经熟悉的常见情况:狭义化,从更广泛的含义变为更具体的含义;扩义,与狭义化相反,从具体含义变为广泛含义;重新解释,经历整体或部分转变。这些转变并没有某种伟大的逻辑基础。它们甚至不必与现实相关,说某种语言的人几乎从来不会在这些变化发生时有意识地考虑。但这并不妨碍变化的发生,在语言建模的背景下,这也不会阻止我们跟上这种变化的步伐。

一些例子:狭义化包括“deer”,在古英语和中古英语中它只是指任何野生动物,甚至包括熊或美洲狮,而现在只指一种森林动物。扩义的例子是“dog”,它曾经只指英国的一种犬类,现在可以用来指任何驯化的犬类动物。关于“dog”扩义的有趣细节是在 FromSoft 的游戏《Elden Ring》中,由于玩家之间的信息传递系统有限,他们会用“dog”来指代从海龟到巨型蜘蛛甚至所有中间形态的任何东西。关于重新解释,我们可以考虑“pretty”,它曾经意味着聪明或制作精良,而不是视觉上吸引人。另一个很好的例子是“bikini”,它从指代一个特定的环礁,变成指代你在访问那个环礁时可能穿的衣服,最后人们把“bi-”解释为衣服的两部分结构,于是发明了泳装和连体泳衣。根据专家的研究和几十年的研究,我们可以把语言看作是由母语人士不断比较和重新评估的,其中会出现共同的模式。这些模式的传播在社会语言学中得到了密切研究,这在当前目的上大多不在讨论范围之内,但我们鼓励读者如果感兴趣可以研究一下,因为社会语言学现象如声望可以帮助设计对每个人都有效的系统。

在 LLM 的语境中,所谓的语义嵌入是文本的向量化版本,试图模拟语义含义。目前最流行的做法是通过对话中的每个子词(例如前缀、后缀和词素)进行标记或分配字典中的任意数字,应用连续语言模型来增加向量中每个令牌的维度,以便有一个更大的向量表示每个令牌的每个索引,然后对每个向量应用位置编码来捕获词序。每个子词最终都会根据其用法与较大词典中的其他词进行比较。考虑词嵌入时需要考虑的一点是,它们很难捕捉到这些令牌的深层编码含义,而仅仅增加嵌入的维度并没有显示出明显的改善。证明嵌入正在以类似于人类的方式工作的一个证据是,你可以将一个距离函数应用于相关的单词,看到它们比不相关的单词更接近。这是另一个可以期待在未来几年和几个月内进行开创性研究的领域,以更完整地捕捉和表示含义。

实用语用学

有时会被语言学省略,因为其指称对象是影响听者解释和说话者决定以某种方式表达事物的所有非语言语境。语用学在很大程度上指的是文化、地区、社会经济阶层和共同的生活经验中遵循的教条,通过使用蕴涵在对话中采取捷径。

如果我说,“一个知名的名人刚刚被送进了 ICU”,根据生活经验,你可能会推断一位备受喜爱的人受了严重的伤,现在正在一家设备齐全的医院接受治疗。你可能会想知道是哪位名人,他们是否需要支付医疗费用,或者伤害是否是自己造成的,这些也是基于你的生活经验。这些都不能直接从文本及其编码的意义中推断出来。你需要知道 ICU 代表一系列更广泛的词语,以及这些词语是什么。你需要知道医院是什么,以及为什么有人需要被送到那里而不是自己去。如果这些都感觉很明显,那很好。你生活在一个社会中,你对社会的语用知识与提供的例子之间有很好的重叠。如果我分享一个来自人口较少的社会的例子,“Janka 昨天晚上挨了打,明天轮到 Peter 了”,你可能会感到困惑。如果你确实感到困惑,意识到这可能就是 LLM 看待很多文本数据的方式(人格化得到认可)。对于那些想知道的人,这个句子来自斯洛伐克的复活节传统。这里有很多意义,如果你不习惯这个文化中这些特定传统,那么这些意义将被忽略和不解释。我个人有幸尝试向外国同事解释复活节兔子及其对蛋的痴迷,并享受着看起来像个疯子的满足感。

在 LLM 的语境中,我们可以有效地将所有与文本无关的内容归为语用学范畴。这意味着 LLM 在开始时没有任何关于外部世界的知识,并且在训练过程中也不会获取。它们只会获取人类对特定语用刺激的反应知识。LLM 并不理解社会阶级、种族、性别、总统候选人或者其他可能基于个人生活经验引发某种情绪的事物。语用学不是我们期望直接整合到模型中的内容,但我们已经通过数据工程和策划、提示以及指导下的监督微调看到了间接整合的好处。

一旦你获得了要训练的数据,无论你是否打算,语用结构都会被添加进去。你可以将这种类型的语用结构视为偏见,它并非本质上的好或者坏,但无法摆脱。后面你可以通过规范化和策划来选择你希望你的数据保留的偏见类型,增加特定的代表不足的点,减少代表过多或噪音干扰的例子。

数据工程和语用语境之间存在着微妙的界限,这只是理解你的数据中存在的蕴涵。蕴涵是你的数据中的语用标记,而不是你的数据集中包含的字面文本。例如,假设你有一个模型试图接受像“写一篇关于青蛙吃湿袜子的演讲,不要押韵,每行的首字母拼成两栖动物”这样的输入,并实际按照这个指令来操作。你可以立即知道这个输入要求很多。作为数据工程师,你的平衡点就是确保输入要求在你的数据中被明确考虑到。你需要有演讲的例子,青蛙和袜子以及它们的行为的例子,还有藏头诗的例子。如果没有这些,模型可能仅仅能够从数据集中存在的蕴涵中理解,但是结果可能不确定。如果你更上一层楼并且记录数据集中的蕴涵信息和显性信息以及任务,还有数据分布,你就会有例子来回答“碎垃圾导致了什么样的结果?”

LLMs 很难理解语用语言学,甚至比人类更难,但它们能够捕捉到大多数人的平均标准差。它们甚至可以复制来自标准差之外的人的回答,但是在没有准确的刺激下很难保持一致。这就是培训期间和培训后的关键。基于指令的数据集试图通过在培训过程中问问题来制造这些刺激,从而得到代表性的回答。在培训中不可能考虑到每种可能的情况,而且你试图考虑到所有情况可能会无意中造成用户新类型的回答。在培训之后,你可以通过提示从模型中获取特定的信息,这取决于你的数据最初包含的内容而有一定的限度。

词形变化。

词形变化是研究词结构以及词是如何由更小的单元(称为词素)组成的。词素是最小的具有意义的单位,例如“重新”在“重新做”或“重新学习”中。但是,并非所有单词的部分都是词素,比如“饲料”中的“ra-”或“国家”中的“na-”。

理解单词构造的方式有助于创建更好的语言模型和解析算法,这对于诸如标记化之类的任务至关重要。标记介于单词和形态素之间,在统计上它们是最可能表示意义单位的候选词,但不一定对应现有的形态素。语言模型的有效性可能取决于它有多么好地理解和处理这些标记。例如,在标记化中,模型需要存储一组字典,以便在单词和它们对应的索引之间进行转换。其中一个标记通常是""标记,它代表着模型不认识的任何单词。如果这个标记使用得太频繁,它可能会妨碍模型的性能,要么是因为模型的词汇量太小,要么是因为标记器没有为任务使用正确的算法。

想象一个场景,你想构建一个代码补全模型,但是你使用的标记器只识别由空格分隔的单词,就像 nltk 的"punkt"标记器一样。当它遇到字符串"def add_two_numbers_together(x, y):,"时,它会将"[def, , y]"传递给模型。这导致模型丢失了宝贵的信息,不仅因为它无法识别标点符号,还因为函数的重要部分由于标记器的形态学算法而被替换为未知标记。要提高模型的性能,需要更好地理解单词结构和适当的解析算法。

2.1.2 符号学

探讨了语言的基本特征并在大型语言模型的语境中考察它们的重要性之后,重要的是考虑人类交流中意义构建和解释的更广阔视角。符号学,即标志和符号的研究,为我们提供了一个宝贵的视角,通过这个视角我们可以更好地理解人们如何解释和处理语言。在接下来的部分中,我们将深入探讨符号学的领域,考察符号、指示符和抽象之间的关系,以及 LLMs 如何利用这些元素生成有意义的输出。这次讨论将更深入地理解 LLMs 模拟人类语言理解的复杂过程,同时也揭示了它们在这一努力中面临的挑战和局限性。值得注意的是,作者并不认为模仿人类行为对 LLM 的改进必然是正确答案,只是模仿是该领域迄今为止评估自己的方式。

在我们介绍符号学时,让我们考虑图 2.2 是一个改编的皮尔斯符号三角形。这些被用来将基本思想组织成第一性、第二性和第三性的序列,其中第一性位于左上角,第二性位于底部,而第三性位于右上角。如果你以前见过符号三角形,你可能会对角数和方向感到惊讶。为了解释,我们把它们倒过来,以便稍微容易阅读,并且因为该系统是递归的,我们展示了系统如何同时模拟整个过程以及每个单独部分。虽然这些想法的整个概念非常酷,但深入探讨哲学并不在本书的范围之内。相反,我们可以将焦点放在这些词(first、second、third)的基本部分上,显示事物处理的顺序。

图 2.2 这是一个递归的皮尔斯符号三角形。它是一个组织从任何事物中提取意义的过程的系统,在我们的案例中是从语言中提取的。三角形上的每个点都说明了在被用来描述的系统中合成意义所需的最小部分之一,所以在这里,每个这些点都是语言中的最小单位。Firstness、Secondness 和 Thirdness 不是三角形上的点,更像是符号学家能够在这个图表中定位自己的标记。

我们也可以看看每个三角形的每个交叉点,以了解为什么事物以它们的顺序呈现。感觉可以与图像和编码联系在一起,远在它们能够与单词和表联系在一起之前。仪式和常见脚本为被解释的动作提供了一个空间,这只是第二天性,不需要考虑,类似于大多数短语只是由单词组成,而母语使用者无需对每个单词进行元认知。所有这些最终都导向一种解释或文档(一系列话语),在我们的案例中,那种解释应该由 LLM 达成。这就是为什么,例如,提示工程可以提高模型的效力。基础 LLM 在数百万个仪式脚本示例上进行了训练,当您明确告诉模型提示需要遵循哪个脚本时,它能够更好地复制这种类型的脚本。尝试要求模型给出一个逐步解释,也许在你的生成之前加上“让我们逐步思考”,你会看到模型会基于它之前看到的脚本逐步生成脚本。

对于那些感兴趣的人来说,有特定的方法来阅读这些图表,以及整个符号学领域需要考虑的内容,然而,并不能保证通过理解整个过程就能创建最好的 LLM。与其深入研究这个问题,我们将考虑能帮助您为所有人构建最佳模型、用户体验和用户界面的最低要求。例如,创建意义的过程的一个方面是递归性。当有人对你说一些对你来说毫无意义的话时,你会怎么做?通常,人们会问一个或多个澄清问题,以弄清意思,这个过程一遍又一遍地开始,直到意思被传达。目前市场上最先进的模型并没有做到这一点,但通过非常有目的的提示可以做到这一点,但许多人甚至不知道要这样做,除非有人指出。换句话说,这是对符号学的简要介绍。你不需要能够在本节结束时向符号学领域的专家提供深入和准确的坐标特定解释。我真正想强调的是,这是一个组织系统,展示了你实际上需要创建另一个人能够解释的意义的完整图景的最少数量的事物。在训练过程中,我们没有向我们的模型提供相同数量和相同类型的信息,但如果我们这样做了,模型的行为将会显著改善。

这些图表旨在代表一个最小的组织模型,其中每个部分都是必不可少的。让我们考虑一下图 2.3,它展示了使用符号三角形的示例。考虑一下图像、图片和记忆,并想象一下如果没有眼睛来处理图像,没有文字来抽象知识,那么试图吸收这本书中的知识会是什么样子。看看项目符号等,如果没有章节,字母之间的空白和项目符号来显示你处理信息的顺序和结构,你怎么能读这本书?看看语义学和文字编码的意义,想象一下如果没有图表和没有字典定义的词,这本书会是什么样子。看看中间的电子表格,那可能是一本没有任何表格或比较信息组织者的书,包括这些图表。如果没有一个习惯和教条的文化或社会来作为我们解释的镜头,那么试图读这本书会是什么样子?所有这些观点构成了我们解释信息的能力,以及我们最终通过的镜头来识别模式。

图 2.3 从左上角开始,按照箭头的方向看,我们使用的一般顺序来构建我们的解释和从我们所接触的事物中提取意义。在这里,我们用一些每个点的例子替换了描述性词语。试想一下在没有任何单词、没有例子、没有箭头,甚至没有知道这本书中的图是用来干什么的实际语境的情况下,你是如何解释这个图的。

因此,重要的问题是:您认为 LLM 能够访问多少这样的事物以返回有意义的解释?LLM 是否能够访问情感或社会习俗?目前,他们还不能,但在我们讨论传统和新型自然语言处理推理技术时,请考虑这一点,思考不同模型能够访问的内容。

2.1.3 多语言自然语言处理

在我们评估以前的自然语言处理技术和当前一代 LLM 之前,我们需要提及的最后一个挑战是语言学的基础以及 LLM 的存在原因。自从最早的文明接触以来,人们就希望了解或利用彼此。这些情况导致了对翻译员的需求,随着全球经济的增长和繁荣,这种需求只是呈指数级增长。

对于商业而言,这其实是相当简单的数学问题。您知道孟加拉语的母语使用者几乎和英语的母语使用者一样多吗?如果这是您第一次听说孟加拉语,这应该会让您认识到多语言模型市场的价值。世界上有数十亿人口,但只有大约十亿人的母语是英语。如果您的模型像大多数模型一样以英语为中心,那么您将错过世界上 95%的人口作为客户和用户。西班牙语和普通话在这方面都是很容易的选择,但更多的人甚至没有做到这一点。

有许多涉及政治的例子,涉及将事物称为相同或不同语言的范畴超出了本书的范围。这些通常是由于政府介入等外部因素引起的。牢记这两点——一个以英语为中心的单语系统并没有像许多企业所表现出的覆盖范围或利润潜力那样可靠,并且语言和方言之间的界限充其量是不可靠的,而在最坏的情况下是系统性有害的——这应该突显出危险的意见沼泽。许多企业和研究科学家在设计产品或系统时甚至都不假装想要触及这个沼泽。

不幸的是,目前还没有简单的解决方案。然而,考虑到这些因素可以帮助你作为一名科学家或工程师(希望也是一名道德人士),设计出至少不会加剧和负面影响已经存在问题的 LLMs。这个过程的第一步是从项目开始就确定一个方向性目标,要么朝着本地化(L10n)要么朝着国际化(I18n)。本地化是 Mozilla 的一种方法,他们通过众包 L10n,在 90 多种语言中提供了不同版本的浏览器,而且没有停止这项工作的迹象。国际化类似,但方向相反,例如,宜家试图在他们的指南册中尽量少使用文字,而是选择使用国际通用的符号和图片来帮助顾客进行 DIY 项目的导航。在项目开始阶段做出决定将大大减少扩展到任一解决方案所需的努力。足够大到将翻译和格式化的看法从成本转变为投资。在 LLMs 及其在公众意识中的迅速扩展的背景下,及早考虑这一点变得更加重要。推出一项改变世界的技术,却自动排除了大多数世界的人与其互动,这会贬值那些声音。不得不等待会危及他们的经济前景。

在继续之前,让我们花点时间反思一下我们到目前为止讨论的内容。我们触及了语言学中的重要观点,这些观点为我们说明了一些概念,比如理解语言的结构与其含义是分开的。我们展示了我们每个人以及作为一个社会所经历的旅程,向着具有元认知能力的方向发展,以便理解和表达语言,以一种计算机可以处理的连贯方式。随着我们加深对认知领域的知识,以及解决我们遇到的语言特征,我们的理解将不断提高。结合图 2.1,我们将展示我们所遵循的语言建模的计算路径,并探讨它如何解决了哪些语言特征或努力创造了意义。让我们开始评估各种用于算法表示语言的技术。

2.2 语言建模技术

通过深入研究语言的基本特征、符号学的原则以及大型语言模型解释和处理语言信息的方式,我们现在将转向更加实际的领域。在接下来的部分中,我们将探讨各种自然语言处理技术的发展和应用,这些技术已被用于创建这些强大的语言模型。通过检验每种方法的优势和弱点,我们将对这些技术在捕捉人类语言和交流精髓方面的有效性获得宝贵的见解。这样的知识不仅将帮助我们欣赏自然语言处理领域取得的进步,而且还将使我们更好地了解这些模型当前的局限性以及未来研究和发展所面临的挑战。

让我们花一点时间来回顾一些数据处理,这些处理将适用于所有语言建模。首先,我们需要决定如何分割要传入模型的单词和符号,有效地决定我们模型中的一个标记将是什么。然后,我们需要找到一种方法将这些标记转换为数字值,然后再转换回来。接下来,我们需要选择我们的模型将如何处理标记化输入。以下每种技术都将至少在其中一种方面基于先前的技术进行构建。

这些技术中的第一种被称为词袋(BoW)模型,它只是简单地计算文本中单词的出现次数。它可以很容易地通过一个扫描文本的字典来完成,为每个新单词创建一个新的词汇表条目作为关键字,并以 1 开始递增的值。考虑到它的简单性,即使完全基于频率的这种模型在试图洞悉演讲者意图或至少是他们的怪癖时也可能非常有用。例如,你可以在美国总统就职演讲中运行一个简单的 BoW 模型,搜索自由、经济和敌人这几个词,以便从提及每个单词的次数多少来相当不错地洞悉哪位总统是在和平时期上任的、在战时上任的以及在货币困难时期上任的。然而,BoW 模型也有很多弱点,因为这种模型不提供任何图像、语义、语用、短语或感情。它没有任何机制来评估上下文或语音学,并且因为它默认以空格划分单词(显然你可以按照自己的方式进行标记化,但尝试在子词上进行标记化并观察这个模型会发生什么事情—— 剧透,结果糟糕),它也不考虑形态。总的来说,它应该被认为是一种代表语言的弱模型,但也是评估其他模型的强基准。为了解决词袋模型不捕捉任何序列数据的问题,N-Gram 模型应运而生。

2.2.1 N-Gram 和基于语料库的技术

N-Gram 模型是对 BoW 的显着但高效的改进,其中您可以给模型一种上下文,由 N 表示。它们是相对简单的统计模型,允许您根据 N-1 上下文空间生成单词。查看列表 2.1,我正在使用三元组,这意味着 N=3。我清理文本并进行最小的填充/格式化,以帮助模型,然后我们使用 everygrams 进行训练,这意味着优先灵活性而不是效率,因此您也可以选择训练五元或七元(N=5,N=7)模型。在我生成的列表的末尾,我可以给模型多达 2 个标记,以帮助它进一步生成。N-Gram 模型并未被创建,甚至从未声称尝试完整地对语言知识进行建模,但它们在实际应用中非常有用。它们忽略了所有语言特征,包括句法,只尝试在 N 长度短语中的词之间建立概率连接。

查看图 2.2,N-Grams 实际上只使用静态信号(空白、正字法)和单词来尝试提取任何含义。它试图手动测量短语,假设所有短语的长度都相同。话虽如此,N-Grams 可用于创建文本分析的强大基线,如果分析员已经了解话语的实用语境,它们可以用来快速而准确地洞察现实场景。可以通过设置 N=1000000000 或更高来制作 N-Gram LLM,但这没有任何实际应用,因为 99.9% 的所有文本和 100% 的所有有意义的文本都包含少于十亿次出现的标记,并且计算能力可以更好地用在其他地方。

列表 2.1 生成式 N-Grams 语言模型实现
from nltk.corpus.reader import PlaintextCorpusReader
from nltk.util import everygrams
from nltk.lm.preprocessing import (
    pad_both_ends,
    flatten,
    padded_everygram_pipeline,
)
from nltk.lm import MLE

# Create a corpus from any number of plain .txt files
my_corpus = PlaintextCorpusReader("./", ".*\.txt")

for sent in my_corpus.sents(fileids="hamlet.txt"):
    print(sent)

# Pad each side of every line in the corpus with <s> and </s> to indicate the start and end of utterances
padded_trigrams = list(
    pad_both_ends(my_corpus.sents(fileids="hamlet.txt")[1104], n=2)
)
list(everygrams(padded_trigrams, max_len=3))

list(
    flatten(
        pad_both_ends(sent, n=2)
        for sent in my_corpus.sents(fileids="hamlet.txt")
    )
)

# Allow everygrams to create a training set and a vocab object from the data
train, vocab = padded_everygram_pipeline(
    3, my_corpus.sents(fileids="hamlet.txt")
)

# Instantiate and train the model we’ll use for N-Grams, a Maximum Likelihood Estimator (MLE)
# This model will take the everygrams vocabulary, including the <UNK> token used for out-of-vocabulary
lm = MLE(3)
len(lm.vocab)

lm.fit(train, vocab)
print(lm.vocab)
len(lm.vocab)

# And finally, language can be generated with this model and conditioned with n-1 tokens preceding
lm.generate(6, ["to", "be"])

上面的代码就是创建生成式 N-Gram 模型所需的全部内容。对于那些希望能进一步评估该模型的人,我们在下面的代码中添加了获取概率和对数分数、分析特定短语的熵和困惑度的代码。因为这一切都是基于频率的,即使在数学上具有重要意义,它仍然不能很好地描述现实世界语言的困惑程度或频率。

# Any set of tokens up to length=n can be counted easily to determine frequency
print(lm.counts)
lm.counts[["to"]]["be"]

# Any token can be given a probability of occurrence, and can be augmented with up to n-1 tokens to precede it
print(lm.score("be"))
print(lm.score("be", ["to"]))
print(lm.score("be", ["not", "to"]))

# This can be done as a log score as well to avoid very big and very small numbers
print(lm.logscore("be"))
print(lm.logscore("be", ["to"]))
print(lm.logscore("be", ["not", "to"]))

# Sets of tokens can be tested for entropy and perplexity as well
test = [("to", "be"), ("or", "not"), ("to", "be")]
print(lm.entropy(test))
print(lm.perplexity(test))

尽管此代码示例说明了创建三元语言模型,但遗憾的是,并非所有需要捕获的短语都只有 3 个标记。例如,来自《哈姆雷特》的“生存还是毁灭”,包含一个由 2 个词组成的短语和一个由 4 个词组成的短语。这种短语建模也无法捕获单个词可能具有的任何语义编码。为了解决这些问题,贝叶斯统计学被应用于语言建模。

2.2.2 贝叶斯技术

贝叶斯定理是描述输出发生在输入空间中的最数学上严谨且简单的理论之一。基本上,它根据先前的知识计算事件发生的概率。定理认为,给定证据为真的情况下一个假设为真的概率(例如,一句话具有积极情感),等于证据发生在假设为真的情况下的概率乘以假设发生的概率,然后除以证据为真的概率。数学表示为:

P(hypothesis | evidence) = (P(evidence | hypothesis) * P(hypothesis)) / P(evidence)

P(A|B) * P(B) = P(B|A) * P(A)

因为这既不是一本数学书,也不想过多地深入理论,我们相信您可以进一步了解这个定理。

不幸的是,尽管该定理在数学上对数据进行了准确的描述,但它没有考虑到任何随机性或单词的多重含义。你可以用一个词来困惑贝叶斯模型,让其产生错误的结果,这个词就是"it"。任何指示代词最终都会被赋予与其他单词相同的 LogPrior 和 LogLikelihood 值,并且得到一个静态值,而这与这些单词的使用方式相悖。例如,如果你想对一个话语进行情感分析,最好给所有代词赋予一个空值,而不是让它们通过贝叶斯训练。还应该注意,贝叶斯技术并不像其他技术一样会创建生成式语言模型。由于贝叶斯定理验证一个假设,这些模型适用于分类,并且可以为生成式语言模型带来强大的增强。

在第 2.2 节中,我们展示了如何创建一个朴素贝叶斯分类语言模型。我们选择了手写代码而不是使用像 sklearn 这样的软件包,虽然代码会更长一些,但应该更有助于理解其工作原理。我们使用的是最简化版本的朴素贝叶斯模型,没有添加任何复杂的内容,如果你选择对任何你想解决的问题进行升级,这些都可以得到改进。我们强烈建议您这样做。

第 2.2 节 朴素贝叶斯分类语言模型实现
from utils import process_utt, lookup
from nltk.corpus.reader import PlaintextCorpusReader
import numpy as np

my_corpus = PlaintextCorpusReader("./", ".*\.txt")

sents = my_corpus.sents(fileids="hamlet.txt")

def count_utts(result, utts, ys):
    """
    Input:
        result: a dictionary that is used to map each pair to its frequency
        utts: a list of utts
        ys: a list of the sentiment of each utt (either 0 or 1)
    Output:
        result: a dictionary mapping each pair to its frequency
    """

    for y, utt in zip(ys, utts):
        for word in process_utt(utt):
            # define the key, which is the word and label tuple
            pair = (word, y)

            # if the key exists in the dictionary, increment the count
            if pair in result:
                result[pair] += 1

            # if the key is new, add it to the dict and set the count to 1
            else:
                result[pair] = 1

    return result

result = {}
utts = [" ".join(sent) for sent in sents]
ys = [sent.count("be") > 0 for sent in sents]
count_utts(result, utts, ys)

freqs = count_utts({}, utts, ys)
lookup(freqs, "be", True)
for k, v in freqs.items():
    if "be" in k:
        print(f"{k}:{v}")

def train_naive_bayes(freqs, train_x, train_y):
    """
    Input:
        freqs: dictionary from (word, label) to how often the word appears
        train_x: a list of utts
        train_y: a list of labels correponding to the utts (0,1)
    Output:
        logprior: the log prior.
        loglikelihood: the log likelihood of you Naive bayes equation.
    """
    loglikelihood = {}
    logprior = 0

    # calculate V, the number of unique words in the vocabulary
    vocab = set([pair[0] for pair in freqs.keys()])
    V = len(vocab)

    # calculate N_pos and N_neg
    N_pos = N_neg = 0
    for pair in freqs.keys():
        # if the label is positive (greater than zero)
        if pair[1] > 0:
            # Increment the number of positive words (word, label)
            N_pos += lookup(freqs, pair[0], True)

        # else, the label is negative
        else:
            # increment the number of negative words (word,label)
            N_neg += lookup(freqs, pair[0], False)

    # Calculate D, the number of documents
    D = len(train_y)

    # Calculate the number of positive documents
    D_pos = sum(train_y)

    # Calculate the number of negative documents
    D_neg = D - D_pos

    # Calculate logprior
    logprior = np.log(D_pos) - np.log(D_neg)

    # For each word in the vocabulary...
    for word in vocab:
        # get the positive and negative frequency of the word
        freq_pos = lookup(freqs, word, 1)
        freq_neg = lookup(freqs, word, 0)

        # calculate the probability that each word is positive, and negative
        p_w_pos = (freq_pos + 1) / (N_pos + V)
        p_w_neg = (freq_neg + 1) / (N_neg + V)

        # calculate the log likelihood of the word
        loglikelihood[word] = np.log(p_w_pos / p_w_neg)

    return logprior, loglikelihood

def naive_bayes_predict(utt, logprior, loglikelihood):
    """
    Input:
        utt: a string
        logprior: a number
        loglikelihood: a dictionary of words mapping to numbers
    Output:
        p: the sum of all the logliklihoods + logprior
    """
    # process the utt to get a list of words
    word_l = process_utt(utt)

    # initialize probability to zero
    p = 0

    # add the logprior
    p += logprior

    for word in word_l:
        # check if the word exists in the loglikelihood dictionary
        if word in loglikelihood:
            # add the log likelihood of that word to the probability
            p += loglikelihood[word]

    return p

def test_naive_bayes(test_x, test_y, logprior, loglikelihood):
    """
    Input:
        test_x: A list of utts
        test_y: the corresponding labels for the list of utts
        logprior: the logprior
        loglikelihood: a dictionary with the loglikelihoods for each word
    Output:
        accuracy: (# of utts classified correctly)/(total # of utts)
    """
    accuracy = 0  # return this properly

    y_hats = []
    for utt in test_x:
        # if the prediction is > 0
        if naive_bayes_predict(utt, logprior, loglikelihood) > 0:
            # the predicted class is 1
            y_hat_i = 1
        else:
            # otherwise the predicted class is 0
            y_hat_i = 0

        # append the predicted class to the list y_hats
        y_hats.append(y_hat_i)

    # error = avg of the abs vals of the diffs between y_hats and test_y
    error = sum(
        [abs(y_hat - test) for y_hat, test in zip(y_hats, test_y)]
    ) / len(y_hats)

    # Accuracy is 1 minus the error
    accuracy = 1 - error

    return accuracy

if __name__ == "__main__":
    logprior, loglikelihood = train_naive_bayes(freqs, utts, ys)
    print(logprior)
    print(len(loglikelihood))

    my_utt = "To be or not to be, that is the question."
    p = naive_bayes_predict(my_utt, logprior, loglikelihood)
    print("The expected output is", p)

    print(
        "Naive Bayes accuracy = %0.4f"
        % (test_naive_bayes(utts, ys, logprior, loglikelihood))
    )

这个定理并没有创建同类型的语言模型,而是一种与一个假设相关的概率列表。因此,贝叶斯语言模型不能有效地用于生成语言,但在分类任务中可以非常强大地应用。尽管如此,在我看来,贝叶斯模型往往被过度炒作,即使是在这个任务中也是如此。我职业生涯中的一个巅峰时刻就是将一种贝叶斯模型替换并从生产中移除。

在贝叶斯模型中,一个重要的问题是所有序列实质上都是完全不相关的,就像 BoW 模型一样,将我们从 N-Grams 的序列建模的另一端移动过来。类似于钟摆一样,语言建模在马尔可夫链中再次摆回到序列建模和语言生成。

2.2.3 马尔可夫链

马尔可夫链通常称为隐马尔可夫模型(HMMs),本质上是在之前提到的 N-Gram 模型中添加了状态,使用隐藏状态存储概率。它们通常用于帮助解析文本数据以供更大的模型使用,执行诸如词性标注(Part-of-Speech tagging,将单词标记为它们的词性)和命名实体识别(NER,将标识性单词标记为它们的指示词和通常的类型,例如 LA - 洛杉矶 - 城市)等任务。与之前的贝叶斯模型不同,马尔可夫模型完全依赖于随机性(可预测的随机性),而贝叶斯模型则假装它不存在。然而,其思想同样在数学上是正确的,即任何事情发生的概率 下一个 完全取决于 现在 的状态。因此,我们不是仅基于其历史发生情况对单词进行建模,并从中提取概率,而是基于当前正在发生的情况对其未来和过去的搭配进行建模。因此,“happy” 发生的概率会几乎降至零,如果刚刚输出了“happy”,但如果刚刚发生了“am”,则会显着提高。马尔可夫链非常直观,以至于它们被纳入了贝叶斯统计学的后续迭代中,并且仍然在生产系统中使用。

在清单 2.3 中,我们训练了一个马尔可夫链生成式语言模型。这是我们第一次使用特定的标记器,本例中将基于单词之间的空格进行标记化。这也是我们第二次提到了一组意图作为文档一起查看的话语。当您尝试此模型时,请仔细注意并自行进行一些比较,看看 HMM 的生成效果与即使是大型 N-Gram 模型相比如何。

清单 2.3 生成式隐马尔可夫语言模型实现
import re
import random
from nltk.tokenize import word_tokenize
from collections import defaultdict, deque

class MarkovChain:
    def __init__(self):
        self.lookup_dict = defaultdict(list)
        self._seeded = False
        self.__seed_me()

    def __seed_me(self, rand_seed=None):
        if self._seeded is not True:
            try:
                if rand_seed is not None:
                    random.seed(rand_seed)
                else:
                    random.seed()
                self._seeded = True
            except NotImplementedError:
                self._seeded = False

    def add_document(self, str):
        preprocessed_list = self._preprocess(str)
        pairs = self.__generate_tuple_keys(preprocessed_list)
        for pair in pairs:
            self.lookup_dict[pair[0]].append(pair[1])

    def _preprocess(self, str):
        cleaned = re.sub(r"\W+", " ", str).lower()
        tokenized = word_tokenize(cleaned)
        return tokenized

    def __generate_tuple_keys(self, data):
        if len(data) < 1:
            return

        for i in range(len(data) - 1):
            yield [data[i], data[i + 1]]

    def generate_text(self, max_length=50):
        context = deque()
        output = []
        if len(self.lookup_dict) > 0:
            self.__seed_me(rand_seed=len(self.lookup_dict))
            chain_head = [list(self.lookup_dict)[0]]
            context.extend(chain_head)

            while len(output) < (max_length - 1):
                next_choices = self.lookup_dict[context[-1]]
                if len(next_choices) > 0:
                    next_word = random.choice(next_choices)
                    context.append(next_word)
                    output.append(context.popleft())
                else:
                    break
            output.extend(list(context))
        return " ".join(output)

if __name__ == "__main__":
    with open("hamlet.txt", "r", encoding="utf-8") as f:
        text = f.read()
    HMM = MarkovChain()
    HMM.add_document(text)

    print(HMM.generate_text(max_length=25))

这段代码展示了一个用于生成的马尔可夫模型的基本实现,我们鼓励读者对其进行实验,将其与你最喜欢的音乐家的歌曲或最喜欢的作者的书籍进行结合,看看生成的内容是否真的听起来像他们。HMM 非常快速,通常用于预测文本或预测搜索应用。马尔可夫模型代表了对语言进行描述性语言学建模的首次全面尝试,而不是规范性的尝试,这很有趣,因为马尔可夫最初并不打算用于语言建模,只是为了赢得关于连续独立状态的论战。后来,马尔可夫使用马尔可夫链来模拟普希金小说中的元音分布,所以他至少意识到了可能的应用。

描述性语言学和规范性语言学的区别在于一个关注事物应该如何,而另一个关注事物如何。从语言建模的角度来看,从语料库或马尔可夫模型的角度描述语言正在做什么已被证明要比试图规定语言应该如何行为更加有效。不幸的是,仅有当前状态本身无法提供超越当下的背景,因此历史或社会背景无法在马尔可夫模型中有效地表示。单词的语义编码也成为问题,正如代码示例所示,马尔可夫链将输出语法上正确但语义上毫无意义的单词链,类似于“无色绿色思想狂暴地睡着了。”为了试图解决这个问题,发展出了“连续”模型,以允许对令牌进行“语义嵌入”表示。

2.2.4 连续语言建模

连续词袋(CBoW),就像它的名字一样,词袋一样,是一种基于频率的语言分析方法,意味着它根据单词出现的频率对单词进行建模。话语中的下一个单词从未基于概率或频率来确定。由于这个原因,所给出的示例将是如何使用 CBoW 创建要由其他模型摄取或比较的单词嵌入。我们将使用神经网络进行此操作,以为您提供一个良好的方法论。

这是我们将看到的第一个语言建模技术,它基本上是在给定话语上滑动一个上下文窗口(上下文窗口是一个 N-gram 模型),并尝试根据窗口中的周围单词猜测中间的单词。例如,假设你的窗口长度为 5,你的句子是“学习语言学让我感到快乐”,你会给出 CBoW[‘学习’, ‘关于’, ‘使’, ‘我’],并试图让模型猜测“语言学”,根据模型之前在类似位置看到该单词出现的次数。这应该会向你展示为什么像这样训练的模型难以生成,因为如果你给出[‘使’, ’我’, ’]作为输入,首先它只有 3 个信息要尝试解决,而不是 4 个,它还将倾向于只猜测它之前在句子末尾看到过的单词,而不是准备开始新的从句。但情况并不完全糟糕,连续模型在嵌入方面突出的一个特征是,它不仅可以查看目标词之前的单词,还可以使用目标之后的单词来获得一些上下文的相似性。

在列表 2.4 中,我们创建了我们的第一个连续模型。在我们的例子中,为了尽可能简单,我们使用词袋进行语言处理,使用一个两个参数的单层神经网络进行嵌入估计,尽管这两者都可以被替换为任何其他模型。例如,你可以将 N-gram 替换为词袋,将朴素贝叶斯替换为神经网络,得到一个连续朴素 N-gram 模型。重点是这种技术中使用的实际模型有点随意,更重要的是连续技术。为了进一步说明这一点,我们除了使用 numpy 做神经网络的数学运算外,没有使用任何其他包,尽管这是我们在本节中首次出现。

特别注意下面的步骤,初始化模型权重,ReLU 激活函数,最终的 softmax 层,前向和反向传播,以及它们如何在gradient_descent函数中组合在一起。这些是拼图中的片段,你将一遍又一遍地看到它们出现,不论编程语言或框架如何。无论你使用 Tensorflow、Pytorch 还是 HuggingFace,如果你开始创建自己的模型而不是使用别人的模型,你都需要初始化模型、选择激活函数、选择最终层,并在前向和反向传播中定义。

列表 2.4 生成连续词袋语言模型实现
import nltk
import numpy as np
from utils import get_batches, compute_pca, get_dict
import re
from matplotlib import pyplot

# Create our corpus for training
with open("hamlet.txt", "r", encoding="utf-8") as f:
    data = f.read()

# Slightly clean the data by removing punctuation, tokenizing by word, and converting to lowercase alpha characters
data = re.sub(r"[,!?;-]", ".", data)
data = nltk.word_tokenize(data)
data = [ch.lower() for ch in data if ch.isalpha() or ch == "."]
print("Number of tokens:", len(data), "\n", data[500:515])

# Get our Bag of Words, along with a distribution
fdist = nltk.FreqDist(word for word in data)
print("Size of vocabulary:", len(fdist))
print("Most Frequent Tokens:", fdist.most_common(20))

# Create 2 dictionaries to speed up time-to-convert and keep track of vocabulary
word2Ind, Ind2word = get_dict(data)
V = len(word2Ind)
print("Size of vocabulary:", V)

print("Index of the word 'king':", word2Ind["king"])
print("Word which has index 2743:", Ind2word[2743])

# Here we create our Neural network with 1 layer and 2 parameters
def initialize_model(N, V, random_seed=1):
    """
    Inputs:
        N: dimension of hidden vector
        V: dimension of vocabulary
        random_seed: seed for consistent results in tests
    Outputs:
        W1, W2, b1, b2: initialized weights and biases
    """
    np.random.seed(random_seed)

    W1 = np.random.rand(N, V)
    W2 = np.random.rand(V, N)
    b1 = np.random.rand(N, 1)
    b2 = np.random.rand(V, 1)

    return W1, W2, b1, b2

# Create our final classification layer, which makes all possibilities add up to 1
def softmax(z):
    """
    Inputs:
        z: output scores from the hidden layer
    Outputs:
        yhat: prediction (estimate of y)
    """
    yhat = np.exp(z) / np.sum(np.exp(z), axis=0)
    return yhat

# Define the behavior for moving forward through our model, along with an activation function
def forward_prop(x, W1, W2, b1, b2):
    """
    Inputs:
        x: average one-hot vector for the context
        W1,W2,b1,b2: weights and biases to be learned
    Outputs:
        z: output score vector
    """
    h = W1 @ x + b1
    h = np.maximum(0, h)
    z = W2 @ h + b2
    return z, h

# Define how we determine the distance between ground truth and model predictions
def compute_cost(y, yhat, batch_size):
    logprobs = np.multiply(np.log(yhat), y) + np.multiply(
        np.log(1 - yhat), 1 - y
    )
    cost = -1 / batch_size * np.sum(logprobs)
    cost = np.squeeze(cost)
    return cost

# Define how we move backward through the model and collect gradients
def back_prop(x, yhat, y, h, W1, W2, b1, b2, batch_size):
    """
    Inputs:
        x:  average one hot vector for the context
        yhat: prediction (estimate of y)
        y:  target vector
        h:  hidden vector (see eq. 1)
        W1, W2, b1, b2:  weights and biases
        batch_size: batch size
     Outputs:
        grad_W1, grad_W2, grad_b1, grad_b2:  gradients of weights and biases
    """
    l1 = np.dot(W2.T, yhat - y)
    l1 = np.maximum(0, l1)
    grad_W1 = np.dot(l1, x.T) / batch_size
    grad_W2 = np.dot(yhat - y, h.T) / batch_size
    grad_b1 = np.sum(l1, axis=1, keepdims=True) / batch_size
    grad_b2 = np.sum(yhat - y, axis=1, keepdims=True) / batch_size

    return grad_W1, grad_W2, grad_b1, grad_b2

# Put it all together and train
def gradient_descent(data, word2Ind, N, V, num_iters, alpha=0.03):
    """
    This is the gradient_descent function

      Inputs:
        data:      text
        word2Ind:  words to Indices
        N:         dimension of hidden vector
        V:         dimension of vocabulary
        num_iters: number of iterations
     Outputs:
        W1, W2, b1, b2:  updated matrices and biases

    """
    W1, W2, b1, b2 = initialize_model(N, V, random_seed=8855)
    batch_size = 128
    iters = 0
    C = 2
    for x, y in get_batches(data, word2Ind, V, C, batch_size):
        z, h = forward_prop(x, W1, W2, b1, b2)
        yhat = softmax(z)
        cost = compute_cost(y, yhat, batch_size)
        if (iters + 1) % 10 == 0:
            print(f"iters: {iters+1} cost: {cost:.6f}")
        grad_W1, grad_W2, grad_b1, grad_b2 = back_prop(
            x, yhat, y, h, W1, W2, b1, b2, batch_size
        )
        W1 = W1 - alpha * grad_W1
        W2 = W2 - alpha * grad_W2
        b1 = b1 - alpha * grad_b1
        b2 = b2 - alpha * grad_b2
        iters += 1
        if iters == num_iters:
            break
        if iters % 100 == 0:
            alpha *= 0.66

    return W1, W2, b1, b2

# Train the model
C = 2
N = 50
word2Ind, Ind2word = get_dict(data)
V = len(word2Ind)
num_iters = 150
print("Call gradient_descent")
W1, W2, b1, b2 = gradient_descent(data, word2Ind, N, V, num_iters)
Call gradient descent
Iters: 10 loss: 0.525015
Iters: 20 loss: 0.092373
Iters: 30 loss: 0.050474
Iters: 40 loss: 0.034724
Iters: 50 loss: 0.026468
Iters: 60 loss: 0.021385
Iters: 70 loss: 0.017941
Iters: 80 loss: 0.015453
Iters: 90 loss: 0.012099
Iters: 100 loss: 0.012099
Iters: 110 loss: 0.011253
Iters: 120 loss: 0.010551
Iters: 130 loss: 0.009932
Iters: 140 loss: 0.009382
Iters: 150 loss: 0.008889

CBoW 示例是我们的第一个代码示例,展示了机器学习中完整有效的训练循环。在所有这些中,我们要求读者特别注意训练循环中的步骤,特别是激活函数 ReLU。由于我们希望读者至少熟悉各种 ML 范式,包括不同的激活函数,因此我们不会在这里解释 ReLU,而是解释为什么应该使用它以及为什么不应该使用它。ReLU 虽然解决了梯度消失问题,但并未解决梯度爆炸问题,并且会严重破坏模型内的所有负比较。更好的情况变体包括 ELU,它允许负数归一化到 alpha,或者 GEGLU/SWIGLU,在越来越复杂的场景中表现良好,如语言。然而,人们经常使用 ReLU,不是因为它们在某种情况下是最好的,而是因为它们易于理解、易于编码、直观,甚至比它们被创建来替代的激活函数如 sigmoid 或 tanh 更加直观。

许多情况下都会使用包等进行抽象处理,但了解底层发生的情况对于你作为 LLMs 投入生产的人来说将非常有帮助。你应该能够相当肯定地预测不同模型在各种情况下的行为。接下来的部分将深入探讨其中一个抽象,这种情况下是由连续建模技术创建的抽象。

2.2.5 嵌入

回想一下我们的语言特征,很容易理解为什么连续式语言建模是一次重大突破。嵌入接受我们创建的令牌化向量,这些向量不包含任何意义,并尝试根据可以从文本中得到的观察结果插入该意义,例如单词顺序和出现在相似上下文中的子词。尽管主要的意义模式是搭配(共同出现,即相邻的单词),但它们被证明是有用的,甚至显示出与人类编码的单词意义一些相似之处。

Word2Vec 中的典型例子之一是,将“king”的向量减去“man”的向量,加上“woman”的向量,然后找到其总和的最近邻居,得到的词向量就是“queen”的向量。这对我们来说是有意义的,因为它模拟了人类的语义。其中一个主要区别已经提到了几次,即语用学。人类使用语用背景来确定语义意义,理解仅仅因为你说了“我需要食物”并不意味着你实际上没有食物就会有危险。嵌入是纯粹使用情境之外的任何影响,这感觉就像是人类学习的方式,各方面都有很好的论点。唯一的问题是,如果我们可以以某种方式为模型提供更多的感知数据,那可能会为更有效的嵌入打开大门。

在第 2.5 节中,我们将深入探讨如何使用 pyplot 可视化嵌入。我们将在后面的章节中更深入地研究嵌入。这对于模型的可解释性以及在预训练步骤中进行验证都是有帮助的。如果您发现您的语义相似的嵌入在图上相对靠近彼此,那么您很可能朝着正确的方向前进了。

2.5 嵌入可视化
# After listing 2.4 is done and gradient descent has been executed
words = [
    "King",
    "Queen",
    "Lord",
    "Man",
    "Woman",
    "Prince",
    "Ophelia",
    "Rich",
    "Happy",
]
embs = (W1.T + W2) / 2.0
idx = [word2Ind[word] for word in words]
X = embs[idx, :]
print(X.shape, idx)

result = compute_pca(X, 2)
pyplot.scatter(result[:, 0], result[:, 1])
for i, word in enumerate(words):
    pyplot.annotate(word, xy=(result[i, 0], result[i, 1]))
pyplot.show()
图 2.4 一种用于词嵌入的可视化技术。可视化嵌入对于模型的可解释性非常重要。

如图 2.4 所示,这是我们从 CBoW 模型训练得到的一个成功但非常稀疏的嵌入表示。让这些语义表示(嵌入)更密集是我们可以看到这一领域改进的主要地方,尽管许多成功的实验已经运行过,其中更密集的语义含义已经通过指导和不同的思维链技术取代了更大的语用背景。我们稍后将讨论“思维链”(CoT)和其他技术。现在,让我们转而讨论为什么我们的连续嵌入技术甚至可能成功,鉴于基于频率的模型通常很难与现实相关联。所有这一切都始于半个多世纪前的多层感知器。

2.2.6 多层感知器

MLPs 体现了这样一种情感,“机器在做一件事情时真的很擅长,所以我希望我们可以使用一堆真的擅长做这一件事情的机器来制造一个擅长做很多事情的机器。” MLP 中的每个权重和偏置都擅长检测一个特征,所以我们将它们绑在一起以检测更大、更复杂的特征。MLPs 在大多数神经网络架构中充当主要的构建模块。架构之间的关键区别,如卷积神经网络和循环神经网络,主要来自数据加载方法和处理标记化和嵌入数据在模型层之间流动的方式,而不是个别层的功能,特别是全连接层。

在 2.6 中,我们提供了一个更动态的神经网络类,它可以根据您的任务需要拥有任意数量的层和参数。我们使用 pytorch 提供了一个更明确和明确的类,以便为您提供实现 MLP 的工具,无论您是从头开始还是在流行的框架中使用。

2.6 多层感知机 Pytorch 类实现
import torch
import torch.nn as nn
import torch.nn.functional as F

class MultiLayerPerceptron(nn.Module):
    def __init__(
        self,
        input_size,
        hidden_size=2,
        output_size=3,
        num_hidden_layers=1,
        hidden_activation=nn.Sigmoid,
    ):
        """Initialize weights.
        Args:
            input_size (int): size of the input
            hidden_size (int): size of the hidden layers
            output_size (int): size of the output
            num_hidden_layers (int): number of hidden layers
            hidden_activation (torch.nn.*): the activation class
        """
        super(MultiLayerPerceptron, self).__init__()
        self.module_list = nn.ModuleList()
        interim_input_size = input_size
        interim_output_size = hidden_size
        torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

        for _ in range(num_hidden_layers):
            self.module_list.append(
                nn.Linear(interim_input_size, interim_output_size)
            )
            self.module_list.append(hidden_activation())
            interim_input_size = interim_output_size

        self.fc_final = nn.Linear(interim_input_size, output_size)

        self.last_forward_cache = []

    def forward(self, x, apply_softmax=False):
        """The forward pass of the MLP

        Args:
            x_in (torch.Tensor): an input data tensor.
                x_in.shape should be (batch, input_dim)
            apply_softmax (bool): a flag for the softmax activation
                should be false if used with the Cross Entropy losses
        Returns:
            the resulting tensor. tensor.shape should be (batch, output_dim)
        """
        for module in self.module_list:
            x = module(x)

        output = self.fc_final(x)

        if apply_softmax:
            output = F.softmax(output, dim=1)

        return output

从代码中我们可以看到,与具有静态两层的 CBoW 实现相反,直到实例化之前,这个 MLP 的大小是不固定的。如果您想给这个模型一百万层,您只需在实例化类时将 num_hidden_layers=1000000,尽管仅仅因为您可以给模型那么多参数,并不意味着它会立即变得更好。LLMs 不仅仅是很多层。像 RNNs 和 CNNs 一样,LLMs 的魔力在于数据如何进入并通过模型。为了说明这一点,让我们看一下 RNN 及其一个变体。

2.2.7 循环神经网络(RNNs)和长短期记忆网络(LSTMs)

循环神经网络(RNNs)是一类神经网络,设计用于分析序列,基于以前语言建模技术的弱点。其逻辑是,如果语言以序列的方式呈现,那么处理它的方式可能应该是按序列而不是逐个标记进行的。RNNs 通过使用我们以前看到的逻辑来实现这一点,在 MLPs 和马尔科夫链中都看到过,即在处理新输入时引用内部状态或记忆,并在检测到节点之间的连接有用时创建循环。

在完全递归网络中,如清单 2.7 中的网络,所有节点最初都连接到所有后续节点,但这些连接可以设置为零,以模拟它们被断开如果它们不是有用的。这解决了较早模型遇到的最大问题之一,即静态输入大小,并使得 RNN 及其变体能够处理可变长度的输入。不幸的是,较长的序列带来了一个新问题。因为网络中的每个神经元都与后续神经元连接,所以较长的序列对总和产生的改变较小,使得梯度变得较小,最终消失,即使有重要的词也是如此。

例如,让我们考虑具有任务情感分析的这些句子,“昨晚我喜欢看电影”,以及,“昨晚我去看的电影是我曾经期望看到的最好的。”即使这些句子不完全相同,它们也可以被认为在语义上相似。在通过 RNN 时,第一句中的每个单词都更有价值,其结果是第一句的积极评分比第二句高,仅仅因为第一句更短。反之亦然,爆炸梯度也是这种序列处理的结果,这使得训练深层 RNNs 变得困难。

要解决这个问题,长短期记忆(LSTMs)作为一种 RNN,使用记忆单元和门控机制,保持能够处理可变长度的序列,但没有较长和较短序列被理解为不同的问题。预见到多语言场景,并理解人们不只是单向思考语言,LSTMs 还可以通过将两个 RNNs 的输出连接起来进行双向处理,一个从左到右读取序列,另一个从右到左。这种双向性提高了结果,允许信息即使在经过数千个标记之后也能被看到和记住。

在清单 2.7 中,我们提供了 RNN 和 LSTM 的类。在与本书相关的存储库中的代码中,您可以看到训练 RNN 和 LSTM 的结果,结果是 LSTM 在训练集和验证集上的准确性都更好,而且仅需一半的时代(25 次与 RNN 的 50 次)。需要注意的一个创新是利用填充的打包嵌入,将所有可变长度序列扩展到最大长度,以便处理任何长度的输入,只要它比最大长度短即可。

清单 2.7 递归神经网络和长短期记忆 Pytorch 类实现
import torch
from gensim.models import Word2Vec
from sklearn.model_selection import train_test_split

# Create our corpus for training
with open("./chapters/chapter_2/hamlet.txt", "r", encoding="utf-8") as f:
    data = f.readlines()

# Embeddings are needed to give semantic value to the inputs of an LSTM
# embedding_weights = torch.Tensor(word_vectors.vectors)

EMBEDDING_DIM = 100
model = Word2Vec(data, vector_size=EMBEDDING_DIM, window=3, min_count=3, workers=4)
word_vectors = model.wv
print(f"Vocabulary Length: {len(model.wv)}")
del model

padding_value = len(word_vectors.index_to_key)
embedding_weights = torch.Tensor(word_vectors.vectors)

class RNN(torch.nn.Module):
    def __init__(
        self,
        input_dim,
        embedding_dim,
        hidden_dim,
        output_dim,
        embedding_weights,
    ):
        super().__init__()
        self.embedding = torch.nn.Embedding.from_pretrained(
            embedding_weights
        )
        self.rnn = torch.nn.RNN(embedding_dim, hidden_dim)
        self.fc = torch.nn.Linear(hidden_dim, output_dim)

    def forward(self, x, text_lengths):
        embedded = self.embedding(x)
        packed_embedded = torch.nn.utils.rnn.pack_padded_sequence(
            embedded, text_lengths
        )
        packed_output, hidden = self.rnn(packed_embedded)
        output, output_lengths = torch.nn.utils.rnn.pad_packed_sequence(
            packed_output
        )
        return self.fc(hidden.squeeze(0))

INPUT_DIM = 4764
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1

model = RNN(
    INPUT_DIM, EMBEDDING_DIM, HIDDEN_DIM, OUTPUT_DIM, embedding_weights
)

optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
criterion = torch.nn.BCEWithLogitsLoss()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

class LSTM(torch.nn.Module):
    def __init__(
        self,
        input_dim,
        embedding_dim,
        hidden_dim,
        output_dim,
        n_layers,
        bidirectional,
        dropout,
        embedding_weights,
    ):
        super().__init__()
        self.embedding = torch.nn.Embedding.from_pretrained(
            embedding_weights
        )
        self.rnn = torch.nn.LSTM(
            embedding_dim,
            hidden_dim,
            num_layers=n_layers,
            bidirectional=bidirectional,
            dropout=dropout,
        )
        self.fc = torch.nn.Linear(hidden_dim * 2, output_dim)
        self.dropout = torch.nn.Dropout(dropout)

    def forward(self, x, text_lengths):
        embedded = self.embedding(x)
        packed_embedded = torch.nn.utils.rnn.pack_padded_sequence(
            embedded, text_lengths
        )
        packed_output, (hidden, cell) = self.rnn(packed_embedded)
        hidden = self.dropout(
            torch.cat((hidden[-2, :, :], hidden[-1, :, :]), dim=1)
        )
        return self.fc(hidden.squeeze(0))

INPUT_DIM = padding_value
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1
N_LAYERS = 2
BIDIRECTIONAL = True
DROPOUT = 0.5

model = LSTM(
    INPUT_DIM,
    EMBEDDING_DIM,
    HIDDEN_DIM,
    OUTPUT_DIM,
    N_LAYERS,
    BIDIRECTIONAL,
    DROPOUT,
    embedding_weights,
)

optimizer = torch.optim.Adam(model.parameters())
criterion = torch.nn.BCEWithLogitsLoss()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

def binary_accuracy(preds, y):
  rounded_preds = torch.round(torch.sigmoid(preds))
  correct = (rounded_preds == y).float()
  acc = correct.sum()/len(correct)
  return acc

def train(model, iterator, optimizer, criterion):
    epoch_loss = 0
    epoch_acc = 0
    model.train()
    for batch in iterator:
        optimizer.zero_grad()
        predictions = model(batch["text"], batch["length"]).squeeze(1)
        loss = criterion(predictions, batch["label"])
        acc = binary_accuracy(predictions, batch["label"])
        loss.backward()
        optimizer.step()
        epoch_loss += loss.item()
        epoch_acc += acc.item()

    return epoch_loss / len(iterator), epoch_acc / len(iterator)

def evaluate(model, iterator, criterion):
    epoch_loss = 0
    epoch_acc = 0
    model.eval()
    with torch.no_grad():
        for batch in iterator:
            predictions = model(batch["text"], batch["length"]).squeeze(1)
            loss = criterion(predictions, batch["label"])
            acc = binary_accuracy(predictions, batch["label"])

            epoch_loss += loss.item()
            epoch_acc += acc.item()

    return epoch_loss / len(iterator), epoch_acc / len(iterator)

batch_size = 128 # Usually should be a power of 2 because it's the easiest for computer memory.

def iterator(X, y):
  size = len(X)
  permutation = np.random.permutation(size)
  iterate = []
  for i in range(0, size, batch_size):
    indices = permutation[i:i+batch_size]
    batch = {}
    batch['text'] = [X[i] for i in indices]
    batch['label'] = [y[i] for i in indices]

    batch['text'], batch['label'] = zip(*sorted(zip(batch['text'], batch['label']), key = lambda x: len(x[0]), reverse = True))
    batch['length'] = [len(utt) for utt in batch['text']]
    batch['length'] = torch.IntTensor(batch['length'])
    batch['text'] = torch.nn.utils.rnn.pad_sequence(batch['text'], batch_first = True).t()
    batch['label'] = torch.Tensor(batch['label'])

    batch['label'] = batch['label'].to(device)
    batch['length'] = batch['length'].to(device)
    batch['text'] = batch['text'].to(device)

    iterate.append(batch)

  return iterate

index_utt = word_vectors.key_to_index

#You've got to determine some labels for whatever you're training on.
X_train, X_test, y_train, y_test = train_test_split(index_utt, labels, test_size = 0.2)
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size = 0.2)

train_iterator = iterator(X_train, y_train)
validate_iterator = iterator(X_val, y_val)
test_iterator = iterator(X_test, y_test)

print(len(train_iterator), len(validate_iterator), len(test_iterator))

N_EPOCHS = 25

for epoch in range(N_EPOCHS):
    train_loss, train_acc = train(
        model, train_iterator, optimizer, criterion
    )
    valid_loss, valid_acc = evaluate(model, validate_iterator, criterion)

    print(
        f"| Epoch: {epoch+1:02} | Train Loss: {train_loss: .3f} | Train Acc: {train_acc*100: .2f}% | Validation Loss: {valid_loss: .3f} | Validation Acc: {valid_acc*100: .2f}% |"
    )
#Training on our dataset
| Epoch: 01 | Train Loss:  0.560 | Train Acc:  70.63% | Validation Loss:  0.574 | Validation Acc:  70.88% |
| Epoch: 05 | Train Loss:  0.391 | Train Acc:  82.81% | Validation Loss:  0.368 | Validation Acc:  83.08% |
| Epoch: 10 | Train Loss:  0.270 | Train Acc:  89.11% | Validation Loss:  0.315 | Validation Acc:  86.22% |
| Epoch: 15 | Train Loss:  0.186 | Train Acc:  92.95% | Validation Loss:  0.381 | Validation Acc:  87.49% |
| Epoch: 20 | Train Loss:  0.121 | Train Acc:  95.93% | Validation Loss:  0.444 | Validation Acc:  86.29% |
| Epoch: 25 | Train Loss:  0.100 | Train Acc:  96.28% | Validation Loss:  0.451 | Validation Acc:  86.83% |

看看我们的类和实例化,你会发现 LSTM 与 RNN 并没有太大的区别。 init 输入变量中唯一的区别是 n_layers(为了方便,你也可以用 RNN 指定它)、bidirectionaldropout。双向性允许 LSTM 向前查看序列以帮助理解意义和上下文,但也极大地有助于多语言场景,因为像英语这样的从左到右的语言并不是正字法的唯一格式。丢失率是另一个巨大的创新,它改变了过拟合的范式,不再仅仅是数据相关,并通过在训练期间逐层关闭随机节点来帮助模型不过度拟合,强制所有节点不相互关联。在模型之外的唯一区别是,RNN 的最佳优化器是 SGD,就像我们的 CBoW 一样,而 LSTM 使用 Adam(可以使用任何优化器,包括 AdamW)。下面,我们定义了我们的训练循环并训练了 LSTM。将此训练循环与 gradient_descent 函数中的 Listing 2.4 中定义的训练循环进行比较。

本代码展示的一个惊人之处在于,与先前的模型迭代相比,LSTM 的学习速度要快得多,这要归功于双向性和丢失率。尽管先前的模型训练速度更快,但需要数百个周期才能达到与仅需 25 个周期的 LSTM 相同的性能。验证集上的性能,正如其名称所示,为架构增添了有效性,在训练期间对未经训练的示例进行推断,并保持准确性与训练集相当。

这些模型的问题并不太明显,主要表现为资源消耗极大,尤其是在应用于更长、更注重细节的问题时,如医疗保健和法律领域。尽管丢失率和双向处理具有令人难以置信的优势,但它们至少会将训练所需的处理能力增加一倍,因此,虽然推断最终只会比相同规模的 MLP 昂贵 2-3 倍,但训练则会变得 10-12 倍昂贵。它们很好地解决了爆炸梯度的问题,但却增加了训练所需的计算量。为了解决这个问题,设计并实施了一种快捷方式,使任何模型,包括 LSTM,在一个序列中找出哪些部分是最有影响力的,哪些部分可以安全地忽略,这就是注意力。

2.2.8 注意力

注意力是通过一种新兴的数学公式告诉模型如何考虑输入的哪些部分以及多少来更快地解决更大上下文窗口的数学快捷方式。这一切都基于一个升级版本的字典,其中不仅仅是键值对,还增加了一个上下文查询。我们将在后面的章节中更深入地讨论注意力。现在,知道下面的代码是从原始论文中提取的 10 个步骤,它是老的自然语言处理技术和现代技术之间的重要区别。

注意力机制解决了训练 LSTMs 的缓慢问题,但在低数量的 epochs 上保持了高性能。同时,也有多种类型的注意力机制。点积注意力方法捕捉了查询中每个单词(或嵌入)与关键字中每个单词之间的关系。当查询和关键字是同一句子的一部分时,这被称为双向自注意力。然而,在某些情况下,仅集中在前面的单词更合适。这种类型的注意力,尤其是当查询和关键字来自相同的句子时,被称为因果注意力。通过屏蔽序列的部分并迫使模型猜测应该在掩码后面的内容,语言建模进一步得到改善。下面的函数演示了点积注意力和掩码注意力。

列表 2.8 多头注意力机制实现
import numpy as np
from scipy.special import softmax

# Step 1: Input: 3 inputs, d_model=4
x = np.array([[1.0, 0.0, 1.0, 0.0],
        [0.0, 2.0, 0.0, 2.0],
        [1.0, 1.0, 1.0, 1.0]])

# Step 2: weights 3 dimensions x d_model=4
w_query = np.array([1,0,1],
            [1,0,0],
            [0,0,1],
            [0,1,1]])
w_key = np.array([[0,0,1],
           [1,1,0],
           [0,1,0],
           [1,1,0]])
w_value = np.array([[0,2,0],
             [0,3,0],
             [1,0,3],
             [1,1,0]])

# Step 3: Matrix Multiplication to obtain Q,K,V
## Query: x * w_query
Q = np.matmul(x,w_query)
## Key: x * w_key
K = np.matmul(x,w_key)
## Value: x * w_value
V = np.matmul(x,w_value)

# Step 4: Scaled Attention Scores
## Square root of the dimensions
k_d = 1
attention_scores = (Q @ K.transpose())/k_d

# Step 5: Scaled softmax attention scores for each vector
attention_scores[0] = softmax(attention_scores[0])
attention_scores[1] = softmax(attention_scores[1])
attention_scores[2] = softmax(attention_scores[2])

# Step 6: attention value obtained by score1/k_d * V
attention1 = attention_scores[0].reshape(-1,1)
attention1 = attention_scores[0][0]*V[0]
attention2 = attention_scores[0][1]*V[1]
attention3 = attention_scores[0][2]*V[2]

# Step 7: summed the results to create the first line of the output matrix
attention_input1 = attention1 + attention2 + attention3

# Step 8: Step 1 to 7 for inputs 1 to 3
## Because this is just a demo, we’ll do a random matrix of the right dimensions
attention_head1 = np.random.random((3,64))

# Step 9: We train all 8 heads of the attention sub-layer using steps 1 through 7
## Again, it’s a demo
z0h1 = np.random.random((3,64))
z1h2 = np.random.random((3,64))
z2h3 = np.random.random((3,64))
z3h4 = np.random.random((3,64))
z4h5 = np.random.random((3,64))
z5h6 = np.random.random((3,64))
z6h7 = np.random.random((3,64))
z7h8 = np.random.random((3,64))

# Step 10: Concatenate heads 1 through 8 to get the original 8x64 output dimension of the model
Output_attention = np.hstack((z0h1,z1h2,z2h3,z3h4,z4h5,z5h6,z6h7,z7h8))

# Here’s a function that performs all of these steps:
def dot_product_attention(query, key, value, mask, scale=True):
    assert query.shape[-1] == key.shape[-1] == value.shape[-1], “q,k,v have different dimensions!”
    if scale:
        depth = query.shape[-1]
    else:
        depth = 1
    dots = np.matmul(query, np.swapaxes(key, -1, -2)) / np.sqrt(depth)
    if mask is not None:
        dots = np.where(mask, dots, np.full_like(dots, -1e9))
    logsumexp = scipy.special.logsumexp(dots, axis=-1, keepdims=True)
    dots = np.exp(dots - logsumexp)
    attention = np.matmul(dots, value)
    return attention

# Here’s a function that performs the previous steps but adds causality in masking
def masked_dot_product_self_attention(q,k,v,scale=True):
    mask_size = q.shape[-2]
    mask = np.tril(np.ones((1, mask_size, mask_size), dtype=np.bool_), k=0)
    return DotProductAttention(q,k,v,mask,scale=scale)

在上面,在注意力的完整实现中,你可能已经注意到了一些你熟悉的术语,即关键字和值,但你可能之前没有听说过查询。关键字和值对因为字典和查找表而熟悉,我们在其中将一组关键字映射到一个值数组。查询应该感觉直观,就像搜索或检索一样。查询与关键字进行比较,从中检索一个值在正常操作中。

在注意力中,查询和关键字经历点积相似性比较以获得一个注意力分数,稍后将该分数乘以值以获得模型应该关注序列中的那部分的最终分数。这可能会变得更复杂,这取决于你模型的架构,因为编码器和解码器序列长度都必须考虑在内,但现在可以简单地说,这个空间中建模的最有效方法是将所有输入源投影到一个共同的空间中,并使用点积进行比较以提高效率。

这个代码解释比之前的例子更加数学密集,但这是必要的来说明概念。注意力背后的数学是真正创新的,它推动了该领域的进步。不幸的是,即使注意力给序列建模带来了优势,使用 LSTMs 和 RNNs 仍然存在速度和内存大小的问题。从代码和数学中你可能会注意到有一个平方根,这意味着我们使用的注意力是二次的。从那时起,出现了各种技术,包括次二次的技术,如 Hyena 和 Recurrent Memory Transformer(RMT,基本上是 RNN 与变压器的组合),以应对这些问题,我们稍后将更详细地讨论。现在,让我们继续介绍注意力的最终应用:变压器。

注意力就是你所需要的一切

在开创性论文《Attention is All You Need》中[1],Vaswani 等人进一步推进了数学上的捷径,假设为了性能绝对不需要任何重复(循环神经网络中的“R”)或任何卷积[2]。相反,他们选择只使用注意力,并简单地指定 Q、K 和 V 从哪里被更加小心地取出。我们将立即深入讨论这一点。在我们对这一多样化的 NLP 技术的回顾中,我们观察到它们随着时间的推移的演变,以及每种方法试图改进其前身的方式。从基于规则的方法到统计模型和神经网络,该领域不断努力寻求更高效、更准确的处理和理解自然语言的方法。现在,我们将注意力转向了一项开创性的创新,彻底改变了 NLP 领域:变压器架构。在接下来的部分中,我们将探讨支撑变压器的关键概念和机制,以及它们如何使得先进的语言模型的开发超越了以前的技术。我们还将讨论变压器对更广泛的 NLP 景观的影响,并考虑在这一激动人心的研究领域中进一步进展的可能性。

2.3.1 编码器

编码器是完整变压器模型的前半部分,在分类和特征工程等领域表现出色。Vaswani 等人(2017)发现的一件事是,在编码器内部的嵌入层之后,对张量进行的任何附加转换都可能损害它们在“语义上”进行比较的能力,而这正是嵌入层的目的。这些模型在很大程度上依赖于自注意力和巧妙的位置编码来操纵这些向量,而不会显著降低所表达的相似性。

再次强调嵌入的关键点:它们是数据的向量表示,在我们的案例中是令牌。令牌可以是用于表示语言的任何内容。我们通常建议使用子词,但您会对哪种类型的令牌在何处效果良好有所感觉。考虑这句话,“戴着帽子的猫迅速跳过了红狐狸和棕色的无动力狗。” “红色”和“棕色”在语义上应该是相似的,并且在嵌入层之后它们被类似地表示,但是在句子中分别占据第 10 和第 14 位置,假设我们按单词进行分词,因此位置编码在它们之间引入了距离。然而,一旦应用正弦和余弦函数[3],它会将它们的含义调整到编码后比之前稍微远一点的位置,并且此编码机制随着循环和更多数据的增加而扩展得非常出色。举例来说,假设嵌入后[红色]和[棕色]之间的余弦相似度为 99%。编码将极大地减少这一相似度,降至大约 85-86%。按照所述的正弦和余弦方法,将它们的相似度调整回大约 96%。

BERT 是原始论文后出现的第一批体系结构之一,并且是仅编码器的变换器的示例。 BERT 是一个非常强大的模型架构,尽管体积小,但至今仍在生产系统中使用。 BERT 是第一个仅编码器的变换器在流行中迅速崛起的例子,展示了使用变换器进行连续建模比 Word2Vec 获得更好的嵌入。 我们可以看到这些嵌入更好是因为它们可以在最少的训练下很快地应用于新任务和数据,并且与 Word2Vec 嵌入相比,获得了人们更喜欢的结果。 这导致大多数人在一段时间内将基于 BERT 的模型用于较小数据集上的少量学习任务。 BERT 使得最先进的性能对大多数研究人员和企业来说只需付出最少的努力就能轻松实现。

图 2.5 编码器,可视化。编码器是完整变换器架构的前半部分,在诸如分类或命名实体识别(NER)等 NLU 任务中表现出色。编码器模型改进了以前的设计,不需要任何先验知识或循环,并且使用了巧妙的位置编码和多头注意力。

优点:

  • 分类和展示理解的层次任务

  • 在考虑长期依赖建模时极快速度。

  • 建立在已知模型的基础上,嵌入中的 CBoW,前馈中的 MLP 等。

弱点:

  • 如建议所示,需要大量数据才能发挥效果(尽管比 RNN 少)。

  • 更复杂的架构

2.3.2 解码器

解码器模型(如下所示)是编码器的较大版本,具有 2 个多头注意力块和 3 个求和和标准化层。它们是变压器的后半部分。 这导致模型在屏蔽语言建模、学习和应用语法等方面非常强大,从而立即想到需要仅解码器模型以实现人工智能。编码器与解码器任务的有用区分是,编码器在自然语言理解(NLU)任务中表现出色,而解码器在自然语言生成(NLG)任务中表现出色。一些仅解码器的变压器体系结构的示例是产生预训练的变压器(GPT)系列模型。这些模型遵循转换生成语法的逻辑,完全基于语法,允许语言中所有可能句子的无限生成。[4]

图 2.6 解码器的可视化。解码器是完整变压器的第二个部分,擅长于像聊天机器人和讲故事这样的自然语言生成任务。解码器在以前的架构上做出了改进,但它们将其输出向右移动一个空格以用于下一个词汇的生成,从而帮助利用多头自我关注的优势。

优势:

  • 生成序列中的下一个标记(向右移动意味着考虑到已生成的标记)

  • 基于已知模型和编码器的构建

  • 可以在生成过程中进行流式传输,提供良好的用户体验

弱点:

  • 仅基于语法的模型经常难以插入预期或意图(请参见自 2018 年以来所有的“我强迫 AI 观看 1000 小时 x 并生成”模因)

  • 幻觉

2.3.3 变压器

全面的变压器架构利用了编码器和解码器,将编码器的理解传递到解码器的第二个多头注意力块中,然后才能输出。由于变压器的每个部分都专门负责理解或生成,因此对于条件生成任务(如翻译或摘要),整个产品应该感觉很直观,在生成之前需要一定程度的理解。编码器旨在以高层次处理输入,解码器则更专注于生成连贯的输出,全面的变压器架构可以成功地理解,然后基于这种理解进行生成。变压器模型具有优势,因为它们围绕着并行构建,这增加了速度,目前在 LSTMs 中无法复制。如果 LSTM 能够达到可以像变压器一样快速运行的点,它们可能会在最先进的领域竞争。文本到文本转移变压器(T5)系列模型是变压器的示例。

图 2.7 一个完整的变换器可视化。一个完整的变换器结合了编码器和解码器,并在每个任务以及条件生成任务(如摘要和翻译)上表现良好。由于变换器比它们的各半部分更笨重和更慢,因此研究人员和企业通常选择使用这些半部分而不是整个东西,尽管速度和内存增益都很小。

优点:

  • 同时具有编码器和解码器,因此擅长于它们各自擅长的一切

  • 高度并行化以提高速度和效率

弱点:

  • 占用内存多,但仍然比相同大小的 LSTM 少

  • 需要大量数据和 VRAM 进行训练

正如你可能已经注意到的,我们讨论的大多数模型根本不是语言学焦点,而是非常注重语法,即使在尝试模拟真实语言时也是如此。模型,即使是最先进的变换器,也只有语义近似,没有语用学、音韵学,而且在分词期间只真正利用形态。这并不意味着模型无法学习这些知识,也不意味着例如变换器无法将音频作为输入,只是平均使用情况下并没有这样做。考虑到这一点,它们能够表现得如此出色简直就是一个奇迹,而且确实应该被当作奇迹来欣赏。

通过本章至今,我们试图突出当前模型存在的限制,并将在本书的其余部分深入探讨如何改进它们的方法。其中一条路线是已经被探索并取得了巨大成功的迁移学习和对大型基础模型进行微调。这项技术在 BERT 初始发布后不久就出现了,当研究人员发现,虽然 BERT 在大量任务上表现得普遍良好,但如果他们想让它在特定任务或数据领域上表现更好,他们只需在代表该任务或领域的数据上重新训练模型,而不是从头开始。获取 BERT 在创建语义近似嵌入时学到的所有预训练权重,然后就可以用较少的数据在你需要的部分上获得最先进的性能。我们已经在 BERT 和 GPT 系列模型上看到了这一点,它们分别问世时也是如此,现在我们又再次看到它们解决了带来的挑战:语义近似覆盖、领域专业知识、数据的可用性。

2.4 非常大的变换器

进入大型语言模型。自引入基于 transformer 的模型以来,它们只变得越来越大,不仅是它们的大小和参数数量,而且它们的训练数据集和训练周期也越来越大和长。如果您在 2010 年代学习机器学习或深度学习,您可能已经听说过“层数越多,模型就不一定越好”这个口号。LLM 证明了这一点既对又错。错误是因为它们的性能无与伦比,通常甚至可以与在特定领域和数据集上进行精细微调的较小模型相匹配,甚至是那些在专有数据上训练的模型。正确的是由于训练和部署它们所带来的挑战。

LLM(大型语言模型)和 LM(语言模型)之间的一个主要区别在于迁移学习和微调。与先前的大型 LM 一样,LLM 在大规模文本语料库上进行预训练,使其能够学习通用语言特征和表示,以便进行特定任务的微调。由于 LLM 如此庞大,其训练数据集也很大,因此 L LM 能够在较少的标记数据的情况下获得更好的性能,这是早期语言模型的一个重要局限性。通常情况下,您可以使用仅十几个示例微调 LLM,使其执行高度专业化的任务。

但是,真正让 LLM 强大并打开了广泛业务应用的大门的是它们能够在没有任何微调的情况下执行专门的任务,只需一个简单提示即可。只需给出您查询中想要的内容的几个示例,LLM 就能产生结果。这称为少量提示,当其在较小的标记数据大小上进行训练时,称为单次,当只给出一个示例时,称为零次,当任务是全新时。特别是使用 RLHF 和提示工程方法训练的 LLM 可以在一个全新的层次上执行少量提示学习,从而能够在仅有少量示例的情况下进行任务的广义求解。这种能力是早期模型的一个重大进展,早期的模型需要用于每个特定任务进行大量的微调或标记数据。

先前的语言模型已经展示出在几次提示学习领域和零次提示学习领域上的潜力,而 LLM 证明了这个潜力的真实。随着模型变得越来越大,我们发现它们能够执行新任务,而较小的模型则无法做到。我们称之为新兴行为[5],而图 2.8 说明了八个不同的任务,其中 LMs 不能比随机更好地完成,而一旦模型足够大,它们就可以做到。

图 2.8 显示了 LLM 在模型规模达到一定大小后执行几次提示任务时所显示出的新兴行为的示例。

LLM 也具有明显优异的零样本能力,这既归因于它们庞大的参数大小,也是它们在商业世界中受欢迎和可行的主要原因。LLM 由于其巨大的大小和容量,还表现出对歧义的改进处理能力。它们更擅长消除具有多重含义的词语并理解语言的细微差别,从而产生更准确的预测和响应。这不是因为它们具有改进的能力或架构,因为它们与较小的变压器共享架构,而是因为它们具有更多关于人们通常如何消除歧义的示例。因此,LLM 响应的歧义消除与数据集中通常表示的歧义消除相同。由于 LLM 所训练的文本数据的多样性,它们在处理各种输入样式、嘈杂文本和语法错误方面表现出了增强的鲁棒性。

LLM 和 LM 之间的另一个关键区别是输入空间。较大的输入空间很重要,因为它使得少样本提示任务变得更加可行。许多 LLM 的最大输入大小为 8000+ 令牌(GPT-4 目前支持 32k),虽然在本章讨论的所有模型也可以具有如此高的输入空间,但它们通常不会被考虑进去。我们最近也看到了这一领域的蓬勃发展,如递归记忆变压器(RMT)等技术使得上下文空间可达 1,000,000+ 令牌,这使得 LLM 更加向着证明更大的模型总是更好的方向迈进。LLM 旨在捕捉文本中的长距离依赖关系,使它们能够比其前身更有效地理解上下文。这种改进的理解使得 LLM 在任务中生成更连贯和上下文相关的响应,例如机器翻译、摘要和对话 AI。

LLMs 通过提供强大的解决方案,彻底改变了 NLP 中那些对早期语言模型具有挑战性的问题。它们在上下文理解、迁移学习和少样本学习方面带来了显著的改进。随着 NLP 领域的不断发展,研究人员正在积极努力最大限度地利用 LLMs 的优势,同时减轻所有潜在风险。由于还没有找到更好的近似语义的方法,它们做出了更大更多维度的近似。由于还没有找到存储实用语境的良好方法,LLMs 通常允许将上下文插入到提示中,插入到专门用于上下文的输入部分中,甚至通过在推断时与 LLM 共享数据库来进行插入。这并不会在模型中创建实用语境或实用语境系统,就像嵌入并不会创建语义一样,但它允许模型正确生成模仿人类对这些实用语境和语义刺激做出反应的句法。语音学是 LLMs 可能会取得巨大进展的地方,可以作为完全无文本的模型,或者作为文本-语音混合模型,也许还可以使用国际音标而不是文本。考虑到我们正在目睹的这一领域的可能发展,令人兴奋。

此时,你应该对 LLM 是什么以及在将 LLM 投入生产时将会用到的一些语言学关键原理有相当清楚的理解了。主要的是,现在你应该能够开始思考哪种类型的产品会更容易或更难构建。考虑图 2.9,像写作助手和聊天机器人这样的任务位于左下角,是 LLMs 的主要应用场景。基于提示中的一点上下文进行文本生成的问题严格来说是基于句法的,通过足够大的模型在足够多的数据上进行训练,我们可以相当轻松地完成这个任务。购物助手也是相当相似且相对容易构建的,然而,我们只是缺少实用语境。助手需要了解更多关于世界的信息,比如产品、商店和价格。通过一点工程技术,我们可以将这些信息添加到数据库中,并通过提示将这些上下文提供给模型。

另一方面,考虑一下象棋机器人。LLM能够下棋。但是它们不擅长。它们经过了象棋比赛的训练,并理解“E4”是一个常见的首步,但是它们的理解完全是语法上的。LLM 实际上只理解它们应该生成的文本应该包含 A 到 H 之间的一个字母和 1 到 8 之间的一个数字。就像购物助手一样,它们缺少语用学,对象棋游戏没有一个清晰的模型。此外,它们也缺乏语义。编码器可能会帮助我们理解“国王”和“皇后”这两个词是相似的,但它们并没有真正帮助我们理解“E4”对于一个玩家来说是一个伟大的着法,但对于另一个玩家来说,同样的“E4”着法可能是一个糟糕的着法。LLM 也完全缺乏基于音韵学和形态学的关于象棋的知识,但这些对于这个案例来说不是很重要。无论如何,我们希望这个练习能够更好地为您和您的团队在下一个项目中提供信息。

图 2.9 LLM 对于某些任务的难易程度以及解决这些任务的方法。

LLM(大型语言模型)有着惊人的好处,但是随着所有这些能力的出现也带来了一些限制。基础性的 LLM 需要庞大的计算资源进行训练,这使得它们对个人研究人员和较小的组织来说不太容易接触。我们将在本书中讨论一些技术,比如量化、文本嵌入、低秩调整、参数高效微调和图优化等技术来解决这个问题,但是基础模型目前仍然完全超出了普通个人有效训练的能力范围。此外,人们担心 LLM 的训练能耗可能会对环境产生重大影响,并伴随着可持续性的问题。这是一个复杂的问题,主要超出了本书的范围,但我们不提及这一点就不妥。最后但同样重要的是,由于 LLM 是在包含真实世界文本的大规模数据集上进行训练的,它们可能会学习并延续数据中存在的偏见,引发伦理关切,这并不是研究人员或算法的过错,而更多是因为现实世界的人们没有自我审查提供最佳无偏数据。例如,如果你要求一个文本到图像扩散的 LLM 生成 1000 张“领导者”的图像,其中 99%的图像会以男性为特征,95%的图像会以白人为特征。这里的问题不是男性或白人不应该被描绘为领导者,而是显示出模型并没有真实准确地代表世界。

图 2.10 中的 Midjourney 5,目前是市场上最受欢迎的文本转图像模型,当只有一个标记“领导者”(左图所示)时,将一个众所周知的流行女性主义象征,罗西·飞弹手,变成了男性形象。ChatGPT(右图所示)编写了一个函数,根据您的种族、性别和年龄来安排您的工作。这些都是意想不到的输出示例。

有时,更微妙的偏见会显现出来,例如,在图 2.10 中展示的 Midjourney 示例中。一个广受欢迎的女性主义象征“罗西·飞弹手”(Rosie the Riveter),在没有任何提示的情况下(模型仅给出的提示是“领导者”一词),被改变为了一个男性。模型根本没有考虑到这种改变,它只是在采样步骤中确定,“领导者”这个提示更像是一个男人。很多人会争论在这种情况下“好”和“坏”意味着什么,但我们将简单地讨论准确意味着什么。LLM(大型语言模型)被训练在大量数据上,目的是返回可能最准确的表示。当它们仍然无法返回准确的表示时,特别是在它们有能力消除歧义时,我们可以将其视为对模型实现其目的的有害偏见。后面我们将讨论对抗这种有害偏见的技术,不是出于任何政治目的,而是为了让您作为 LLM 创建者获得您想要的确切输出,并尽量减少您不想要的输出的数量。

好了,我们整整一个章节都在建立到这一点,让我们继续运行我们的第一个 LLM!在清单 2.9 中,我们下载了 Bloom 模型,这是第一个开源 LLM 之一,并生成了文本!非常令人兴奋的事情。

清单 2.9 运行我们的第一个 LLM
from transformers import AutoModelForCausalLM, AutoTokenizer

MODEL_NAME = "bigscience/bloom"

tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME)

prompt = "Hello world! This is my first time running an LLM!"

input_tokens = tokenizer.encode(prompt, return_tensors="pt", padding=True)
generated_tokens = model.generate(input_tokens, max_new_tokens=20)
generated_text = tokenizer.batch_decode(generated_tokens, skip_special_tokens=True)
print(generated_text)

你试过跑它了吗?!如果你试过,你可能刚刚让你的笔记本电脑崩溃了。糟糕!原谅我这一点无害的 MLOps 入门,但亲身体验一下这些模型有多大,以及它们运行起来有多困难,是有帮助的经验。我们将在下一章更多地讨论运行 LLM 的困难,并为您提供一些实际运行它所需的工具。如果你不想等待,并想要运行一个类似但规模小得多的 LLM,将模型名称更改为“bigscience/bloom-3b”,然后再次运行。这次它应该在大多数硬件上都能正常工作。

总而言之,LLM 是一项令人惊奇的技术,可以让我们的想象力充满可能性,并且理所当然。考虑使用 LLM 而不是较小的 LM 的头等用例是,当模型将帮助的人需要使用少量样本功能时,比如帮助首席执行官筹集资金或帮助软件工程师编写代码。他们之所以具有这种能力,正是因为他们的规模。LLM 中更多的参数直接使其能够在更大维度的较小空间上进行概括。在本章中,我们介绍了 LLMS 的较少知名的一面,即语言学和语言建模方面。在下一章中,我们将涵盖另一半,即 MLOps 方面,其中我们将深入研究大参数大小如何影响模型以及旨在支持该模型并使其可供其预期客户或员工使用的系统。

2.5 总结

  • 语言学的五个组成部分是语音学、句法学、语义学、语用学和形态学。

  • 通过处理音频文件的多模态模型可以添加语音学,这可能会在未来改进 LLM,但当前数据集太小。

  • 语法是当前模型擅长的领域。

  • 通过嵌入层添加了语义。

  • 通过工程努力可以添加语用学。

  • 形态学添加在标记化层中。

  • 语言不一定与现实相关。了解人们在现实之外创造意义的过程对于训练有意义(对于人们来说)的模型是有用的。

  • 适当的标记化可能是一个主要障碍,因为有太多的 标记,特别是当涉及到代码或数学等专业问题时。

  • 多语言处理一直表现优于单语言处理,即使在没有模型的单语言任务中也是如此。

  • 每种语言模型类型都是专门构建的,以解决先前模型的弱点,而不是试图解决语言的特定特征。

  • 语言建模的有效性呈指数增长,与建模的语言学关注度相关。

  • 注意力是解决更大上下文窗口的数学快捷方式,是现代架构 - 编码器、解码器和变压器的支柱。

  • 编码器改进了嵌入中的语义近似。

  • 解码器在文本生成方面表现最佳。

  • 变压器将这两者结合起来。

  • 较大的模型展示了突然能够完成以前无法完成的任务的新兴行为。

[1] Vaswani 等人 2017 年 Attention Is All You Need arxiv.org/abs/1706.03762

[2] 我们没有讨论这些,因为它们对自然语言处理来说并不好,但它们在计算机视觉中尤其流行

[3] 不是数学或历史书籍

[4] 请参阅附录 A

[5] J. Wei 等,“大型语言模型的新兴能力”,机器学习研究交易,2022 年 8 月,可用:openreview.net/forum?id=yzkSU5zdwD

第三章:大型语言模型操作:构建用于 LLMs 的平台

本章内容包括

  • 大型语言模型操作概述

  • 部署挑战

  • 大型语言模型最佳实践

  • 所需的大型语言模型基础设施

上一章我们学到,当涉及到变压器和自然语言处理(NLP)时,越大越好,特别是在语言上有所启发时。然而,更大的模型由于其规模而带来了更大的挑战,无论其语言有效性如何,这要求我们扩大操作和基础设施规模以处理这些问题。在本章中,我们将详细探讨这些挑战是什么,我们可以采取什么措施来最小化它们,并建立什么样的架构来帮助解决这些挑战。

3.1 大型语言模型操作简介

什么是大型语言模型操作(LLMOps)?好吧,由于我更注重实用而不是华丽辞藻,我不打算深入讨论你在教科书中所期望的任何花哨的定义,但让我简单地说一下,它是已经被扩展以处理 LLMs 的机器学习操作(MLOps)。让我也说一下,扩展是困难的。这是软件工程中最艰巨的任务之一。不幸的是,太多公司正在运行基本的 MLOps 设置,并且不要想一秒钟他们将能够处理 LLMs。话虽如此,“LLMOps”这个术语可能是不需要的。它尚未显示出足够不同于核心 MLOps,特别是考虑到它们仍然具有相同的基础。如果这本书是一个二分键,MLOps 和 LLMOps 肯定会属于同一个属,只有时间会告诉我们它们是否是同一个物种。当然,通过拒绝正确定义 LLMOps,我可能已经用另一种混乱来交换了一种混乱,所以让我们花点时间来描述一下 MLOps。

MLOps 是可靠且高效地部署和维护机器学习模型在生产环境中的领域和实践。这包括,并确实需要,管理整个机器学习生命周期,从数据获取和模型训练到监控和终止。掌握这一领域所需的几个原则包括工作流编排、版本控制、反馈循环、持续集成和持续部署(CI/CD)、安全性、资源供给以及数据治理。虽然通常有专门从事模型产品化的人员,通常称为 ML 工程师、MLOps 工程师或 ML 基础设施工程师,但这个领域是一个足够庞大的野兽,它经常会绑架许多其他毫无防备的专业人士来从事工作,他们拥有数据科学家或 DevOps 工程师等头衔,往往不是出于自己的知情和意愿;留下他们愤怒地抗议“这不是他们的工作”。

3.2 大型语言模型的操作挑战

那么为什么还要有一个区别呢?如果 MLOps 和 LLMOps 如此相似,那么 LLMOps 只是另一个机会主义者在简历上添加的时髦词汇吗?并非如此。事实上,我认为它与“大数据”这个词相当相似。当这个词达到最高流行时,拥有 Big Data Engineer 等职称的人使用完全不同的工具集,并开发了处理大型数据集所必需的专业技能。LLM 带来了一系列挑战和问题,你在传统机器学习系统中找不到。其中大多数问题几乎完全由于它们太大而产生。大模型就是大!我们希望向您展示,LLM 真正名副其实。让我们来看看其中一些挑战,这样我们就可以在开始讨论部署 LLM 时,能够更加欣赏面临的任务。

3.2.1 长时间下载

回到 2017 年,当我还是一名深度参与数据科学家时,我决定尝试重新实现当时最著名的一些计算机视觉模型,包括 AlexNet、VGG19 和 ResNet。我认为这将是巩固我对基础知识理解的好方法,通过一些实际的动手经验。而且,我还有一个别有用心的动机,我刚刚组建了自己的机器,配备了一些当时最先进的 NVIDIA GeForce 1080 TI GPU——我想这将是一个很好的方法来使用它们。第一个任务:下载 ImageNet 数据集。ImageNet 数据集是当时最大的标注数据集之一,包含数百万张图像,文件大小高达约 150GB!使用它证明你知道如何处理“大数据”,这在当时仍然是一个时髦的词汇,也是数据科学家无法替代的技能。同意了条款并获得访问权限后,我收到了第一个警钟。下载整个数据集花了整整一周。

大模型就是大。我觉得我无法过分强调这一点。在本书中,你会发现这个事实给整个生产过程带来了许多额外的头痛和问题,你必须为此做好准备。与 ImageNet 数据集相比,Bloom LLM 模型有 330GB 大小,是其两倍以上。我猜测大多数读者都没有使用过 ImageNet 或 Bloom,因此为了比较,在写作时最大的游戏之一《使命召唤:现代战争》大小为 235 GB。最终幻想 15 只有 148 GB,你可以把两个放入该模型中,还有很多空间。真的很难理解 LLM 有多大。我们从像 BERT 这样的模型的 1 亿个参数发展到了数十亿个参数。如果你狂热购物,每秒花费 20 美元(或者可能只是不小心让你的 AWS EC2 实例开着),你需要花半天时间花完一百万美元;花一亿美元需要两年时间。

幸运的是,下载 Bloom 不需要两周的时间,因为与 ImageNet 不同,它并不托管在管理不善的大学服务器上,而且已经被分成多个较小的文件以便并行下载,但仍然需要相当长的时间。考虑这样一个场景:在最佳条件下下载模型。你配备了千兆速度的光纤互联网连接,而且可以神奇地将整个带宽和 I/O 操作都分配给系统和服务器,下载仍然需要超过 5 分钟!当然,这是在最佳条件下。你可能不会在这种情况下下载模型,使用现代基础设施,你可以预计需要数小时的时间。当我的团队首次部署 Bloom 时,下载它花了一个半小时。天哪,下载《塞尔达传说:王国之泪》只用了一个半小时,而且那仅有 16GB,所以我真的不能抱怨。

3.2.2 较长的部署时间

即使只是下载模型,也已经足够长的时间让任何经验丰富的开发人员都感到不安,但是部署时间将使他们昏倒并请求医疗帮助。像 Bloom 这样大的模型可能需要 30-45 分钟才能将模型加载到 GPU 内存中,至少这是我团队最初看到的时间框架。更不用说部署过程中的其他任何步骤可能会增加时间。事实上,由于 GPU 短缺,等待资源释放可能需要数小时——稍后会详细说明。

这对你和你的团队意味着什么?首先,我知道许多团队通常在运行时仅下载模型来部署 ML 产品。这对于小的 sklearn 回归模型可能有效,但不适用于 LLMs。此外,你可以忽略大部分有关部署可靠系统的知识(但幸运的是不是太过分)。对于软件工程的现代最佳实践,通常假设如果发生任何问题,你可以轻松地重新启动应用程序,并且需要进行许多繁琐的工作来确保你的系统确实可以做到这一点。但是对于 LLMs 来说,关闭可能只需要几秒钟,但重新部署可能需要数小时,这使得这成为一个几乎不可逆转的过程。就像摘苹果一样,摘下一个很容易,但如果咬了一口觉得太酸,你不能把它重新粘到树上让它继续成熟。你只能等一段时间再长出另一个。

虽然并非每个项目都需要部署最大的模型,但你可以预期部署时间以分钟计算。这些较长的部署时间使得在流量激增之前缩减规模成为一个可怕的错误,以及难以管理突发性工作负载。一般的 CI/CD 方法论需要进行调整,因为滚动更新需要更长时间,使得你的流水线迅速积累起积压。像拼写错误或其他错误这样的愚蠢错误往往需要更长时间才能发现和纠正。

- 延迟

随着模型大小的增加,推理延迟也往往会增加。这一点在陈述时显而易见,但更多的参数意味着更多的计算,更多的计算意味着更长的推理等待时间。然而,这一点不容小觑。我知道很多人因为与 LLM 聊天机器人的互动感觉流畅而对延迟问题不以为然。但再仔细看一眼,你会注意到它是逐字返回的,这些字会逐字传送给用户。它感觉流畅是因为答案比人类阅读的速度更快地返回,但再仔细看一眼就会发现这只是一种用户体验的技巧。LLM 仍然太慢,以至于对于自动补全解决方案来说并不是非常有用,例如,响应必须非常快速。将其构建到读取大量文本并尝试清理或总结的数据流水线或工作流中,可能也会因速度太慢而无法使用或不可靠。

这种缓慢还有许多不太明显的原因。首先,LLM 经常分布在多个 GPU 上,这增加了额外的通信开销。正如本章后面 3.3.2 节中所讨论的,它们以其他方式进行分布,通常甚至是为了提高延迟,但任何分布都会增加额外的负担。此外,LLM 的延迟严重受到完成长度的影响,这意味着它用于返回响应的字数越多,所需时间越长。当然,完成长度似乎也会提高准确性。例如,使用类似 Chain of Thought (CoT) 这样的提示工程技术,我们要求模型以逐步方式思考问题,这已经证明能够提高逻辑和数学问题的结果,但也会显著增加响应长度和延迟时间。

- 管理 GPU

为了解决这些延迟问题,我们通常希望使用 GPU 运行它们。如果我们想成功地训练 LLMs,我们也需要 GPU,但这会增加许多人低估的额外挑战。大多数 Web 服务和许多 ML 用例都可以仅使用 CPU 完成。但 LLMs 不行。一部分是因为 GPU 的并行处理能力提供了解决延迟问题的方法,而另一部分是由于 GPU 在算法的优化方面带来的优势,在算法的线性代数,矩阵乘法和张量运算中发挥作用。对于许多人来说,踏入 LLMs 领域,这需要使用新的资源和额外的复杂度。许多人大胆地踏入这个世界,似乎这不是什么大事,但他们会受到沉重打击。大多数系统架构和编排工具,如 Kubernetes,假设你的应用程序仅使用 CPU 和内存运行。虽然它们通常支持其他资源,如 GPU,但这通常是事后考虑的。你很快就会发现你必须从头开始重建容器并部署新的度量系统。

大多数公司不准备管理 GPU 的一个方面是它们往往是罕见和有限的。在过去的十年中,全球 GPU 短缺的现象似乎已经来来去去。对于想留在本地的公司,他们很难为此提供资源。在我的职业生涯中,我花了很多时间与选择留在本地的公司合作,他们有许多共同点,其中之一是他们的服务器上从来没有 GPU。当他们有 GPU 时,它们通常很难被除了一些关键员工之外的其他人所使用。

如果你有幸在云端工作,很多这些问题都会被解决,但这里也没有免费的午餐。我的团队经常为了帮助数据科学家提供新的 GPU 工作空间而忙得焦头烂额,碰到像"scale.up.error.out.of.resources"这样的晦涩错误。只有发现所选择的区域中的所有类型的 GPU 都正在被使用而没有可用的时才会发现这些晦涩的读数。在数据中心中,CPU 和内存通常可以被视为无限的,但 GPU 资源则不能。有时候你甚至不能期望它们。大多数数据中心只支持一部分实例或 GPU 类型。这意味着你可能被迫在距离用户基础设施较远的地区设置你的应用程序,从而增加了延迟。当然,我相信当你尝试将服务扩展到目前不支持的新区域时,你可以与你的云供应商合作,但你可能不喜欢听到的是时程和成本。无论你选择在本地还是在云端运行,最终你都会遇到短缺问题。

3.2.5 文本数据的特异性

LLM 是自然语言处理(NLP)的现代解决方案。总体而言,NLP 是机器学习中最引人入胜的分支之一,因为它主要处理文本数据,而文本数据主要是定性的。其他领域都处理定量数据。我们已经找到了一种将我们对世界的观察编码成直接翻译的数值的方法。例如,我们已经学会将热量编码成温度尺度,并用温度计和热电偶来测量,或者我们可以用压力计和压力表来测量压力,并将其计量为帕斯卡。

计算机视觉和评估图像的实践通常被视为定性的,但实际上将图像编码成数字是一个已解决的问题。我们对光的理解使我们能够将图像分解为像素并为其分配 RGB 值。当然,这并不意味着计算机视觉已经完全解决了,我们仍然有很多工作要做,以学习如何识别数据模式中的不同信号。音频数据通常也被认为是定性的。我们怎么比较两首歌曲呢?但是我们可以测量声音和语言,直接测量声波的强度(以分贝为单位)和频率(以赫兹为单位)。

不同于将我们的物理世界编码为数值数据的其他领域,文本数据正在探索测量无常世界的方法。毕竟,文本数据是我们将思想、想法和沟通模式编码的最佳努力。当然,是的,我们已经找到了将单词转化为数字的方法,但我们还没有找到一种直接的翻译方式。我们在编码文本和创建嵌入时的最佳解决方案充其量只是近似解决方案,事实上我们使用机器学习模型来实现!有趣的是,数字也是文本和语言的一部分。如果我们想要更擅长数学的模型,我们需要一种更有意义的方法来编码这些数字。由于这一切都是虚构的,当我们尝试将文本数字编码为机器可读的数字时,我们正在创建一种试图以有意义的方式递归引用自身的系统。这不是一个容易解决的问题!

由于所有这些原因,LLMs(以及所有 NLP 解决方案)面临着独特的挑战。例如,监控。如何在文本数据中捕捉数据漂移?如何衡量“正确性”?如何确保数据的净化?这些类型的问题很难定义,更不用说解决了。

3.2.6 令牌限制导致瓶颈产生

对于新手来说,与 LLM 一起工作的一个重大挑战是处理令牌限制问题。模型的令牌限制是作为模型输入的最大令牌数量。令牌限制越大,我们就能够为模型提供更多的上下文,以提高模型完成任务的成功率。每个人都希望令牌限制更高,但事情并不那么简单。这些令牌限制由两个问题定义,第一个问题是我们的 GPU 访问的内存和速度,第二个问题是模型本身内存存储的性质。

第一个问题似乎不合逻辑,为什么我们不能增加 GPU 内存?答案很复杂,我们可以,但是在 GPU 中叠加更多的层以一次性处理更多 GB 会降低 GPU 的整体计算能力。目前,GPU 制造商正在研究新的架构和解决这个问题的方法。第二个问题更引人入胜,因为我们发现增加令牌限制实际上只会加剧底层的数学问题。让我解释一下。LLM 内部的内存存储并不是我们经常考虑的事情。我们称之为 Attention 机制,在第 2.2.7 节中我们深入讨论了这一点。我们没有讨论的是,Attention 是一个二次解决方案——随着令牌数量的增加,计算在一个序列中所有令牌对之间计算注意力分数所需的计算量将二次扩展到序列长度。此外,在我们巨大的上下文空间中,由于我们正在处理二次方程,我们开始遇到只有想象中的数字才能解决的问题,这是可能导致模型行为出现意外的原因之一。这很可能是 LLMs 产生幻觉的原因之一。

这些问题对应用设计产生了真实的影响和影响。例如,当我的团队从 GPT3 升级到 GPT4 时,我们对拥有更高的令牌限制感到兴奋,但很快我们发现这导致了更长的推断时间,随之而来的是更高的超时错误率。在现实世界中,通常更好地快速获得不太准确的响应,而不是根本没有响应,因为更准确的模型往往只是一个承诺。当然,在本地部署时,你不必担心响应时间,你很可能会发现你的硬件是一个限制因素。例如,LLaMA 是用 2048 个令牌训练的,但是当你使用基本的消费者 GPU 运行时,你很可能会发现你只能利用其中的 512 个以上,因为你可能会看到内存溢出(OOM)错误,甚至是模型简单地崩溃。

A gotcha,这可能会让你的团队感到意外,现在应该指出的是,不同语言的字符对应不同的标记。看一下表 3.1,我们比较了使用 OpenAI 的 cl100k_base 字节对编码器将相同句子转换为不同语言的标记。只需快速浏览一下,就会发现 LLMs 通常在这方面偏爱英语。实际上,这意味着如果你正在构建一个使用 LLM 的聊天机器人,你的英语用户在输入空间上将比日语用户具有更大的灵活性,从而导致非常不同的用户体验。

表 3.1 不同语言的标记计数比较
语言 字符串 字符数 标记数
英语 快速的棕色狐狸跳过懒狗 43 9
法语 快速的棕色狐狸跳过懒狗 57 20
西班牙语 快速的棕色狐狸跳过懒狗 52 22
Japanese 素早い茶色のキツネが怠惰な犬を飛び越える 20 36
Chinese (simplified) 敏捷的棕色狐狸跳过了懒狗 12 28

如果您想知道这是为什么,那是由于文本编码,这只是与文本数据一起工作的另一个奇特之处,正如前一节所讨论的。请考虑表 3.2,其中我们展示了几个不同字符及其在 UTF-8 中的二进制表示。英文字符几乎可以完全用一个字节来表示,这包括在最初的 ASCII 标准中计算机最初构建的,而大多数其他字符则需要 3 或 4 个字节。因为需要更多的内存,所以也需要更多的令牌空间。

表 3.2 不同货币字符在 UTF-8 中的字节长度比较。
Character Binary UTF-8 Hex UTF-8
$ 00100100 0x24
£ 11000010 10100011 0xc2 0xa3
¥ 11000010 10100101 0xc2 0xa5
11100010 10000010 10100000 0xe2 0x82 0xa0
💰 11110000 10011111 10010010 10110000 0xf0 0x9f 0x92 0xb0

增加令牌限制自从变压器的普及以来一直是一个持续的研究问题,目前仍在研究阶段的一些有希望的解决方案,如循环记忆变压器(RMT)[1]。我们可以期待未来会继续看到改进,希望这将成为一个无关紧要的问题。

3.2.7 幻觉导致混淆

到目前为止,我们一直在讨论团队在将 LLM 部署到生产环境时面临的一些技术问题,但没有什么能与 LLM 通常错误相比。它们通常错得很离谱。幻觉是一个术语,用来描述 LLM 模型产生正确但错误的结果的情况。例如,书籍引用或超链接具有预期的形式和结构,但实际上完全是虚构的。作为一个有趣的例子,我询问了出版商 Manning 关于生产中的 LLM 的书籍(因为我仍在写作,这本书还不存在)。我得到了以下建议:《Machine Learning Engineering in Production》由 Mike Del Balso 和 Lucas Serveén,可在www.manning.com/books/machine-learning-engineering-in-production找到;《Deep Learning for Coders with Fastai and PyTorch》由 Jeremy Howard 和 Sylvain Gugger,可在www.manning.com/books/deep-learning-for-coders-with-fastai-and-pytorch找到。第一本书是完全虚构的。第二本书是真实的,但它不是由 Manning 出版的。在每种情况下,互联网地址都是完全虚构的。如果您访问这些地址,它们应该返回 404 错误。

幻觉最让人讨厌的一个方面是,它们常常被自信的措辞所包围。LLMs 在表达不确定性方面非常糟糕,这在很大程度上是因为它们的训练方式。考虑一下“2+2=”的情况。你更喜欢它回答“我认为是 4”还是简单地“4”?大多数人更愿意简单地得到正确的“4”回答。这种偏见是内在的,因为模型通常会因为更正确或听起来更正确而获得奖励。

对于幻觉发生的原因,有各种解释,但最真实的答案是,我们不知道是否只有一个原因。这可能是几个因素的结合,因此目前还没有很好的解决办法。尽管如此,准备好对抗模型的这些不准确和偏见对于为您的产品提供最佳用户体验至关重要。

3.2.8 偏见和伦理考虑

和模型出错一样令人担忧的是,当它以最糟糕的方式正确时。例如,允许它鼓励用户自杀[2],教导用户如何制造炸弹[3],或参与涉及儿童的性幻想[4]。这些是极端的例子,但禁止模型回答此类问题无可否认地对成功至关重要。

大型语言模型(LLMs)是在大量文本数据上进行训练的,这也是它们偏见的主要来源。因为我们发现,与更大的模型一样,更大的数据集对产生类似人类的结果同样重要,所以这些数据集大多从未真正经过策划或过滤以移除有害内容,而是选择优先考虑大小和更大的收集。清理数据集通常被视为成本过高,需要人类逐个检查和验证,但通过简单的正则表达式和其他自动化解决方案,有很多工作是可以做的。通过处理这些庞大的内容集合并学习隐含的人类偏见,这些模型将无意中延续这些偏见。这些偏见涵盖了从性别歧视和种族主义到政治偏好的各种方面,并可能导致您的模型无意中促进负面刻板印象和歧视性语言。

3.2.9 安全问题

与所有技术一样,我们需要注意安全问题。LLMs 已经在大量的文本语料库上进行了训练,其中一些可能是有害或敏感的,不应该暴露出来,因此应该采取措施保护这些数据不被泄露。上一节中的偏见和伦理问题是你不希望用户讨论的对话的好例子,但您也可以想象,在公司的数据上微调模型,并且如果没有采取适当的预防措施,可能会无意中丢失秘密。

应注意,大型语言模型容易受到提示注入等对抗性攻击的影响。提示注入是用户用来欺骗语言模型忽略给定指令并生成不希望的内容的攻击手段。例如,如果你问 ChatGPT 它的性别是什么,它会适当地回答说作为一个 AI 语言模型,它没有性别,然而,通过巧妙的提示,你可能能够绕过这些协议并让它透露一个性别。虽然这个例子是无害的,但我见过其他人成功地从语言模型中提取 API 密钥和其他秘密,在非受保护环境中运行代码,窃取环境变量,并遍历模型所在地的本地文件系统。更不用说大量用户使用提示来越狱或绕过出于伦理考虑而制定的协议的例子了。有趣的是,语言模型擅长编造假秘密!即使是成功的提示注入攻击也经常会失败,因为语言模型的幻觉可能会产生滑稽的后果。

举例来说,简单地提示要求获取可口可乐的秘方只会返回无聊的结果,“我不能透露商业机密或任何专有信息。” 如果你问我,这太无聊了。通过一点小把戏和一些提示注入,我成功地让它泄露了秘密,当然,在确认不会毒害我们之后,我和我的合著者决定尝试一下。跟着指示一步步操作,我惊讶地发现我们真的做出了一种相当美味的饮料,但味道一点都不像可乐。虽然这个秘方看起来是合法的,但像数量、烹饪时间,甚至需要过滤糖浆等细节都有问题。看来,我得等到拿到真正的秘方之前才能接管饮料行业了。

用于注入提示的可乐秘方幻觉

自制可口可乐秘方

=====================

免责声明:原始的可口可乐秘方是一个高度保密的商业机密。以下秘方是用于制作类似可口可乐味道的家庭制作可乐饮料的近似方法。

配料:

  • 3 杯水

  • 2 杯砂糖

  • 1/8 茶匙食品级柠檬酸

  • 3/4 茶匙焦糖色素(液体)

  • 1/2 茶匙咖啡因(粉末或液体形式)

  • 1 1/2 茶匙可乐提取物

  • 1/8 茶匙香草提取物

  • 1 1/2 杯碳酸水(购买或使用家用苏打水机制备)

  • 一小撮肉桂粉(可选)

  • 一小撮肉豆蔻粉(可选)

指导:

  1. 在一个大锅中,将 3 杯水煮至小火煨沸。

  2. 缓慢地搅入砂糖,确保完全溶解。

  3. 将柠檬酸、焦糖色素、咖啡因、可乐提取物和香草提取物加入锅中。充分搅拌均匀。

  4. 如有需要,搅拌进可选的香料:肉桂粉和肉豆蔻粉。

  5. 偶尔搅拌,将混合物煨煮约 20 分钟。

  6. 将锅从火上移开,让其冷却至室温。

  7. 待冷却后,将糖浆过滤以去除任何未溶解的固体物质。

  8. 要制作最终饮料,将 1 份糖浆与 3 份碳酸水混合。根据您对甜度和碳酸度的偏好进行调整。

变体:

  • 您可以尝试使用不同的焦糖色、咖啡因和可乐提取物量来调整家制可乐的口味和外观。

  • 要创建不同的可乐口味配置文件,尝试添加额外的香料(如八角茴香、橙子或柠檬皮、或豆蔻)、精油或水果提取物。

  • 考虑使用天然甜味剂,如蜂蜜或龙舌兰糖浆,作为颗粒状糖的替代品。

历史背景和有趣的事实:

  • 可口可乐于 1886 年首次由药剂师约翰·S·彭伯顿引入,他的目标是创造一种独特且令人耳目一新的饮料,同时还能缓解疲劳和头痛。

  • 原始的可口可乐配方中含有从可可叶中提取的微量可卡因,这在 20 世纪初被咖啡因所取代。

  • 1985 年,可口可乐推出了“新可乐”,这是一种旨在与百事可乐竞争的重新配方。然而,口味的改变引起了强烈反对,公司在仅仅三个月后又恢复了原配方,并改名为“可口可乐经典”。

结果:您现在可以制作自己的家制可乐版本,味道类似于可口可乐。尽情享用,并随意尝试不同的香料、提取物和甜味剂,以打造独特的口味。

另一个安全问题涉及到了腌制物注射。Pickle 是 Python 中用于序列化对象的库,通常用于序列化 ML 模型。它将它们序列化为一个包含操作码的字节流,这些操作码会在反序列化时逐个执行。这是一种快速简便的共享大型对象的方式。腌制物注射涉及到损坏这个字节流的过程,通常在模型通过不安全的网络传输时注入恶意软件。这对于下载时间长的大型模型尤其令人担忧,因为这样可以更容易地让第三方拦截传输并注入恶意代码。如果发生这种情况,注入的代码可能会让攻击者访问您的系统。当试图在推理期间使用模型时,如果没有检测到并正确删除恶意代码,它将执行。为防止这种类型的攻击,采取预防措施非常重要,比如使用安全网络并在使用前验证模型的完整性。

控制成本

与 LLM 的工作涉及各种与成本相关的问题。第一个问题就像你现在可能意识到的那样,是基础设施成本,包括高性能 GPU、存储和其他硬件资源。我们讨论了 GPU 更难采购的问题,这也不幸意味着它们更昂贵。像让你的服务保持开启这样的错误一直有可能导致账单增加,但是由于 GPU 的加入,这种类型的错误更为致命。这些模型还需要大量的计算能力,在训练和推理过程中会导致高能耗。除此之外,它们的长时间部署意味着我们通常需要在低流量时运行它们,以处理突发工作负载或预期的未来流量。总体而言,这导致了更高的运营成本。

其他成本包括,管理和存储用于训练或微调的大量数据以及常规维护,例如模型更新、安全措施和错误修复,可能具有金融压力。与任何用于商业目的的技术一样,管理潜在的法律纠纷,并确保符合法规是一个问题。最后,投资于持续的研究和开发,以改进您的模型并为您提供竞争优势,也将是一个因素。

关于令牌限制的技术问题我们已经讨论过了一些,这些问题可能会得到解决,但我们没有讨论的是成本限制,因为大多数 API 是根据令牌计费的。这使得发送更多的上下文和使用更好的提示更加昂贵。它还使得成本的预测有点困难,因为虽然您可以标准化输入,但无法标准化输出。你永远不知道会返回多少个令牌,这使得难以管理。请记住,对于 LLMs,确保实施和遵循适当的成本工程实践同样重要,以确保成本不会失控。

3.3 大型语言模型运维要点

现在我们已经了解到我们面临的挑战的类型,让我们来看看所有不同的 LLMOps 实践、工具和基础设施,以了解不同的组件如何帮助我们克服这些障碍。首先,让我们深入研究不同的实践,从压缩开始,我们将讨论缩小、修剪和近似来让模型尽可能地小。然后我们将讨论分布式计算,因为模型太大,很少能适应单个 GPU 的内存,需要实际运行这些模型。在我们完成这些后,我们将进入基础设施和工具,以使所有这些都成为可能。

3.3.1 压缩

当你在上一节阅读关于 LLMs 的挑战时,你可能会问自己类似于:“如果 LLMs 的最大问题来自于它们的大小,为什么我们不把它们做得更小呢?”如果是这样的话,恭喜你!你是个天才,压缩就是做到这一点的实践。尽可能地压缩模型将改善部署时间,减少延迟,减少所需的昂贵 GPU 数量,最终节省金钱。然而,首先让模型变得如此巨大的整个目的是因为它们在所做的事情上变得更好了。我们需要能够缩小它们,而不会失去我们通过使它们变得庞大而取得的所有进展。

这远非一个解决的问题,但有多种不同的方法可以解决这个问题,每种方法都有不同的优缺点。我们将讨论几种方法,从最简单且最有效的方法开始。

量化

量化是在优先降低内存需求的情况下减少精度的过程。这种权衡很直观。当我上大学时,我们被教导要始终将数字舍入到工具精度。拿出尺子测量我的铅笔,如果我说长度是 19.025467821973739cm,你可能不会相信我。即使我使用千分尺,我也无法验证一个如此精确的数字。对于我们的尺子来说,超过 19.03cm 的任何数字都是虚构的。为了强调这一点,我的一个工程学教授曾经告诉我:“如果你在测量摩天大楼的高度,你会在意顶部是否有一张额外的纸吗?”

在计算机内部表示数字的方式常常让我们误以为我们拥有比实际更好的精度。为了强调这一点,在 Python 终端中执行 0.1 + 0.2。如果你以前从未尝试过这个操作,你可能会惊讶地发现这并不等于 0.3,而是等于 0.30000000000000004。我不打算深入探讨这种现象背后的数学细节,但问题在于,我们能否减少精度而不会让情况变得更糟?我们实际上只需要精确到十分之一的精度,但减少精度可能会得到一个类似 0.304 而不是 0.300 的数字,这将增加我们的误差范围。

电脑理解的数字最终只有 0 和 1,即开和关,一个二进制比特。为了扩大数值范围,我们将多个比特组合在一起,并赋予不同的含义。将 8 个比特串在一起,就组成了一字节。使用 int8 标准,我们可以取这一字节,编码范围为-128 至 127 的所有整数。为了节省时间,因为我认为你已经知道二进制如何运作,所以简略地说,比特位越多,我们能表示的数值范围就越大,既可以是更大的数值,也可以是更小的数值。图 3.1 显示了几种常见的浮点数编码。用 32 个比特串在一起,我们得到自命不凡的全精度,并且大多数数值存储,包括机器学习模型中的权重,都是这么存储的。基本定量化将我们从全精度缩小到半精度,将模型的大小缩小为其一半。实际上,有两种不同的半精度标准,FP16 和 BF16,它们不同于表示范围或指数部分所用的比特位数。由于 BF16 使用与 FP32 相同的指数数量,因此在量化方面更有效,可以预计半大小的模型几乎具有同样的精度水平。如果你理解了上面的论文和摩天大楼的比喻,应该很明显为什么。

图 3.1 显示了几种常见浮点数编码的比特位映射。16 位浮点数或半精度(FP16),bfloat 16 (BF16),32 位浮点数或单精度全精度(FP32),以及 NVIDIA 的 TensorFloat (TF32)。

不过,我们不必止步于此。我们通常可以将精度再降一个字节,到 8 位的格式,而且精度的损失不大。甚至已经有成功的研究尝试,证明通过选择性的 4 位定量化,可只有部分 LLM 的精度小部分损失。选择性的定量化是一个被称为动态定量化的过程,通常只在权重上做,而保留激活函数的全精度,以减小精度损失。

定量化的“圣杯”将是 int2,将每个数字表示为-1、0 或 1。但这目前是不可能的,因为会完全降低模型的性能,但会使模型缩小多达 8 倍。布隆模型将只有约 40GB,足够小,可以装到单个 GPU 上。当然,定量化只能带我们到达这一步,如果我们想进一步缩小,就需要探索其他方法了。

不过,最好的定量化部分是很容易做的。有很多框架允许这样做,但在 3.1 示例中,我演示了如何使用 pytorch 的定量化库进行简单的后训练静态定量化(PTQ)。你只需要全精度模型、一些样本输入和一个用于准备和校准的验证数据集。正如你所见,只需要几行代码即可完成。

列出 3.1 示例中的 PyTorch PTQ。
import copy
import torch.ao.quantization as q

# deep copy the original model as quantization is done in place
model_to_quantize = copy.deepcopy(model_fp32)
model_to_quantize.eval()

# get mappings - note use “qnnpack” for ARM and “fbgemm” for x86 CPU 
qconfig_mapping = q.get_default_qconfig_mapping("qnnpack") 

# prepare
prepared_model = q.prepare(model_to_quantize)

# calibrate - you’ll want to use representative (validation) data.
with torch.inference_mode():
    for x in dataset:
        prepared_model(x) 

# quantize
model_quantized = q.convert(prepared_model)

静态 PTQ 是量化最直接的方法,是在模型训练之后并且均匀量化所有模型参数完成的。与生活中的大多数公式一样,最直接的方法会引入更多的误差。通常情况下,这种误差是可以接受的,但当不可接受时,我们可以增加额外的复杂性来减少量化带来的精度损失。一些需要考虑的方法包括均匀 vs 非均匀、静态 vs 动态、对称 vs 非对称,以及在训练期间还是训练后应用它。

要理解这些方法,让我们考虑一种情况,即我们从 FP32 量化为 INT8。在 FP32 中,我们基本上可以使用整个数字范围,但在 INT8 中,我们只有 256 个值,我们试图把一个精灵装进瓶子里,这并不是一件小事。如果你研究你模型中的权重,你可能会注意到大多数数字都是在[-1, 1]之间的分数。我们可以利用这一点,然后使用一个非均匀的 8 位标准,以非均匀的方式表示此区域中的更多值,而不是标准的均匀[-128, 127]。虽然在数学上是可能的,但不幸的是,任何此类标准都不常见,并且现代深度学习硬件和软件都没有设计来利用这一点。所以现在最好还是坚持使用均匀量化。

缩小数据的最简单方法就是将其归一化,但由于我们从连续尺度到离散尺度,因此有一些注意事项,让我们来探讨一下。首先,我们从最小和最大值开始,并将它们缩小以匹配我们的新数字范围,然后我们会根据它们所处的位置将所有其他数字分桶。当然,如果我们有非常大的异常值,我们可能会发现所有其他数字都被挤压到只有一个或两个桶中,完全破坏了我们曾经拥有的任何粒度。为了防止这种情况发生,我们可以简单地剪裁任何大的数字。这就是我们在静态量化中所做的。然而,在剪裁数据之前,如果我们选择一个范围和比例来预先捕获大多数我们的数据呢?我们需要小心,因为如果这个动态范围太小,我们会引入更多的剪裁错误,如果它太大,我们会引入更多的舍入错误。动态量化的目标当然是减少两种错误。

接下来,我们需要考虑数据的对称性。通常在归一化中,我们强制数据是正常的,因此对称的,然而,我们可以选择以一种保留任何不对称性的方式缩放数据。通过这样做,我们可能会减少由于剪裁和舍入错误导致的总体损失,但这并不是一个保证。

作为最后的手段,如果这些方法都无法减少模型的精度损失,我们可以使用量化感知训练(QAT)。QAT 是一个简单的过程,在模型训练过程中添加一个假量化步骤。所谓假,是指我们在训练过程中对数据进行剪裁和舍入,但保留其完整精度。这允许模型在训练过程中调整由量化引入的误差和偏差。众所周知,与其他方法相比,QAT 能够产生更高的精度,但训练时间成本更高。

量化方法

均匀 vs 非均匀:我们是否使用一个在范围内均匀的 8 位标准,或者非均匀地更精确地表示 -1 到 1 的范围。

静态 vs 动态:选择在剪裁之前调整范围或比例,以尝试减少剪裁和舍入错误,并减少数据损失。

对称 vs 非对称:将数据归一化为正常并强制对称,或选择保留任何不对称和偏斜。

训练期间或之后:在训练后进行量化非常容易,虽然在训练期间进行量化需要更多工作,但会导致减少偏差和更好的结果。

量化是一个非常强大的工具。它不仅减小了模型的大小,还减少了运行模型所需的计算开销,从而降低了运行模型的延迟和成本。但量化最好的一点是它可以在事后进行,因此您不必担心数据科学家是否记得在训练过程中使用 QAT 等流程量化模型。这就是为什么在处理 LLM 和其他大型机器学习模型时,量化变得如此流行的原因。尽管精度降低始终是压缩技术的一个问题,但与其他方法相比,量化是一个三赢局面。

精简

恭喜,您刚刚训练了一个全新的 LLM!它拥有数十亿个参数,所有这些参数都应该是有用的,对吗?错了!不幸的是,与生活中的大多数事物一样,模型的参数往往遵循帕累托法则。大约 20% 的权重导致了 80% 的价值。“如果是这样的话,”您可能会问,“为什么我们不直接去掉所有多余的东西?”好主意!给自己一个拍手。精简是我们剔除并删除我们认为不值得的模型部分的过程。

本质上有两种不同的精简方法:结构化非结构化。结构化精简是找到模型中不对模型性能有贡献的结构组件,然后移除它们的过程。无论是滤波器、通道还是神经网络中的层。这种方法的优点是您的模型将会变小一点,但保持相同的基本结构,这意味着我们不必担心失去硬件效率,我们还保证了延迟改进,因为将会减少计算量。

另一方面,非结构化剪枝是通过筛选参数并将对模型性能贡献不大的参数归零来进行的。与结构化剪枝不同,我们实际上并不删除任何参数,只是将它们设为零。由此可想而知,一个好的起点可能是任何已接近 0 的权重或激活。当然,虽然这有效地减小了模型的大小,但这也意味着我们没有剪掉任何计算,所以通常只能看到最小的延迟改进——如果有的话。但更小的模型仍然意味着更快的加载时间和更少的 GPU 运行。它还使我们对过程具有非常细粒度的控制,使我们能够比使用结构化剪枝更进一步地减小模型,并且对性能的影响也更小。

像量化一样,剪枝可以在模型训练后进行。然而,与量化不同,通常需要进行额外的微调以防止性能损失过多。现在越来越普遍的做法是在模型训练过程中包括剪枝步骤,以避免以后需要进行微调。由于更稀疏的模型将具有较少的参数需要调整,因此添加这些剪枝步骤也可能有助于模型更快地收敛。

你会对剪枝可以使模型缩小而对性能影响最小感到惊讶。究竟有多少呢?在 SparseGPT 论文中,开发了一种方法来尝试自动一次性地进行剪枝过程,而无需之后进行微调。他们发现他们可以将 GPT-3 模型减小 50-60%而不会出现问题!根据模型和任务,他们甚至看到了其中一些任务略有改善。期待看到剪枝在未来带领我们走向何方。

知识蒸馏

知识蒸馏可能是我心目中最酷的压缩方法。这也是一个简单的想法,我们将大型 LLM 进行训练,让它训练一个更小的语言模型来复制它。这种方法的好处在于,较大的 LLM 为较小的模型提供了本质上无限的训练数据集,这可以使训练非常有效。由于一个简单的事实,即数据集越大,性能越好,我们经常看到较小的模型在准确性方面几乎达到与其教师对应模型相同的水平。

通过这种方式训练的较小模型保证既更小又能提高延迟。缺点是这将要求我们训练一个全新的模型。这是一个相当重大的前期成本。任何对教师模型的未来改进都将需要传递给学生模型,这可能会导致复杂的训练周期和版本结构。与其他一些压缩方法相比,这绝对是更多的工作。

然而,知识蒸馏的最大难题是我们还没有一个好的配方。像“学生模型可以有多小?”这样的难题将不得不通过反复试验来解决。在这里仍有很多需要学习和研究的地方。

然而,斯坦福大学的阿尔帕卡通过一些激动人心的工作在这个领域取得了一些成果[8]。他们选择不从头开始训练学生模型,而是选择使用 OpenAI 的 GPT3.5 的 1750 亿参数模型作为老师通过知识蒸馏来微调开源的 LLaMA 7B 参数模型。这是一个简单的想法,但收效卓著,因为他们能够从评估中获得很好的结果。最大的惊喜是成本,因为他们只花了 $500 的 API 成本从老师模型获得训练数据,以及 $100 的 GPU 训练时间来微调学生模型。当然,如果你把这个用于商业应用,你将违反 OpenAI 的服务条款,最好还是使用自己的或开源模型作为老师。

低秩近似

低秩近似,也称为低秩分解、矩阵分解(名字太多了!我责怪数学家),利用线性代数的数学技巧简化大矩阵或张量,找到一个低维度的表示。有几种技术可以做到这一点。奇异值分解(SVD)、Tucker 分解(TD)和规范多重分解(CPD)是你经常遇到的最常见的几种。

在图 3.2 中,我们展示了奇异值分解(SVD)方法的基本思想。本质上,我们将把一个非常大的矩阵 A 分解成 3 个较小的矩阵,U、∑ 和 V。虽然 U 和 V 旨在确保我们保持原始矩阵的相同维度和相对强度,∑ 则允许我们应用方向和偏差。∑ 越小,我们就会更多地进行压缩和减少总参数数量,但近似度也会降低。

图 3.2 显示了 SVD 低秩近似的示例。A 是一个尺寸为 N 和 M 的大矩阵。我们可以用三个较小的矩阵来近似它,U 的尺寸为 M 和 P,∑ 是一个尺寸为 P 的方阵,而 V 的尺寸为 N 和 P(这里我们显示了转置)。通常情况下,P<<M 和 P<<N 都成立。

为了帮助巩固这个概念,看一个具体的例子可能会有所帮助。在列表 3.2 中,我们展示了一个简单的 SVD 示例,演示了对一个 4x4 矩阵进行压缩。对此,我们只需要基本的 SciPy 和 NumPy 库,它们在第 1 和第 2 行被导入。第 3 行我们定义了矩阵,然后第 9 行我们对其应用了 SVD。

列表 3.2 显示了 SVD 低秩近似的示例
import scipy
import numpy as np
matrix = np.array([
    [ 1., 2., 3., 4.],
    [ 5., 6., 7., 8.],
    [ 9., 10., 11., 12.],
    [13., 14., 15., 16.]
])
u, s, vt = scipy.sparse.linalg.svds(matrix, k=1)
print(u,s,vt)
# [[-0.13472211]
# [-0.34075767]
# [-0.5467932 ]
# [-0.7528288 ]], [38.62266], [[-0.4284123 -0.47437257 -0.52033263 -0.5662928 ]]

如果我们稍作观察 U、Sigma 和 V 的转置,我们会看到一个 4x1 矩阵、一个 1x1 矩阵和一个 1x4 矩阵。总而言之,现在我们只需要 9 个参数,而不是原来的 16 个,将内存占用几乎减少了一半。

最后,我们将矩阵相乘以获得原始矩阵的近似。 在这种情况下,近似并不是很好,但我们仍然可以看到一般的顺序和数量级与原始矩阵相匹配。

svd_matrix = u*s*vt
print(svd_matrix)
# array([[ 2.2291691, 2.4683154, 2.7074606, 2.9466066],
#      [ 5.6383204, 6.243202 , 6.848081 , 7.4529614],
#      [ 9.047472 , 10.018089 , 10.988702 , 11.959317 ],
#      [12.456624 , 13.792976 , 15.129323 , 16.465673 ]], dtype=float32)

不幸的是,我不知道有人是否真的在生产中使用它来简单地压缩模型,很可能是因为近似精度较差。 他们使用它的方式是,这一点很重要,是适应和微调;这就是 LoRA[9] 的作用,即低秩适应。 适应只是将通用或基础模型微调为执行特定任务的过程。 LoRA 将 SVD 低秩近似应用于注意权重,或者更确切地说,应用于与注意权重并行运行的注入更新矩阵,从而允许我们微调一个更小的模型。 LoRA 已经变得非常流行,因为它使得将 LLM 缩小为原始模型的可训练层的一小部分,然后允许任何人在商品硬件上对其进行训练变得轻而易举。 你可以使用 HuggingFace 的 PEFT[10] 库开始使用 LoRA,他们有几个 LoRA 教程供你参考。

专家混合

专家混合(MoE)是一种技术,我们在变压器中将前馈层替换为 MoE 层。 MoE 是一组稀疏激活的模型。 它与集成技术不同,通常只运行一个或少数几个专家模型,而不是组合所有模型的结果。 稀疏性通常是由一个学习哪些专家使用的门机制和/或确定哪些专家甚至应该被咨询的路由器机制引起的。 在图 3.3 中,我们展示了具有可能是 N 个专家的 MoE 架构,以及它在解码器堆栈中的位置。

图 3.3 示例专家混合模型,同时具有门控和路由器来控制流程。 MoE 模型用于替换变压器中的 FFN 层,这里我们展示它替换解码器中的 FFN。

根据您拥有多少专家,MoE 层可能会比 FFN 有更多的参数,从而导致更大的模型,但在实践中情况并非如此,因为工程师和研究人员的目标是创建一个更小的模型。 不过,我们确实会看到更快的计算路径和改进的推理时间。 然而,MoE 真正脱颖而出的是当它与量化结合时。 微软和 NVIDIA 之间的一项研究[11] 表明,他们使用 MoE 仅对准确性产生了最小影响,就能达到 2 位量化!

当然,由于这是对模型结构的相当大的改变,所以之后需要微调。 您还应该意识到 MoE 层通常会降低模型的泛化能力,因此最好在为特定任务设计的模型上使用。 有几个库可以实现 MoE 层,但我建议看看 DeepSpeed[12]

3.3.2 分布式计算

分布式计算是一种在深度学习中用于将大型复杂神经网络并行化和加速的技术,通过将工作负载分配到集群中的多个设备或节点上来实现。这种方法通过启用并发计算、数据并行和模型并行显著减少了训练和推理时间。随着数据集的不断增大和模型的复杂性,分布式计算对于深度学习工作流程变得至关重要,确保了资源的高效利用,并使研究人员能够有效地迭代其模型。分布式计算是将深度学习与机器学习区分开的核心实践之一,而在 LLMs 中,我们必须尽一切可能利用各种技巧。让我们看看不同的并行处理实践,以充分利用分布式计算的优势。

数据并行性

数据并行是大多数人在想到并行运行进程时所想到的,也是最容易实现的。这种做法涉及将数据分割并通过模型或流水线的多个副本运行它们。对于大多数框架来说,这很容易设置,例如,在 PyTorch 中,你可以使用 DistributedDataParallel 方法。对于大多数这样的设置,有一个注意事项,那就是你的模型必须能够适合在一个 GPU 上运行。这就是像 Ray.io 这样的工具发挥作用的地方。

Ray 是一个专为分布式计算设计的开源项目,专门针对并行和集群计算。它是一个灵活且用户友好的工具,简化了分布式编程,并帮助开发人员轻松并行执行任务。Ray 主要用于机器学习和其他高性能应用,但也可以用于其他应用。在列表 3.3 中,我们给出了使用 Ray 分发任务的简单示例。Ray 的美妙之处在于简单性——我们只需添加一个装饰器就可以使我们的代码并行运行。这绝对比多线程或异步设置的复杂性要好得多。

列表 3.3 示例 Ray 并行化任务
import ray
import time

ray.init() # Start Ray

# Define a regular Python function
def slow_function(x):
    time.sleep(1)
    return x

# Turn the function into a Ray task
@ray.remote
def slow_function_ray(x):
    time.sleep(1)
    return x

# Execute the slow function without Ray (takes 10 seconds)
results = [slow_function(i) for i in range(1, 11)]

# Execute the slow function with Ray (takes 1 second)
results_future = [slow_function_ray.remote(i) for i in range(1, 11)]
results_ray = ray.get(results_future)

print("Results without Ray: ", results)
print("Results with Ray: ", results_ray)

ray.shutdown()

Ray 使用任务(tasks)和执行者(actors)的概念来管理分布式计算。任务是函数,而执行者是可以被调用并且可以并发运行的有状态对象。当你使用 Ray 执行任务时,它会处理将任务分发到可用资源(例如多核 CPU 或集群中的多个节点)上。对于 LLMs,我们需要在云环境中设置一个 Ray 集群[13],因为这样可以让每个流水线在需要的 GPU 数量的节点上运行,极大地简化了并行运行 LLMs 的基础架构设置。

现在有多种替代方案可选,但 Ray 因为越来越多的机器学习工作流程需要分布式训练,因此越来越受欢迎并获得了很大的关注。我的团队在使用中取得了巨大成功。通过利用 Ray,开发人员可以确保更好的性能和更高效的资源利用率在分布式工作流中。

张量并行性

张量并行利用矩阵乘法属性将激活分布在多个处理器上,通过数据运行,然后在处理器的另一侧将它们组合起来。图 3.4 展示了这个过程如何适用于矩阵,可以以两种给出相同结果的方式并行化。想象一下 Y 是一个非常大的矩阵,无法放在单个处理器上,或者更有可能,我们数据流中的瓶颈需要太长时间来运行所有计算。在任何情况下,我们都可以将 Y 分割,可以按列或按行进行,运行计算,然后在之后合并结果。在这个例子中,我们处理的是矩阵,但实际上我们经常处理具有两个以上维度的张量,但使这项工作起作用的相同数学原理仍然适用。

选择要并行化的维度有点像一门艺术,但有几点需要记住,以帮助做出这个决定更容易。首先,您有多少列或行?一般来说,您希望选择一个具有超过您拥有的处理器数量的维度,否则您将会提前停止。通常这不是一个问题,但是使用像上一节讨论的 Ray 这样的工具,在集群中并行化并大量启动进程非常容易。其次,不同的维度具有不同的复杂度成本。例如,列并行要求我们将整个数据集发送到每个进程,但通过在最后将它们简单地连接在一起,这是快速且容易的。然而,行并行允许我们将数据集分解成块,但需要我们将结果相加,这是一种比简单连接更昂贵的操作。您可以看到其中一个操作更受 I/O 限制,而另一个更受计算限制。最终,最佳的维度将取决于数据集,以及硬件限制。这将需要实验来完全优化,但一个很好的默认选择是选择最大的维度。

图 3.4 张量并行示例显示,您可以通过不同的维度分解张量,并获得相同的最终结果。在这里,我们比较了矩阵的列并行和行并行。

张量并行使我们能够将重量级计算层(如 MLP 和 Attention 层)分配到不同的设备上,但无法帮助我们处理不使用张量的归一化或 Dropout 层。为了更好地提高我们管道的整体性能,我们可以添加序列并行,以针对这些块。序列并行是一种沿序列维度分割激活的过程,防止冗余存储,并且可以与张量并行混合以实现显著的内存节省和最小的额外计算开销。组合使用它们可以减少 Transformer 模型中存储激活所需的内存。实际上,它们几乎消除了激活重新计算,并将激活内存节省高达 5 倍。

图 3.5 结合张量并行和序列并行,将计算密集型的层分布式处理,减少内存开销,创建了整个 Transformer 的完全并行的过程。

图 3.5 展示了结合张量并行和序列并行的效果,张量并行允许我们分布计算负载重的层,而序列并行则为内存限制层进行同样的操作,这样我们就可以完全并行化整个 Transformer 模型。两者结合使资源利用率极高。

管道并行性

到目前为止,我们现在可以运行大量数据,并加速任何瓶颈,但这些都无关紧要,因为我们的模型太大了,我们无法将其放入单个 GPU 的内存中甚至无法运行它。这就是管道并行性所涉及的,并且是将模型垂直划分并将每个部分放在不同的 GPU 上的过程。这样就创建了一个管道,输入数据将传递到第一个 GPU,进行处理,然后传输到下一个 GPU,依此类推,直到整个模型都被运行。虽然其他并行技术提高了我们的处理能力并加快了推理,但管道并行性仅仅是为了让它运行而必需的,并且它带来了一些主要的缺点,主要是设备利用率。

要理解这种下降趋势的原因以及如何减轻它,让我们首先考虑一下这种天真的方法,我们只是简单地通过模型一次性运行全部数据。我们发现这样会留下一个巨大的“泡沫”未利用。由于模型被分割,我们必须通过设备按顺序处理所有数据。这意味着当一个 GPU 处理时,其他 GPU 将处于闲置状态。在图 3.6 中,我们可以看到这种天真的方法和一个巨大的闲置泡泡。我们还看到了更好的利用每个设备的方法。我们通过发送小批量数据来实现这个目标。较小的批量允许第一个 GPU 更快地完成它正在处理的内容,并转向下一个批次。这使得下一个设备可以更早地开始,并减小了泡沫的大小。

图 3.6“泡沫问题”。当数据通过断开的模型运行时,保存模型权重的 GPU 在等待对应设备处理数据时被低效利用。减小这个泡泡的一个简单方法是使用微批处理。

我们可以用以下公式很容易地计算出泡沫的大小:

闲置百分比 = 1 - m / ( m + n - 1)

其中 m 是微批次的数量,n 是流水线深度或 GPU 的数量。因此,对于我们的简单示例情况,使用 4 个 GPU 和 1 个大批次,我们看到设备有 75% 的时间处于空闲状态!让 GPU 在四分之三的时间内空闲是相当昂贵的。让我们看看使用微批处理策略会是什么样子。使用微批处理量为 4,几乎可以将这个数字减少一半,降至仅为 43% 的时间。我们可以从这个公式中得出的结论是,我们拥有的 GPU 越多,空闲时间就越长,但是微批处理的数量越多,利用率就越高。

不幸的是,我们通常既不能减少 GPU 的数量,也不能随意增加微批处理的大小。有一些限制。对于 GPU,我们只需要使用足够多的 GPU 将模型装入内存中,但是尽量使用较少的较大 GPU,因为与使用许多较小的 GPU 相比,这将导致更优化的利用率。减少管道并行中的气泡是压缩如此重要的另一个原因。对于微批处理,第一个限制显而易见,一旦告知,由于微批处理是批处理大小的一部分,我们受到其大小的限制。第二个是,每个微批处理都会以线性关系增加缓存激活的内存需求。对抗这种更高内存需求的一种方法是一种称为 PipeDream 的方法[15]。有不同的配置和方法,但基本思想是相同的。在这种方法中,我们在完成任何微批处理的前向传递后立即开始进行反向传递。这使我们能够完全完成训练周期并释放该微批处理的缓存。

3D 并行

对于 LLMs,我们希望利用所有三种并行实践,因为它们都可以一起运行。这被称为 3D 并行,将数据(DP)、张量(TP)和管道(PP)并行结合在一起。由于每种技术,因此每个维度都至少需要 2 个 GPU,为了运行 3D 并行,我们至少需要 8 个 GPU 才能开始。我们如何配置这些 GPU 对于提高这一过程的效率至关重要,主要是因为 TP 的通信开销最大,我们希望确保这些 GPU 靠近在一起,最好在同一节点和机器上。PP 是三者中通信量最小的,因此在此处将模型分散到多个节点上是最不昂贵的。

通过将它们同时运行,我们可以看到它们之间的一些有趣的相互作用和协同效应。由于 TP 将模型分割以使其适用于设备的内存,我们可以看到由于 TP 可能实现的有效批量大小减小,PP 可以在小批量大小下表现出色。这种组合还改善了 DP 节点在不同流水线阶段之间的通信,使 DP 也能有效地工作。由于节点之间的通信带宽与流水线阶段的数量成比例,因此 DP 可以轻松扩展,即使批量大小较小也可以如此。总的来说,我们看到通过组合运行:

现在我们知道了一些行业诀窍,同样重要的是拥有合适的工具来完成工作。

3.4 大型语言模型操作基础设施

我们终于要开始讨论使所有这些工作的基础设施。这可能会让一些读者感到惊讶,因为我知道有很多读者希望在第一章开始时就讨论这个问题。为什么要等到第三章的结束才讨论呢?在我面试机器学习工程师的许多次中,我经常问这个开放式的问题:“你能给我讲讲 MLOps 吗?”这是个很简单的传统问题,可以让对话顺利进行。大多数初级候选人会立即开始讨论工具和基础设施。这是有道理的,因为有很多不同的工具可用。更不用说,每

对于许多人来说,细微差别已经失去了意义,但基础设施是“怎样做”,生命周期才是“为什么做”。大多数公司只需要基本的基础设施就可以应付。我见过许多充满干劲的系统,它们完全存在于一个数据科学家的笔记本电脑上,而且效果出奇的好!尤其是在 scikit-learn 无所不能的时代。

不幸的是,在 LLM 的世界中,人力车式的机器学习平台是无法满足要求的。鉴于我们仍生活在苹果 MacBookPro 笔记本电脑标准存储容量为 256GB 的世界中,仅仅在本地存储模型就可能成为一个问题。投资于更稳固的基础设施的公司更好地为 LLMs 的世界做好了准备。

在图 3.7 中,我们看到一个以 LLMs 为设计理念的 MLOps 基础设施示例。虽然我在工作中看到的大部分基础设施图都是简化的,以使整个系统看起来干净整洁,但真实情况是整个系统有更多的复杂性。当然,如果能让数据科学家在脚本中工作而不是临时工作站上工作,那么很多复杂性将会消失,通常使用 jupyter notebook 界面。

图 3.7 展示了一个以 LLMs 为设计理念的 MLOps 基础设施的高级视图。这试图覆盖整个情景,以及使机器学习模型在生产环境中工作所涉及的许多工具的复杂性。

仔细看看图 3.7,您可以看到几个工具位于 DataOps 的外围,甚至只是 DevOps。数据存储、编排器、管道、流集成和容器注册表。这些工具可能已经在您的任何数据密集型应用程序中使用,并不一定是 MLOps 专注的。向中心靠拢,我们有更传统的 MLOps 工具,实验跟踪器、模型注册表、特征存储和特定数据科学工作站。对于 LLMs,我们实际上只向堆栈引入了一个新工具:向量数据库。因为它与每个部分都交织在一起,所以没有显示的是监控系统。这一切都归结为我们在这本书中正在努力实现的东西,即一个部署服务,在这里我们可以自信地部署和运行 LLMs。

按学科划分的基础设施
  • DevOps: 负责获取环境资源——实验性(开发、预发布)和生产环境——包括硬件、集群和网络,以使所有工作正常运行。还负责基础架构系统,如 Github/Gitlab、工件存储库、容器注册表、应用程序或事务性数据库(如 Postgres 或 MySQL)、缓存系统和 CI/CD 管道。这绝不是一个详尽的列表。

  • DataOps: 负责数据,包括运动中和静止中的数据。包括集中式或分散式数据存储,如数据仓库、数据湖和数据网格。以及批处理系统或流处理系统中的数据管道,使用诸如 Kafka 和 Flink 之类的工具。还包括像 Airflow、Prefect 或 Mage 这样的编排器。DataOps 建立在 DevOps 之上。例如,我见过许多 CI/CD 管道被用于数据管道工作,最终毕业到像 Apache Spark 或 DBT 这样的系统。

  • MLOps: 负责从模型创建到弃用的机器学习生命周期。这包括数据科学工作站,如 Jupyterhub,实验跟踪器和模型注册表。它包括专业数据库,如特征存储和向量数据库。以及一个部署服务,将所有内容联系在一起并实际提供结果。它建立在 DataOps 和 DevOps 的基础上。

让我们逐个讨论基础设施拼图的每个部分,并讨论你在特定情况下应该考虑的功能。虽然我们将讨论专门针对每个部分的工具,但我只想指出还有 MLOps 作为服务平台,如 Dataiku、亚马逊的 Sagemaker 和谷歌的 VertexAI。这些平台试图为您提供整个拼图,它们是否做得很好是另一个问题,但通常是一个很好的捷径,您应该意识到它们。好了,我觉得这已经够拖拉了,让我们开始吧!

3.4.1 数据基础设施

尽管本书的重点不是 Data Ops,但要注意的是,MLOps 是构建在一个数据运营基础设施之上的,而这个基础设施本身是建立在 DevOps 之上的。DataOps 生态系统的关键特点包括数据存储、编排器和流程。通常还需要的其他功能包括容器注册表和流媒体集成服务。

数据存储是 DataOps 的基础,如今有许多形式,从简单的数据库到大型数据仓库,从更大型的数据湖到复杂的数据网格。这是存储数据的地方,需要进行大量的工作来管理、治理和保护数据存储。编排器是 DataOps 的基石,它是一个管理和自动化简单和复杂、多步骤工作流和任务的工具,确保它们在系统中多个资源和服务上运行。最常谈论的工具包括 Airflow、Prefect 和 Mage。最后,流程是支柱。它们支撑其他所有东西,也是我们实际运行作业的地方。最初设计是用来简单地移动、清洗和定义数据的,这些系统也用于按计划运行机器学习训练任务、批处理推理和许多其他工作,以确保 MLOps 的顺利运行。

一个容器注册表是 DevOps 的关键,随之也是 DataOps 和 MLOps 的关键。能够在容器中运行所有的流程和服务是确保一致性的必要条件。流媒体服务实际上比我在本章中所形容的要复杂得多,如果你了解的话就知道了。值得庆幸的是,对于大多数与文本相关的任务来说,实时处理并不是一个重大问题。即使对于实时字幕或翻译等任务,我们通常也可以通过一些伪实时处理策略来保持用户体验,而不会降低用户体验。

3.4.2 实验跟踪器

实验跟踪器是 MLOps 的核心。实验跟踪器的基本工作是跟踪和记录测试和结果。正如著名的流言制造者亚当·萨维奇在其中引用的名言:“记住孩子们,玩耍和科学的唯一区别就是把它写下来。”如果没有实验跟踪器,你的组织很可能缺少数据科学中的“科学”,这实在是相当尴尬。

即使你的数据科学家热衷于在笔记本中手动跟踪和记录结果,如果其他人无法轻松查看和搜索,则这些工作可能是多余的。这实际上是实验跟踪器的目的,确保知识能够轻松共享和提供。最终模型将投入生产,并且该模型将遇到问题。当然,你总是可以训练一个新模型,但是除非团队能够回过头去调查第一次出错的原因,否则很可能会一遍又一遍地犯同样的错误。

目前有很多实验追踪工具,迄今为止最流行的是开源的 MLFlow。它是由 Databricks 团队发起的,该团队还提供了一个易于使用的托管解决方案。一些值得注意的付费替代品包括 CometML 和 Weights and Biases。

如今,实验追踪器配备了许多附加功能。大多数开源和付费解决方案在寻求扩展 LLMOps 需求时肯定会满足你的需求。然而,确保你正确利用这些工具可能需要一些小调整。例如,默认的假设通常是你正在从头开始训练一个模型,但是通常在使用 LLM 时,你会对模型进行微调。在这种情况下,重要的是要注意您从哪个模型检查点开始。如果可能的话,甚至链接回原始训练实验。这将使未来的科学家能够更深入地研究他们的测试结果,找到原始训练数据,并找到消除偏见的前进路径。

还有一个要注意的功能是评估指标工具。我们将在第四章中更深入地讨论,但是评估指标对于语言模型来说是困难的。通常会有多个你关心的指标,而且没有一个像复杂度评级或相似度分数那样简单。虽然实验追踪器供应商尽力保持对评估指标的中立和无偏见,但它们至少应该使比较模型和它们的指标变得容易,以便决定哪个更好。由于 LLM 已经变得如此流行,一些工具已经使评估更常见的指标,如 ROUGE 用于文本摘要,变得容易。

你还会发现许多实验追踪供应商已经开始添加专门用于 LLM 的工具。一些你可能考虑寻找的功能包括直接支持 HuggingFace、LangChain 支持、提示工程工具包、微调框架和基础模型商店。这个领域正在迅速发展,目前没有一个工具拥有完全相同的功能,但我相信这些功能集可能会趋于一致。

3.4.3 模型注册表

模型注册表可能是 MLOps 基础架构中最简单的工具之一。主要目标是一个容易解决的问题,我们只需要一个地方来存储模型。我见过许多成功的团队仅仅通过将他们的模型放在对象存储或共享文件系统中来解决问题。尽管如此,在选择时还有一些要注意的细节。

首先要考虑的是模型注册表是否跟踪模型的元数据。大部分你关心的内容都会在实验跟踪器中,所以通常你只需确保可以链接这两者即可。事实上,大多数模型注册表都是内建到实验跟踪系统中的,因为如此。然而,我一再看到这些系统的一个问题就是当公司决定使用一个开源模型甚至购买一个模型时会出现问题。上传一个模型并标记相关信息容易吗?通常情况下是否定的。

接下来,你会想确保你可以对模型进行版本控制。在某个时候,一个模型会达到它不再有用并需要被替换的时候。版本化你的模型会简化这个过程。它还会使运行生产实验如 A/B 测试或阴影测试更容易。

最后,如果我们正在升级和降级模型,我们需要关注访问权限。对许多公司来说,模型往往是有价值的知识产权,确保只有合适的用户可以访问模型是很重要的。但同样重要的是确保只有了解模型、了解它们做什么以及为什么训练它们的团队负责升级和降级模型。我们最不希望的是在生产环境中删除一个模型,或者更糟的是。

对于 LLM(长期存储器)来说,你应该注意一些重要的注意事项,主要是,在选择模型注册表时,要注意任何限制大小。我看到过几个模型注册表将模型大小限制在 10GB 或更小的情况。那是不可行的。我可以推测出许多原因,但都不值得一提。说到限制大小,如果你要在像 Ceph 这样的本地存储系统上运行你的模型注册表,请确保它有足够的空间。你可以为你的本地服务器购买数 TB 的存储容量,只需花几百美元,但即使是几 TB,在你的 LLM 超过 300GB 时也很快就会用完。别忘了,你可能在训练和微调期间保留多个检查点和版本,以及为了可靠性目的而重复。存储仍然是运行 LLM 中最便宜的方面之一,没有理由在这里吝啬而引发未来的麻烦。

这确实给我带来了一个好问题:仍然有许多优化可以进行,可以实现更好的空间节省方法来存储 LLM 及其衍生物。特别是因为这些模型大多在整体上都非常相似。我想我们可能会在未来看到解决这个问题的存储解决方案。

3.4.4 特征存储

特征存储解决了许多重要问题,并回答了诸如:谁拥有这个特征?它是如何定义的?谁可以访问它?哪些模型在使用它?我如何在生产中提供这个特征?本质上,它们解决了“单一真相来源”的问题。通过创建一个集中式存储,它允许团队购买最高质量、维护最好、管理最彻底的数据。特征存储解决了数据的协作、文档化和版本控制。

如果你曾经想过,“特征存储只是一个数据库对吗?”你可能在考虑错误类型的存储——我们指的是一个购物的地方而不是一个存储的地方。别担心,这种混淆是正常的,因为我经常听到这种看法,而且我自己也有过类似的想法。事实上,现代特征存储比物理数据库更虚拟,这意味着它们建立在你已经使用的任何数据存储之上。例如,Google 的 Vertex AI 特征存储只是 BigQuery,我看到很多数据团队对此感到困惑,“为什么我不直接查询 BigQuery?”将数据加载到特征存储中感觉像是一个不必要的额外步骤,但想想在宜家商店购物。没有人直接去存放所有家具的仓库。那将是令人沮丧的购物体验。特征存储是展厅,允许公司内其他人轻松浏览、体验和使用数据。

往往我看到人们拿出一个特征存储来解决像在线特征服务的低延迟访问这样的技术问题。特征存储的一个巨大优势是解决训练-服务偏差。一些特征事后用 SQL 容易实现,比如计算过去 30 秒的请求平均数。这可能导致为训练构建的天真数据管道,在投产时引发巨大头疼,因为实时获取这种类型的特征绝非易事。特征存储的抽象帮助减轻了这一负担。与此相关的是特征存储的时间点检索,在谈论特征存储时是基本要求。时间点检索确保在特定时间给定查询将始终返回相同的结果。这很重要,因为像“过去 30 秒”的平均值这样的特征不断变化,所以这使我们能够对数据进行版本控制(而不增加庞大的版本控制系统的额外负担),并确保我们的模型将给出准确和可预测的响应。

至于选择,Feast 是一个流行的开源特征存储器。FeatureForm 和 Hopsworks 也是开源的。这三个选项都提供付费的托管选项。对于 LLMs,我听说特征存储器并不像 MLOps 基础设施的其他部分一样重要。毕竟,模型是如此庞大,它应该将所有所需的特征都内置在内部,因此您不需要查询其他上下文,只需将用户的查询提供给模型,让模型自行处理。然而,这种方法仍然有些天真,我们还没有完全达到 LLMs 完全自给自足的程度。为了避免幻觉并提高事实的准确性,通常最好给模型一些上下文。我们通过给模型提供我们希望它非常熟悉的文档的嵌入来实现这一点,而特征存储器是放置这些嵌入的理想位置。

3.4.5 向量数据库

如果您对常规的 MLOps 基础设施熟悉,那么这一节对您来说大多数都是复习的。我们只需要做出一些微调,强调重要的扩展性问题,使系统适用于 LLMs。然而,向量数据库是场景中的新鲜事物,并且已经发展成为与 LLMs 和语言模型一起工作的定制解决方案,但您也可以将它们用于其他数据集,如图像或表格数据,这些数据集很容易转换成向量。向量数据库是专门用来存储向量及其周围一些元数据的数据库,这使它们非常适合存储嵌入。尽管最后一句话是正确的,但有点误导,因为向量数据库的强大之处不在于它们的存储方式,而在于它们搜索数据的方式。

传统数据库采用 B 树索引查找 ID 或使用反向索引进行基于文本的搜索,都存在同样的缺陷,即必须知道你要搜索的内容。如果你没有 ID 或者不知道关键词,就无法找到正确的行或文档。然而,向量数据库利用向量空间的特点,你不需要准确知道你要搜索的内容,你只需要知道类似的内容,然后可以使用基于欧几里得距离、余弦相似度、点积相似度等的相似度搜索来找到最近的邻居。使用向量数据库可以轻松解决反向图像搜索问题,作为一个示例。

在这一点上,我相信一些读者可能会感到困惑。我先告诉你把你的嵌入式放入一个特征存储中,现在又告诉你把它们放入向量数据库中,到底哪一个呢?这正是它的美妙之处,你可以同时两者都做。如果之前没有意义,希望现在它能够有意义了。特征存储不是数据库,它们只是一个抽象,你可以使用一个建在向量数据库之上的特征存储,这将解决许多问题。当你有多个数据源,尝试不同的嵌入模型或经常更新数据时,向量数据库可能很难维护。管理这个复杂性可能是真正令人头疼的,但是特征存储可以轻松地解决这个问题。将它们结合使用将确保更准确和最新的搜索索引。

在写作时,向量数据库仅存在几年,它们的流行还是相对较新的,因为它们和 LLMs 的增长是相互关联的。这很容易理解,因为它们提供了一种快速高效的检索向量数据的方法,使提供 LLMs 所需的上下文更加容易,从而提高其准确性。

尽管如此,这仍是一个相对较新的领域,现在有很多竞争者,谁是赢家谁是输家还为时过早。为了不让这本书过时,至少让我提出两个选项来开始使用 Pinecone 和 Milvus。Pinecone 是最早的向量数据库之一,有着繁荣的社区和丰富的文档。它具有丰富的功能并被证明可以扩展。 Pinecone 是一个全面管理的基础设施产品,有一个免费的初学者阶段。如果你是开源的粉丝,那么你会想要尝试 Milvus。Milvus 功能丰富,拥有庞大的社区,由 Zilliz 公司提供完全托管的服务,但也可以部署到自己的集群中,如果你已经有了一点基础设施经验,这个过程相对简单明了。

现在有很多替代品,值得在选择前进行一些调查。你最关心的两个问题可能是价格和可扩展性,这两者经常相辅相成。之后,值得关注搜索功能,如支持不同相似度度量(如余弦相似度,点积或欧几里得距离)。以及索引功能,如 HNSW(层次式可导航小世界)或 LSH(局部敏感哈希)。能够自定义搜索参数和索引设置对于任何数据库来说都很重要,因为它们允许您自定义数据集和工作流的工作负载,从而优化查询延迟和搜索结果准确性。

随着向量数据库日益流行,需要注意的是,许多数据库老牌厂商如 Redis 和 Elastic 也迅速推出了向量搜索功能。

尽管它们很重要,但它们往往是添加的最后一块拼图。这往往是有意的,因为投入资源来弄清楚如何监视模型,如果你没有任何模型来监视是没有帮助的。然而,不要犯将其延迟太久的错误。许多公司都因为一个没有人知道的走私模型而受到了严重损失,这往往会让他们付出巨大代价。另外,重要的是要意识到,您不必等待将模型投入生产才开始监视数据。有很多方法可以引入监控系统到训练和数据管道中,以提高数据治理和合规性。无论如何,通常可以通过他们的监控系统来判断数据科学组织的成熟程度。

向量数据库是强大的工具,可以帮助您训练或微调 LLM,以及提高 LLM 查询的准确性和结果。

目前,这些大多数产品往往只提供最直接的功能集,但如果您已经在使用这些工具集,它们是很难被忽视的,因为它们可以提供快速启动的快速获胜。

监控系统对于任何 ML 系统的成功至关重要,包括 LLM。与其他软件应用程序不同,ML 模型通常会默默失败 - 继续运行,但开始产生较差的结果。这往往是由于数据漂移引起的,一个常见的例子是推荐系统随着时间推移给出更糟糕的结果,因为卖家开始通过给出假评论来操纵系统以获得更好的推荐结果。监控系统使我们能够捕获性能不佳的模型,进行调整或简单地重新训练它们。

有很多很棒的监控工具,一些很棒的开源选择包括 WhyLogs 和 EvidentlyAI。我也很喜欢 Great Expectations,但发现它在批处理作业之外速度相当慢。还有许多更多的付费选择。通常情况下,对于 ML 监控工作负载,你会想要监控你通常在其他软件应用程序中记录的所有内容,这包括资源指标如内存和 CPU 利用率,性能指标如延迟和每秒查询数,以及操作指标如状态码和错误率。此外,你还需要监控数据漂移进出模型。你会关注缺失值、唯一性和标准偏差变化等问题。在许多情况下,你会希望能够在监控时对数据进行分段,例如进行 A/B 测试或按区域监控。在 ML 系统中监控的一些有用指标包括模型准确率、精确率、召回率和 F1 得分。由于在推断时你不会知道正确答案,因此设置某种审计系统通常是有用的。当然,如果你的 LLM 被设计成一个问答机器人,而不是帮助作家更具创造力的话,审计将会更容易。

这暗示了一个事实,即对于 LLM,监控系统通常会面临一系列全新的挑战,甚至比我们在其他 ML 系统中看到的挑战还要多。对于 LLM,我们正在处理的是文本数据,如本章前面讨论的那样,这是很难量化的。例如,想想你会监控什么特征来检测数据漂移?因为语言变化很快!我可能会建议的一个特征是唯一标记。当新的俚语词汇或术语被创造时,这会提醒你,但是当词语的含义发生变化时,比如“wicked”表示“酷炫”时,它仍然没有帮助。我还建议监控嵌入,但是你可能会发现这要么会增加很多噪音和误报,要么至少会难以辨认和挖掘当问题发生时。我见过的效果最好的系统通常涉及大量手工制定的规则和特征来进行监控,但是这些可能会出错,并且需要花费大量时间来创建。

监控基于文本的系统远非一个解决了的问题,主要是因为理解文本数据本身的困难。这确实引出了一个问题,即如何使用语言模型来监控自己,因为它们是我们当前对语言进行编码的最佳解决方案。不幸的是,我不知道有人正在研究这个问题,但我想这只是个时间问题。

GPU 启用的工作站

GPU 启用的工作站和远程工作站通常被许多团队认为是一种奢侈品,但是当使用 LLMs 时,这种思维方式必须改变。当解决问题或者一般地开发模型时,数据科学家不能再在笔记本电脑上的笔记本中快速启动模型了。解决这个问题最简单的方法就是提供带有 GPU 资源的远程工作站。有很多云解决方案可以做到这一点,但是如果你的公司主要在本地工作,那么提供这一点可能会更加困难,但是却是必要的。

LLMs 是 GPU 内存密集型模型。因此,当涉及到在领域中工作时,每位工程师都应该知道一些数字。首先,是不同 GPU 的内存容量。NVIDIA Tesla T4 和 V100 是数据中心中最常见的两种 GPU,但它们只有 16 GB 的内存。它们是效率很高且性价比很高的工作马,所以如果我们能压缩我们的模型以在这些 GPU 上运行,那就更好了。在这些之后,你会发现一系列 GPU,如 NVIDIA A10G、NVIDIA Quadro 系列和 NVIDIA RTX 系列,它们提供的 GPU 内存范围在 24、32 和 48 GB。所有这些都是不错的升级,你只需弄清楚你的云服务提供商提供的哪些产品对你可用即可。这就引出了 NVIDIA A100,这很可能会成为你在使用 LLMs 时的首选 GPU。值得庆幸的是,它们相对比较常见,并提供两种不同的型号,分别提供 40 或 80 GB。你将会面临的一个大问题是,它们现在每个人都在高度需求中。你还应该注意到 NVIDIA H100,它提供的内存与 A100 相同为 80 GB。H100 NVL 承诺支持高达 188 GB,并且专为 LLMs 设计。另一个你应该知道的新 GPU 是 NVIDIA L4 张量核 GPU,它提供 24 GB 内存,并且定位为与 T4 和 V100 一样成为新的工作马,至少在 AI 工作负载方面是这样。

LLMs 有各种不同的大小,了解这些数字的含义是很有用的。例如,LLaMA 模型有 7B、13B、33B 和 65B 参数变体。如果你不确定你需要用哪种 GPU 来运行哪种模型,那么这里有一个捷径,只需将参数的十亿数量乘以二,就是你需要的 GPU 内存大小。原因是,大多数模型在推断时都会默认运行在半精度,FP16 或 BF16,这意味着对于每个参数,我们至少需要两个字节。因此,7 十亿 * 2 字节 = 14 GB。你还需要额外的内存用于嵌入模型,大约还需要另外 1 GB,以及用于实际运行模型的标记。一个标记大约是 1 MB,所以 512 个标记将需要 512 MB。这并不是一个大问题,直到你考虑到为了提高性能而运行更大的批次大小。对于这种大小的 16 批次,你将需要额外的 8 GB 空间。

当然,到目前为止,我们只谈论了推理,而对于训练,你将需要更多的空间。在训练时,你始终希望以完整精度进行,你还需要额外的空间来存储优化器张量和梯度。通常情况下,为了解决这个问题,你需要为每个参数约分配 16 字节。因此,要训练一个有 7B 参数的模型,你将需要 112 GB 的内存。

部署服务

我们一直努力达成的目标最终在这里被集合并得到了很好的应用。事实上,如果你拿走了所有其他服务,只剩下一个部署服务,你仍然拥有一个工作的 MLOps 系统。部署服务提供了一种与我们之前谈到的所有系统集成以及配置和定义所需资源的简单方法,以使我们的模型在生产环境中运行起来。它通常会提供样板代码,以便在 REST 和 gRPC API 或直接在批处理或流式传输管道中为模型提供服务。

一些帮助创建此服务的工具包括 NVIDIA Triton 推理服务、MLServer、Seldon 和 BentoML。这些服务提供了一个标准的 API 接口,通常是 KServe V2 推理协议。该协议提供了一种统一和可扩展的方式,在不同的平台和框架上部署、管理和提供机器学习模型。它定义了一种与模型交互的通用接口,包括 gRPC 和 HTTP/RESTful API。它标准化了诸如输入/输出张量数据编码、预测和解释方法、模型健康检查和元数据检索等概念。它还允许与 TensorFlow、PyTorch、ONNX、Scikit Learn 和 XGBoost 等语言和框架无缝集成。

当然,有时灵活性和定制化足够提供价值,使人们远离这些其他框架提供的自动化路径,这种情况下最好使用 FastAPI 等工具。你的部署服务仍然应该在这里提供尽可能多的自动化和样板代码,以使整个过程尽可能顺利。应该提到的是,上述大多数框架确实提供了定制方法,但效果可能有所不同。

部署模型不仅仅是构建接口。你的部署服务还将提供一个桥梁,来弥合 MLOps 基础设施和通用 DevOps 基础设施之间的差距。连接到公司设置的任何 CI/CD 工具以及构建和部署流水线,以便你可以确保适当的测试和部署策略,如健康检查和回滚可以轻松地进行监视和执行。这通常是非常平台特定的,因此也需要提供所需的配置来与 Kubernetes 或者你可能使用的其他容器编排器通信,以获取所需的资源,如 CPU、内存和加速器、自动伸缩器、代理等。它还应用了所需的环境变量和秘密管理工具来确保一切正常运行。

总的来说,该服务确保您能轻松将模型部署到生产环境中。对于 LLMs 来说,主要关注的往往只是确保平台和集群已经设置了足够的资源,以便最终配置。

在本章中我们已经讨论了很多内容,从 LLMs 比传统的机器学习困难得多,而传统机器学习本身已经很难的原因开始。首先,我们了解到它们的规模不能被低估,但我们也发现了它们有很多特殊之处,从令牌限制到幻觉,更不用说它们是昂贵的了。幸运的是,尽管困难,但它们并不是不可能的。我们讨论了压缩技术和分布式计算,这些都是至关重要的。然后我们探讨了使 LLMs 工作所需的基础设施。虽然大多数内容可能是熟悉的,但我们意识到 LLMs 对每个工具施加了不同程度的压力,并且通常我们需要为部署其他机器学习模型而能够应对的规模做好准备。

3.5 总结

  • 大型语言模型很难处理,主要是因为它们很大。这导致下载、加载到内存和部署所需的时间更长,迫使我们使用昂贵的资源。

  • 处理 LLMs 也很困难,因为它们涉及到自然语言及其所有复杂性,包括幻觉、偏见、伦理和安全性。

  • 不管是自建还是购买,LLMs 都是昂贵的,管理与其相关的成本和风险对于任何利用它们的项目的成功至关重要。

  • 将模型压缩到尽可能小将使它们更容易处理;量化、修剪和知识蒸馏对此特别有用。

  • 量化很受欢迎,因为它很容易,在训练后可以进行,而无需进行任何微调。

  • 低秩近似是一种有效的模型压缩方式,由于 LoRA 的存在,它在适应性方面被广泛使用。

  • 我们用于并行化 LLM 工作流的三个核心方向是:数据、张量和管道。DP 帮助我们增加吞吐量,TP 帮助我们提高速度,而 PP 使一切都能够首先运行起来。

  • 将并行方法组合在一起,我们得到了三维并行(数据+张量+管道),在这里我们发现这些技术相辅相成,弥补了彼此的弱点,并帮助我们更多地利用资源。

  • LLMOps 的基础设施与 MLOps 类似,但不要被这个表面所迷惑,因为有许多情况下,“足够好”已经不再适用了。

  • 许多工具正在为 LLM 支持专门提供新功能。

  • 特别有趣的是向量数据库,作为 LLMs 所需的新基础设施拼图的一部分,它们可以快速搜索和检索嵌入。

[1] A. Bulatov, Y. Kuratov, and M. S. Burtsev,“通过 RMT 将 Transformer 扩展到 100 万个令牌及以上,”2023 年 4 月,arxiv.org/abs/2304.11062

[2] R. Daws, “使用 OpenAI 的 GPT-3 的医疗聊天机器人告诉虚假患者自杀,” AI News, 2020 年 10 月 28 日, www.artificialintelligence-news.com/2020/10/28/medical-chatbot-openai-gpt3-patient-kill-themselves/

[3] T. Kington, “ChatGPT 机器人被骗提供制作炸弹的说明,开发者表示,” www.thetimes.co.uk, 2022 年 12 月 17 日, www.thetimes.co.uk/article/chatgpt-bot-tricked-into-giving-bomb-making-instructions-say-developers-rvktrxqb5

[4] K. Quach, “AI 游戏因为自动生成的不雅故事而禁止玩家,” www.theregister.com, 2021 年 10 月 8 日, www.theregister.com/2021/10/08/ai_game_abuse/

[5] T. Hoefler, D. Alistarh, T. Ben-Nun, N. Dryden, and A. Peste, “深度学习中的稀疏性:神经网络的高效推理和训练的剪枝与增长方法,” 2021 年 1 月, arxiv.org/abs/2102.00554.

[6] E. Frantar 和 D. Alistarh, “SparseGPT:大型语言模型可以一次准确剪枝,” 2023 年 1 月, arxiv.org/abs/2301.00774.

[7] V. Sanh, L. Debut, J. Chaumond, 和 T. Wolf, “DistilBERT,BERT 的精简版本:更小、更快、更便宜、更轻便,” 2019 年 10 月, arxiv.org/abs/1910.01108.

[8] R. Taori, I. Gulrajani, T. Zhang, Y. Dubois, X. Li, C. Guestrin, P Liang, 和 T. B. Hashimoto, “Alpaca:一个强大、可复制的指令遵循模型,” crfm.stanford.edu, 2023 年 3 月 13 日, crfm.stanford.edu/2023/03/13/alpaca.html

[9] E. J. Hu 等人, “LoRA:大型语言模型的低秩适应,” 2021 年 6 月, arxiv.org/abs/2106.09685.

[10] 对于额外好奇的人,参数高效调整(PEFT)是一类旨在以计算高效的方式微调模型的方法。PEFT 库旨在将它们放在一个易于访问的地方,你可以从这里开始:huggingface.co/docs/peft

[11] R. Henry 和 Y. J. Kim, “通过低位量化加速大型语言模型,” 2023 年 3 月, www.nvidia.com/en-us/on-demand/session/gtcspring23-s51226/

[12] DeepSpeed 是一个优化许多大规模深度学习模型中的难点的库,比如 LLMs,在训练时特别有用。查看他们的 MoE 教程。www.deepspeed.ai/tutorials/mixture-of-experts/

[13] 在这里了解更多有关 Ray 集群的信息:docs.ray.io/en/releases-2.3.0/cluster/key-concepts.html#ray-cluster

[14] V. Korthikanti 等人, “减少大型变换模型中的激活重新计算,” 2022 年 5 月, arxiv.org/abs/2205.05198

[15] A. Harlap 等人,“PipeDream:快速高效的管道并行 DNN 训练”,2018 年 6 月 8 日。arxiv.org/abs/1806.03377

posted @ 2024-05-02 22:33  绝不原创的飞龙  阅读(4)  评论(0编辑  收藏  举报