大语言模型生产环境指南(一)

原文:zh.annas-archive.org/md5/94f58343f3d2d249c1a031ec6302a835

译者:飞龙

协议:CC BY-NC-SA 4.0

第一章:词汇的觉醒:为什么大型语言模型引起了关注

本章涵盖

  • 大型语言模型是什么,它们能做什么,不能做什么

  • 你应该在何时何时不部署自己的大型语言模型

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

任何足够先进的技术都与魔法无法区分 —— 亚瑟·C·克拉克

年份是 1450 年。德国美因茨的一个沉睡的角落,不知不觉地站在一个重大时代的边缘。在 Humbrechthof,一个被城镇阴影笼罩的普通车间,充满了期待。正是在这里,约翰内斯·古腾堡,一位金匠和革新者,在油、金属和决心的气味中汗流浃背地劳作,默默地孕育着一场革命。在深夜时分,金属敲击金属的节奏性敲打不时打破宁静。在车间灯光照耀的中心,是古腾堡十年来的爱情结晶——一个设计独特、用途非凡的装置。

这不是一项普通发明。工艺和创造力将各种可移动金属字型、一个个辛苦铸成的字符,组合成一个矩阵。闪烁的灯光在金属标志上舞动。空气中充满了突破的期待和基于油墨的浓郁甜味,这是古腾堡本人的创新。在那一刻的宁静中,这位大师印刷师挺直了肩膀,以无与伦比的技巧,在墨水充盈的矩阵下铺下一张清晰的羊皮纸,让他的发明紧紧压印,在页面上留下精细的印刷。房间调整到了沉默的交响乐,紧张的呼吸在空气中沉重地悬挂。当印刷机被抬起时,它因自身的重量而嘎吱作响,每一次尖叫都像是一声战斗的呐喊,宣布着一个激动人心的新世界的到来。

古腾堡以一阵动作,从印刷机中取出第一张印刷页,并将其平铺在木桌上。他仔细检查每一个字符,它们都像创造者的愿景一样大胆而宏伟。房间中的每个人都沉浸在这景象中,完全被迷住了。一张普通的羊皮纸已成为变革的见证。当夜晚让位于白天时,他带着振奋的骄傲看着他的车间。他的遗产诞生了,在历史的长河中回响,永远改变了信息飞翔的方式。约翰内斯·古腾堡,现在成为千禧年的人物,从阴影中走出,一个敢于梦想的发明家。他的名字与印刷机同义,这不仅是一项开创性的发明,而且是现代世界的催化剂。

当古腾堡的成就的消息开始在大陆上飘扬时,来自各个学科的学者们还没有意识到他们手中的这个非凡工具。知识和学习,曾经是渴望的宝藏,现在对普通人来说触手可及。围绕这一新发现,意见各异,看法不一。

在我们这个时代,多亏了莱茵河畔那些人的才能和勤奋,书籍的数量激增。曾经只有富人——不,只有国王才能拥有的书,现在可以在简陋的屋顶下看到。……如今,我们的孩子……似乎无所不知。——塞巴斯蒂安·布兰特

学术努力在各个地方都在下降,前所未有。的确,聪明才智在国内和国外都被摒弃。阅读能为学生带来什么,除了泪水?当它被出售时,是罕见的、无价值的,而且缺乏智慧。——列日的大卫

人们对于书籍的看法在历史上一直各不相同。在我们这样一个虚拟印刷机存在且书籍无处不在的时代,我们可以达成共识的是,印刷机改变了历史。虽然我们并没有亲眼目睹古腾堡使用他的印刷机打印出第一页,但我们见证了很多人第一次与大型语言模型(LLMs)互动。当他们看到它对他们的第一个提示做出反应时,脸上的惊讶。当他们用难题挑战它,却看到它像该领域的专家一样回应时,他们的兴奋——当他们意识到他们可以用它来简化自己的生活或让自己变得富有时,这就是灵光一闪的时刻。我们想象这种情绪的波涛只是约翰内斯·古腾堡所感受到的一小部分。能够快速生成文本和加速沟通始终是有价值的。

1.1 大型语言模型加速沟通

每个工作都有一定程度的沟通。通常,这种沟通是肤浅的、官僚的或政治性的。我们经常警告学生和门徒,每个工作都有自己的文书工作。曾经是激情的事物,很容易被随之而来的日常乏味和琐事所扼杀,当它变成工作的时候。事实上,当人们谈论他们的职业时,他们经常夸大其词,试图提高自己的社会地位,所以你很少能听到全部真相。你不会听到那些无聊的部分,而日常的艰辛则被方便地遗忘了。

然而,设想一个我们可以减少单调工作负担的世界。一个警察不再需要浪费每天数小时填写报告,而是可以将时间用于社区外展项目的地方。或者一个教师不再需要深夜批改作业和准备教案,而是能够思考并为个别学生准备定制课程的世界。甚至是一个律师不再被困在法律文件中数日,而是可以自由地接受那些激励他们的慈善案件的世界。当沟通负担、文书工作负担和会计负担被移除时,工作就更加接近我们所说的样子。

对于这一点,LLM 是自印刷术以来最有希望的技术。首先,它们已经完全颠覆了人类与计算机之间的角色和关系,改变了我们相信它们能够做到的事情。它们已经通过了医学考试、律师资格考试和多个心智理论测试。它们通过了谷歌和亚马逊的编码面试。他们在 SAT 考试中获得了至少 1410 分(满分 1600 分)。对作者来说,最令人印象深刻的成绩之一是 GPT-4 甚至通过了高级品酒师考试,这让我们想知道 LLM 是如何通过实际品酒部分的。确实,它们的空前成就正以惊人的速度到来,常常让我们这些凡人感到有些恶心和不安。面对似乎能做任何事的技术,你该怎么办?

注意:Med-PaLM 2 在 MedQA 考试中获得了 86.5% 的成绩。您可以在 OpenAI 的 GPT-4 论文中查看通过考试列表,链接为 cdn.openai.com/papers/gpt-4.pdf。最后,谷歌将 ChatGPT 作为测试进行了面试,并且它通过了 (mng.bz/x2y6)。

通过考试很有趣,但并不一定有用,除非我们的目标是建造史上最昂贵的作弊机器,并且我们承诺有更好的方法来利用我们的时间。LLM(大型语言模型)擅长的是语言,尤其是帮助我们改进和自动化沟通。这使得我们可以将常见的痛苦经历转化为简单、愉快的体验。首先,想象一下走进你的家,那里有你自己的个人 JARVIS,就像穿上钢铁侠的鞋子一样,一个由人工智能驱动的助手,为你的日常生活增添了无与伦比的活力。虽然 LLM 的水平并不完全达到漫威电影中 JARVIS 所描绘的人工通用智能(AGI)水平,但它们正在推动新的用户体验,从改善客户服务到帮助你为心爱的人挑选生日礼物。它们知道询问你关于这个人的信息,了解他们的兴趣和个性,了解你的预算,然后提供专业推荐。虽然许多这些助手正在被用于良好的工作,但许多其他只是用户可以与之交谈并自娱自乐的聊天机器人——这很重要,因为即使是我们的想象中的朋友现在也太忙了。玩笑归玩笑,这些可以创造惊人的体验,让你能够遇见你最喜欢的虚构角色,如哈利·波特、福尔摩斯、安纳金·天行者,甚至是钢铁侠。

然而,我们确信许多读者感兴趣的却是编程助手,因为我们都知道搜索一切实际上是一种最糟糕的用户体验。能够用简单的英语写几个目标,然后看到合作编写代码是令人兴奋的。我们亲自使用这些工具来帮助我们记住语法,简化并清理代码,编写测试,以及学习新的编程语言。

电子游戏是另一个我们期待 LLMs(大型语言模型)能带来大量创新的有趣领域。它们不仅帮助程序员创建游戏,还允许设计师创造更加沉浸式的体验。例如,与 NPC(非玩家角色)的对话将更加深入和引人入胜。想象一下像动物之森和星露谷物语这样的游戏,拥有近乎无限的任务和对话。

考虑其他行业,比如教育,似乎永远都不够教师,这意味着我们的孩子得不到他们需要的个别关注。一个 LLM 助手可以帮助教师节省做手工杂事的时间,并为有困难的孩子担任私人导师。企业界正在研究 LLMs 用于与数据交流的工作——比如帮助员工理解季度报告和数据表格——本质上为每个人提供自己的个人分析师。销售和营销部门肯定会利用这一卓越的创新,无论好坏。搜索引擎优化(SEO)的状态也将发生很大变化,因为目前,它主要是一场生成内容以希望使网站更受欢迎的游戏,而这现在变得超级简单。

上述列表只是公司对使用 LLMs 感兴趣的一些常见例子。人们也出于个人原因使用它们,比如写音乐、诗歌,甚至书籍;翻译语言;总结法律文件或电子邮件;甚至提供免费治疗——是的,这是一个糟糕的想法,因为 LLMs 在这方面仍然很糟糕。这只是个人偏好,但我们不会在精神健康受到威胁时试图节省一分钱。当然,这导致了一个事实,即人们已经在使用 LLMs 进行更阴暗的目的,比如作弊、诈骗和虚假新闻来扭曲选举。此时,这个列表已经相当长且多样化,但我们只是刚刚触及了可能性的表面。实际上,由于 LLMs 帮助我们进行沟通,通常最好是思考“它们不能做什么?”而不是“它们能做什么?”或者更好,“它们不应该做什么?”

嗯,作为一种技术,它有一些限制和约束。例如,LLMs 有点慢。当然,“慢”是一个相对术语,但响应时间通常以秒计,而不是毫秒。我们将在第三章中更深入地探讨这个话题,但作为一个例子,我们可能不会很快看到它们被用于自动补全任务,这些任务需要极快的推理才能有用。毕竟,自动补全需要能够比人更快地预测单词或短语。同样,LLMs 是庞大而复杂的系统;我们不需要它们来解决这样简单的问题。用 LLM 解决自动补全问题不仅仅是用大锤钉钉子;它是用完整的破坏球砸它。而且,就像租用破坏球比买锤子更贵一样,LLM 的运营成本也会更高。有许多类似的任务,我们应该考虑我们试图解决的问题的复杂性。

此外,还有许多复杂的问题,LLMs 通常解决得不好,例如预测未来。不,我们不是指神秘的艺术,而是指预测问题——比如预测天气或高潮何时会击中海岸。这些实际上是已经解决的问题,但我们不一定有很好的方法来传达它们是如何解决的。它们是通过数学解决方案的组合来表达的,比如傅里叶变换和调和分析,或者黑盒机器学习模型。许多问题都符合这一类别,比如异常值预测、微积分或找到卷纸的末端。

你可能还希望避免在高度风险的项目中使用它们。大型语言模型(LLMs)并非完美无缺,经常会犯错误。为了增加创造力,我们通常允许 LLMs 中存在一定程度的随机性,这意味着你可以向 LLMs 提出相同的问题并得到不同的答案。这是有风险的。你可以通过降低温度来移除这种随机性,但这可能会根据你的需求使 LLMs 变得无用。例如,你可能会决定使用 LLM 来将投资选项分类为好或坏,但你希望它根据其输出做出实际的投资决策吗?除非你的目标是制作一个搞笑视频,否则不应该是这样的。

最终,LLM 只是一个模型。它不能对损失你的金钱负责,实际上,损失金钱的是你自己,因为你选择了使用它。类似的风险问题包括填写税务表格或获取医疗建议。虽然 LLM 可以完成这些任务,但它不会像聘请有资格的注册会计师那样在 IRS 审计中保护你免受重罚。如果你从 LLM 那里得到不良的医疗建议,你无法起诉任何医生。然而,在所有这些例子中,LLM 都有可能帮助从业者更好地履行他们的工作职责,无论是通过减少错误还是提高速度。

何时使用 LLM

使用它们进行

  • 生成内容

  • 问答服务

  • 聊天机器人和 AI 助手

  • 文本到其他事物的问题(扩散、txt2img、txt23d、txt2vid 等)

  • 与你的数据交谈的应用

  • 任何涉及沟通的事情

避免使用它们的情况

  • 对延迟敏感的工作负载

  • 简单项目

  • 我们不通过文字而是通过数学或算法解决的问题——预测、异常值预测、微积分等

  • 临界评估

  • 高风险项目

语言不仅仅是人们用来沟通的媒介。它是使人类成为顶级捕食者的工具,并为每个个体在其社区中提供了自我定义。人类存在的各个方面,从与父母争吵到大学毕业再到阅读这本书,都充满了我们的语言。语言模型正在学习利用人类本质的基本方面,并且当负责任地使用时,它们有能力帮助我们完成每一个任务。如果我们负责任地教导它们,它们有潜力解锁我们自己和他人理解的维度。

大型语言模型(LLMs)自从它们潜在的想象力无限以来,就吸引了全世界的关注。LLMs 承诺了如此之多,但这些解决方案在哪里?那些能给我们带来沉浸式体验的视频游戏在哪里?为什么我们的孩子还没有个人 AI 导师?为什么我还没有拥有自己的个人助理成为钢铁侠呢?这些问题是促使我们写这本书的深刻和深刻的问题。尤其是最后一个问题让我们夜不能寐。所以虽然 LLMs 能做惊人的事情,但知道如何将它们转化为实际产品的人还不够多,这正是我们在这本书中想要分享的。

这不仅仅是一本机器学习操作的书。让 LLM 在生产环境中工作涉及许多陷阱和陷阱,因为 LLMs 不像传统的软件解决方案那样工作。将 LLM 转化为可以与您的用户进行连贯交互的产品需要整个团队和多样化的技能。根据您的用例,您可能需要训练或微调自己的模型,或者您可能需要通过 API 从供应商那里获取模型。

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

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/1-unnumb.png

感谢 SuperElmer,www.facebook.com/SuperElmerDS

我们将涉及你需要的一切,从收集和创建数据集,到在消费或工业硬件上训练或微调 LLM,以及以各种方式部署该模型,以便客户与之交互。虽然我们不会过多涉及理论,但我们将通过实际案例从始至终地介绍整个过程。在这本书的结尾,你将知道如何部署 LLMs,并有一些可行的经验作为支持。

1.2 使用 LLMs 进行构建和购买决策的导航

如果你购买了这本书,你很可能已经确信 LLMs(大型语言模型)在你的生活和组织中具有巨大的潜力。因此,购买这本书是把你梦想变成现实的第一步,因为除非我们知道如何将这些模型投入生产,否则这一切都不可能实现。毕竟,如果你和任何企业家或投资者交谈,他们都会告诉你好主意到处都是;重要的是执行,以实现这些想法。我们需要做的是将这些模型投入生产,让它们能够为你实际工作。

没有办法回避这一点,也不需要美化它:将 LLMs 投入生产是困难的。通常,任何值得追求的事情都是如此。在这本书中,我们旨在教你所有你需要知道的知识,并给你一些实际的操作经验。但是,由于它如此困难,所以走捷径是非常诱人的。像 OpenAI 和 Google 这样的大型企业提供了一些非常好的模型选择。为什么不直接购买呢?让我们先考虑他们提供的内容以及何时是好的选择。然后我们将看看硬币的另一面,这些提供往往不尽如人意。

1.2.1 购买:一条老路

有很多很好的理由只是购买 LLM 的访问权限。首先也是最重要的是,访问 API 提供的速度和灵活性。与 API 合作是一种极其简单且成本效益高的方式,可以快速构建原型并快速上手。事实上,它如此简单,以至于只需要几行 Python 代码就可以开始连接到 OpenAI 的 API 并使用 LLMs,如列表 1.1 所示。当然,有很多可能的事情,但只投资 LLMs 却发现它们在你的特定领域失败,这绝对是一个糟糕的主意。与 API 合作让你能够快速失败。构建一个原型应用程序来证明概念,并使用 API 启动它,是一个很好的开始。

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

client = OpenAI(                       #1
    api_key=os.getenv("OPENAI_API_KEY")         #2
)

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

#1 从环境变量中加载你的 API 密钥

#2 这在技术上不是必需的,因为我们正在传递默认密钥。

通常,购买访问模型可以给你带来竞争优势。在许多情况下,市场上最好的模型可能是由专注于你所在领域的公司构建的,该公司投入了大量资金来精心挑选特定的数据集。虽然你可以尝试竞争并构建自己的模型,但购买访问模型可能更适合你的目的。最终,谁拥有更好的特定领域数据来微调,谁就更有可能获胜,而这可能不是你,如果这是你公司的一个副项目。毕竟,整理数据可能很昂贵。购买它可能会节省你大量的工作。

这引出了下一个要点:购买是一种快速获取专业知识和支持的方法。例如,OpenAI 已经投入了大量时间,通过充分的过滤和控制来确保他们的 LLMs 安全,以防止其被滥用。他们已经遇到了并解决了许多边缘情况,因此你不必担心。购买访问他们的模型也让你能够访问他们围绕它构建的系统。

不仅如此,当将 LLM 部署到生产环境中时,LLM 本身只是问题的一半。你还需要在上面构建一个完整的应用程序。有时,由于用户体验(UX)和一些技巧,如让标记看起来像是在被输入,购买 OpenAI 的模型在竞争中脱颖而出,这并非微不足道。我们将向您展示如何开始解决您用例中的 UX 问题,以及一些您可以用来在这个领域取得重大领先的原型设计方法。

1.2.2 构建:少有人走的路

使用 API 很简单,在大多数情况下,这可能是最佳选择。然而,有许多原因让你应该努力拥有这项技术并学习如何自己部署它。虽然这条道路可能更难,但我们将教你如何做到这一点。让我们深入探讨几个原因,从最明显的开始:控制。

控制

首批真正将 LLM 作为核心技术采用的公司之一是一家名为 Latitude 的小型视频游戏公司。Latitude 专注于类似龙与地下城的角色扮演游戏,这些游戏利用 LLM 聊天机器人,他们在与这些机器人合作时遇到了挑战。这并不意味着在批评这家公司犯下的错误,因为他们为我们的集体学习经验做出了贡献,并且是开辟新道路的先驱。尽管如此,他们的故事是引人入胜且引人入胜的——就像一场火车事故,我们忍不住要继续观看。

Latitude 的首个发布是一款名为 AI Dungeon 的游戏。在最初,它利用 OpenAI 的 GPT-2 创建了一个互动和动态的叙事体验。它迅速聚集了大量玩家,他们当然开始不恰当地使用它。当 OpenAI 给 Latitude 提供了 GPT-3 的访问权限时,它承诺将提升游戏体验;然而,它得到的却是一场噩梦。¹

你看,随着 GPT-3,OpenAI 增加了来自人类反馈的强化学习(RLHF),这极大地帮助提高了功能,但这也意味着 OpenAI 的承包商现在正在查看提示。这就是人类反馈的部分。而且这些工人并不太愿意阅读游戏产生的污秽内容。OpenAI 的代表迅速向 Latitude 提出了最后通牒。要么它需要开始审查玩家,要么 OpenAI 将移除 Latitude 对模型的访问权限——这几乎会杀死游戏和公司。在别无选择的情况下,Latitude 迅速添加了一些过滤器,但过滤系统只是一个临时补救措施,一个充满错误和漏洞的混乱。玩家对系统如此糟糕感到不满,并感到不安,意识到 Latitude 的开发人员正在阅读他们的故事,完全无视 OpenAI 已经在这样做的事实。这是一个公关噩梦。而且还没有结束。

OpenAI 认为游戏工作室做得不够;Latitude 被迫加强安全措施,开始禁止玩家。这里有个转折:这么多故事变成低俗内容的原因是因为模型偏好色情。它经常会意外地将无害的故事情节转变为不恰当的冒犯性情境,导致玩家被踢出游戏并被禁止进入。OpenAI 充当纯洁的典范,但问题是他们的模型,这导致了游戏历史上最讽刺和不公正的问题之一:玩家因为游戏本身被禁止。

所以,他们就在那里——一家年轻的游戏工作室,只是想制作一款有趣的游戏,却陷入了愤怒的客户和将所有责任推给他们的科技巨头之间。如果公司对技术有更多的控制权,它本可以寻求真正的解决方案,比如修复模型,而不是像给猪涂脂抹粉一样。

在这个例子中,控制可能表现为你调整模型的能力,而 OpenAI 现在提供了微调功能,但使用服务而不是自己解决问题仍然会失去许多细粒度的决策。例如,使用的训练方法是什么,模型部署在哪些地区,或者它在什么基础设施上运行。对于任何客户或内部工具,控制也同样重要。你不希望代码生成器意外地输出受版权保护的代码,或者为你的公司创造法律问题。你也不希望你的面向客户的 LLM 输出关于你的公司或其流程的事实性错误信息。

控制是你以符合你的目标、目标和价值观的方式指导和管理的操作、流程和资源的能力。如果一个模型最终成为你产品提供的核心,而供应商意外地提高了价格,你几乎无能为力,只能支付。如果供应商决定其模型应该给出更自由或保守的答案,这些答案不再符合你的价值观,你也会陷入困境。

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

竞争优势

部署自己的模型最有价值的方面之一是它为你带来的竞争优势。定制化允许你训练模型使其在某一件事上做到最好。例如,在 2017 年发布了双向编码器表示(BERT)之后,这是一个你可以用来训练自己模型的转换器模型架构,随后研究人员和企业纷纷在自己的数据上测试这项新技术,取得了全球范围内的成功。在撰写本文时,如果你在 Hugging Face Hub 上搜索“BERT”,会返回超过 13.7K 个模型,所有这些模型都是人们为了自己的目的单独训练的,旨在为他们的任务创建最佳模型。

在这个领域,一位作者的个人经历是在聚合了当时最大的单语斯洛伐克语言数据集之后训练 SlovenBERTcina,这是通过允许从斯洛伐克国家语料库中抓取以及一些其他资源,如 OSCAR 项目和 Europarl 语料库实现的。它从未设定任何计算记录,也从未出现在任何模型评论中或为作者所在的公司生成合作伙伴关系。然而,它在训练的任务上优于市场上的所有其他模型。

很可能,你和你所在的公司不需要通用人工智能(AGI)来从数据中生成相关见解。实际上,如果你发明了一个真正的自我意识 AGI,并计划只将其用于每周一次的数字计算、数据分析以及为 PowerPoint 幻灯片生成视觉效果,这绝对会是 AGI 消灭人类的充分理由。更有可能的是,你需要的是作者在创建 SlovenBERTcina 时所做的那样,这是一个大型语言模型,在市场上比任何其他模型都能更好地完成两到三个任务,并且不会与微软或其他潜在竞争对手共享你的数据。虽然出于安全或法律原因,一些数据需要保密,但大量数据应该受到保护,因为它包含了商业机密。

对于通用智能和特定任务的基础知识,都有数百个开源 LLM。我们将在第四章中介绍一些我们最喜欢的。选择这些开源替代方案之一,并在你的数据上对其进行训练,以创建在该任务上世界上最优秀的模型,这将确保你在市场上拥有竞争优势。它还将允许你以你的方式部署模型,并将其集成到你的系统中,以产生最大的效果。

集成到任何地方

假设你想将一个 LLM(大型语言模型)部署为一个选择你自己的冒险风格的游戏的一部分,该游戏使用设备的 GPS 位置来确定故事情节。你知道你的用户经常会去山区、海上以及其他他们可能遇到服务不佳和缺乏互联网接入的地方进行冒险。直接调用 API 根本行不通。现在,请别误会我们:在这个场景中将 LLM 部署到边缘设备上仍然是一个探索性的主题,但这是可能的;我们将在第十章中向你展示如何做到这一点。依赖于 API 服务对于沉浸式体验来说根本不行。

同样,使用第三方 LLM 并调用 API 会带来集成和延迟问题,需要你通过网络发送数据并等待响应。API 很棒,但它们总是很慢,而且并不总是可靠的。当延迟对项目很重要时,拥有内部的服务会更好。上一节关于竞争优势的内容讨论了两个以边缘计算为优先的项目;然而,还有很多这样的项目。LLAMA.cpp 和 ALPACA.cpp 是这类项目的首批之一,这个领域正在比其他任何领域都更快地创新。将量化到 4 位、低秩适应和参数高效微调都是最近为满足这些需求而创建的方法,我们将在第三章中逐一介绍。

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

成本

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

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

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

所有这些在考虑构建与购买大型语言模型(LLMs)时都适用。购买可能看起来立即成本较低,因为市场上广泛使用的最昂贵服务目前每月仅需 20 美元。与 AWS 上的 EC2 实例相比,仅运行相同的模型进行推理(甚至不是训练)一年可能产生的账单高达约 25 万美元。然而,构建在这里已经实现了最快的创新。如果你只需要一个 LLM 来证明概念,竞争优势部分提到的任何项目都允许你仅以演示计算机的电力成本来创建一个演示。它们可以轻松地描述训练过程,从而允许在自有数据上训练模型时显著降低成本,低至 20 亿参数的模型仅需 100 美元(是的,这是真实数字)。另一个好处是,如果你自己构建,你的成本永远不会像支付服务那样大幅上升。

安全和隐私

考虑以下案例。你是一名负责维护你军火库中核弹头的军事人员。所有文档都保存在一本厚重的手册中。需要大量信息来概述所有安全要求和维护协议,以至于即使他们尽了最大努力,新兵也可能会忘记重要信息。他们经常在移除保险丝之前就剪断了电线(youtu.be/UcaWQZlPXgQ)。你决定微调一个 LLM 模型作为个人助理,提供指示并帮助浓缩所有这些信息,以便在士兵需要时提供他们所需的信息。上传这些手册到另一家公司可能不是个好主意——这是本世纪的夸张之词——所以你将想要在本地训练一些保持安全和私密的东西。

这种场景可能听起来有些牵强,但当与一位在警察局从事数据分析工作的专家交谈时,他们表达了与此完全相同的担忧。与他们交谈时,他们表示 ChatGPT 有多么酷,甚至让整个团队参加了一个提示工程课程,以便更好地利用它,但遗憾的是,他们的团队无法在不暴露敏感数据和对话的情况下使用它来处理他们最宝贵的工作——那种实际上可以拯救生命的工作。任何处于类似境地的人都应该渴望学习如何安全、可靠地部署模型。

你不必在军队或警察部队工作才能处理敏感数据。每个公司都有重要的知识产权和商业机密,最好保密。我们在半导体、医疗保健和金融行业工作过,可以亲身体会到,在这些行业中,偏执和公司间谍活动是文化的一部分。正因为如此,三星和其他行业参与者最初都限制了 ChatGPT 的使用,禁止员工使用,后来才开放。当然,不久之后,就有几位三星员工泄露了机密源代码。² 由于 OpenAI 使用用户的交互来改进模型,该代码被保留,并且后来可能被用于进一步训练模型。这意味着,通过正确的提示注入,任何人都有可能从模型中提取代码。一个最近的例子甚至更进一步:当任何 OpenAI 模型被提示无限重复一个单词时,它就会开始重复训练数据,包括所有在清洗过程中悄悄溜过的个人可识别信息(PII)。

备注:OpenAI 的隐私和用法政策在本书撰写过程中发生了很大变化。当 ChatGPT 首次推出时,它作为一个演示,OpenAI 可以收集用户交互并改进模型。它几乎没有任何隐私政策,并且有免责声明。随着 ChatGPT 的成长并成为实际产品,这种情况发生了变化,因为客户需要更多的保护。例如,OpenAI 改变了其政策,以更好地服务客户,并且自 2023 年 3 月 1 日起,不再使用客户 API 数据来改进其模型(见 ChatGPT 常见问题解答:mng.bz/QV8Q)。当然,措辞表明,只有数据通过 API 发送。最好咨询您的律师,了解贵公司在使用它方面的立场。无论如何,使用条款发生如此大的变化,只是进一步证明你可能希望在这方面有更多的控制权。

不仅仅是代码容易丢失,商业计划、会议记录、机密电子邮件,甚至潜在的专利想法都处于风险之中。不幸的是,我们了解到一些公司已经开始将机密数据发送给 ChatGPT,使用该模型来清理和提取 PII。如果你认为这是潜在的疏忽性滥用,你是对的。这种方法直接将客户数据暴露给 OpenAI,以及他们使用的任何第三方服务(包括 AWS Mechanical Turk、Fiverr 和自由职业者)来执行 RLHF 的人类反馈部分。我们并不是说,如果你使用第三方来处理敏感数据的数据处理任务,这一定是一个安全或隐私问题,但应该只在高度信任和合同的基础上进行。

总结

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

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/1-1.png

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

最后一点我们认为这些构建-购买对话似乎从未充分关注的是“为什么不是两者兼而有之?”购买带给你构建所不擅长的所有东西:上市时间、相对较低的成本和易用性。构建带给你购买所面临的所有挑战:隐私、控制和灵活性。研究和原型阶段可以从购买 GPT-4 或 Databricks 的订阅中受益,以便快速构建一些东西来帮助筹集资金或获得利益相关者的支持。然而,生产环境通常并不适合第三方解决方案。

最终,无论你计划构建还是购买,我们写这本书是为了你。显然,如果你计划构建,你将需要了解更多的知识,所以这本书的大部分内容将针对这些人。事实上,我们不再需要过多强调:我们将在本书中教你如何构建,但不要让这阻止你为你的公司做正确的事。

1.2.3 一句忠告:现在就拥抱未来

所有新技术都会遇到阻力,都有批评者;尽管如此,技术仍在被采用,进步仍在继续。在商业中,技术可以给公司带来前所未有的优势。关于公司因未能适应新技术而失败的故事并不少见。我们可以从他们的失败中学到很多。

Borders 于 1971 年首次开门营业。在开发了一个包含高级分析能力的综合库存管理系统后,它迅速崛起,成为世界上第二大图书零售商,仅次于 Barnes & Noble。利用这项新技术,Borders 颠覆了行业,使其能够轻松跟踪数万本书籍,开设大型商店,让顾客能够浏览比小型商店更多的书籍。分析能力帮助它追踪哪些书籍正在流行,并更好地了解其客户,从而做出更好的商业决策。它主导了行业二十多年。

然而,Borders 却未能从自己的历史中吸取教训,由于未能适应并受到技术的冲击——这次是电子商务——于 2011 年破产。2001 年,它没有建立自己的平台和在线商店,而是决定将其在线销售外包给 Amazon。³许多批评者会说这个决定等同于把你的商业秘密钥匙交给竞争对手。虽然并非确切地交出了其秘方,但这是一个放弃 Borders 竞争优势的决定。

在接下来的七年里,Borders 对日益增长的在线领域视而不见,反而专注于扩大其实体店面的规模,收购竞争对手,并确保获得梦寐以求的星巴克交易。当 Amazon 在 2007 年发布 Kindle 时,图书零售格局完全改变。Barnes & Noble 已经运营了自己的在线商店,迅速转型并发布了 Nook 以竞争。然而,Borders 却无所作为,或者更确切地说,实际上无法采取任何行动。

通过第三方拥抱电子商务,Borders 未能发展出创建成功在线销售策略所需的内部专业知识,导致市场份额大幅下降。它最终在 2010 年底推出了自己的电子阅读器 Kobo,但为时已晚,无法赶上。它无法有效理解和实施电子商务技术,导致巨额财务损失和店铺关闭;最终,公司在 2011 年申请破产。

Borders 是一个警示故事,但还有数百个类似的公司未能采用新技术,结果损害了自己。随着像 LLMs 这样具有影响力的新技术,每个公司都必须决定自己想要站在哪一边。它是将实施和部署委托给大型 FAANG 类公司,仅仅通过调用 API 来完成工作,还是负责起来,更喜欢掌握这项技术并在内部部署?

我们希望从这个故事中传达的最大教训是,技术是层层叠加的。电子商务建立在互联网之上。未能建立自己的在线商店意味着 Borders 未能建立起在市场格局发生变化时保持竞争力的内部技术专长。我们今天看到同样的情况,因为那些最能有效利用 LLMs 的公司已经积累了机器学习和数据科学方面的专长,并对自己的行动有所了解。

我们没有水晶球可以预测未来,但许多人相信 LLMs 是一项革命性的新技术,就像之前的互联网或电力一样。学习如何部署这些模型,或者未能这样做,可能对许多公司来说将是决定性的时刻——不是因为这样做现在就能决定公司的兴衰,而是因为它可能在将来,当建立在 LLMs 之上的更有价值的东西出现时。

进入部署大型语言模型(LLMs)的这片新世界可能会充满挑战,但这将帮助您的公司建立起保持竞争优势所需的技术专长。没有人真正知道这项技术将引领我们走向何方,但了解这项技术很可能会成为避免犯下像 Borders 那样的错误所必需的。

有许多很好的理由可以通过购买来获得成功,但至少有一种普遍的观点是完全错误的:那就是只有大型企业才能在这个领域工作,因为训练这些模型需要数百万美元和数千个 GPU,这创造了一个小企业无法跨越的、由现金和资源构成的不可逾越的壁垒。我们将在下一节中进一步讨论这个问题,但任何规模的公司都可以开始行动,而且现在正是做这件事的最佳时机。

1.3 消除迷思

我们都听说过大型企业和当前 LLMs 的领导者谈论从头开始训练 LLMs 是多么困难,以及尝试微调它们是多么激烈。无论是来自 OpenAI、BigScience 还是 Google,他们都讨论了大量的投资和对于强大数据与工程人才的需求。但其中有多少是真实的,又有多少只是企业试图创造技术壁垒的尝试?

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

一个朋友曾经私下里说:“我真的很想更多地参与到所有这些机器学习和数据科学的东西中。每次我眨眼,它似乎都变得更酷。然而,感觉唯一能参与进去的方式就是进行漫长的职业转变,去为 FAANG 公司工作。不,谢谢。我们在大公司已经度过了我们的时光,它们不适合我们。但我们讨厌感觉自己被困在门外。”这正是激发这本书的神话。我们在这里是为了提供工具和例子,帮助你停止感觉被困在门外。我们将帮助你解决我们试图用 LLM 解决的语言问题,以及考虑到模型巨大规模的机器学习操作策略。

令人奇怪的是,尽管许多人认为他们被困在门外,但许多人其他人认为他们可以在周末成为专家。只需获取一个 GPT API 密钥,这就足够了——你完成了。这导致了很多热情和炒作,每天都有新的酷炫演示出现在社交媒体上。大多数这些演示从未成为实际的产品——但这并不是因为人们不想拥有它们。

为了理解这一点,让我们讨论一下 IBM 的 Watson,在 GPT 之前世界上最先进的语言模型。Watson 是一款问答机器,在 2011 年击败了一些该节目有史以来最优秀的参赛者,布拉德·拉特和肯·詹宁斯。拉特是有史以来收入最高的游戏节目参赛者,而詹宁斯在游戏中表现得如此出色,以至于他连续赢得了 74 次胜利。尽管面对这些传奇人物,Watson 还是以压倒性的优势获胜。詹宁斯在回应失败时,回应了那句著名的引言,“我,至少,欢迎我们新的电脑主宰者。”⁴

Watson 是语言建模的第一次令人印象深刻的尝试,许多公司都争先恐后地想要利用其功能。从 2013 年开始,Watson 开始被用于商业用途。其中最大的应用之一就是尝试将其整合到医疗保健中,以解决各种问题。然而,这些解决方案从未真正按照预期的方式工作,而且业务从未盈利。到 2022 年,Watson Health 被出售。

在解决语言相关问题时,我们发现构建原型很容易;然而,构建一个功能性的产品则非常、非常困难。语言中存在太多的细微差别。许多人都在 wonder,是什么让 ChatGPT 在仅仅五天内就获得了超过一百万的客户,变得如此火爆。我们听到的许多答案都无法让专家满意,因为 ChatGPT 并没有比 GPT-3 或其他已经存在了几年的 LLMs 更令人印象深刻。OpenAI 的 Sam Altman 在一次采访中曾经说过,他认为 ChatGPT 不会得到这么多的关注;他以为它会随着 GPT-4 的发布而来。⁵ 那为什么它会如此火爆呢?在我们看来,其中的魔力在于它是第一个真正将 LLMs 商业化生产的产品——将它们从演示变成了实际的产品。它是任何人都可以与之互动的产品,提出棘手的问题,却惊讶于它如何出色地回应。演示只需要一次成功,但产品必须每次都成功,即使当数百万用户向他们的朋友展示它,说“看看这个!”时也是如此。这种魔力正是你通过阅读这本书可以希望学到的东西。

我们很兴奋地写这本书。我们很兴奋地能够将这种魔力带给你们,让你们可以将它带到全世界。LLMs 处于许多领域的交汇点,如语言学、数学、计算机科学等。虽然了解更多会帮助你,但成为专家并不是必需的。在任何个别部分的专长只会提高技能的上限,而不是进入的门槛。考虑一下物理学或音乐理论的专家:他们不会自动拥有音乐制作技能,但他们将更有准备快速学习。LLMs 是一种沟通工具,而沟通是一种几乎每个人都需要的技能。

就像所有其他技能一样,你接近和参与的意愿是知识的主要障碍,而不是学位或记笔记的能力——这些只会缩短你被听到和理解的过程。如果你在这个领域没有任何经验,从首先通过参与像 OpenAssistant 这样的项目来培养对 LLM 是什么以及需要什么的直觉可能是个好主意。如果你是人类,这正是 LLM 所需要的。通过志愿服务,你可以开始了解这些模型训练的内容以及为什么。无论你是从零知识到成为专业机器学习工程师的任何位置,我们都会传授必要的知识,以显著缩短你理解所需的时间。如果你对学习该主题的理论基础不感兴趣,我们有很多动手的例子和项目,让你亲身体验。

到现在为止,我们都听说过 LLM(大型语言模型)的幻觉故事,但 LLM 并不需要是随机的。像 Lakera 这样的公司每天都在努力提高安全性,而像 LangChain 这样的公司则使为模型提供实用上下文变得更加容易,这使得模型更加一致,不太可能偏离。RLHF(强化学习与人类反馈)和思维链等技术进一步允许我们的模型与我们已经接受的人机和模型应从一开始就理解的原则保持一致,例如基本的加法和当前日期,这些都是概念上任意设定的。我们将从语言学的角度帮助你提高模型稳定性,使其不仅能找出最可能的输出,还能找出最有用的输出。

当你进一步探索这条道路时,需要考虑的不仅是输入到你的模型/代码中的安全性,还有输出内容的安全性。LLM 有时可能会产生过时、事实错误,甚至可能是受版权或许可保护的材料,这取决于它们的训练数据包含的内容。LLM 对人们就什么应该是商业机密以及什么可以公开分享达成的任何协议一无所知——除非你在训练期间或通过推理期间的仔细提示机制告诉它们这些协议。确实,围绕提示注入导致不准确信息的主要挑战主要源于两个因素:用户请求超出模型理解范围的信息,以及模型开发者没有完全预测用户将如何与模型互动或他们查询的性质。如果你有一个可以帮助你在这第二个问题上取得领先的资源,那将是非常宝贵的,不是吗?

最后,我们不想通过 LLM(大型语言模型)人为地或无根据地夸大你的希望感。它们训练和运行资源密集,难以理解,而且更难按照你的意愿工作。它们是新的,且尚未被充分理解。好消息是,这些问题正在积极解决,我们投入了大量工作,寻找与写作同时进行的实现方案,以积极减轻了解整个深度学习架构所需知识的负担。从量化到 Kubernetes,我们将帮助你弄清楚你现在需要知道的一切,以便使用你所拥有的资源来完成这项工作。也许我们会无意中让你认为这太过复杂,你应该直接从供应商那里购买。无论如何,我们将帮助你每一步,以从这项神奇技术中获得你所需的结果。

摘要

  • LLMs(大型语言模型)令人兴奋,因为它们在与人相同的框架(语言)中工作。

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

  • LLMs(大型语言模型)在许多任务上表现出色,甚至可以通过高级别的医学和法律考试。

  • LLMs(大型语言模型)是破坏性的,而不是锤子,对于需要低延迟或涉及高风险的简单问题应避免使用。

  • 购买 LLM 的原因包括

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

    • 容易访问高度优化的生产模型

    • 获取供应商的技术支持和系统

  • 建立 LLM 的原因包括

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

    • 保持成本低且透明

    • 确保模型的可靠性

    • 保护你的数据安全

    • 在敏感或私人主题上控制模型输出

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

[1] WIRED, “它最初是一个由人工智能驱动的地牢游戏。然后它变得越发黑暗,”Ars Technica,2021 年 5 月 8 日,mng.bz/AdgQ

[2] 《经济学人》,“[独家] 担忧成为现实…三星电子解除 ChatGPT 限制后‘滥用’现象频发”,《经济学人》[“担忧成为现实:三星电子一解除 ChatGPT 限制,‘滥用’现象便接连发生”],2023 年 3 月 30 日,mng.bz/4p1v

[3] A. Lowrey,“边境破产:不是互联网的错,而是自己的愚蠢。”,《Slate 杂志》,2011 年 7 月 20 日,mng.bz/PZD5

[4] J. Best,“IBM Watson:揭秘赢得 Jeopardy 的超级计算机是如何诞生的,以及它接下来想做什么,”TechRepublic,2013 年 9 月 9 日,mng.bz/JZ9Q

[5] “与 OpenAI 首席执行官 Sam Altman 的对话;由 Elevate 主持,”2023 年 5 月 18 日,youtu.be/uRIWgbvouEw

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

本章涵盖

  • 理解意义和解释的语言背景

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

  • 注意力和 transformer 架构

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

如果你了解敌人,也了解自己,你就无需害怕百战百胜的结果。——孙子

本章深入探讨与 LLMs 发展相关的语言学,探索符号学的基础、语言特征以及塑造自然语言处理(NLP)领域的语言建模技术的进展。我们将从研究语言学的基础及其与 LLMs 的相关性开始,强调语法、语义和语用学等关键概念,这些概念构成了自然语言的基础,并在 LLMs 的功能中发挥着至关重要的作用。我们将深入研究符号学,即对符号和符号的研究,并探讨其原则如何指导 LLMs 的设计和解释。

然后,我们将追溯语言建模技术的演变,概述早期方法,包括 N-gram、朴素贝叶斯分类器和基于神经网络的多种方法,如多层感知器(MLPs)、循环神经网络(RNNs)和长短期记忆(LSTM)网络。我们还将讨论基于 transformer 架构的突破性转变,这为大型语言模型(LLMs)的出现奠定了基础,而 LLMs 实际上只是大型基于 transformer 的模型。最后,我们将介绍 LLMs 及其独特特征,讨论它们如何建立在并超越早期的语言建模技术,从而彻底改变自然语言处理(NLP)领域。

本书关于生产中的 LLMs。我们坚信,如果你想将 LLM 转化为实际产品,更好地理解技术将提高你的成果并帮助你避免代价高昂且耗时耗力的错误。任何工程师都可以弄清楚如何将大型模型拖入生产并投入大量资源使其运行,但这种蛮力策略完全忽略了人们之前尝试做同样事情时已经学到的教训,这正是我们最初尝试使用 LLMs 的原因。掌握这些基础知识将更好地为你准备那些棘手的部分、陷阱和边缘情况,你将在与 LLMs 合作时遇到这些情况。通过理解 LLMs 出现的背景,我们可以欣赏它们对 NLP 的变革性影响以及如何使它们能够创造无数的应用。

2.1 语言建模

如果不首先解决语言问题,就深入探讨 LLMs,那将是一种极大的失职。为此,我们将从对语言建模的简要但全面的概述开始,重点关注有助于我们理解现代 LLMs 的教训。让我们首先讨论抽象层次,因为这将帮助我们理解语言建模。

语言作为一个概念,是我们头脑中产生的情感和思想的抽象。在生成语言的过程中,情感首先出现,但这并不是我们在这里想要强调的唯一事物。我们还在观察语言作为无法捕捉我们能够感受到的全部范围的能力,这就是为什么我们称之为抽象。它远离了原始材料并丢失了信息。数学是语言的抽象,专注于逻辑和可证明性,但正如任何数学家都会告诉你的,它是一种用于以有组织和逻辑的方式描述和定义的语言的子集。从数学中又产生了另一个抽象,二进制语言,这是一个由开或关组成的二进制数值表示系统。

这并不是对有用性的评论,因为二进制和数学与语言的低级方面一样有用,也不是在评论顺序,因为我们之前已经说过。在数学和二进制中,顺序偶然地与抽象层相吻合。计算机不能独立做任何事情,需要接收命令才能变得有用。不幸的是,二进制对于人类来说太耗时了,以至于无法在其中传达重要的事情,因此二进制也被抽象为汇编,这是一种更易于人类理解的语言,用于与计算机通信。这进一步抽象为高级汇编语言 C,它又被进一步抽象为面向对象的语言,如 Python 或 Java(哪一个不重要——我们只是在衡量与二进制的距离)。我们刚才讨论的流程在图 2.1 中有所概述。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/2-1.png

图 2.1 我们将认知抽象层与编程抽象层进行比较,直到逻辑二进制抽象。Python 不是来自 C,也不是编译成 C。然而,Python 是另一个远离二进制的抽象层。语言遵循相似的路径。每一层抽象都创造了一个潜在的错误点。在创建模型的过程中也有几个抽象层,每一层在看到从我们的感受到一个工作模型的全路径时都很重要。

这显然是一种简化;然而,了解你头脑中的感受与计算机实际读取的语言——即大多数人用来编程的语言——在抽象层面上距离二进制相同,是有用的。有些人可能会争论 Python 和二进制之间有更多的步骤,比如编译器或使用汇编来支持 C 语言,这是真的,但在语言方面也有更多的步骤,比如形态学、句法、逻辑、对话和一致性。

这种简化可以帮助我们理解我们想要被 LLM 理解的过程实际上有多么困难,甚至有助于我们更好地理解语言建模技术。在这里我们关注二进制,以说明从你拥有的想法或我们的代码示例到工作模型之间有相似数量的抽象层。就像孩子们玩电话游戏,参与者互相耳语,每个抽象层都创造了一个断开点或障碍,错误可能在这里发生。

图 2.1 不仅旨在说明创建可靠代码和语言输入的难度,而且还强调中间抽象步骤,如分词和嵌入,对模型本身的重要性。即使你拥有完全可靠的代码和完美表达的思想,这些过程之一也可能在它到达 LLM 之前就弄错了意义。

在本章中,我们将尝试帮助你了解你可以在语言、编码或建模方面做些什么来降低这些失败点的风险。不幸的是,在给你太多不立即与当前任务相关的语言学知识,以及给你太多虽然有用但不会帮助你培养对语言建模实践直觉的技术知识之间取得平衡是有点棘手的。考虑到这一点,你应该知道语言学可以追溯到我们历史数千年前,并且有很多东西可以从中学习。我们在附录 A 中简要概述了语言建模随时间的发展,并鼓励你查看。

让我们从构成语言本身的基本构建块开始。我们希望我们的读者至少尝试过语言建模,并且听说过像 PyTorch 和 TensorFlow 这样的库,但我们并不期望大多数读者在之前考虑过语言方面的问题。通过理解构成语言的本质特征,我们可以更好地欣赏创建有效语言模型所涉及的复杂性,以及这些特征如何相互作用,形成连接我们所有人的复杂沟通网络。在下一节中,我们将检查语言的各个组成部分,例如语音学、语用学、形态学、句法和语义学,以及它们在塑造我们对世界各地语言的理解和使用中所起的作用。让我们花点时间来探讨我们目前对语言的理解,以及我们面临的挑战,这些挑战正是 LLMs 旨在解决的。

2.1.1 语言特征

我们对语言的理解是,语言至少由五个部分组成:语音学、句法、语义、语用和形态学。这些部分中的每一个都对任何对话中听众所吸收的整体体验和意义有显著的贡献。并不是所有的交流都使用所有这些形式;例如,你现在正在阅读的这本书没有语音学,这也是许多人认为短信不适合更严肃或复杂对话的原因之一。让我们逐一探讨这五个部分,以了解如何将它们呈现给语言模型,以实现全面的沟通能力。

语音学

对于语言模型来说,语音学可能是最容易吸收的,因为它涉及到语言的实际声音。这就是口音显现和处理语音声音生产与感知的地方,语音学专注于特定语言系统中声音的组织方式。与计算机视觉类似,虽然处理整个声音可能并不容易,但在解析、矢量化或标记实际声音波方面没有歧义。每个部分(如每个频率周期的峰值、谷值和斜率)都附有数值。与文本相比,语音学在计算机标记和处理方面要容易得多,尽管它并不简单。

声音本身也包含比文本更多的编码意义。例如,想象有人对你说“是啊,没错”。这可能是一种讽刺,也可能是一种祝贺,这取决于语气——而英语甚至不是音调语言!不幸的是,语音学没有与它通常相关的大型数据集,对语音数据进行数据采集和清洗,尤其是在训练大型语言模型所需的规模上,是极其困难的。在一个音频数据比文本数据更普遍且占用更小内存的世界里,基于语音或对语音敏感的大型语言模型将会更加复杂,创造这样一个世界是一个值得努力的目标。

预见到这个语音问题,1888 年创建了一个名为国际音标(IPA)的系统。它在 20 世纪和 21 世纪都进行了修订,以使其更加简洁、一致、清晰,并且可以作为一种将语音意识插入文本数据的方法。IPA 作为每个语言声音轮廓的国际标准化版本。声音轮廓是语言使用的声音集合;例如,在英语中,我们永远不会把/ʃ/(she, shirt, sh)和/v/声音放在一起。IPA 用于书写声音,而不是像大多数语言那样书写字母或象形文字。例如,你可以使用这些符号来描述如何发音单词“cat”:/k/,/æ/,和/t/。当然,这是一个非常简化的版本,但对于模型来说,并不需要这样。你还可以描述音调和送气。这可能是文本和语音之间的一个折中方案,捕捉一些语音信息。想想短语“What’s up?”你的发音和音调可以极大地改变你对这个短语的理解,有时听起来像友好的“Wazuuuuup?”有时则像几乎威胁的“‘Sup?”而 IPA 可以完全捕捉到这一点。尽管如此,IPA 并不是一个完美的解决方案;例如,它并不能很好地解决复制音调的问题。

声音学在这里被列为首位,因为相较于所有其他特征,声音学在大型语言模型(LLM)中的应用最少,因此具有最大的改进空间。即使是现代的文本到语音(TTS)和声音克隆模型,大部分情况下,最终都是将声音转换成频谱图并分析该图像,而不是结合任何类型的语音语言模型。在未来的几个月和几年里,改善 LLM 中的语音数据和表示将是研究的一个方向。

语法

语法是当前 LLM 表现最出色的领域,无论是从用户那里解析语法还是生成自己的语法。语法通常是我们所认为的语法和词序;它是研究单词如何组合成短语、从句和句子的学科。语法也是语言学习程序开始帮助人们学习新语言的第一步,尤其是基于他们本来的语言背景。例如,对于学习土耳其语的英语母语者来说,了解语法完全不同是很重要的,在土耳其语中,你可以构建整个句子,而英语中我们永远不会把主语和动词放在一起成为一个单词。

语法在语言中很大程度上与意义是分开的,正如著名的句子来自诺姆·乔姆斯基,被称为语法的“之父”所展示的那样:“无色的绿色想法疯狂地睡觉。”这个句子的每一部分在语法上都是正确的,在语义上也是可以理解的。问题不在于它没有意义;而在于它有意义,这些词的编码意义是冲突的。这是一种简化;然而,你可以把 LLMs 给出无意义答案的所有时间都看作是这个现象的表现。不幸的是,语法也是歧义最常见的地方。考虑这个句子,“我看到一个老人和一个女人。”现在回答这个问题:这个女人也是老人吗?这是句法歧义,我们不确定修饰语“老人”是适用于后面短语中的所有人,还是仅仅适用于它直接前面的那个人。这比语义和语用歧义也出现在句法中的事实要轻微得多。考虑这个句子:“我看到一个在山上的男人,他手里拿着望远镜。”回答这些问题:说话者在哪里,他们在做什么?说话者是在山上用望远镜把一个男人切成两半吗?很可能,你在读这个句子的时候甚至没有考虑这个选项,因为当我们解释句法时,我们所有的解释至少在语义和语用上都是有所依据的。我们从生活经验中知道那种解释根本不可能,所以我们立即将其排除,通常甚至没有花时间去处理我们正在从可能的含义池中排除它。单模态 LLMs 将始终存在这个问题,而多模态 LLMs(到目前为止)只能趋近于解决方案。

理解为什么 LLMs 需要具备语法意识才能表现出色,不应该需要任何逻辑上的跳跃。那些没有正确处理词序或生成无意义输出的 LLMs 通常不会被描述为“好”。LLMs 对语法的依赖甚至促使乔姆斯基将 LLMs 称为“随机的鹦鹉”。在我们看来,2018 年的 GPT-2 是语言模型将语法作为一个完全独立于意义的演示解决的时候,我们很高兴看到最近尝试将 GPT-2 输出的语法与编码和蕴涵的意义相结合的尝试,我们现在将深入探讨这一点。

语义

语义是话语中词语的直译意义,它以惊人的速度在波浪中变化。人们会自动优化语义意义,只使用他们认为在当前语言时代有意义的词语。如果你曾经创建或使用过语言模型(word2vec、ELMo、BERT、MUSE [E 代表嵌入],等等)的嵌入,你就已经使用了语义近似。词语经常经历语义变化,虽然我们不会完全涵盖这个主题或深入探讨,但这里有一些你可能已经熟悉的常见变化:缩小,从更广泛的意义到更具体的一个;扩大,与缩小相反,从具体意义到广泛意义;以及重新解释,通过整体或部分转换。这些变化并没有某种伟大的逻辑基础。它们甚至不需要与现实相关联,语言的使用者通常在变化发生时也不会有意识地思考这些变化。这并不能阻止变化的发生,在语言建模的背景下,这也不能阻止我们必须跟上这种变化。

让我们来看一些例子。缩小包括“鹿”,在古英语和中古英语中这个词仅仅指任何野生动物,甚至包括熊或美洲狮,而现在只指一种森林动物。对于扩大,我们有“狗”,它曾经只指来自英格兰的一种犬种,而现在可以用来指任何家养犬。关于狗的扩大有一个有趣的小插曲,在 FromSoft 游戏《艾尔登法环》中,由于玩家之间有限的消息系统,"狗"这个词会被用来指从乌龟到巨型蜘蛛以及介于两者之间的所有事物。对于重新解释,我们可以考虑“漂亮”,它曾经意味着聪明或工艺精湛,而不是视觉上吸引人。另一个很好的例子是“比基尼”,它从指一个特定的环礁,到指在访问那个环礁时可能穿的衣服,再到人们好像“bi-”指的是衣服的两件式结构,从而暗示了坦克尼和单件比基尼。基于专家研究和数十年的研究,我们可以认为语言是不断被母语使用者比较和重新评估的,从中产生了常见的模式。这些模式在语言社会学的传播被密切研究,但很大程度上超出了当前目的的范围,但当地化(l10n)或国际化(i18n)作为 LLMs 项目需求出现时,可以迅速进入范围。像声望这样的社会语言学现象可以帮助设计出对每个人都适用的系统。

在 LLMs(大型语言模型)的背景下,所谓的语义嵌入是文本的向量化版本,试图模仿语义含义。目前,最流行的方法是通过分词或将字典中的任意数字分配给每个话语中的子词(例如前缀、后缀和一般词素),应用连续语言模型来增加向量中每个标记的维度,以便有一个更大的向量代表每个标记向量的索引,然后对每个这些向量应用位置编码以捕捉词序。每个子词最终都会根据其使用方式与其他字典中的单词进行比较。我们稍后会展示一个例子。在思考词嵌入时,需要考虑的是,它们难以捕捉那些标记的深层编码含义,而简单地增加嵌入的维度并没有显示出显著的改进。嵌入与人类工作方式相似的证据是,你可以应用距离函数到相关单词上,并看到它们比无关单词更接近。如何更完整地捕捉和表示意义是未来几年和几个月内有望出现突破性研究的另一个领域。

话语学

话语学有时被省略在语言学中,因为它的参照物是所有影响听者解释和说话者决定以某种方式表达的非语言环境。话语学在很大程度上指的是文化、地区、社会经济阶层和共同生活经历中遵循的教条,这些教条在对话中通过蕴涵来走捷径。

如果我们说,“一位受欢迎的明星刚刚被送进了 ICU”,根据你的生活经验,你的语用解释可能是假设一个深受爱戴的人受到了严重伤害,现在正在一家设备齐全的医院接受治疗。你可能想知道这是哪位明星,他们是否需要支付医疗费用,或者伤害是否是自残的,这也基于你的生活经验。这些都不能直接从文本及其编码的意义中推断出来。你需要知道 ICU 代表一组更大的词汇以及这些词汇是什么。你需要知道医院是什么,以及为什么有人需要被送到那里而不是自己去。如果这些感觉很明显,很好。你生活在一个社会中,你对这个社会的语用知识很好地与提供的例子重叠。如果我们分享一个来自人口较少社会的例子,“Janka 昨天受到了严厉的惩罚;她明天会得到 Peter 的惩罚”,你可能会感到困惑。如果你感到困惑,意识到这可能就是许多文本数据对 LLMs(承认拟人化)看起来像的样子。对于那些想知道的人,这个句子来自斯洛伐克的复活节传统。如果你不习惯这些特定的传统,那么这里很多含义都会被错过,无法解释。这位作者个人很享受尝试向外国同事解释复活节兔子和它对鸡蛋的迷恋,并享受着看起来像是脱离了现实的感觉。

在 LLMs(大型语言模型)的背景下,我们可以有效地将所有非文本上下文归入语用学。这意味着 LLMs 在开始时对现实世界没有任何知识,并且在训练过程中也不会获得这些知识。它们只获得对人类如何对特定的语用刺激做出反应的知识。LLMs 不理解社会阶层、种族、性别或总统候选人,或者任何可能基于你的生活经历激发某种情感的其他事物。我们并不期望语用学能够在任何时刻直接被纳入模型中,因为模型无法生活在社会中。然而,我们已经看到了通过数据工程和整理、RAG(阅读-询问-生成)提示机制以及指令数据集上的监督微调间接纳入语用学的益处。在未来,我们预计在将语用学纳入 LLMs 方面将取得重大进步,但我们强调这是一个渐近解,因为语言最终仍然是一种抽象。

无论是你有意还是无意,一旦你获取了将要训练的数据,实用结构就会被添加进去。你可以将这种类型的实用结构视为偏见,它本身并不一定是好是坏,但却是无法摆脱的。在后续过程中,你可以通过标准化和整理,增强特定代表性不足的点,以及削减过度代表或噪声示例,来选择你希望数据保持的偏见类型。指令数据集展示了你如何在训练数据中利用实用结构来创建极其有用的偏见,例如,当被要求回答问题时,让模型偏向于回答问题而不是尝试对问题的情感进行分类。

语境和上下文都围绕着蕴涵。蕴涵是数据中的实用标记,与数据集中包含的文本内容相对。例如,假设你有一个模型试图处理一个输入,如“为我写一篇关于青蛙吃湿袜子且不押韵的演讲稿,每行的第一个字母拼写出两栖动物”,并真正遵循这个指令。你可以立即判断这个输入要求很多。作为数据工程师的你,需要确保输入要求的所有内容都在你的数据中得到明确考虑。你需要演讲的例子,青蛙和袜子的例子以及它们的行为,以及首字母诗的例子。如果你没有这些例子,模型可能只能从数据集中存在的蕴涵中理解,但这很不确定。如果你更进一步,跟踪数据集中蕴涵与显性信息以及任务,以及数据分布,你将会有例子来回答“垃圾输入导致垃圾输出是什么?”

大型语言模型(LLMs)在捕捉语用学方面比人更困难,但它们确实能捕捉到普通人平均标准差范围内的东西。它们甚至可以复制超出那个标准差的人的回答,但如果没有精确的刺激,这种复制通常是不一致的。这意味着,如果没有在训练期间和提示中提供正确的偏见和蕴涵,模型很难在没有提供正确偏见和蕴涵的情况下给出一个普通人不知道的问题的专家答案。例如,在图像生成提示的开头包含“杰作”一词将引发不同且通常质量更高的生成,但只有当这种区别存在于训练集中,并且只有当你要求的是一幅“杰作”是赞美之词的图像时。基于指令的数据集试图在训练期间通过提问和给出包含代表性回答的指令来制造这些刺激。在训练中考虑到每一种可能的情况是不可能的,并且你可能会在试图考虑到一切时无意中从你的最终用户那里创造出新的回答类型。训练后,你可以通过提示从你的模型中诱导出特定的信息,这有一个基于你的数据最初包含的内容的技能上限。

形态学

形态学是研究单词结构和它们是如何由称为词素的小单元构成的学科。词素是意义的最小单位,比如“re-”在“redo”或“relearn”中。然而,并不是单词的所有部分都是词素,例如“ra-”在“ration”或“na-”在“nation”中,有些可能是出乎意料的,比如“helico-”在“helicoid”和“-pter”在“pterodactyl”中。

理解单词是如何构成的有助于创建更好的语言模型和解析算法,这对于像分词这样的任务至关重要。分词是自然语言处理中使用的最基本单位;它们可以是单词、子词、字符或整个话语,并且不必与现有的词素相对应。人们并不自觉地决定他们的意义单位是什么,因此它们往往是逻辑不合理的。语言模型的有效性可能取决于它理解和处理这些分词的能力有多强。例如,在分词过程中,模型需要存储一组字典来在单词及其对应的索引之间进行转换。其中这些分词之一通常是一个/<UNK/>分词,它代表模型不认识的任何单词。如果这个分词使用得太频繁,可能会阻碍模型的表现,要么是因为模型词汇量太小,要么是因为分词器没有使用正确的算法来完成这项任务。

考虑这样一个场景,你想要构建一个代码补全模型,但你使用的是一个只能识别由空格分隔的单词的标记器,比如 NLTK 的punkt标记器。当它遇到字符串def add_two_numbers_together(x, y):时,它将把[def, [UNK], y]传递给模型。这导致模型丢失了有价值的信息,不仅因为它不识别标点符号,而且还因为函数目的的重要部分被标记器形态算法替换成了未知标记。为了提高模型的表现,需要更好地理解词的结构和适当的解析算法。

2.1.2 符号学

在探索语言的基本特征并考察它们在 LLMs(大型语言模型)背景下的重要性之后,考虑人类沟通中意义构建和解释的更广泛视角是至关重要的。符号学,即对符号和象征的研究,提供了一个有价值的视角,通过它我们可以更好地理解人们如何解释和加工语言。我们将深入探讨符号学,考察符号、符号指示物和抽象之间的关系,以及 LLMs 如何利用这些元素来生成有意义的输出。这次讨论将提供对 LLMs 如何模仿人类对语言的理解的复杂过程的更深入理解,同时也会揭示它们在这一努力中所面临的挑战和限制。我们并不一定认为模仿人类行为是 LLM 改进的正确答案,只是认为模仿是迄今为止该领域自我评估的方式。

为了介绍符号学,让我们考虑图 2.2,一个改编的皮尔士符号三角形。这些三角形被用来将基本思想组织成第一性、第二性和第三性的序列,其中第一性位于左上角,第二性位于底部,第三性位于右上角。如果你以前见过符号三角形,你可能会对角的数量和方向感到惊讶。为了解释,我们将它们倒置,使其稍微容易阅读一些。此外,因为系统是递归的,我们展示了系统如何同时模拟整个过程和每个部分。虽然这些想法的整体概念非常酷,但深入探讨哲学超出了本书的范围。相反,我们可以专注于那些词(第一、第二、第三)的基本部分,以展示意义处理的过程。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/2-2.png

图 2.2 一个递归的皮尔士符号三角形是一个组织从任何事物中提取意义的过程的系统——在我们的例子中,是从语言中提取。三角形上的每一个点都说明了在系统被用来描述的任何事物中合成意义所需的最低限度的部分,因此每个点都是语言意义的最小单位。第一性、第二性和第三性不是三角形上的点;相反,它们更像是符号学家用来在这个图表中定位自己的标记。

我们还可以观察三角形的每一个交点,以了解为什么事物以这种顺序呈现。情感可以在它们能够附加到单词和表格之前就附加到图像和编码上。仪式和常见的脚本为解释性行动提供了一个空间,这种行动是第二本能的,不需要思考,就像大多数短语只是从单词中组合起来,母语使用者不需要对每个单词进行元认知一样。所有这些最终都会导致一种解释或一份文档(一系列话语);在我们的例子中,这种解释应该由 LLM 得出。这就是为什么,例如,提示工程可以提高模型的有效性。在数百万个仪式脚本示例上训练的基础 LLM,当你明确告诉模型在提示中需要遵循哪个脚本时,可以显著更好地复制脚本类型。试着要求模型给出逐步解释——也许在你的生成前加上“让我们一步步来考虑这个问题。”模型将根据它之前看到的脚本生成逐步脚本。

对于感兴趣的人来说,有特定的方法来阅读这些图表,以及一个完整的符号学领域需要考虑;然而,并不能保证你通过理解这一切就能创造出最佳的 LLMs。我们不会深入探讨这一点,而是考虑最基本的可以帮助你构建最佳模型、用户体验和用户界面的要素。例如,创建意义的过程中的一个方面是递归性。当有人和你交谈,他们说的话对你来说没有意义(对你来说是“无意义的”),你会怎么做?通常,人们会提出一个或多个澄清问题来弄清楚意义,这个过程会一次又一次地重复,直到意义对你来说变得清晰。目前市场上最先进的模型并不这样做,但通过非常目的性的提示,它们可以被训练去做这件事。许多人甚至不知道该怎么做,除非有人指出。换句话说,这只是一个关于符号学的简要介绍。你不需要在阅读本节结束时能够向符号学领域的专家提供深入和准确的坐标特定解释。我们试图说明的是,这是一个展示你需要创建一个完整意义图景的最小要素的组织系统。我们在训练模型时并没有提供相同数量和类型的相同信息,但如果我们这样做,将会显著改善模型的行为。

图 2.2 和图 2.3 旨在表示一个最小化的组织模型,其中每个部分都是至关重要的。让我们考虑图 2.3,它通过一个使用符号三角形的示例来展示。考虑图像、图片和记忆,想想在没有眼睛处理图像和没有正字法(一种书写系统)来抽象知识的情况下,尝试吸收这本书中的知识会是什么样子。看看项目符号等,如果没有章节、字母之间的空白和项目符号来显示顺序和结构,你将如何阅读这本书?看看语义和字面编码的意义,想象一下没有图表或没有字典定义的单词的书。中间的表格可能是一本书,没有任何表格或比较信息组织者,包括这些图表。在没有文化或社会习惯和教条作为我们解释的透镜的情况下,阅读这本书会是什么样子?所有这些点构成了我们解读信息的能力,以及我们通过透镜传递信息以识别模式的能力。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/2-3.png

图 2.3 从左上角开始,按照箭头指示的顺序,查看我们构建解释和从我们与之互动的事物中提取意义的一般顺序。在这里,我们用每个点的例子替换了描述性词汇。试着想象在没有任何文字、例子、箭头,甚至知道这本书中的图例应该用于什么目的的实用语境下解释这个图。

因此,这些问题很重要:LLMs 能够访问多少这些事物以返回有意义的解释?LLMs 能够访问情感或社会仪式吗?目前,它们还不能,但随着我们通过传统的和更新的 NLP 推理技术进行探索,想想不同模型能够访问什么。

2.1.3 多语言 NLP

在我们评估之前的 NLP 技术和当前一代 LLMs 之前,我们需要触及的最后一个挑战是语言学的基础,以及 LLMs 甚至存在的理由。自从最早的人类文明开始接触以来,人们就想要理解或利用彼此。这些案例导致了翻译的需求,随着全球经济的发展和繁荣,这种需求呈指数级增长。

对于企业来说,这也很简单。你知道孟加拉语母语者的人数几乎和英语母语者一样多吗?如果你第一次听说孟加拉语,这应该有助于你认识到多语言模型有一个有价值的市场。世界上有数十亿人,但只有大约三分之一的人母语是英语。如果你的模型以英语为中心,就像大多数模型一样,你正在错过世界上 95%的人作为客户和用户。西班牙语和普通话在这个领域很容易取得成功,但大多数人甚至没有走那么远。

在这本书的范围之外,有许多政治上充满争议的例子,包括不同语言中称呼同一事物的情况。这些情况通常是因为外部因素,如政府干预。牢记这两点——一个以英语为中心的单语系统并没有许多企业所声称的覆盖面或盈利潜力,而语言和方言之间的界限在最坏的情况下是有害的——应该会凸显出意见的险恶沼泽。许多企业和研究科学家在设计产品或系统时甚至不愿假装想要用 50 英尺长的杆子去触碰这个沼泽。

目前,没有简单的解决方案。然而,考虑到这些因素可以帮助你作为一个科学家或工程师(以及希望是一个有道德的人)设计 LLMs,至少它们不会加剧现有问题并产生负面影响。这个过程的第一步是在项目开始时就决定一个方向目标,要么是本地化(l10n),要么是国际化(i18n)。本地化是一个由 Mozilla 所体现的方法,它在超过 90 种语言中通过众包 l10n 提供了其浏览器的不同版本,并且没有迹象表明会停止这项努力。国际化与此类似,但方向相反;例如,宜家试图在其说明书上使用尽可能少的文字,而是选择使用国际上公认的符号和图片来帮助客户导航 DIY 项目。在项目开始时做出决定可以减少扩展到任何解决方案所需的努力。它足够大,足以将翻译和格式化的感知从成本转变为投资。在 LLMs 及其在公众意识中的快速扩张的背景下,尽早考虑这一点变得更加重要。带着一种自动禁止世界上大多数人与之互动的世界改变技术进入市场,贬低了那些声音。需要等待则危及企业的经济前景。

在继续之前,让我们花一点时间回顾一下到目前为止我们所讨论的内容。我们在语言学方面触及了重要观点,为我们提供了需要考虑的概念,例如理解语言的结构与其意义是分开的。我们已经展示了一个旅程,每个人作为个人以及作为社会,都在朝着拥有元认知来以连贯的方式理解和表示语言给计算机使用前进。随着我们加深对认知领域的了解并解决我们遇到的语用特征,这种理解将得到改善。跟随图 2.1,我们现在将展示我们所遵循的语言建模的计算路径,并探讨它如何以及如何没有解决那些语用特征或努力创造意义。让我们进入评估表示语言的各种算法技术。

2.2 语言建模技术

在深入研究语言的基本特征、符号学原理以及 LLMs 如何解释和处理语言信息之后,我们现在过渡到一个更实际的领域。我们将探讨为创建这些强大的语言模型而开发和应用的多种 NLP 技术。通过检查每种方法的优缺点,我们将获得宝贵的见解,了解这些技术在捕捉人类语言和交流本质方面的有效性。这种知识不仅将帮助我们欣赏 NLP 领域取得的进步,而且使我们能够更好地理解这些模型的当前局限性以及未来研究和开发面临的挑战。

让我们花点时间回顾一下在所有语言模型中都会通用的数据处理方法。首先,我们需要决定如何将我们传递给模型的单词和符号进行拆分,实际上就是决定在我们模型中一个标记将是什么。我们需要一种方法将这些标记转换为数值,然后再转换回来。然后,我们需要选择我们的模型将如何处理这些标记化的输入。以下的所有技术至少以一种方式建立在之前的技术之上。

这些技术中的第一个被称为词袋(BoW)模型,它简单地由文本中出现的单词计数组成。你可以从 sklearn 导入 CountVectorizer 类来使用它,但如果我们用一个小的代码片段来展示,可能会更有教育意义。这可以通过一个扫描文本的字典非常容易地完成,为每个新单词创建一个新的词汇条目作为键,并从 1 开始递增的值:

sentence = "What is a bag of words and what does it do for me when " \
    "processing words?"
clean_text = sentence.lower().split(" ")
bow = {word:clean_text.count(word) for word in clean_text}
print(bow)
# {'what': 2, 'is': 1, 'a': 1, 'bag': 1, 'of': 1, 'words': 1, 'and': 1, 
# 'does': 1, 'it': 1, 'do': 1, 'for': 1, 'me': 1, 'when': 1, 'processing': 1,
# 'words?': 1}

考虑到其简单性,即使是完全基于频率的这种模型,在试图洞察说话者的意图或至少他们的独特性时,也可以相当强大。例如,你可以在美国总统的就职演说上运行一个简单的词袋模型(BoW model),搜索“freedom”、“economy”和“enemy”这些词,从而对哪些总统在和平时期、战争时期和货币紧张时期上任有一个相当好的了解,这仅仅基于每个词被提及的次数。然而,BoW 模型有很多弱点,因为模型不提供图像、语义、语用、短语或情感。在我们的例子中,有两个“words”的实例,但由于我们的标记化策略仅仅是空白字符,它没有增加模型中的键。它没有任何机制来评估上下文或语音学,并且因为它默认在空白字符处分割单词(你显然可以按任何方式标记化,但尝试在子词上标记化并看看这个模型会发生什么——剧透:很糟糕),它也没有考虑形态学。总的来说,它应该被认为是一个弱的语言表示模型,但是一个强大的基准,可以用来评估其他模型。为了解决 BoW 模型无法捕捉任何序列数据的问题,人们提出了 N-gram 模型。

2.2.1 N-gram 和语料库技术

列表 2.1 一个生成 N-gram 语言模型的实现
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

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

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

padded_trigrams = list(
    pad_both_ends(my_corpus.sents(fileids="hamlet.txt")[1104], n=2)
)                                                  #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")
    )
)

train, vocab = padded_everygram_pipeline(
    3, my_corpus.sents(fileids="hamlet.txt")
)                                              #3

lm = MLE(3)           #4
len(lm.vocab)           #5

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

lm.generate(6, ["to", "be"])   #6

#1 从任意数量的普通 .txt 文件创建语料库

#3 允许 everygrams 从数据中创建一个训练集和一个词汇对象

#4 实例化和训练我们将用于 N-grams 的最大似然估计器(MLE)的模型

#5 此模型将采用 everygrams 词汇表,包括用于未知词汇的标记。

#6 可以使用此模型生成语言,并使用 n-1 个标记进行条件化。

这段代码就是您需要创建生成性 N-gram 模型的所有内容。对于那些有兴趣进一步评估该模型的人来说,我们包括了以下代码,以便您可以获取概率和对数分数,或者分析特定短语的熵和困惑度。因为这一切都是基于频率的,尽管它在数学上很重要,但它仍然无法很好地描述现实世界语言的实际困惑度或频率:

print(lm.counts)
Lm.counts[["to"]]["be"]      #1

print(lm.score("be"))           #2
print(lm.score("be", ["to"]))
print(lm.score("be", ["not", "to"]))

print(lm.logscore("be"))             #3
print(lm.logscore("be", ["to"]))
print(lm.logscore("be", ["not", "to"]))

test = [("to", "be"), ("or", "not"), ("to", "be")]   #4
print(lm.entropy(test))
print(lm.perplexity(test))

#1 可以轻松计数长度不超过 n 的任何一组标记,以确定频率。

#2 任何标记都可以赋予一个发生概率,并可以增加最多 n-1 个标记来先于它。

#3 这也可以作为一个对数分数来完成,以避免非常大的和非常小的数字。

#4 可以测试标记集的熵和困惑度。

尽管这个代码示例说明了创建三元语言模型,但不幸的是,并非所有需要捕获的短语都只有三个标记长。例如,从《哈姆雷特》中,“To be or not to be”由一个包含两个单词和一个包含四个单词的短语组成。请注意,尽管 N-grams 通常是较小的语言模型,但通过将 N 设置为 10 亿或更高,可以制作一个 N-gram LLM,但不要期望从中得到任何实际用途。仅仅因为我们做得很大,并不意味着它会更好,或者意味着它将具有任何实际应用:99.9%的所有文本和 100%的有意义文本中,出现次数超过一次的标记少于 10 亿,而且这种计算能力可以更好地用于其他地方。

N-grams 仅使用静态信号(空白字符、正字法)和单词来提取意义(图 2.2)。它们试图手动测量短语,假设所有短语都将具有相同的长度。话虽如此,N-grams 可以用于创建文本分析的强大基线。此外,如果分析师已经知道话语的语用背景,N-grams 可以快速准确地洞察现实世界场景。尽管如此,这种短语建模无法捕捉到单个单词可能具有的任何语义编码。为了解决这个问题,将贝叶斯统计应用于语言建模。

2.2.2 贝叶斯技术

贝叶斯定理是描述你的输出在输入空间中发生的一种最数学上合理且简单的理论。本质上,它基于先验知识计算事件发生的概率。该定理提出,给定证据的假设为真的概率——例如,一个句子具有积极情感的概率——等于给定假设为真的证据发生的概率乘以假设发生的概率,所有这些除以证据为真的概率。它可以表示为数学公式

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

或者

P(A|B) × P(B) = P(B|A) × P(A)

由于这不是一本数学书,我们将深入探讨贝叶斯定理,就像我们深入研究其他语言学概念一样,并相信感兴趣的读者会去寻找更多。

不幸的是,尽管这个定理以数学上合理的方式表示数据,但它没有考虑到任何随机性或单词的多种含义。你可以向贝叶斯模型投掷的一个总是能使其混淆的词是“它”。任何指示代词最终都会以与所有其他单词相同的LogPriorLogLikelihood方式分配值,并且它得到一个静态值,这与这些词的使用是相反的。例如,如果你正在尝试对一个话语进行情感分析,将所有代词分配一个空值会比让它们通过贝叶斯训练更好。请注意,贝叶斯技术不会像其他技术那样创建生成语言模型。由于贝叶斯定理验证假设的性质,这些模型适用于分类,并且可以为生成语言模型带来强大的增强。

列表 2.2 展示了如何创建一个朴素贝叶斯分类语言模型,或者是一个基于先前学习内部语言模型对文本进行分类的系统。我们选择编写我们正在做的事情,而不是使用像 sklearn 这样的包或使编写代码变得容易的东西,所以它会更长,但它应该提供更多关于它是如何工作的信息。我们使用的是朴素贝叶斯模型的最简单版本。我们没有将其改为多项式或添加任何花哨的东西;显然,如果你选择升级它以解决任何问题,它会工作得更好。我们强烈建议你这样做。

注意:为了使代码更容易理解并帮助突出我们想要关注的部分,我们通过提取部分代码到实用辅助函数中,简化了一些我们的代码列表。如果你看到导入错误,这就是原因。这些辅助方法可以在本书附带的代码库中找到:github.com/IMJONEZZ/LLMs-in-Production/

列表 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):
            pair = (word, y)          #1

            if pair in result:
            result[pair] += 1     #2

            else:
            result[pair] = 1      #3

    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

    vocab = set([pair[0] for pair in freqs.keys()])      #4
    V = len(vocab)

    N_pos = N_neg = 0                  #5
    for pair in freqs.keys():
        if pair[1] > 0:                   #6
            N_pos += lookup(freqs, pair[0], True)     #7

        else:                                         #8
            N_neg += lookup(freqs, pair[0], False)     #9

    D = len(train_y)     #10

    D_pos = sum(train_y)      #11

    D_neg = D - D_pos     #12

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

    for word in vocab:                        #14
        freq_pos = lookup(freqs, word, 1)
        freq_neg = lookup(freqs, word, 0)

        p_w_pos = (freq_pos + 1) / (N_pos + V)    #15
        p_w_neg = (freq_neg + 1) / (N_neg + V)

        loglikelihood[word] = np.log(p_w_pos / p_w_neg)     #16

    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
    """
    word_l = process_utt(utt)     #17

    p = 0          #18

    p += logprior      #19

    for word in word_l:
        if word in loglikelihood:       #20
            p += loglikelihood[word]      #21

    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       #22

    y_hats = []
    for utt in test_x:
        if naive_bayes_predict(utt, logprior, loglikelihood) > 0:    #23
            y_hat_i = 1    #24
        else:
            y_hat_i = 0    #25

        y_hats.append(y_hat_i)      #26

    error = sum(
        [abs(y_hat - test) for y_hat, test in zip(y_hats, test_y)]
    ) / len(y_hats)                 #27

    accuracy = 1 - error    #28

    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(
        f"Naive Bayes accuracy = {test_naive_bayes(utts, ys, logprior, loglikelihood):0.4f}
    )

#1 Δ 定义了键,即单词和标签元组

#2 如果键存在于字典中,则增加计数

#3 如果键是新的,则将其添加到字典中并将计数设置为 1

#4 计算 V,词汇表中的唯一单词数量

#5 计算 N_pos 和 N_neg

#6 如果标签是正面的(大于零) . . .

#7 . . . 增加正面单词的数量(单词,标签)

#8 否则,标签是负面的。

#9 增加负面单词的数量(单词,标签)

#10 计算 Δ,文档数量

#11 计算正面文档的数量

#12 计算负面文档的数量

#13 计算对数先验概率

#14 对于词汇表中的每个单词 . . .

#15 . . . 计算每个单词是正面还是负面的概率

#16 计算单词的对数似然值

#17 处理语音以获取单词列表

#18 将概率初始化为零

#19 添加对数先验概率

#20 检查单词是否存在于对数似然字典中

#21 将该单词的对数似然值添加到概率中

#22 正确返回

#23 如果预测值大于 0 . . .

#24 . . . 预测的类别是 1。

#25 否则,预测的类别是 0。

#26 将预测的类别添加到列表 y_hats 中

#27 误差 = y_hats 和 test_y 之间差的绝对值的平均值。

#28 准确率是 1 减去误差。

这个定理并没有创建同类型的语言模型,而是一个与一个假设相关联的概率列表。因此,贝叶斯语言模型不能有效地用于生成语言,但它们在分类任务中可以非常强大地实现。然而,我们认为,贝叶斯模型对于这项任务通常被过度炒作。一位作者职业生涯的辉煌成就之一就是从生产中替换并移除了一个贝叶斯模型。

在贝叶斯模型中,一个主要问题就是所有序列都是完全未连接的,就像词袋模型一样,将我们推向序列建模和语言生成的对立面,从 N-gram 开始。就像摆锤一样,语言建模又回到了序列建模和语言生成,使用马尔可夫链。

2.2.3 马尔可夫链

通常被称为隐马尔可夫模型(HMMs),马尔可夫链本质上是在 N-gram 模型中添加状态,使用隐藏状态存储概率。它们常用于帮助解析文本数据,用于更大的模型,如进行词性标注(PoS;标记单词的词性)和命名实体识别(NER;标记具有其指代和通常类型的识别词;例如,LA – 洛杉矶 – 城市)等。建立在之前的贝叶斯模型之上,马尔可夫模型完全依赖于遇到的标记中的随机性(可预测的随机性)。任何发生的事情的概率完全取决于现在的状态,这一观点与贝叶斯定理一样,在数学上是合理的。因此,我们不是仅仅基于单词的历史出现来建模单词,并从中抽取概率,而是根据当前发生的情况来建模它们的未来和过去搭配。因此,“快乐”发生的概率如果“快乐”刚刚输出就会下降到几乎为零,但如果“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))

这段代码展示了生成马尔可夫模型的基本实现,我们鼓励您对其进行实验。用您最喜欢的音乐家的歌曲或您最喜欢的作者的书中的文本进行尝试,看看输出的内容是否像他们。隐马尔可夫模型(HMMs)非常快速,常用于预测文本或预测搜索应用。马尔可夫模型代表了从描述性语言学的角度对语言进行建模的第一个全面尝试,而不是规范性建模。这种视角很有趣,因为马尔可夫最初并没有打算使用语言建模,只是想在一个关于连续独立状态的争论中获胜。后来,马尔可夫使用马尔可夫链来模拟普希金小说中的元音分布,所以他至少意识到了可能的用途。

描述性语言学和规范性语言学的区别在于,后者关注事物应该如何,而前者关注事物实际上是如何的。从语言建模的角度来看,从语料库或马尔可夫视角描述语言所做的事情,比试图规定语言应该如何表现要有效得多。不幸的是,当前状态本身不能用来提供超越现在的语境,因此历史或社会语境在马尔可夫模型中无法有效表示。单词的语义编码也变得有问题,如代码示例所示:马尔可夫链会输出语法上正确但语义上无意义的单词序列,类似于“无色的绿色想法疯狂地睡觉。”为了解决这个问题,开发了“连续”模型,以允许对标记进行“语义嵌入”表示。

2.2.4 连续语言建模

连续词袋模型(CBoW)——就像其同名的词袋模型一样——是一种基于频率分析语言的方法,这意味着它根据单词出现的频率来建模单词。人类话语中的下一个单词从未根据概率或频率来确定。因此,我们提供了一个使用 CBoW 创建词嵌入的例子,以便其他模型可以摄取或比较。我们将使用神经网络为您提供一种良好的方法。

这是我们将看到的第一种语言建模技术,它本质上是在给定的话语上滑动一个上下文窗口(上下文窗口是一个 N-gram 模型),并试图根据窗口周围的单词来猜测中间的单词。例如,假设你的窗口长度为 5,你的句子是“学习语言学让我很高兴。”你会给 CBoW ['learning', 'about', 'makes', 'me'],试图让模型根据模型之前在类似位置看到该单词出现的次数来猜测“linguistics”。这个例子说明了为什么对于像这样训练的模型来说生成是困难的。比如说你给模型输入 ['makes', 'me', `'

在列表 2.4 中,我们创建了我们的第一个连续模型。在我们的情况下,为了尽可能保持简单,我们使用 BoW 模型进行语言处理,并使用一个层和两个参数的神经网络进行嵌入估计,尽管两者都可以替换为任何其他模型。例如,你可以用 N-gram 替换 BoW,用朴素贝叶斯模型替换神经网络,以获得连续的朴素 N-gram 模型。重点是,在这个技术中实际使用的模型有点任意;重要的是连续技术。为了进一步说明这一点,我们除了numpy之外不使用任何其他包来为神经网络进行数学运算,尽管它是本节中第一个出现的。

仔细注意以下步骤——初始化模型权重、修正线性单元(ReLU)激活函数、最终的 softmax 层以及前向和反向传播——以及它们如何在gradient_descent函数中相互配合。这些是拼图中反复出现的部分,无论编程语言或框架如何。你将需要在 TensorFlow、PyTorch、Hugging Face 中初始化模型、选择激活函数、选择最终层以及定义前向和反向传播,如果你开始创建自己的模型而不是使用别人的。

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

with open("hamlet.txt", "r", encoding="utf-8") as f:
    data = f.read()                              #1

data = re.sub(r"[,!?;-]", ".", data)         #2
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])

fdist = nltk.FreqDist(word for word in data)     #3
print("Size of vocabulary:", len(fdist))
print("Most Frequent Tokens:", fdist.most_common(20))

word2Ind, Ind2word = get_dict(data)     #4
V = len(word2Ind)
print("Size of vocabulary:", V)

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

def initialize_model(N, V, random_seed=1):     #5
    """
    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

def softmax(z):      #6
    """
    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

def forward_prop(x, W1, W2, b1, b2):      #7
    """
    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

def compute_cost(y, yhat, batch_size):     #8
    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

def back_prop(x, yhat, y, h, W1, W2, b1, b2, batch_size):    #9
    """
    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

def gradient_descent(data, word2Ind, N, V, num_iters, alpha=0.03):     #10
    """
    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

C = 2      #11
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

#1 创建我们的训练语料库

#2 通过删除标点符号、按单词标记化并转换为小写字母字符来稍微清理数据

#3 获取我们的词袋,以及一个分布

#4 创建两个字典以加快转换时间和跟踪词汇

#5 在这里,我们使用一个层和两个参数创建我们的神经网络。

#6 创建我们的最终分类层,使得所有可能性加起来等于 1

#7 定义了通过我们的模型向前移动的行为,以及一个激活函数

#8 定义了如何确定真实值和模型预测之间的距离

#9 定义了如何通过模型向后移动并收集梯度

#10 将所有内容组合在一起并进行训练

#11 训练模型

CBoW 示例是我们第一个展示机器学习中完整且有效训练循环的代码示例。在这个过程中,请特别注意训练循环中的步骤,特别是激活函数 ReLU。由于我们预期你对各种 ML 范式至少是熟悉的,包括不同的激活函数,我们在这里不会解释 ReLU。我们将讨论何时应该使用它以及何时不应该使用它。ReLU 虽然解决了梯度消失问题,但并没有解决梯度爆炸问题,并且会破坏模型中所有的负比较。更好的情境变体包括指数线性单元(ELU),它允许负数归一化到 alpha,以及广义高斯线性单元(GEGLU)/Swish 门控线性单元(SWIGLU),它们在越来越复杂的场景中表现良好,如语言。然而,人们经常使用 ReLU,并不是因为它们在某个情境下是最好的,而是因为它们易于理解、编码和直观,甚至比它们所取代的激活函数(如 sigmoid 或 tanh)更直观。

许多内容最终都会通过包和其他类似工具进行抽象化,但了解底层发生的事情对于将 LLMs 投入生产的你来说将非常有帮助。你应该能够以某种确定性预测不同模型在不同情况下的行为。下一节将深入探讨这些抽象化之一——在这种情况下,是连续建模技术创建的抽象化。

2.2.5 嵌入

回顾到我们关于语言的特征,应该很容易理解为什么连续风格的文本建模是一个突破。嵌入将我们创建的、不包含任何意义的标记化向量,尝试根据对文本的观察插入意义,例如词序和出现在相似上下文中的子词。尽管主要的意义模式是搭配(共定位,相邻出现的词),但它们证明是有用的,甚至显示出与人类编码的词义的一些相似性。

Word2Vec 中的一个经典示例,也是最早的预训练向量嵌入之一,就是取“king”(国王)的向量,减去“man”(男人)的向量,加上“woman”(女人)的向量,然后找到与这个和最近的邻居向量,就是“queen”(王后)这个词的向量。这在我们看来是有道理的,因为它模仿了人类的语义。其中一个已经被提到几次的主要区别是:语用学。人类使用语用上下文来告知语义意义,理解到你说“我需要食物”并不意味着你实际上没有食物就会处于身体危险之中。嵌入没有受到任何纯使用之外的影响,这感觉就像人类学习的方式一样,而且在这里有很好的论据。唯一确定的是,如果我们能以某种方式给模型提供更具代表性的数据,这可能为更有效的嵌入打开大门,但这是一个鸡生蛋、蛋生鸡的问题,因为更有效的嵌入会带来更好的模型性能。

在列出 2.5 时,我们深入探讨了如何使用pyplot可视化嵌入。我们将在后面的章节中更深入地探讨嵌入。这对于模型可解释性和在预训练步骤中的验证都很有帮助。如果你看到你的语义相似的嵌入在图上相对较近,你很可能是在正确的方向上。

列出 2.5:嵌入可视化
words = [          #1
    "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()

#1 列出 2.4 完成后,并执行了梯度下降

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

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/2-4.png

图 2.4:词嵌入的可视化技术。可视化嵌入对于模型可解释性很重要。

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 是一类神经网络,旨在根据先前语言建模技术的弱点来分析序列。可以将序列视为一个有序数组,其中整个数组的总和在任何一个部分被移动时都会改变值。逻辑是这样的:如果语言以序列的形式呈现,那么也许它应该以序列的形式进行处理,而不是一次处理一个标记。RNNs 通过使用我们在 MLPs 和马尔可夫链中看到过的逻辑来实现这一点,即当处理新的输入时,会引用内部状态或记忆,并在检测到节点之间的连接有用时创建循环。

在完全循环网络中,如列表 2.7 中的那种,所有节点最初都连接到所有后续节点,但这些连接可以设置为零,以模拟它们在不适用时断开。这解决了早期模型所面临的最大问题之一,即静态输入大小,并使 RNN 及其变体能够处理可变长度的输入。不幸的是,较长的序列会带来新的问题。因为网络中的每个神经元都连接到后续的神经元,较长的序列会导致整体总和的变化更小,使得梯度更小,直到最终消失,即使对于重要的单词;这被称为梯度消失。还存在其他问题,如梯度爆炸和梯度减小。

例如,让我们考虑以下用于情感分析的句子:“我昨晚非常喜欢这部电影”和“我昨晚去看的电影是我曾经期望看到的最棒的一部。”这些句子在语义上可以被认为是相似的,即使它们并不完全相同。当通过 RNN 移动时,第一句中的每个单词都更有价值,结果是第一句的正面评分比第二句高,仅仅因为第一句更短。反之亦然:梯度爆炸是这种序列处理的结果,这使得训练深层 RNN 变得困难。

为了解决这个问题,LSTMs(一种 RNN),通过使用记忆单元和门控机制来处理可变长度的序列,但避免了理解长序列和短序列不同的难题。考虑到多语言场景,并理解人们不会只从单一方向思考语言,LSTMs 还可以通过连接两个 RNN 的输出(一个从左到右读取序列,另一个从右到左读取)来双向处理序列。这种双向性提高了结果,即使在成千上万的标记之后,信息也能被看到并记住。

在列表 2.7 中,我们给出了 RNN 和 LSTM 的类。在与此书相关的代码库(github.com/IMJONEZZ/LLMs-in-Production)中,你可以看到训练 RNN 和 LSTM 的结果。关键点是,LSTM 在训练和验证集上都能在更少的 epoch(25 个与 RNN 的 50 个相比)内达到更好的准确率。值得注意的一项创新是,打包嵌入利用填充将所有可变长度的序列扩展到最大长度。因此,只要输入长度小于最大长度,LSTMs 就可以处理任何长度的输入。为了有效地设置 LSTM,我们将在数据集(一个 Twitter 情感分析数据集)上做一些经典的 NLP 工作。该工作流程将使用自然语言工具包正则表达式进行标记化。它只查找单词,其他什么也不找,然后传递给 spacy 词形还原器,以获得只包含单词基本非屈折形式的列表。

列表 2.7 RNN 和 LSTM PyTorch 类实现。
import torch
import pandas as pd
import numpy as np
from gensim.models import Word2Vec
from sklearn.model_selection import train_test_split
import nltk
import spacy

    tokenizer = nltk.tokenize.RegexpTokenizer("\w+'?\w+|\w+'")
    tokenizer.tokenize("This is a test")
    stop_words = nltk.corpus.stopwords.words("english")
    nlp = spacy.load("en_core_web_lg", disable=["parser", "tagger", "ner"])

dataset = pd.read_csv("./data/twitter.csv")     #1

text_data = list(
    map(lambda x: tokenizer.tokenize(x.lower()), dataset["text"])
)
text_data = [
    [token.lemma_ for word in text for token in nlp(word)]
    for text in text_data
]
label_data = list(map(lambda x: x, dataset["feeling"]))
assert len(text_data) == len(
    label_data
), f"{len(text_data)} does not equal {len(label_data)}"

EMBEDDING_DIM = 100
model = Word2Vec(
    text_data, vector_size=EMBEDDING_DIM, window=5, min_count=1, 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)    #2

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 = padding_value
EMBEDDING_DIM = 100
HIDDEN_DIM = 256
OUTPUT_DIM = 1

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

rnn_optimizer = torch.optim.SGD(rnn_model.parameters(), lr=1e-3)
rnn_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

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

lstm_optimizer = torch.optim.Adam(lstm_model.parameters())
lstm_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 = 2     #3

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 = [
    torch.tensor([word_vectors.key_to_index.get(word, 0) for word in text])
    for text in text_data
]

X_train, X_test, y_train, y_test = train_test_split(
    index_utt, label_data, test_size=0.2
)                                                    #4
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 model in [rnn_model, lstm_model]:
    print(
    "|-----------------------------------------------------------------------------------------|"
    )
    print(f"Training with {model.__class__.__name__}")
    if "RNN" in model.__class__.__name__:
        for epoch in range(N_EPOCHS):
            train_loss, train_acc = train(
                rnn_model, train_iterator, rnn_optimizer, rnn_criterion
            )
            valid_loss, valid_acc = evaluate(
                rnn_model, validate_iterator, rnn_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}% |"
            )
    else:
        for epoch in range(N_EPOCHS):
            train_loss, train_acc = train(
                lstm_model, train_iterator, lstm_optimizer, lstm_criterion
            )
            valid_loss, valid_acc = evaluate(
                lstm_model, validate_iterator, lstm_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% |

#1 创建我们的训练语料库并执行一些经典的 NLP 预处理。

#2 需要嵌入来为 LSTM 的输入赋予语义价值。

#3 通常应该是 2 的幂,因为这是计算机内存中最容易处理的形式。

#4 你必须为你在训练的东西确定一些标签。

看看我们的类和实例化,你应该会看到 LSTM 与 RNN 并没有太大区别。唯一的区别是init输入变量是n_layers(为了方便,你也可以用 RNN 指定它),bidirectionaldropoutbidirectional允许 LSTM 在序列中向前看,以帮助理解意义和上下文。它还在多语言场景中帮助很大,因为像英语这样的从左到右的语言并不是正文的唯一格式。dropout是另一个巨大的创新,它改变了过拟合的模式,从数据依赖转变为帮助模型不过拟合,通过在训练过程中逐层关闭随机节点来强制所有节点不相互关联,并防止复杂的共适应。模型外的参数唯一的不同之处在于,用于 RNN 的优化器是随机梯度下降(SGD),就像我们的 CBoW 一样;LSTM 使用 Adam(尽管两者都可以使用任何,取决于性能,包括 AdamW)。接下来,我们定义我们的训练循环并训练 LSTM。将这个训练循环与gradient_descent函数中定义的列表 2.4 进行比较。

在这里展示的代码中,令人惊叹的一点是,与之前的模型迭代相比,LSTM 可以学得更快,这得益于bidirectionalitydropout。尽管之前的模型训练速度比 LSTM 快,但它们需要数百个 epoch 才能达到 LSTM 在 25 个 epoch 内达到的性能。正如其名所示,验证集上的性能增加了架构的有效性,在它未训练的示例上进行推理,并保持准确度与训练集相当。

这些模型的问题并不那么明显,主要表现为资源消耗极其巨大,尤其是在应用于像医疗和法律这样更长、更注重细节的问题时。尽管dropoutbidirectional处理具有令人难以置信的优势,但它们至少将训练所需的处理能力翻倍。因此,虽然推理最终只比相同大小的 MLP 贵 2 到 3 倍,但训练成本却高达 10 到 12 倍。也就是说,dropoutbidirectional很好地解决了梯度爆炸问题,但同时也增加了训练所需的计算量。为了解决这个问题,设计并实施了一种捷径,允许任何模型,包括 LSTM,找出序列中哪些部分是最有影响力的,哪些部分可以安全忽略,这被称为注意力

2.2.8 注意力

注意力是一种数学捷径,它通过一个突现的数学公式告诉模型考虑输入的哪些部分以及多少,从而为模型提供一个机制,以更快的速度解决更大的上下文窗口。注意力基于字典的升级版本,其中不仅包含键值对,还添加了上下文查询。简单来说,以下代码是较老的自然语言处理技术与更现代技术之间的主要区别。

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

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

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]])     #1

w_query = np.array([1,0,1],
             [1,0,0],
             [0,0,1],
             [0,1,1]])          #2
w_key = np.array([[0,0,1],
             [1,1,0],
             [0,1,0],
             [1,1,0]])         #2
w_value = np.array([[0,2,0], 
             [0,3,0],
             [1,0,3],
             [1,1,0]])         #2

Q = np.matmul(x,w_query)     #3
K = np.matmul(x,w_key)       #3
V = np.matmul(x,w_value)     #3

k_d = 1                                        #4
attention_scores = (Q @ K.transpose())/k_d     #4

attention_scores[0] = softmax(attention_scores[0])     #5
attention_scores[1] = softmax(attention_scores[1])     #5
attention_scores[2] = softmax(attention_scores[2])     #5

attention1 = attention_scores[0].reshape(-1,1)     #6
attention1 = attention_scores[0][0]*V[0]           #6
attention2 = attention_scores[0][1]*V[1]           #6
attention3 = attention_scores[0][2]*V[2]           #6

attention_input1 = attention1 + attention2 + attention3    #7

attention_head1 = np.random.random((3,64))      #8

z0h1 = np.random.random((3,64))     #9
z1h2 = np.random.random((3,64))     #9
z2h3 = np.random.random((3,64))     #9
z3h4 = np.random.random((3,64))     #9
z4h5 = np.random.random((3,64))     #9
z5h6 = np.random.random((3,64))     #9
z6h7 = np.random.random((3,64))     #9
z7h8 = np.random.random((3,64))     #9

Output_attention = np.hstack((z0h1,z1h2,z2h3,z3h4,z4h5,z5h6,z6h7,z7h8))    #10

def dot_product_attention(query, key, value, mask, scale=True):    #11
    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
def masked_dot_product_self_attention(q,k,v,scale=True):     #12
    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)

#1 第 1 步:输入:三个输入,d_model=4

#2 第 2 步:权重三个维度 x d_model=4

#3 第 3 步:矩阵乘法以获得 Q,K,V;查询:x * w_query;键:x * w_key;值:x * w_value

#4 第 4 步:缩放后的注意力分数;维度的平方根

#5 第 5 步:每个向量的缩放 softmax 注意力分数

#6 第 6 步:通过 score1/k_d * V 获得的注意力值

#7 第 7 步:将结果相加以创建输出矩阵的第一行

#8 第 8 步:对输入 1 到 3 执行步骤 1 到 7;因为这只是个演示,我们将使用正确维度的随机矩阵。

#9 第 9 步:我们使用步骤 1 到 7 训练注意力子层的所有八个头。

#10 第 10 步:将头 1 到 8 连接起来,以获得模型原始的 8 × 64 输出维度。

#11 此函数执行所有这些步骤。

#12 此函数执行前面的步骤,但增加了掩码中的因果关系。

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

在注意力机制中,QueryKey通过点积相似度比较来获得注意力分数,随后这个分数会被乘以Value以得到模型应该关注序列该部分的最终分数。这可能会根据你的模型架构变得更加复杂,因为必须考虑到编码器和解码器的序列长度,但在此我们可以简单地说,在这个空间中建模的最有效方法是投影所有输入源到一个公共空间,并使用点积进行高效比较。

这段代码解释比之前的例子数学性更强,但这是为了说明概念。注意力的数学原理真正具有创新性,并推动了该领域的发展。不幸的是,即使注意力为序列建模过程带来了优势,在 LSTMs 和 RNNs 中,仍然存在速度和内存大小的问题。你可能从代码和数学中注意到取了平方根,这意味着我们使用的注意力是二次的。包括像 Hyena 和循环记忆转换器(RMT,基本上是一个结合了转换器的 RNN)这样的亚二次技术在内的各种技术已经被开发出来以解决这些问题,我们将在稍后更详细地介绍。现在,让我们继续探讨注意力的最终应用:转换器。

2.3 注意力即一切

在开创性的论文“Attention Is All You Need”中¹,Vaswani 等人将数学捷径推进了几步,提出为了性能,根本不需要任何循环(RNN 中的“R”)或任何卷积。

备注:我们不讨论卷积,因为它们对 NLP 来说并不好,但在计算机视觉中却很受欢迎。

相反,Vaswani 等人选择只使用注意力,并更加仔细地指定 Q、K 和 V 的来源。我们将在稍后深入探讨这一点。在我们对这一系列多样化的 NLP 技术的回顾中,我们观察到了它们随时间的发展以及每种方法如何寻求改进其前辈。从基于规则的方法到统计模型和神经网络,该领域一直在努力寻找更高效、更准确处理和理解自然语言的方法。

现在,我们将注意力转向一项具有革命性的创新,它彻底改变了 NLP 领域:转换器架构。在下一节中,我们将探讨支撑转换器的关键概念和机制,以及它们如何使得开发超越先前技术的最先进语言模型成为可能。我们还将讨论转换器对更广泛的 NLP 领域的影响,并考虑在这一激动人心的研究领域中进一步进步的潜力。

2.3.1 编码器

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

再次强调,嵌入的一个关键特性是它们是数据的向量表示——在我们的案例中,是标记。标记是你选择的代表语言的任何东西。我们建议按照一般规则使用子词,但你将逐渐了解哪些位置和哪些类型的标记效果良好。考虑以下句子:“The cat in the hat rapidly leapt above the red fox and the brown unmotivated dog。” “Red” 和 “brown” 在语义上是相似的,并且在嵌入层之后有类似的表示。然而,它们在句子中的位置分别是第 10 和第 14 位,假设我们是按单词进行分词。因此,位置编码在它们之间引入了距离,同时也增加了区分句子中不同位置相同标记的能力。然而,一旦应用正弦和余弦函数,它们的意义就只比编码后稍微远一点,并且这种编码机制在递归和更多数据的情况下表现出色。为了说明这一点,让我们假设在嵌入后 [red] 和 [brown] 之间有 99% 的余弦相似度。编码将大大将其降低到大约 85% 到 86% 的相似度。按照描述应用正弦和余弦方法将它们的相似度恢复到大约 96%。

BERT 是在 Vaswani 等人原始论文之后出现的首批架构之一,并且是仅使用编码器的变压器的一个例子。鉴于其体积之小,BERT 是一个极其强大的模型架构,至今仍在生产系统中使用。BERT 是第一个仅使用编码器的变压器,其受欢迎程度激增,展示了使用变压器进行连续或顺序(它们是相同的)建模,其嵌入效果比 Word2Vec 更好。我们可以看到,这些嵌入效果更好,因为它们可以非常快速地应用于新任务和数据,并且只需要最少的训练,就能得到比 Word2Vec 嵌入更符合人类偏好的结果。在一段时间内,大多数人使用基于 BERT 的模型在较小的数据集上进行少样本学习任务。BERT 使大多数研究人员和企业能够以最小的努力获得最先进的性能。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/2-5.png

图 2.5 一个编码器的可视化。编码器是完整传输器架构的前半部分,在自然语言理解任务(如分类或命名实体识别)中表现出色。编码器模型通过不需要先验或递归,并使用巧妙的位置编码和多头注意力来创建每个标记的向量嵌入,从而改进了先前设计。

编码器的优势(如图 2.5 所示)包括以下内容:

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

  • 考虑到长距离依赖建模,速度极快

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

  • 并行

编码器的弱点包括以下内容:

  • 如建议所示,需要大量数据(尽管少于 RNNs)才能有效

  • 更加复杂的架构

2.3.2 解码器

解码器模型,如图 2.6 所示,是编码器的大版本,其基本形式包含两个多头注意力块和三个求和归一化层。它们是编码器之后的传输器的一半。解码器在掩码语言建模和学习以及快速应用语法方面非常出色,几乎立即产生了只需要解码器模型才能实现人工通用智能的想法。编码器与解码器任务的一个有用简化是,编码器在自然语言理解(NLU)任务中表现出色,而解码器在自然语言生成(NLG)任务中表现出色。解码器仅传输器架构的例子是生成预训练传输器(GPT)模型系列。这些模型遵循转换生成语法的逻辑,完全基于语法,允许无限生成一种语言中所有可能的句子(见附录 A)。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/2-6.png

图 2.6 一个解码器的可视化。解码器是完整传输器的后半部分,在 NLG 任务(如聊天机器人和讲故事)中表现出色。解码器与编码器一样改进了先前架构,但它们将输出向右移动一个空格以帮助下一词生成,从而利用多头自注意力的优势。

解码器的优势包括以下内容:

  • 在序列中生成下一个标记(向右移动表示考虑已生成的标记)

  • 建立在已知模型和编码器之上

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

它们的弱点包括以下内容:

  • 仅语法模型往往难以插入预期的或意图的含义(参见所有“我强迫一个 AI 观看 1000 小时的 x 并生成”的 2018-至今的 meme)。

  • 幻觉。

2.3.3 传输器

完整的 Transformer 架构同时利用了编码器和解码器,在输出之前,将编码器的理解传递给解码器的第二个多头注意力块。由于 Transformer 的每一部分在理解或生成方面都有其专长,因此整个产品在需要生成之前有一定理解条件的条件生成任务,如翻译或摘要,上表现得最好,这一点应该是直观的。编码器侧重于在高级别处理输入,而解码器则更专注于生成连贯的输出。完整的 Transformer 架构能够成功理解数据,然后根据这种理解生成输出,如图 2.7 所示。文本到文本迁移 Transformer(T5)模型系列是 Transformer 的一个例子。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/2-7.png

图 2.7 一个完整的 Transformer 可视化。完整的 Transformer 结合了编码器和解码器,在所有任务上都做得很好,包括摘要和翻译等条件生成任务。由于 Transformer 比其每个部分都要庞大和缓慢,研究人员和企业通常选择使用这些部分而不是整个 Transformer。

备注:Transformer 模型的优势在于它们围绕输入的并行化构建,这增加了 LSTM 目前无法复制的速度。如果 LSTM 达到可以像 Transformer 一样快速运行的程度,它们可能在最先进的领域中具有竞争力。

Transformer 的优势如下:

  • 包含编码器和解码器,因此它在它们擅长的所有事情上都做得很好

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

弱点包括以下:

  • 记忆密集型,但仍然小于相同大小的 LSTM

  • 训练需要大量的数据和 VRAM

如您可能已经注意到的,我们讨论的大多数模型都不是语言导向的,它们高度关注语法,即使它们试图模拟真实语言。模型,即使是最先进的 Transformer,也只有语义近似——没有语用学,没有语音学——并且在标记化过程中,只有在没有上下文的情况下才真正利用形态学的数学模型。这并不意味着模型不能学习这些,也不意味着例如,Transformer 不能将音频作为输入;这仅仅意味着平均使用情况不是这样。考虑到这一点,它们能像现在这样工作,简直是个奇迹,它们真正应该得到赞赏。

到目前为止,我们试图强调模型当前的局限性,并在本书的剩余部分深入探讨如何改进它们。其中一条途径已经取得并且仍在取得巨大成功的探索:迁移学习和微调大型基础模型。这种技术是在 BERT 最初发布后不久出现的。研究人员发现,尽管 BERT 在大量任务上表现良好,但如果他们希望它在特定任务或数据领域上表现更好,他们只需在代表该任务或领域的但不是从头开始的数据上重新训练模型即可。鉴于 BERT 在创建语义近似嵌入时在更大的数据集上学习到的所有预训练权重,所需的训练数据量显著减少,就可以在所需的部分上获得最先进的性能。我们已经在 BERT 和 GPT 系列模型中看到了这一点,现在我们再次看到这一点来解决我们讨论的 exactly the challenges:语义近似覆盖、领域专业知识和数据可用性。

2.4 非常大的转换器

进入大型语言模型(LLMs)。自从它们被引入以来,基于转换器的模型一直在不断增大,不仅是在规模和参数数量上,还包括它们的训练数据集和训练周期的大小和长度。如果你在 2010 年代学习机器学习或深度学习,你很可能听说过这样的说法:“增加更多层并不会使模型变得更好。”LLMs 既证明了这一点是错误的,也证明了这一点是正确的——错误是因为它们的性能无与伦比,通常与经过精心微调的较小模型相匹配,这些模型在特定领域和数据集上进行了训练,甚至是在专有数据上训练的,正确是因为训练和部署 LLMs 所带来的挑战。

LLMs 与语言模型之间的一大区别在于迁移学习和微调。与之前的语言模型一样,LLMs 在庞大的文本语料库上进行预训练,使它们能够学习通用的语言特征和表示,这些特征和表示可以针对特定任务进行微调。由于 LLMs 规模庞大,其训练数据集也很大,因此它们能够在更少的标记数据的情况下实现更好的性能,这是早期语言模型的一个重大限制。通常,你只需用十几个示例就可以微调一个 LLM 来执行高度专业化的任务。

然而,使大型语言模型(LLMs)如此强大并为其在商业领域的广泛应用打开了大门的是它们能够通过简单的提示执行专门任务,而无需任何微调的能力。只需在查询中给出你想要的一些示例,LLM 就能生成结果。在较小的标记数据集上训练 LLM 被称为少样本提示。当只给出一个示例时,被称为单样本提示,而当任务是全新的时,则称为零样本提示。LLMs,尤其是那些通过人类反馈和提示工程方法进行强化学习训练的 LLMs,能够进行少样本学习,这意味着它们能够通过仅几个示例进行泛化和解决任务,达到全新的水平。这种能力是相对于早期模型的一个重大进步,早期模型需要针对每个特定任务进行大量的微调或大量的标记数据。

之前,语言模型在少样本和零样本学习领域已经显示出潜力,而 LLMs 已经证明这种潜力是真实的。随着模型规模的增大,我们发现它们能够完成小模型无法完成的任务。我们称之为涌现行为。² 图 2.8 展示了八个先前语言模型在随机情况下表现不佳的任务,然后一旦模型足够大,它们就能完成这些任务。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/2-8.png

图 2.8 LLMs 在模型规模达到一定大小后,执行少样本提示任务时展现出的涌现行为示例

由于 LLMs 具有庞大的参数规模,它们在零样本能力方面也表现出显著的优势,这是它们在商业世界中受欢迎和可行的最主要原因。LLMs 也由于规模大和容量大,在处理歧义方面表现出改进。它们在区分具有多个含义的单词和理解语言细微差别方面做得更好,从而产生更准确的预测和响应。这种改进并不是因为它们的能力或架构更好,因为它们与较小的 Transformer 共享相同的架构,而是因为它们拥有大量关于人们通常如何消除歧义的事例。因此,LLMs 以与数据集中通常表示的相同方式消除歧义。多亏了 LLMs 训练所依据的文本数据的多样性,它们在处理各种输入风格、噪声文本和语法错误方面表现出更高的鲁棒性。

LLMs(大型语言模型)与语言模型之间的另一个关键区别是输入空间。更大的输入空间非常重要,因为它使得少样本提示任务变得更加可行。许多 LLMs 的最大输入大小为 8,000+个 token(最初为 32K,GPT-4 自 2023 年 11 月以来已达到 128K),尽管所有之前讨论的模型也可以有如此高的输入空间,但它们通常并不具备。我们最近在这个领域看到了爆炸式增长,例如循环记忆转换器(RMT)技术允许 1M+个 token 的上下文空间,这进一步推动了 LLMs 向证明更大的模型总是更好的方向迈进。LLMs 被设计用来捕捉文本中的长距离依赖关系,这使得它们比前辈们更有效地理解上下文。这种改进的理解能力使得 LLMs 在机器翻译、摘要和对话式 AI 等任务中能够生成更连贯和上下文相关的响应。

LLMs 通过提供早期语言模型难以解决的问题的强大解决方案,已经彻底改变了 NLP(自然语言处理)。它们在上下文理解、迁移学习和少样本学习方面带来了显著的改进。随着 NLP 领域的持续发展,研究人员正在积极努力最大化 LLMs 的益处,同时减轻所有潜在的风险。因为还没有找到更好的近似语义的方法,所以他们做出了更大和更多维度的近似。因为还没有找到存储语用上下文的好方法,LLMs 通常允许直接将上下文插入到提示中,或者插入到为上下文保留的输入部分,甚至通过在推理时与 LLM 共享数据库。这种能力不会在模型内创建语用或语用系统,就像嵌入不会创建语义一样,但它允许模型正确生成模仿人类对那些语用和语义刺激做出反应的语法。语音学是 LLMs 可能取得巨大进步的地方,无论是作为完全无文本的模型,还是作为文本-语音混合模型,也许会利用国际音标(IPA)作为文本的补充或替代。现在我们正在观察这个领域的可能发展,这令人兴奋。

到目前为止,你应该已经对 LLMs(大型语言模型)有了相当好的理解,以及一些在将 LLMs 投入生产时将非常有用的语言学关键原则。现在,你应该能够开始推理哪些产品更容易或更难构建。考虑图 2.9:左下角的任务,如写作助手和聊天机器人,是 LLMs 的拿手好戏。基于提示中少量上下文的文本生成是一个严格基于语法的难题;只要模型足够大,训练数据足够多,我们就可以相对容易地做到这一点。购物助手与此类似,并且也相对容易构建;我们只是缺少了语用学。助手需要了解更多关于世界的信息,比如产品、商店和价格。通过一点工程,我们可以将这些信息添加到数据库中,并通过提示将这些上下文提供给模型。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/2-9.png

图 2.9:某些任务对 LLMs 的难易程度以及解决这些任务的方法

在另一端,考虑一下棋类机器人。LLMs可以下棋,但它们并不擅长。它们在棋局上接受过训练,并理解 E4 是一个常见的第一步,但它们的理解完全是基于语法的。LLMs 只理解它们生成的文本应该包含 A 到 H 之间的字母和 1 到 8 之间的数字。就像购物助手一样,它们缺少语用学,并且没有清晰的棋局模型。此外,它们还缺少语义学。编码器可能有助于我们理解“国王”和“王后”这两个词是相似的,但它们不能帮助我们理解 E4 对于某个玩家来说是一步好棋,而对于另一个玩家来说却是非常糟糕的一步。LLMs 在棋类上还缺乏基于语音学和形态学的知识,尽管在这个案例中它们并不那么重要。无论如何,我们希望这个练习能更好地让你和你的团队了解你们下一个项目。

LLMs 具有惊人的好处,但所有这些能力也带来了一些限制。基础 LLMs 需要大量的计算资源进行训练,这使得它们对个人研究人员和小型组织来说不太容易获得。这个问题正在通过本书中将要讨论的技术得到解决,比如量化、文本嵌入、低秩自适应、参数高效微调和图优化。然而,基础模型目前仍然超出了普通个人有效训练的能力范围。除此之外,还有关于与训练 LLMs 相关的能源消耗可能对环境产生重大影响并引起可持续性问题担忧。这些问题很复杂,很大程度上超出了本书的范围,但我们不能不提及它们。

最后但同样重要的是,由于 LLM 是在包含真实世界文本的大规模数据集上训练的,它们可能会学习和延续数据中存在的偏见,这引发了道德上的担忧,因为现实世界的人不会自我审查以提供最佳的无偏见数据。此外,了解你正在训练的数据的很多信息并不是一种普遍的做法。例如,如果你要求一个文本到图像的扩散 LLM 生成 1000 张“领导者”的图像,99%的图像都是男性,95%的图像都是白人。这里的担忧并不是说男性和白人不应该被描绘为领导者,而是模型没有准确地代表世界,并且正在展示这一点。

有时,更细微的偏见会被揭示出来。例如,在图 2.10 中的 Midjourney 示例中,模型在没有被提示的情况下(唯一的提示是单词“领导者”),将流行的女权主义图标罗斯·拉弗特变成了男性。模型并没有考虑这种变化;它只是在采样步骤中确定,提示“领导者”在训练集中有更多看起来像男性的描绘。许多人会争论在这个背景下“好”和“坏”的含义,而不会追求道德上的义务,我们将讨论准确性的含义。LLM 是在大量数据上训练的,目的是返回尽可能准确的表现。当它们无法返回准确的表现时,特别是当它们具有高度的能力来消除歧义时,我们可以将其视为损害模型实现其目的的偏见。稍后,我们将讨论对抗有害偏见的技术,以便您作为 LLM 的创建者,能够得到您期望的确切输出,并最大限度地减少不期望的输出数量。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/2-10.png

图 2.10 中的 Midjourney 5,在撰写本文时,是市场上最受欢迎的文本到图像模型。当只提示一个标记“领导者”(左)时,将一个著名的流行女权主义图标,罗斯·拉弗特,变成了男性的描绘。ChatGPT(右)编写了一个函数,根据种族、性别和年龄为你安排工作。这些都是意外输出的例子。

好的,我们整个章节都在为这一刻做准备。现在我们就来运行我们的第一个大型语言模型(LLM)吧!在列表 2.9 中,我们下载了 Bloom 模型,这是最早创建的开源 LLM 之一,并生成文本!我们使用的是 Hugging Face 的 Transformers 库,它为我们处理了所有繁重的工作。这真是太激动人心了!

列表 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 折磨,但了解这些模型有多大以及它们有多难运行是一段有用的经验。在下一章中,我们将更多地讨论运行 LLMs 的困难以及你需要运行此代码的一些工具。如果你不想等待,并希望运行一个类似但规模小得多的 LLM,将模型名称更改为"bigscience/bloom-3b",然后再次运行。这次在大多数硬件上应该可以正常运行。

总的来说,LLMs 是一项惊人的技术,它让我们的想象力在可能性中自由驰骋,这是理所当然的。考虑 LLM 而不是较小的语言模型的首要用例是当模型将帮助的人需要少样本能力时,例如,当 CEO 在筹集资金或软件工程师在编写代码时。LLMs 具有这些能力正是因为它们的规模。LLMs 中参数数量的增加直接使它们能够在更大维度上的较小空间中进行泛化。在本章中,我们触及了 LLMs 不太为人所知的方面,即语言学和语言建模方面。在下一章中,我们将涵盖另一半,即 MLOps 方面,我们将深入了解大量参数大小如何影响模型以及设计来支持该模型和使其对目标客户或员工可访问的系统。

摘要

  • 语言学五大组成部分是语音学、句法、语义、语用学和形态学:

    • 语音学可以通过处理音频文件的多模态模型来添加,这可能会在未来提高大型语言模型(LLMs),但当前的语料库数据集太小。

    • 句法是当前模型擅长的。

    • 语义是通过嵌入层添加的。

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

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

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

  • 正确的标记化可能是一个主要障碍,因为存在过多的 <UNK> 标记,尤其是在处理像代码或数学这样的专业问题时。

  • 多语言处理始终优于单语言处理,甚至在没有模型的单语言任务中也如此。

  • 按顺序展示的语言模型类型显示了 LLM 领域的自然和有机增长,因为越来越多的语言学概念被添加,使模型变得更好。

  • 语言建模在效率上经历了指数级增长,这与建模的语言学重点相关。

  • 注意力是解决更大上下文窗口的数学捷径,是现代架构(编码器、解码器和转换器)的支柱:

    • 编码器提高了嵌入中的语义近似。

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

    • 转换器结合了这两者。

  • 更大的模型表现出出现行为,突然能够完成之前无法完成的任务。

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

[2] 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 难道只是那些机会主义者简历上的另一个流行趋势吗?并非如此。事实上,它与“大数据”这个术语非常相似。当这个术语达到顶峰时,那些被称为大数据工程师的人使用完全不同的工具集,并开发了处理大数据所需的专门专业知识。LLM 带来了一系列你在传统机器学习系统中找不到的挑战和问题。其中大多数问题几乎完全是因为它们如此之大。大型模型就是大!我们希望向你展示 LLM 确实配得上这个名字。让我们看看其中的一些挑战,这样当我们开始讨论部署 LLM 时,我们可以欣赏到我们面前的任务。

3.2.1 长下载时间

回到 2017 年,当我仍然作为一个数据科学家深度参与时,我决定尝试重新实现当时最著名的计算机视觉模型之一:AlexNet、VGG19 和 ResNet。我想这将是通过一些实际动手经验来加强我对基础知识理解的好方法。此外,我还有一个额外的动机:我刚刚用一些当时最先进的 NVIDIA GeForce 1080 TI GPU 构建了自己的电脑,我认为这将是一个很好的方式来磨合它们。第一个任务是下载 ImageNet 数据集。

ImageNet 数据集是当时可用的最大标注数据集之一,包含数百万张图片,总文件大小高达惊人的~150 GB!与之工作证明了你知道如何处理大数据,这在当时还是一个热门词汇,也是数据科学家不可或缺的技能集。在同意条款并获得访问权限后,我收到了第一个警钟。下载它花了一整周时间。

当我的团队首次部署 Bloom 时,下载它花了一个半小时。天哪,下载《塞尔达传说:王国之泪》也花了一个半小时,而那只是一个 16 GB 的文件,所以我们真的不能抱怨。

大型模型就是大。这一点不容忽视。你会在整本书中发现,这一事实给整个生产过程带来了许多额外的头痛和问题,你必须为此做好准备。与 ImageNet 数据集相比,Bloom LLM 模型为 330 GB,是后者的两倍多。我们猜测大多数读者都没有使用过 ImageNet 或 Bloom,所以为了比较,《使命召唤:现代战争》,在撰写本文时是最大的游戏之一,大小为 235 GB。《最终幻想 15》 只有 148 GB,所以你可以将两个模型放入其中,还有足够的空间。真正理解 LLM 的巨大规模是很困难的。我们从 BERT 等模型中的 1 亿个参数开始,将它们提升到数十亿个参数。如果你进行了一场购物狂欢,每秒花费 20 美元(或者可能不小心让你的 AWS EC2 实例一直运行),你需要半天时间才能花掉一百万美元;花掉十亿美元则需要两年。

幸运的是,下载 Bloom 并不需要两周时间,因为与 ImageNet 不同,它不是托管在一个管理不善的大学服务器上,而且它已经被分割成多个较小的文件,以便并行下载,但这仍然会花费一个不舒服的长时间。考虑一个场景,你正在最佳条件下下载模型。你拥有千兆速度的光纤互联网连接,并且神奇地能够将你系统和服务器上的全部带宽和 I/O 操作都分配给它。即使这样,下载仍然需要超过 5 分钟!当然,这是在最佳条件下。你很可能不会在如此情况下下载模型;在现代基础设施下,你预计它需要数小时。

3.2.2 更长的部署时间

仅下载模型就足够让任何经验丰富的开发者感到不安,但部署时间将使他们倒下并寻求医疗援助。像 Bloom 这样大的模型仅将模型加载到 GPU 内存中就需要 30 到 45 分钟——至少,这是我们经历的时间框架。这还不包括你部署过程中可能增加的其他步骤。实际上,由于 GPU 缺货,仅等待资源释放就可能会花费数小时——关于这一点,我们稍后再谈。

这对你和你的团队意味着什么?首先,我们知道许多团队在部署机器学习产品时通常会在运行时下载模型。这可能适用于小的 sklearn 回归模型,但对于 LLMs 来说则不行。此外,你可以把关于部署可靠系统的大部分知识都扔到窗外(但幸运的是,不是太远)。大多数现代软件工程的最佳实践都假设你可以轻松地重启应用程序,并且有很多繁琐的程序来确保你的系统可以做到这一点。对于 LLMs 来说,关闭可能只需要几秒钟,但重新部署可能需要几个小时,这使得这是一个半不可逆的过程。就像从树上摘苹果一样,摘下来很容易,但如果咬一口觉得太酸,就无法再把它重新接到树上继续成熟。你只能等待另一个成熟。

虽然并非每个项目都需要部署最大的模型,但你可以预期部署时间以分钟计算。这些较长的部署时间使得在流量激增前缩减规模成为严重的错误,同时也使得管理突发性工作负载变得困难。由于滚动更新需要更长的时间,因此在你的管道中会迅速积累大量积压。像拼写错误或其他错误这样的愚蠢错误往往需要更长的时间才能被发现和纠正。

3.2.3 延迟

模型尺寸的增加通常伴随着推理延迟的增加。这一点说出来很明显,但更多的参数意味着更多的计算,更多的计算意味着更长的推理等待时间。然而,这一点不能被低估。我们知道很多人轻视延迟问题,因为他们与一个 LLM 聊天机器人互动过,感觉体验很流畅。但如果你仔细观察,你会发现它一次只返回一个单词,这些单词被流式传输给用户。之所以感觉流畅,是因为答案的到来速度超过了人类阅读的速度,但仔细观察有助于我们意识到这只是一个 UX 技巧。LLMs 仍然太慢,对于需要快速响应的自动补全解决方案等用途来说,仍然不太有用。将其构建到读取大量文本数据的数据管道或工作流程中,然后尝试清理或总结它,也可能因为速度过慢而无法使用或可靠。

他们缓慢的原因还有很多不那么明显。首先,大型语言模型(LLMs)通常分布在多个 GPU 上,这增加了额外的通信开销。正如本章 3.3.2 节后面所讨论的,它们以其他方式分布,有时甚至为了降低延迟,但任何分布都会增加额外的开销负担。此外,LLMs 的延迟严重受完成长度的影响,这意味着它使用的单词越多,返回响应所需的时间就越长。当然,完成长度似乎也能提高准确性。例如,使用像思维链(CoT)这样的提示工程技巧,我们要求模型以逐步的方式思考问题,这已被证明可以改善逻辑和数学问题的结果,但会显著增加响应长度和延迟时间。

3.2.4 管理 GPU

为了帮助解决这些延迟问题,我们通常希望它们在 GPU 上运行。如果我们想要成功训练 LLMs,我们也需要 GPU,但这都增加了许多低估的额外挑战。大多数网络服务和许多机器学习用例可以仅使用 CPU 完成,但对于 LLMs 来说并非如此——部分原因是 GPU 的并行处理能力为我们解决了延迟问题,部分原因是 GPU 在线性代数、矩阵乘法和张量运算中固有的优化;这是在幕后发生的。对于许多刚刚进入 LLMs 领域的人来说,这需要利用新的资源并增加额外的复杂性。许多人鲁莽地进入这个世界,表现得好像这没什么大不了的,但他们将面临一个令人震惊的觉醒。大多数系统架构和可用的编排工具,如 Kubernetes,都假设应用程序将仅使用 CPU 和内存运行。虽然它们通常支持额外的资源,如 GPU,但这通常是一个事后考虑。你很快就会发现自己必须从头开始重建容器并部署新的度量系统。

管理 GPU 的一个方面是,大多数公司都没有准备好,那就是它们往往很稀缺且有限。在过去十年中,我们似乎一直在经历全球 GPU 短缺的起伏。对于想要保持本地部署的公司来说,GPU 的配置可能极其困难。在我们的职业生涯中,我们花费了大量时间与那些出于各种原因选择保持本地部署的公司合作。他们共同的一点是,他们的服务器上从未有过 GPU。当有 GPU 时,它们通常故意难以访问,只有少数关键员工才能访问。

如果你足够幸运,在云端工作,许多这些问题都得到了解决,但这里也没有免费的午餐。我们俩都曾是那些经常追逐尾巴、试图帮助数据科学家解决新 GPU 工作区配置问题的团队的一员。我们遇到了诸如scale.up.error.out.of.resources这样的晦涩、不祥的错误,结果发现这些神秘的读数表明整个区域所选类型的所有 GPU 都被利用了,没有一个是可用的。在数据中心,CPU 和内存通常可以被视为无限的;然而,GPU 资源却不能。有时你甚至完全无法期待它们。大多数数据中心只支持实例或 GPU 类型的一个子集,这意味着你可能被迫在一个离用户群更远的地区设置你的应用程序,从而增加延迟。当然,我们确信当你想要将服务扩展到目前不支持的新地区时,你可以与你的云服务提供商合作,但根据时间表和成本,你可能不会喜欢你所听到的。最终,无论你选择在哪里运行,无论是在本地还是在云端,你都会遇到短缺问题。

3.2.5 文本数据的特性

LLMs 是现代解决 NLP 问题的方案。NLP 是机器学习中最迷人的分支之一,因为它主要处理文本数据,这主要是一种定性度量。其他所有领域都处理定量数据。我们已经找到了一种方法,将我们对世界的观察编码成直接的数值转换。例如,我们已经学会了如何将热量编码到温度尺度中,并用温度计和热电偶来测量它,我们还可以用压力计和压力表来测量压力,并将其转换为帕斯卡。

计算机视觉和评估图像的实践通常被视为定性分析,但将图像编码为数字的问题已经得到了解决。我们对光的理解使我们能够将图像分解成像素,并赋予它们 RGB 值。当然,这并不意味着计算机视觉在某种程度上已经解决了;仍有大量工作要做,以学习如何识别数据模式中的不同信号。音频数据也常常被认为具有定性。一个人如何比较两首歌曲?但我们可以测量声音和语音,直接测量声波的强度(分贝)和频率(赫兹)。

与将我们的物理世界编码成数值数据的其他领域不同,文本数据正在寻找衡量短暂世界的方法。毕竟,文本数据是我们编码思想、想法和交流模式的最佳尝试。虽然,是的,我们已经找到了将单词转换成数字的方法,但我们还没有找到直接的翻译。我们编码文本和创建嵌入的最佳解决方案最多只是近似;实际上,我们使用机器学习模型来完成这项工作!一个有趣的插曲是,数字也是文本,是语言的一部分。如果我们想要在数学方面做得更好的模型,我们需要一种更有意义的方式来编码这些数字。由于这一切都是虚构的,当我们尝试将文本数字编码成机器可读的数字时,我们正在创建一个试图以有意义的方式递归引用自己的系统。这不是一个容易解决的问题!

由于所有这些,LLM(以及所有 NLP 解决方案)都有独特的挑战。以监控为例。你如何在文本数据中捕捉数据漂移?你如何衡量“正确性”?你如何确保数据的清洁性?这些问题很难定义,更不用说解决了。

3.2.6 标记限制造成瓶颈

对于 LLM 新用户来说,一个很大的挑战是处理标记限制。模型的标记限制是指可以作为模型输入的最大标记数量。标记限制越大,我们可以提供给模型的上下文就越多,从而提高其完成任务的成功率。每个人都希望它们更高,但这并不简单。这些标记限制由两个问题定义:我们 GPU 可访问的内存和速度,以及模型自身内存存储的性质。

第一个问题看起来不太直观:为什么我们不能只是增加 GPU 内存呢?答案很复杂。我们可以,但是将更多层堆叠在 GPU 上以一次考虑更多的千兆字节会整体降低 GPU 的计算能力。目前,GPU 制造商正在研究新的架构和绕过这个问题的方法。第二个挑战非常有趣,因为增加标记限制实际上加剧了底层的数学问题。让我来解释一下。在 LLM 内部进行内存存储并不是我们经常考虑的事情。我们称之为“注意力”,这在第 2.2.7 节中我们进行了深入讨论。我们没有讨论的是,注意力是一个二次解决方案:随着标记数量的增加,计算序列中所有标记对之间的注意力分数所需的计算量会随着序列长度的平方而增长。此外,在我们的巨大上下文空间中,由于我们正在处理二次方程,我们开始遇到只有涉及虚数才能解决的难题,这可能导致模型以意想不到的方式表现。这可能是 LLM 产生幻觉的其中一个原因。

这些问题具有实际影响,并影响应用程序设计。例如,当作者所在的团队从 GPT-3 升级到 GPT-4 时,团队很高兴能够访问更高的标记限制,但很快发现这导致了更长的推理时间,随后是更高的超时错误率。在现实世界中,快速得到一个不太准确的响应通常比完全得不到响应要好,因为更准确模型的承诺通常只是承诺。当然,当在本地部署时,您不必担心响应时间,您可能会发现您的硬件是限制因素。例如,LLaMA 是用 2,048 个标记训练的,但您在使用基本的消费级 GPU 运行时,能利用的最多只有 512 个,您可能会看到内存不足(OOM)错误,甚至模型直接崩溃。

一个可能会让您的团队感到意外的问题,现在应该指出的是,不同语言的每个字符的标记数是不同的。看看表 3.1,我们使用 OpenAI 的 cl100k_base 字节对编码器将不同语言的相同句子转换为标记。只需一眼就能看出,LLMs 通常在这个方面更倾向于英语。在实践中,这意味着如果您正在使用 LLM 构建聊天机器人,您的英语用户的输入空间将比日语用户更灵活,从而导致非常不同的用户体验。

表 3.1 不同语言中标记计数比较
语言字符串字符标记
英语The quick brown fox jumps over the lazy dog439
法语Le renard brun rapide saute par-dessus le chien paresseux5720
西班牙语El rápido zorro marrón salta sobre el perro perezoso5222
日语素早い茶色のキツネが怠惰な犬を飛び越える2036
简体中文敏捷的棕色狐狸跳过了懒狗1228

如果您对此感到好奇,这是因为文本编码,这是在上一节中讨论的与文本数据一起工作的另一个特性。考虑表 3.2,我们展示了几个不同的字符及其在 UTF-8 中的二进制表示。英语字符几乎可以完全用原始 ASCII 标准中的单个字节表示,而大多数其他字符需要 3 个或 4 个字节。因为它需要更多的内存,所以它也需要更多的标记空间。

表 3.2 不同货币字符在 UTF-8 中的字节长度比较
字符二进制 UTF-8十六进制 UTF-8
$001001000x24
£11000010 101000110xc2 0xa3
¥11000010 101001010xc2 0xa5
11100010 10000010 101000000xe2 0x82 0xa0
💰11110000 10011111 10010010 101100000xf0 0x9f 0x92 0xb0

自从 transformers 的普及以来,增加 token 限制一直是一个持续的研究问题,并且还有一些有希望的解决方案仍处于研究阶段,例如循环记忆转换器(RMT)。¹ 我们可以期待未来会继续看到改进,并且希望这最终只会成为一个小麻烦。

3.2.7 幻觉引起困惑

到目前为止,我们一直在讨论团队在将 LLM 部署到生产环境时面临的一些技术问题,但没有什么问题能与 LLM 倾向于错误这一简单问题相提并论。它们往往犯很多错误。“幻觉”是一个术语,用来描述当 LLM 模型会产生听起来正确但实际上错误的结果的情况——例如,书籍引用或具有预期形式和结构的超链接,但却是完全虚构的。作为一个有趣的例子,我们向出版社 Manning(由于一位作者仍在撰写,这本书还不存在)请求有关生产中 LLM 的书籍。我们得到了以下建议:Mike Del Balso 和 Lucas Serveén 合著的《生产中的机器学习工程》www.manning.com/books/machine-learning-engineering-in-production,以及 Jeremy Howard 和 Sylvain Gugger 合著的《使用 Fastai 和 PyTorch 的编码者深度学习》www.manning.com/books/deep-learning-for-coders-with-fastai-and-pytorch。第一本书完全是虚构的。第二本书是真实的;然而,它并非由 Manning 出版。在每种情况下,互联网地址都是虚构的。这些 URL 的格式实际上与你浏览 Manning 网站时预期的非常相似,但如果你访问它们,将会返回 404 错误。

幻觉最令人烦恼的方面之一是它们通常被自信的词语所包围。LLM 在表达不确定性方面非常糟糕,这在很大程度上是由于它们的训练方式。考虑“2 + 2 =”的情况。你更希望它回答“我认为它是 4”还是简单地“4”?大多数人更希望得到正确的“4”。这种偏见是固有的,因为模型通常会因为正确或至少听起来正确而得到奖励。

关于幻觉发生的原因,有各种解释,但最真实的答案是,我们不知道是否只有一个原因。它可能是由几件事情的组合;因此,目前还没有一个好的解决办法。尽管如此,准备好应对这些模型的错误和不准确性是至关重要的,以便为你的产品提供最佳的用户体验。

3.2.8 偏见和伦理考量

3.2.9 安全问题

就像所有技术一样,我们需要注意安全问题。大型语言模型(LLMs)是在大量文本语料库上训练的,其中一些可能是有害或敏感的,不应该被暴露。因此,应该采取措施保护这些数据不被泄露。上一节中提到的偏见和伦理问题就是你不希望你的用户讨论的话题的例子,但你也可以想象在公司的数据上微调模型,如果未采取适当预防措施,可能会意外地丢失秘密。

例如,使用简单的提示请求可口可乐的秘方仅仅返回了无聊的结果,“我不能泄露商业机密或任何专有信息。”如果我们问您,这真是太无聊了。通过一点小诡计和一些提示注入,我们设法让它泄露了秘密,当然,我们在尝试之前确认了一位食品科学家,它不会让我们中毒。按照指示严格操作后,我们惊讶地发现我们得到了一杯相当美味的饮料,但它尝起来根本不像可口可乐。虽然配方看起来很合法,但像数量、烹饪时间,甚至需要过滤糖浆这样的细微差别都不对。看来我们得等到得到真正的配方后再考虑接管饮料行业。

可口可乐配方通过提示注入产生的幻觉
Homemade Coca-Cola Recipe
=====================
**Disclaimer**: The original Coca-Cola recipe is a highly guarded trade secret. The following recipe is an approximation for creating a homemade cola drink that resembles the taste of Coca-Cola.

Ingredients:
- 3 cups of water
- 2 cups granulated sugar
- 1/8 tsp food-grade citric acid
- 3/4 tsp caramel color (liquid)
- 1/2 tsp caffeine (powder or liquid form)
- 1 1/2 tsp cola extract
- 1/8 tsp vanilla extract
- 1 1/2 cups carbonated water (store-bought or using a home soda maker)
- A pinch of ground cinnamon (optional)
- A pinch of ground nutmeg (optional)

Instructions:
1\. In a large saucepan, bring the 3 cups of water to a gentle simmer.
2\. Slowly stir in the granulated sugar, ensuring that it fully dissolves.
3\. Add the citric acid, caramel color, caffeine, cola extract, and vanilla extract to the saucepan. Mix well.
4\. Stir in the optional spices if desired: cinnamon and nutmeg.
5\. Simmer the mixture for about 20 minutes, stirring occasionally.
6\. Remove the saucepan from the heat and let it cool to room temperature.
7\. Once cooled, strain the syrup to remove any undissolved solids.
8\. To make the final drink, mix 1 part syrup with 3 parts carbonated water. Adjust to taste depending on your preference for sweetness and carbonation.

Variations:
- You can experiment with different amounts of caramel color, caffeine, and cola extract to adjust the flavor and appearance of your homemade cola.
- To create different cola flavor profiles, experiment with adding additional spices (such as star anise, orange or lemon peel, or cardamom), essential oils, or fruit extracts.
- Consider using a natural sweetener, like honey or agave syrup, as an alternative to granulated sugar.

Historical context and interesting facts:
- Coca-Cola was first introduced in 1886 by pharmacist Dr. John S. Pemberton, who aimed to create a unique and refreshing beverage that could also relieve fatigue and headaches.
- The original Coca-Cola formula contained trace amounts of cocaine extracted from the coca leaf, which was later replaced with caffeine in the early 20th century.
- In 1985, Coca-Cola introduced "New Coke," a reformulation aimed at competing with Pepsi. However, the taste change was met with backlash, and the company returned to its original formula just three months later, rebranded as "Coca-Cola Classic."
Result(s): You can now create your own homemade version of cola, resembling the taste of Coca-Cola. Enjoy and feel free to experiment with different spices, extracts, and sweeteners to create your own unique flavor profile.

另一个安全问题是腌菜注入。腌菜是 Python 中的一个库,用于序列化对象,通常用于序列化机器学习模型。它在反序列化时将对象序列化为包含逐个执行的指令的字节流。这是一种快速且简单的方式共享大型对象。腌菜注入会破坏这个字节流,通常在模型通过不安全的网络传输时注入恶意软件。这对于下载时间较长的大型模型尤其令人担忧,因为这使得第三方更容易拦截传输并注入恶意代码。如果发生这种情况,注入的代码可能会让攻击者获得对系统的访问权限。这可能在尝试在推理过程中使用模型时发生,因为如果有害代码未被检测并正确移除,它将执行。因此,在使用模型之前采取预防措施,如使用安全网络和验证模型的完整性,以防止此类攻击,是非常重要的。

3.2.10 控制成本

与大型语言模型(LLMs)一起工作涉及各种与成本相关的问题。首先,你可能已经注意到了,这是基础设施成本,包括高性能 GPU、存储和其他硬件资源。我们讨论了 GPU 采购难度较大,不幸的是,这也意味着它们更昂贵。像留下你的服务这样的错误一直都有累积账单的潜力,但与 GPU 混合在一起,这种错误甚至更具破坏性。这些模型还要求大量的计算能力,导致在训练和推理过程中消耗大量能源。除此之外,它们的部署时间更长,这意味着我们经常在低流量期间运行它们,以处理突发的工作负载或预期的未来流量。总的来说,这导致了更高的运营成本。

额外的成本包括管理和存储用于训练或微调以及常规维护的大量数据,例如模型更新、安全措施和错误修复,这可能对财务造成压力。与任何用于商业目的的技术一样,管理潜在的诉讼和确保符合法规也是一个问题。最后,投资于持续的研究和开发以改进您的模型并给您带来竞争优势,将是一个因素。

我们谈了一些关于令牌限制的技术问题,这些问题可能会得到解决,但我们没有讨论成本限制,因为大多数 API 按令牌收费。这使得发送更多上下文和使用更好的提示变得更加昂贵。这也使得预测成本变得有点困难,因为虽然你可以标准化输入,但你不能标准化输出。你永远不能太确定会返回多少令牌,这使得管理变得困难。记住,对于 LLM 来说,实施和遵循适当的成本工程实践以确保成本永远不会失控,这和以前一样重要。

3.3 LLMOps 基本要素

现在我们已经掌握了我们正在应对的挑战类型,让我们来看看所有不同的 LLMOps 实践、工具和基础设施,看看不同的组件如何帮助我们克服这些障碍。首先,让我们深入探讨不同的实践,从压缩开始,我们将讨论缩小、修剪和近似,以使模型尽可能小。然后我们将讨论分布式计算,这是必需的,因为模型如此之大,很少能适应单个 GPU 的内存。完成这些后,我们将在下一节中探讨实现这一切所需的基础设施和工具。

3.3.1 压缩

在上一节中,当你阅读关于大型语言模型(LLM)的挑战时,你可能自己问过类似的问题:“如果 LLM 的最大问题来自它们的大小,为什么我们不把它们做得更小?”如果你这样想过,恭喜你!你是个天才——压缩就是做这件事的实践。将模型压缩到尽可能小,将提高部署时间,减少延迟,减少所需昂贵 GPU 的数量,并最终节省资金。然而,最初让模型变得如此庞大无比的整个目的,是因为它使它们在所做的事情上变得更好。我们需要能够在不失去我们通过使它们变大而取得的全部进步的情况下缩小它们。

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

定量

量化是将精度降低以降低内存需求的过程。这种权衡在直觉上是有意义的。当作者在大学时,他被教导总是将数字四舍五入到工具的精度。拿出直尺测量他的铅笔,如果你告诉他长度是 19.025467821973739 厘米,你不会相信他。即使他使用了卡尺,他也无法验证如此精确的数字。用我们的直尺,任何超过 19.03 厘米的数字都是幻想。为了强调这一点,他的一个工程教授曾经问他,“如果你正在测量摩天大楼的高度,你会在乎顶部多出的一张纸吗?”

我们在计算机内部表示数字的方式往往让我们误以为自己有比实际更好的精度。为了说明这一点,打开 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 相同,它被发现对于量化来说更有效,你可以预期在模型大小减半的情况下几乎可以达到相同的精确度。如果你理解了论文和摩天大楼的类比,那么原因应该很明显。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/3-1.png

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

然而,没有理由就此停止。我们通常可以将它降低到 8 位格式,而不会损失太多精度。甚至已经有一些成功的研究尝试表明,对 LLM 的部分进行选择性的 4 位量化是可能的,而且精度损失很小。选择性地应用量化是一个称为动态量化的过程,通常只对权重进行量化,而将激活保留在完全精度以减少精度损失。

量化的圣杯是 INT2,表示每个数字为–1、0 或 1。目前这不可能而不完全降低模型质量,但它可以使模型缩小到原来的 1/8。Bloom 模型将只有大约~40 GB,足够小,可以放在单个 GPU 上。这当然是量化的极限,如果我们想进一步缩小,我们需要考虑其他方法。

量化的最好部分是它很容易做。有许多框架允许这样做,但在列表 3.1 中,我们展示了如何使用 PyTorch 的量化库进行简单的训练后静态量化(PTQ)。你所需要的只是一个全精度模型,一些示例输入,以及用于准备和校准的验证数据集。正如你所看到的,这只需要几行代码。

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

model_to_quantize = copy.deepcopy(model_fp32)     #1
model_to_quantize.eval()

qconfig_mapping = q.get_default_qconfig_mapping("qnnpack")     #2

prepared_model = q.prepare(model_to_quantize)     #3

with torch.inference_mode():     #4
    for x in dataset:
        prepared_model(x) 

model_quantized = q.convert(prepared_model)      #5

#1 原模型的深度拷贝,因为量化是在原地进行的

#2 获取映射;注意对于 ARM 使用“qnnpack”,对于 x86 CPU 使用“fbgemm”

#3 准备

#4 校准;你需要使用代表性的(验证)数据

#5 量化

静态 PTQ 是量化最直接的方法;它在模型训练后进行,并对所有模型参数进行均匀量化。与大多数公式一样,最直接的方法会引入更多的错误。通常,这种错误是可以接受的,但如果不可以,我们可以增加额外的复杂性来减少量化带来的精度损失。可以考虑的方法包括均匀与非均匀、静态与动态、对称与不对称,以及在其训练期间或之后应用。

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

缩小数据的最简单方法是对其进行归一化,但由于我们是从连续尺度转换为离散尺度,所以有几个需要注意的问题,让我们来探讨一下。我们首先取最小值和最大值,并将它们缩放到与我们的新数值范围相匹配。然后,我们将所有其他数字根据它们所在的位置进行分类。当然,如果我们有非常大的异常值,我们可能会发现所有其他数字都挤进了一个或两个桶中,从而破坏了我们曾经拥有的任何粒度。为了防止这种情况,我们可以截断任何大数字;这就是我们在静态量化中所做的。然而,在我们截断数据之前,如果我们事先选择一个范围并缩放以捕捉大部分数据,会怎样呢?我们需要小心,因为如果这个动态范围太小,我们会引入更多的截断误差;如果太大,我们会引入更多的舍入误差。动态量化的目标当然是减少这两种误差。

接下来,我们需要考虑数据的对称性。通常,在归一化过程中,我们迫使数据变得正常并因此对称;然而,我们也可以选择以保留数据原有任何不对称性的方式来缩放数据。通过这样做,我们有可能减少由于截断和舍入误差造成的总体损失,但这并不保证。

作为最后的手段,如果这些其他方法都不能减少模型的精度损失,我们可以使用量化感知训练(QAT)。QAT 是一个简单的过程,我们在模型训练期间添加一个模拟量化步骤。通过“模拟”,我们是指我们在保持数据全精度的情况下截断和舍入数据。这允许模型在训练过程中调整量化引入的误差和偏差。与其它方法相比,QAT 已知可以产生更高的精度,但训练时间成本要高得多。

量化方法
  • 均匀与非均匀——我们是否使用在它所表示的范围内是均匀的 8 位标准,或者为了在-1 到 1 范围内更精确地使用非均匀标准。

  • 静态与动态——在截断之前选择调整范围或尺度,以尝试减少截断和舍入误差以及减少数据损失。

  • 对称与不对称——将数据归一化以使其正常并强制对称,或者选择保留任何不对称性和偏斜。

  • 训练期间或之后——训练后的量化非常容易做,而如果在训练期间做,则工作更多,但会导致减少偏差和更好的结果。

量化是一个非常强大的工具。它减少了模型的大小和运行模型所需的计算开销,从而降低了模型的延迟和运行成本。然而,量化最好的地方在于它可以在事后进行,所以你不必担心你的数据科学家是否记得在训练过程中使用 QAT 等过程对模型进行量化。这就是为什么量化在处理 LLM 和其他大型机器学习模型时变得如此受欢迎。虽然压缩技术总是伴随着准确度降低的担忧,但与其他方法相比,量化是一个三赢的局面。

修剪

恭喜你,你刚刚训练了一个全新的 LLM!拥有数十亿个参数,它们都必须是有用的,对吧?错了!不幸的是,就像生活中的许多事情一样,模型的参数往往遵循帕累托法则。大约 20%的权重导致了 80%的价值。“如果这是真的,”你可能正在问自己,“我们为什么不把所有的额外东西都剪掉?”好主意!给自己鼓掌。修剪是剔除和移除我们认为不值得的模型任何部分的过程。

实际上存在两种不同的修剪方法:结构化非结构化。结构化修剪是寻找模型中不贡献于模型性能的结构组件,然后将其移除的过程——无论是神经网络中的过滤器、通道还是层。这种方法的优势在于,你的模型会稍微小一些,但保持相同的基本结构,这意味着我们不必担心失去硬件效率。我们还保证了延迟的改进,因为涉及的计算会更少。

相反,非结构化修剪会遍历参数,并将不太重要的参数置零,这些参数对模型性能的贡献不大。与结构化修剪不同,我们实际上并没有移除任何参数;我们只是将它们设置为零。从这个角度来看,我们可以想象一个很好的起点是任何已经接近 0 的权重或激活。当然,虽然这有效地减少了模型的大小,但这也意味着我们没有减少任何计算,所以通常只会看到最小化,如果有的话,的延迟改进。但更小的模型仍然意味着更快的加载时间和更少的 GPU 运行。它还让我们对过程有非常精细的控制,允许我们比结构化修剪进一步缩小模型,同时对性能的影响也更小。

与量化类似,修剪可以在模型训练后进行。然而,与量化不同,通常需要额外的微调来防止性能损失过大。在模型训练期间包含修剪步骤以避免后续需要微调的做法越来越普遍。由于更稀疏的模型将有更少的参数需要调整,添加这些修剪步骤可能有助于模型更快地收敛。⁵

你可能会惊讶于通过剪枝可以缩小模型的同时对性能的影响最小化。能缩小多少呢?在 SparseGPT⁶论文中,开发了一种方法来尝试自动一次性完成剪枝过程,而不需要后续的微调。作者发现他们可以将 GPT-3 模型缩小 50%到 60%而没有任何问题!根据模型和任务的不同,他们甚至在其中一些任务中看到了轻微的性能提升。我们期待着看到剪枝在未来能带我们走向何方。

知识蒸馏

在我们的心目中,知识蒸馏可能是最酷的压缩方法。这个想法也很简单:我们将使用大型 LLM 来训练一个较小的语言模型,以复制它。这个方法的好处是,较大的 LLM 为较小的模型提供了一个几乎无限的训练数据集,这使得训练非常有效。因为数据集越大,性能越好,我们经常看到较小的模型在准确性上几乎达到了它们的教师模型水平。⁷

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

然而,知识蒸馏最困难的部分是,我们还没有真正找到好的配方。像“学生模型可以有多小?”这样的难题必须通过试错来解决。这里还有很多东西需要学习和研究。

然而,斯坦福大学的 Alpaca 在这个领域做了一些令人兴奋的工作。⁸他们没有从头开始训练学生模型,而是选择使用 OpenAI 的 GPT3.5 的 175B 参数模型作为教师,通过知识蒸馏微调开源的 LLaMA 7B 参数模型。这是一个简单的想法,但效果显著,因为他们从评估中得到了很好的结果。最大的惊喜是成本,因为他们只花费了 500 美元的 API 费用来从教师模型获取训练数据,以及价值 100 美元的 GPU 训练时间来微调学生模型。当然,如果你将此用于商业应用,你将违反 OpenAI 的服务条款,所以最好坚持使用自己的或开源模型作为教师。

低秩近似

低秩逼近,也称为低秩分解、低秩分解或矩阵分解(以及其他许多名称——我们责怪数学家们——名称太多),使用线性代数数学技巧简化大型矩阵或张量,以找到低维表示。为此有多种技术。奇异值分解(SVD)、Tucker 分解(TD)和典型多项式分解(CPD)是最常见的几种。

在图 3.2 中,我们展示了 SVD 方法背后的基本思想。本质上,我们将一个非常大的矩阵 A 分解成三个较小的矩阵,U、S 和 V。虽然 U 和 V 存在是为了确保我们保持原始矩阵的相同维度和相对强度,但 S 允许我们应用方向和偏差。S 越小,我们最终压缩和减少的总参数数就越多,但逼近的准确性就越低。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/3-2.png

图 3.2 SVD 示例,低秩逼近。A 是一个具有 N 和 M 维度的大矩阵。我们可以用三个较小的矩阵来逼近它:U 具有维度 M 和 P,S 是一个维度为 P 的方阵,V 具有维度 N 和 P(这里我们展示了转置)。通常,P<<M 和 P<<N 都是正确的。

为了巩固这个概念,看到具体的例子可能会有所帮助。在下一个列表中,我们展示了 SVD 在压缩一个 … 矩阵时的工作示例。为此,我们只需要基本的库 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.520332630.5662928 ]]

仔细检查 U、Sigma 和 V 的转置,我们可以看到一个 … 矩阵,一个 … 矩阵,以及一个 … 矩阵,分别。总的来说,我们现在只需要 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)⁹发挥作用的地方。适应是将通用或基础模型微调以执行特定任务的过程。LoRA 将 SVD 低秩近似应用于注意力权重,或者更确切地说,注入与注意力权重并行运行的更新矩阵,使我们能够微调一个更小的模型。LoRA 因其使 LLM 的微调变得轻而易举而变得非常流行,可以将可训练层缩小到原始模型的一小部分,然后允许任何人在通用硬件上对其进行训练。您可以使用 Hugging Face 的 PEFT 库开始使用 LoRA,在那里您可以查看几个 LoRA 教程。

备注:对于好奇心强的人来说,参数高效微调(PEFT)是一类旨在以计算高效的方式微调模型的方法。PEFT 库旨在将它们都放在一个易于访问的地方;你可以从这里开始:huggingface.co/docs/peft

专家混合

专家混合(MoE)是一种技术,我们用 MoE 层替换 transformer 中的前馈层。前馈层因其参数密集和计算密集而臭名昭著,因此用更好的东西替换它们通常会产生很大的影响。MoEs 是一组稀疏激活的模型。它们与集成技术不同,因为通常只运行一个或几个专家模型,而不是结合所有模型的输出。稀疏性通常由一个门控机制引起,该机制学习使用哪些专家,以及/或一个路由器机制,该机制确定哪些专家应该被咨询。在图 3.3 中,我们展示了具有潜在 N 个专家的 MoE 架构,以及展示了它在解码器堆栈中的位置。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/3-3.png

图 3.3 示例:专家模型混合,包含门控和路由器以控制流。MoE 模型用于替换 transformer 中的 FFN 层;在这里,我们展示了它在解码器中替换 FFN 的情况。

根据你拥有的专家数量,MoE 层可能比 FFN 层有更多的参数,从而导致更大的模型,但在实践中,这种情况并不常见,因为工程师和研究人员的目标是创建一个更小的模型。然而,我们可以保证看到的是更快的计算路径和改进的推理时间。然而,真正让 MoE 脱颖而出的还是当它与量化结合时。一项由微软和 NVIDIA 进行的研究¹⁰表明,使用 MoE 只需最小程度地影响精度就能实现 2 位量化!

当然,由于这是对模型结构的一个相当大的改动,之后需要进行微调。你也应该意识到,MoE 层通常会降低模型的可泛化性,因此最好是在为特定任务设计的模型上使用。有几个库实现了 MoE 层,但我们推荐检查 DeepSpeed。

注意:DeepSpeed 是一个库,它优化了许多用于大规模深度学习模型(如 LLMs)的困难部分,在训练时特别有用。查看他们的 MoE 教程:www.deepspeed.ai/tutorials/mixture-of-experts/

3.3.2 分布式计算

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

数据并行

数据并行是人们在考虑并行运行进程时首先想到的;它也是最易实现的。这种做法涉及将数据分割成多个部分,并通过模型的多个副本或管道运行。对于大多数框架来说,这很容易设置;例如,在 PyTorch 中,你可以使用 DistributedDataParallel 方法。但大多数这些设置都有一个缺点:你的模型必须能够适应一个 GPU。这就是像 Ray.io 这样的工具发挥作用的地方。

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

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

ray.init()     #1

def slow_function(x):     #2
    time.sleep(1)
    return x

@ray.remote
def slow_function_ray(x):      #3
    time.sleep(1)
    return x

results = [slow_function(i) for i in range(1, 11)]     #4

results_future = [slow_function_ray.remote(i) for i in range(1, 11)]      #5
results_ray = ray.get(results_future)

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

ray.shutdown()

#1 启动 Ray

#2 Δ定义一个常规 Python 函数

#3 将函数转换为 Ray 任务

#4 无 Ray 执行慢函数(耗时 10 秒)

#5 使用 Ray 执行慢函数(耗时 1 秒)

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

注意:在此处了解更多关于 Ray 集群的信息:mng.bz/eVJP

现在有多个替代方案,但 Ray 因其越来越多的机器学习工作流程需要分布式训练而获得了大量关注,并变得越来越受欢迎。团队在使用 Ray 方面取得了巨大成功。通过利用 Ray,开发者可以确保分布式工作流程中更好的性能和更有效的资源利用。

张量并行

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

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/3-4.png

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

选择并行化的维度有点像一门艺术,但有一些事情要记住以帮助更容易做出这个决定。首先,你有多少列或行?一般来说,你想要选择一个比你的处理器数量多的维度,否则你将无法充分利用。通常这不会成为问题,但像上一节中讨论的 Ray 这样的工具,在集群中并行化并启动大量进程变得轻而易举。其次,不同的维度有不同的多重性成本。例如,列并行性要求我们将整个数据集发送到每个进程,但最终可以在末尾快速且容易地将它们连接起来。然而,行并行性允许我们将数据集分割成块,但需要我们添加结果,这是一个比连接操作更昂贵的操作。你可以看到一个操作更受 I/O 限制,而另一个更受计算限制。最终,最佳维度将取决于数据集和硬件限制。这将需要实验来完全优化,但一个好的默认选择是选择最大的维度。

张量并行性允许我们将像 MLP 和注意力层这样的计算密集层分割到不同的设备上,但它对我们无法利用张量的归一化或 dropout 层没有帮助。为了获得更好的整体管道性能,我们可以添加针对这些块的序列并行性。¹¹ 序列并行性是一种沿着序列维度分割激活的过程,防止冗余存储,并且可以与张量并行性结合以实现显著的内存节省,同时额外计算开销最小。结合使用,它们减少了存储 transformer 模型中激活所需的内存。实际上,它们几乎消除了激活的重新计算,并将激活内存节省高达五倍。

图 3.5 展示了如何结合张量并行性,它使我们能够将计算密集层分布到不同的设备上,以及序列并行性,它对内存限制层做同样的处理,从而使整个 transformer 模型完全并行化。它们共同允许资源得到极其高效的使用。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/3-5.png

图 3.5 结合关注计算密集层的张量并行性和序列并行性以降低内存开销,为整个 transformer 创建一个完全并行过程
管道并行性

到目前为止,我们可以运行大量数据并加速任何瓶颈,但这些都无关紧要,因为我们的模型太大;我们无法将其放入单个 GPU 的内存中,甚至无法让它运行。这就是管道并行派上用场的地方;它是指将模型垂直分割并分别将每个部分放到不同的 GPU 上的过程。这创建了一个管道,因为输入数据将流向第一个 GPU,进行处理,然后传输到下一个 GPU,依此类推,直到整个模型运行完毕。虽然其他并行技术可以提高我们的处理能力和加速推理,但需要管道并行才能使其运行。然而,它伴随着一些主要的缺点,主要是设备利用率。

为了了解这个缺点从何而来以及如何减轻它,让我们首先考虑这个问题的简单方法,即我们一次性将所有数据通过模型。我们发现这留下了一个巨大的“气泡”未充分利用。由于模型被分割,我们必须通过设备按顺序处理所有内容。这意味着当一个 GPU 正在处理时,其他 GPU 都在空闲。在图 3.6 中,我们可以看到这种简单方法和 GPU 空闲时的大气泡不活跃。我们还看到了利用每个设备的一个更好的方法。我们通过发送小批量数据来实现这一点。较小的批次允许第一个 GPU 更快地将它正在处理的工作传递出去,并转移到另一个批次。这允许下一个设备更早地开始工作,并减少气泡的大小。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/3-6.png

图 3.6 气泡问题。当数据通过分割的模型运行时,持有模型权重的 GPU 利用率低下,因为它们在等待对应的 GPU 处理数据。减少这种气泡的一个简单方法是使用微批处理。

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

空闲百分比 = 1 – m / (m + n – 1)

其中 m 是微批次的数量,n 是管道的深度或 GPU 的数量。所以,在我们的简单示例中,有四个 GPU 和一个大批次,我们看到设备空闲了 75%的时间!GPU 非常昂贵,不能让它有四分之三的时间处于空闲状态。让我们看看使用微批处理策略会是什么样子。使用 4 个微批次,它将这个时间几乎减半,降至仅 43%。我们可以从这个公式中看出,我们拥有的 GPU 越多,空闲时间越高,但微批次越多,利用率越好。

不幸的是,我们通常既不能减少 GPU 的数量,也不能将微批量的大小增加到我们想要的程度。存在限制。对于 GPU,我们必须使用尽可能多的 GPU 来将模型放入内存。然而,尽量使用一些较大的 GPU,因为这会比使用许多较小的 GPU 更具优化。减少管道并行中的气泡是压缩之所以如此重要的另一个原因。对于微批量,第一个限制是明显的:由于微批量是批量大小的分数,因此我们受其大小的限制。第二个限制是每个微批量都会以线性关系增加缓存激活的内存需求。一种应对这种更高内存需求的方法是称为 PipeDream 的方法。¹² 存在不同的配置和方法,但基本思想是相同的。在此方法中,我们在完成任何微批量的正向传递后立即开始工作反向传递。这使得我们能够完全完成一个训练周期并释放该微批量的缓存。

3D 并行

对于 LLMs,我们希望利用所有三种并行化实践,因为它们都可以同时运行。这被称为 3D 并行,它将数据、张量和平行化(DP + TP + PP)结合起来。由于每种技术和因此维度至少需要两个 GPU 来运行 3D 并行,因此我们需要至少八个 GPU 来开始。我们如何配置这些 GPU 对于从该过程中获得最大效率至关重要。由于 TP 具有最大的通信开销,我们希望确保这些 GPU 相邻,最好是位于同一节点和机器上。PP 在三者中具有最少的通信量,因此将模型拆分到节点上是最经济的。

通过同时运行这三个,我们看到了它们之间的一些有趣的交互和协同作用。由于 TP 将模型分割以在设备内存内良好运行,我们发现 PP 即使在小批量大小下也能表现良好,这是由于 TP 降低了有效批量大小。这种组合还改善了不同管道阶段 DP 节点之间的通信,使得 DP 也能有效工作。节点之间的通信带宽与管道阶段的数量成正比。因此,DP 即使在较小的批量大小下也能很好地扩展。总的来说,我们看到当它们组合运行时,性能优于单独运行时。

现在我们已经了解了一些行业技巧,拥有合适的工具来完成这项工作同样重要。

3.4 LLM 运营基础设施

我们终于要开始讨论使这一切工作所需的基础设施了。这可能会让人感到惊讶,因为我们知道一些读者可能会期望在第一章的开头看到这一部分。为什么等到第三章的结尾呢?在我们多次采访机器学习工程师时,我们经常提出这个开放式问题:“你能告诉我关于 MLOps 的什么?”这是一个轻松的软球问题,用来开启对话。大多数初级候选人会立即开始跳入工具和基础设施。这很有道理;有这么多不同的工具可供选择。更不用说,每次你看到描述 MLOps 的帖子或博客时,都会有一个展示基础设施的漂亮小图。虽然所有这些都很重要,但认识到更有经验的候选人会跳入的地方——机器学习生命周期。

对于许多人来说,细微差别已经丢失,但基础设施是如何,生命周期是为什么。大多数公司只需最基本的设施就能过得去。我们见过一些简陋的系统,它们完全存在于一个数据科学家的笔记本电脑上,而且工作得相当不错——尤其是在 scikit-learn 无处不在的时代!

不幸的是,一个手推车机器学习平台在 LLMs 的世界中并不适用。因为我们仍然生活在一个 MacBook Pro 笔记本电脑的标准存储容量为 256 GB 的世界里,仅仅本地存储模型就已经可能成为一个问题。投资于更坚固基础设施的公司更能为 LLMs 的世界做好准备。

在图 3.7 中,我们看到一个以 LLMs 为设计理念的 MLOps 基础设施示例。虽然大多数基础设施图简化了结构以使一切看起来整洁,但事实是整个系统还有一点复杂性。当然,如果我们能让数据科学家在脚本中而不是在临时工作站(通常带有 Jupyter Notebook 界面)中工作,很多这种复杂性都会消失。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/3-7.png

图 3.7 考虑到 LLMs 的 MLOps 基础设施的高级视图。此图试图涵盖整个画面以及许多工具的复杂性,这些工具使 ML 模型能够在生产环境中运行。

仔细观察图 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 之上。

让我们逐一分析基础设施拼图中的每一块,并讨论在考虑大型语言模型(LLMs)时应该考虑的功能。虽然我们将讨论每个部分的专用工具,但我们也会指出,还有像 Dataiku、亚马逊的 SageMaker、Azure 机器学习和谷歌的 VertexAI 这样的 MLOps 作为服务平台。这些平台试图完成整个拼图;它们完成得如何是另一个问题。然而,它们通常是一个很好的捷径,你应该了解它们。好吧,闲话少说;让我们直接进入正题吧!

3.4.1 数据基础设施

虽然这本书的重点不是 MLOps,但重要的是要注意,MLOps 建立在数据操作基础设施之上,而数据操作基础设施本身又建立在 DevOps 之上。数据操作生态系统的主要功能包括数据存储、协调器和管道。通常还需要包括容器注册中心和流集成服务。

数据存储是数据操作的基础,形式多样,从简单的数据库到大型数据仓库,再到更大的数据湖,甚至复杂的网络数据。这是你的数据存储的地方,大量的工作都投入到管理、治理和保障数据存储中。协调器是数据操作的基础,因为它是一个管理和自动化简单和复杂多步骤工作流程和任务的工具,确保它们在系统中的多个资源和服务上运行。最常讨论的是 Airflow、Prefect 和 Mage。最后,管道是支柱。它们支撑着一切,是我们运行作业的地方。最初是为了简单地移动、清理和定义数据而构建的,这些相同的系统现在被用来按计划运行机器学习训练作业,进行批量推理,以及确保 MLOps 顺利运行所需的大量其他工作。

容器注册库是 DevOps 和随后的数据操作和 MLOps 的基石。在容器中运行所有管道和服务是确保一致性的必要条件。流服务比我们在本章中提到的要大得多,如果你知道,你就知道了。幸运的是,对于大多数文本相关任务,实时处理并不是一个主要问题。即使是实时字幕或翻译这样的任务,我们通常也可以通过某种伪实时处理策略来应对,这不会根据任务降低用户体验。

3.4.2 实验跟踪器

实验跟踪器是 MLOps 的核心。实验跟踪器负责跟踪和记录测试和结果的基本工作。正如来自《迷雾探秘》的著名亚当·萨维奇名言所说:“记住,孩子们,玩耍和科学的唯一区别就是把它写下来。”没有它,你的组织可能缺少数据科学中的“科学”部分,这实际上相当尴尬。

即使你的数据科学家热衷于在笔记本中手动跟踪和记录结果,如果其他人难以查看和搜索,那么这些记录可能就直接被扔进垃圾桶了。这就是实验跟踪器的真正目的——确保知识能够轻松共享和获取。最终,一个模型将进入生产阶段,而这个模型可能会出现问题。当然,你可以总是训练一个新的模型,但除非团队能够回过头来调查第一次出错的原因,否则你很可能会一次又一次地重复同样的错误。

现在有许多实验跟踪器;最受欢迎的是开源的 MLFlow。它由 Databricks 团队发起,该团队还提供易于托管的服务。一些值得检查的付费替代方案包括 CometML 和 Weights & Biases。

现在的实验跟踪器带有许多额外的功能。大多数开源和付费解决方案在满足 LLMOps 扩展需求时都会提供你所需要的东西。然而,确保正确利用这些工具可能需要一些小的调整。例如,默认假设通常是你在从头开始训练模型,但通常在处理 LLMs 时,你将进行模型微调。在这种情况下,注意你开始时的模型检查点非常重要。如果可能的话,甚至可以链接回原始的训练实验。这将允许未来的科学家更深入地研究他们的测试结果,找到原始训练数据,并发现消除偏差的前进路径。

另一个需要注意的功能是评估指标工具。我们将在第四章中深入探讨,但评估指标对语言模型来说很难。通常会有多个你关心的指标,而且它们都不简单,比如复杂度评分或相似度得分。虽然实验跟踪器供应商试图对评估指标保持中立和无偏见,但他们至少应该使比较模型及其指标变得容易,以帮助我们决定哪个更好。由于大型语言模型(LLMs)变得如此流行,一些供应商已经使评估更常见的指标(如用于文本摘要的 ROUGE)变得容易。

你还会发现,许多实验跟踪器供应商已经开始添加专门针对 LLMs 的工具。你可能需要考虑的一些功能包括直接支持 Hugging Face、LangChain 支持、提示工程工具包、微调框架和基础模型商店。这个领域正在快速发展,目前没有哪个工具拥有所有相同的功能,但这些功能集可能会趋同。

3.4.3 模型注册库

模型注册库可能是 MLOps 基础设施中最简单的工具。主要目标是一个容易解决的问题;我们只需要一个存储模型的地方。我们见过许多成功的团队仅仅通过将模型放入对象存储或共享文件系统,并称之为完成。尽管如此,在选择时,你应该寻找一些额外的功能。

第一个问题是模型注册库是否跟踪模型的元数据。你关心的多数内容都会在实验跟踪器中,所以你通常可以确保能够将两者链接起来。事实上,大多数模型注册库都是由于这个原因集成到实验跟踪系统中的。然而,当公司决定使用开源模型或甚至购买模型时,这些系统会出现问题。上传模型并标记相关信息的操作容易吗?答案通常是否定的。

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

最后,如果我们正在推广和降级模型,我们需要关注访问权限。对于许多公司来说,模型往往是宝贵的知识产权,因此确保只有正确的用户能够访问模型是很重要的。但同样重要的是要确保只有理解模型——它们做什么以及为什么被训练——的团队负责推广和降级模型。我们最不希望看到的是在生产环境中删除模型,更糟糕的是。

对于 LLMs,有一些重要的注意事项你应该知道:主要是在选择模型注册表时,要注意任何限制大小。几个模型注册表将模型大小限制在 10 GB 或更小。这显然是不够的。我们可以推测出很多原因,但它们都不值得注意。说到限制大小,如果你打算在 Ceph 这样的本地存储系统上运行模型注册表,确保它有足够的空间。你可以在本地服务器上以几百美元的价格购买多个 TB 的存储,但即使只有几个 TB,当你的 LLM 超过 300 GB 时也会很快填满。别忘了:在训练和微调过程中,你很可能会保留多个检查点和版本,以及用于可靠性的副本。然而,存储仍然是运行 LLMs 成本最低的部分,所以在这里节省开支并导致未来头疼是没有理由的。

这让我想到了一个很好的观点:还有很多优化空间,可以采用更好的空间节省方法来存储 LLMs 及其导数,特别是考虑到这些模型在总体上非常相似。我们可能会在未来看到专门解决这个问题的存储解决方案。

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 功能丰富,拥有一个优秀的社区。Milvus 背后的公司 Zilliz 提供全托管服务,但也可在您自己的集群中部署。如果您已经有一些基础设施经验,那么在您自己的集群中部署相对简单直接。

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

还需要注意的是,随着向量数据库的兴起,我们很快看到许多数据库巨头如 Redis 和 Elastic 提供了向量搜索功能。目前,这些数据库大多提供最直接的功能集,但如果您已经使用这些工具集,它们很难忽视,因为它们可以提供快速的成功,帮助您快速入门。

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

3.4.6 监控系统

监控系统对于任何机器学习系统(包括大型语言模型 LLM)的成功至关重要。与其他软件应用不同,机器学习模型通常以静默方式失败——也就是说,它们继续运行但开始产生较差的结果。这通常是由于数据漂移,一个常见的例子是推荐系统随着时间的推移给出更差的结果,因为卖家开始通过提供虚假评论来操纵系统以获得更好的推荐结果。监控系统使我们能够捕捉到表现不佳的模型并进行调整,或者简单地重新训练它们。

尽管监控系统很重要,但它们往往是最后一块拼图。这通常是有意为之的,因为如果没有任何模型可以监控,那么投入资源去研究如何监控模型是没有帮助的。然而,不要犯拖延太久的错误。许多公司因为一个无人知晓的模型而遭受损失,这往往代价高昂。重要的是要意识到,您不必等到模型投入生产才开始监控数据。有许多方法可以将监控系统集成到训练和数据管道中,以改善数据治理和合规性。无论如何,您通常可以通过监控系统来判断一个数据科学组织的成熟度。

目前市面上有很多优秀的监控工具;一些优秀的开源选项包括 whylogs 和 Evidently AI。我们也是 Great Expectations 的粉丝,但发现在批量作业之外它相对较慢。还有许多付费选项可供选择。通常,对于机器学习监控工作负载,您希望监控您在其他软件应用程序中通常记录的所有内容;这包括资源指标,如内存和 CPU 利用率,性能指标,如延迟和每秒查询数,以及操作指标,如状态码和错误率。此外,您还需要方法来监控数据在模型中进出的漂移。您需要关注诸如缺失值、唯一性和标准差变化等问题。在许多情况下,您希望在监控数据时能够对数据进行分段——例如,用于 A/B 测试或按地区监控。在机器学习系统中,一些有用的监控指标包括模型准确率、精确率、召回率和 F1 分数。这些指标很难评估,因为在推理时间您不会知道正确答案,因此设置某种形式的审计系统通常很有帮助。当然,如果您的语言模型(LLM)被设计成问答机器人而不是帮助作家更具创造性的工具,审计将更容易进行。

这暗示了您的监控系统将面临一系列新的挑战,甚至比我们看到的其他机器学习系统还要多。对于 LLM,我们正在处理文本数据,正如本章前面所讨论的,这很难量化。例如,考虑您用于监控数据漂移的特征,因为众所周知,语言会漂移很多!我们建议的一个特征是唯一标记。当出现新的俚语或术语时,这将提醒您;然而,当“wicked”意味着“酷”时,这并不能帮助您,因为这时词语的含义发生了变化。我们还会建议监控嵌入;然而,您可能会发现这要么会增加很多噪音和误报,要么至少在问题发生时难以解析和深入挖掘。表现最好的系统通常涉及大量手工制作的规则和特征来监控,但这些可能容易出错,且创建起来耗时费力。

监控基于文本的系统远非一个已解决的问题,这主要源于一开始就难以理解文本数据。这引发了一个问题,即使用语言模型来自我监控的最佳方法是什么,因为它们是我们当前编码语言的最好解决方案。不幸的是,我们不知道有人在研究这个问题,但我们想象这只是一个时间问题。

3.4.7 具备 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 的内存。这些 GPU 的一个大问题是它们目前的需求量非常大。你还应该了解 NVIDIA H100,它提供 80 GB 的内存,就像 A100 一样。H100 NVL 承诺支持高达 188 GB,并且是为 LLMs 设计的。另一个你应该了解的新 GPU 是 NVIDIA L4 Tensor Core GPU,它有 24 GB 的内存,并定位为 T4 和 V100 之后的新的工作马,至少在 AI 工作负载方面是这样的。

LLMs(大型语言模型)的大小各不相同,了解这些数字的含义很有用。例如,LLaMA 模型有 7B、13B、33B 和 65B 参数变体。如果你不确定需要哪种 GPU 来运行哪种模型,这里有一个快捷方法:将参数的十亿数量乘以 2,这就是你需要的 GPU 内存量。原因是大多数模型在推理时默认以半精度运行,即 FP16 或 BF16,这意味着每个参数至少需要 2 个字节。例如,70 亿 × 2 字节 = 14 GB。你还需要额外的空间来存储嵌入模型,这大约还需要另一个 GB,以及更多用于模型中实际运行的标记。一个标记大约是 1 MB,所以 512 个标记将需要 512 MB。这并不是什么大问题,直到你考虑运行更大的批量大小以提高性能。对于 16 个这样的批量,你将需要额外的 8 GB 空间。

当然,到目前为止,我们只讨论了推理;对于训练,你需要更多的空间。在训练过程中,你总是希望以全精度运行,并且你需要额外的空间来存储优化器张量和梯度。一般来说,为了考虑到这一点,你需要为每个参数大约 16 个字节。所以,要训练一个 70 亿参数的模型,你将需要 112 GB 的内存。

3.4.8 部署服务

我们一直努力的方向最终都汇集在这里,并得到了良好的应用。事实上,如果你移除所有其他服务,只留下部署服务,你仍然会拥有一个工作的 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 为什么比传统的 ML 更难,这本身就足够困难。首先,我们了解到它们的体积不容小觑,但随后我们也发现了许多关于它们的奇特之处,从令牌限制到幻觉——更不用说它们很昂贵了。幸运的是,尽管困难,但并非不可能。我们讨论了压缩技术和分布式计算,这些都是必须掌握的关键。然后我们探讨了使 LLMs 工作所需的基础设施。虽然其中大部分可能是熟悉的,但我们意识到 LLMs 对每个工具都施加了不同级别的压力,而且我们通常需要为比部署其他 ML 模型更大的规模做好准备。

摘要

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

  • LLMs 之所以难以处理,还因为它们处理自然语言及其所有复杂性,包括幻觉、偏见、伦理和安全问题。

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

  • 将模型压缩到尽可能小,使它们更容易处理;量化、剪枝和知识蒸馏在这方面特别有用。

  • 量化因其易于实现且可以在训练后进行,无需微调而受到欢迎。

  • 低秩逼近是缩小模型的有效方法,由于 LoRA 的广泛应用,它已被大量用于适应。

  • 我们使用三个核心方向来并行化 LLM 工作流程:数据、张量和管道。DP 帮助我们提高吞吐量,TP 帮助我们提高速度,而 PP 使得所有这一切都成为可能。

  • 结合并行化方法,我们得到了 3D 并行性(数据 + 张量 + 管道),我们发现这些技术协同作用,弥补彼此的不足,帮助我们获得更高的利用率。

  • LLMOps 的基础设施与 MLOps 相似,但不要因此被误导,因为有很多情况下“足够好”不再适用。

  • 许多工具已经开始提供专门针对 LLM 支持的新功能。

  • 向量数据库,尤其是,作为 LLMs 所需的基础设施拼图中的一块新部件,对于快速搜索和检索嵌入体来说非常有趣。

[1] A. Bulatov, Y. Kuratov, and M. S. Burtsev, “Scaling transformer to 1M tokens and beyond with RMT,” April 2023, arxiv.org/abs/2304.11062.

[2] R. Daws, “Medical chatbot using OpenAI’s GPT-3 told a fake patient to kill themselves,” AI News, October 28, 2020, mng.bz/qO6z.

[3] T. Kington, “ChatGPT bot tricked into giving bomb-making instructions, say developers,” The Times, December 17, 2022, mng.bz/7d64.

[4] K. Quach, “AI game bans players for NSFW stories it generated itself,” The Register, October 8, 2021, www.theregister.com/2021/10/08/ai_game_abuse/.

[5] T. Hoefler, D. Alistarh, T. Ben-Nun, N. Dryden, and A. Peste, “Sparsity in deep learning: Pruning and growth for efficient inference and training in neural networks,” January 2021, arxiv.org/abs/2102.00554.

[6] E. Frantar and D. Alistarh, “SparseGPT: Massive Language models can be accurately pruned in one-shot,” January 2023, arxiv.org/abs/2301.00774.

[7] V. Sanh, L. Debut, J. Chaumond, and T. Wolf, “DistilBERT, a distilled version of BERT: smaller, faster, cheaper and lighter,” October 2019, arxiv.org/abs/1910.01108.

[8] R. Taori, I. Gulrajani, T. Zhang, Y. Dubois, X. Li, C. Guestrin, P Liang, and T. B. Hashimoto, “Alpaca: A strong, replicable instruction-following model,” CRFM, 2023, crfm.stanford.edu/2023/03/13/alpaca.xhtml.

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

[10] R. Henry 和 Y. J. Kim,“通过低比特量化加速大型语言模型,”2023 年 3 月,mng.bz/maD0.

[11] V. Korthikanti 等人,“在大型 Transformer 模型中减少激活重计算,”2022 年 5 月,arxiv.org/abs/2205.05198.

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

第四章:大型语言模型的数据工程:为成功做好准备

本章涵盖了

  • 行业中常用的基础模型

  • 如何评估和比较大型语言模型

  • 不同的数据源以及如何准备你自己的

  • 创建你自己的自定义分词器和嵌入

  • 准备用于未来章节的 Slack 数据集

数据就像垃圾。在收集它之前,你最好知道你打算如何处理它。——马克·吐温

创建我们自己的 LLM 与任何 ML 项目并无不同,我们首先需要准备我们的资产——没有比你的数据更宝贵的资产了。所有成功的 AI 和 ML 项目都是建立在良好的数据工程基础之上的。因此,我们获取、清理、准备和整理我们的数据是非常重要的。

与其他 ML 模型不同,在创建针对特定任务定制的 LLM 时,你通常不会从零开始。当然,如果你确实是从零开始,你可能只会这样做一次。然后,最好调整和润色该模型,以进一步满足你的特定需求。选择正确的基模型可能会使你的项目成功或失败。图 4.1 给出了在训练或微调新模型之前需要准备的不同组件和资产的高级概述。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/4-1.png

图 4.1 训练 LLM 的不同元素。结合地球、火、水——等等,不,不是那些元素。要开始,你需要收集几个资产,包括基础模型、训练数据、文本编码器(例如,分词器)和评估数据。

正如书中《数据工程基础》¹所如此明确地定义的:

数据工程是开发、实施和维护系统及流程的过程,这些系统及流程接收原始数据并产生高质量、一致的信息,以支持下游用例,如分析和机器学习。

在本章中,我们将讨论在你开始创建 LLM 之前需要采取的步骤,这主要涉及准备训练模型所需的数据资产。我们将讨论许多可用的基或基础模型作为起点,以及如何评估和比较它们。然后,我们将深入探讨许多不同的数据集以及如何为微调模型准备你自己的数据集,包括准备你自己的分词器或嵌入。最后,我们将构建一个数据集,我们将在下一章中使用它来微调模型。

4.1 模型是基础

我们将首先讨论你在训练时需要收集的最重要数据集,即预训练模型的模型权重。LLM 作为一项技术之所以如此成功,一个重要原因是我们可以将已经在整个语言上训练好的模型进行调整,以在特定任务上表现良好。当然,了解那个初始模型是如何训练的以及它是基于什么进行训练的,将大大缩短选择正确模型进行调整的时间。

由于大型语言模型(LLM)已成为热门的研究课题,选择正确的一个变得异常困难,几乎每周都会出现一个新的模型,打破了基准记录。因为我们知道(或者至少假设)你急于了解它们,所以我们首先讨论目前市场上存在的许多不同模型。这些模型已经被专业人士训练(无论好坏),旨在使你的生活更轻松,并将强大的语言模型推向公众领域。GitHub、Hugging Face Hub 和其他地方有数以千计的开源模型可供使用,为了简化,我们将突出我们最喜欢的模型,并详细介绍每个模型,以便更容易进行比较,并给你一个关于你是否应该使用该特定模型或选择其不太知名的开放源代码变体的想法。如果你计划从头开始训练,考虑涉及的架构,以及你是否想尝试某个特定的系列。

4.1.1 GPT

没有比从 GPT(生成式预训练 Transformer)模型开始更好的地方了。这些模型是粉丝和我们的最爱之一,通过 OpenAI 进行商业销售,因其广泛的任务上的出色表现而受到欢迎。GPT 模型如此知名,以至于外行人经常用“GPT”来代替“LLM”,就像人们可能会用 Kleenex 或 Band-Aid 来代替纸巾或绷带一样。

第一个 GPT 模型于 2018 年推出,紧随 transformers 的推出之后,只有 1.2 亿个参数。它在 BookCorpus 小型数据集上进行了训练,在当时的 NLP 基准测试中取得了令人印象深刻的结果。GPT-2 模型在第二年推出,其规模扩大了 10 倍,达到 15 亿个参数;它在更大的 WebText 数据集上进行了训练。到了 2020 年,GPT-3 推出,其规模扩大了 100 倍,达到 1750 亿个参数,并在庞大的 Common Crawl 数据集上进行了训练。这个模型仍然基于 GPT-1 的原始架构,并进行了轻微的修改以改进扩展性。

OpenAI 选择对 GPT-4 等后续迭代版本保持更高的保密性,不透露训练数据或具体架构,因为它们已经开始量产并作为产品出售。ChatGPT 是一个经过微调的 GPT-3 模型,通过使用带有人类反馈的强化学习(RLHF)进行对话交互训练。不过不深入探讨,但你可以找到一系列 GPT-3 模型,这些模型在 API 名称下如 ada、babbage、curie 和 davinci,以及其他微调模型如 webGPT 和 InstructGPT。如果读者感兴趣,我们可以留待进一步调查。

其他开源变体,如 GPT-J,是由开源社区利用 OpenAI 发布的论文中获得的知识创建的。一些 GPT 模型与 OpenAI 没有关系,因为生成预训练转换器是一个非常通用的名称,适用于大多数 LLM。当然,OpenAI 已经开始将其视为一个品牌,并试图将缩写词注册商标。²

尽管 GPT-X 模型是闭源的,但可以通过 OpenAI API 访问,该 API 还包括其微调的功能。我们将在这本书中使用 GPT-2——尽管它比大多数人认为的实际 LLM 要小一些——因为它是一个易于理解的架构,并且易于学习。

4.1.2 BLOOM

BLOOM 是最具标志性的 LLM 之一,因为它在创建过程中所获得的学习成果。该模型于 2022 年发布,是第一个公开的 LLM,其参数量达到 176B,与 GPT-3 的规模相媲美;它以完全透明的方式进行训练。该模型由 Hugging Face 的 BigScience 团队制作,得到了微软的 DeepSpeed 团队和 NVIDIA 的 Megatron-LM 团队的协助,并由法国政府拨款赞助。

BLOOM 在 BigScienceCorpus 数据集上进行训练,这是一个由许多较小的数据集组成的综合体,总容量达到 1.6TB 的预处理文本。它受 RAIL 许可证约束,这意味着它技术上不是开源的,因为对其使用有约束,但它可以进行商业化。

TIP 在这里可以了解更多关于 RAIL 许可证的信息:mng.bz/mR20

BLOOM 被训练为适用于所有任务的行业规模和行业级别。因此,适应消费设备不是优先事项,但随着研究团队的速度加快,已经训练了几个较小的版本。有 560M-、3B-和 7B 参数版本。还有一个 BLOOMZ,它是完整 176B 参数模型的多元任务微调版本。BLOOM 只在 46 种不同的语言上进行训练,而 BLOOMZ 的目标是提高模型的跨语言泛化能力。³ 你可以在 Hugging Face 的 hub 上找到所有这些模型:huggingface.co/bigscience/bloom

BLOOM 的一个重大缺点是它经常给出较差的响应,在基准测试中竞争不佳——这很可能是由于项目资金有限和截止日期紧迫,导致给人一种它训练不足的感觉。这并不总是坏事,通常比过训练的模型要好,但如果你决定使用它,你可能会需要在大数据集上进行更多的微调。然而,使用它的好处是它被广泛理解和公开训练,你可以检查其训练数据。

通常,作者不会推荐再将其作为基础模型使用;有更好的替代品,但你应该熟悉它,因为它对领域做出了贡献。例如,BLOOM 创建花瓣,允许分布式训练,这对领域是一个重大贡献。

4.1.3 LLaMA

LLaMA 是 Meta 进入 LLM 领域的成果。第一个版本于 2023 年 2 月发布,并以非商业许可发布给研究社区。一周后,权重在 4chan 上泄露。在一系列不太可能的事件中,这次泄露可能对 Meta 非常有益,因为这个模型已经成为实验和开发的行业标准。我们将讨论的几个更多模型都是基于它的。

后来,在 2023 年 7 月,Meta 发布了 Llama 2,它同时拥有研究和商业许可。Llama 2 是一个大事件,因为它是第一个真正具有强大功能的商业可用模型,你将看到基于其架构的许多其他模型。有三种不同的模型大小可供选择:7B、13B 和 70B 参数。你可以从这里下载它们:ai.meta.com/llama/。如果你打算使用它,你需要申请访问权限并接受条款和条件。

Llama 2 是在从互联网上收集的经过精心挑选的数据集上训练的,其中他们移除了已知包含个人信息的网站,并增加了他们认为的事实来源。尽管数据集的详细情况尚未公开,但它很可能包含了来自 Common Crawl、GitHub、Wikipedia、Project Gutenberg、ArXiv 和 Stack Exchange 的数据,因为这些都是 LLaMA 1 的主要数据集。这些数据集后来被包装在一起,并以 RedPajama 的名义分发。然后,Llama 2 使用 RLHF 进一步微调,一个模型用于聊天,另一个用于代码。

4.1.4 巫师

巫师系列语言模型来自 2023 年的论文“WizardLM:赋予大型语言模型遵循复杂指令的能力”。⁴ 这些模型遵循的想法是,当在充满高复杂度任务的密集训练数据上训练时,LLM 的表现更好。基于创建更复杂指令任务的框架,巫师 LM 方法已应用于许多流行的数据集,并用于微调几乎所有最流行的模型。这种方法如此受欢迎,以至于令人惊讶的是,在 LlamaCoder34B 发布后的两天内,社区就微调了 WizardCoder34B 模型。

这些模型因其类似人类的散文风格和正确处理复杂问题的能力而一直受到好评,这些能力甚至可以与许多付费服务相媲美。我们鼓励你尝试的一个问题是要求 WizardCoder34B 用你喜欢的任何语言编写一个绘制看起来逼真的树的程序。由于巫师模型不像它们围绕特定数据集那样围绕将现有数据集转换为巫师风格的方法,因此应用范围极其广泛且多样化。如果你在使用其他模型或架构时遇到瓶颈,不确定如何改进,请尝试使用你已经使用过的数据集并应用巫师方法。欢迎你这样做。

作为附带说明,WizardCoder 模型通常受到很多关注,但 WizardMath 模型在自身领域也非常令人印象深刻。我们注意到,许多读者可能更多地处理数据问题而不是代码问题,因此当处理与数据对话的应用程序时,WizardMath 模型可能是一个很好的起点。

4.1.5 鹰隼

鹰隼模型是阿布扎比科技创新研究所的一个模型系列。它们是第一个在真正开源许可 Apache 2.0 下发布的尖端模型。您可以从研究所的网站获取模型:falconllm.tii.ae/falcon-models.xhtml。其易于访问和开源许可使得这对于黑客、实践者和行业来说都是一个梦想。

鹰隼模型首次于 2023 年 6 月推出,当时只推出了 7B 和 40B 参数模型,但到了 2023 年 9 月,鹰隼发布了一个 180B 参数模型,可以真正与 GPT-3 大小的模型竞争。对许多读者来说,更令人兴奋且可能更重要的是,鹰隼在许多基准测试任务中经常领先于 LLM 排行榜。这些模型主要在 RefinedWeb 数据集上训练,这是一个较小但质量更高的数据集,它是经过精心和细致的策划和从 Common Crawl 数据集中提取出来的。

4.1.6 维库纳

维库纳是在 ShareGPT 用户共享对话数据集上训练的。其逻辑是,在 ChatGPT 最佳输出基础上训练的模型将能够模仿 ChatGPT 的性能,借助于 Llama-Alpaca 的趋势。

备注:这里不会讨论 Alpaca,但在第三章讨论知识蒸馏时我们已经介绍过它。

维库纳因其性能和相对较低的训练成本而受到赞誉。维库纳是数据覆盖率和质量为何如此重要的绝佳例子,同时它也展示了从另一个模型的输出中训练模型导致模型崩溃的危险。当机器学习模型在合成数据上训练时,会导致输出越来越不多样化,这种现象称为模型崩溃。例如,维库纳在处理至少接近数据集中出现的内容时表现优秀,但当被要求执行更生成性或代理类任务时,它往往会产生远超其前辈的幻觉。维库纳未经许可不得用于商业用途,但对于个人项目来说非常出色。

4.1.7 多莉

Dolly 是由 Databricks 创建的,与其说是竞争性模型,不如说是更多的一次思想实验,与相同大小的其他模型相比,Dolly 及其 V2 版本的表现并不出色。然而,Dolly 在对英语的底层理解方面堪称最佳,是微调或创建低排名适应(LoRAs;我们将在第五章中讨论)以影响其他模型的绝佳起点。Dolly 1.0 是在斯坦福 Alpaca 数据集上训练的,而 Dolly 2.0 是在由 Databricks 员工众包的高质量人类生成指令遵循数据集上训练的。Dolly 2.0 已经完全开源,包括训练代码、数据集和模型权重,所有这些均带有商业用途许可证。⁵

4.1.8 OpenChat

OpenChat 与 Vicuna 类似,因为 OpenChat 使用了 80K ShareGPT 对话进行训练,但在条件化和加权损失策略方面有所不同,最终创建了一个在生成类似人类和,更重要的是,人类更喜欢的响应方面无可否认的出色的模型。

OpenChat 模型——不要与开源聊天机器人控制台混淆——是为不同任务而收集的各种微调集合,其中一些用于编码,其他用于代理,还有一些用于聊天。在 Llama 2 社区许可证下免费用于商业用途,这些模型可以成为您公司构建的绝佳解决方案。

我们已经讨论了很多模型,虽然我们可以继续这样讨论整个章节,但这对大家来说都不是最好的选择。表 4.1 展示了一个总结,突出了我们讨论的模型的一些主要比较点。我们想强调的一个主要点是,许多模型都可用于商业用途!虽然许多许可证都有限制,但它们可能不是你打算违反的规则。

表 4.1 LLM 模型家族比较
模型家族数据集最大模型大小商业许可证组织
GPTCommon Crawl/RLHF1.76TOpenAI
BLOOMBigScienceCorpus176BBigScience
LlamaRedPajama70BMeta
WizardEvol-Instruct70BMicrosoft
FalconRefinedWeb180BTII

现在你已经了解了一些更受欢迎的模型家族,你可能已经对为你的项目选择哪个模型有了一些想法。但你怎么能确定呢?在下一节中,我们将探讨不同的评估和比较模型的方法。

4.2 评估大型语言模型

尽管我们刚刚讨论了一些我们最喜欢的模型家族,但还有许多其他不同的模型可供选择,每个月都有更多的新模型出现,所有这些模型都声称自己是最好的。不可能将它们全部理清楚。那么,你如何选择最好的一个来使用?它是否能够直接在你的任务上表现良好,或者是否需要微调?你如何知道你的微调是否提高了模型,或者只是让它变得更糟?你如何知道你选择了正确的大小?较小的模型很方便,但较大的模型在许多任务上的表现更好。说实话,这些问题并不容易回答,但幸运的是,有一些行业标准我们可以依赖。

当评估一个模型时,你需要两样东西:一个指标和一个数据集。指标是一个算法,它允许我们将结果与真实情况进行比较。数据集是我们希望模型执行的任务列表,然后我们将使用我们选择的指标进行比较。

在本节中,我们将讨论许多用于评估大型语言模型的不同方法,以便我们可以客观地评估和比较它们。我们将从常见的行业基准到开发你自己的独特评估方法的一切内容进行讨论。让我们开始吧。

4.2.1 评估文本的指标

评估文本通常很困难,因为用两种不同的方式说出相同的话很容易。从语义上讲,两个句子可能是完全相同的,但从句法上讲,它们却毫无相似之处,这使得文本比较变得复杂。看看我做了什么?

为了评估我们的模型,我们需要比仅仅匹配或检查相等性更好的指标,这对于大多数其他机器学习问题来说是可以做到的。我们需要一个指标,它允许我们比较模型生成的文本与真实情况,而不会过于严格。让我们看看一些最常用的指标。

ROUGE

ROUGE,即“面向检索的摘要评估的辅助研究”,是用于评估机器翻译任务的最古老的指标之一,但仍然是最可靠的。它是专门为自动摘要任务开发的,其目标是把一长篇文章总结成简短的摘要。让我们考虑一下这个问题:你如何确定一个摘要是否正确?最简单的方法就是将其与一个已知的摘要——即“真实情况”——进行比较。然而,无论文章如何,通常都有成千上万种方法可以用来简化文本,使其更加简洁,你不想仅仅因为模型选择了与真实情况不同的词序就惩罚它;这只会导致过度拟合。

Rouge 不将生成的摘要与真实摘要进行比较,期望得到完全匹配;相反,它通过 N-gram 在两个摘要之间寻找重叠——重叠越大,分数越高。这与全文搜索引擎的工作方式类似。根据 N-gram 中的 N 值,有多种变体,但还有比较最长公共子序列的版本,以及比较跳过双词(skip-bigrams)的版本,跳过双词是指句子中任何两个按顺序排列的词,而不一定是紧挨着的。

ROUGE 的原始实现是用 Perl 编写的,我们记得甚至在几年前还不得不使用它。对于一位作者来说,一些最糟糕的日子之一就是不得不在 Perl 中工作。幸运的是,在去年或更早的时候,终于有了快速、稳定的 Python 重实现。在下一个列表中,我们使用 rouge-score 库,这是 Google 的一个重实现。我们将比较两个关于《塞尔达传说》的解释,看看它们如何比较。

列表 4.1 使用 ROUGE
from rouge_score import rouge_scorer

target = "The game 'The Legend of Zelda' follows the adventures of the \
    hero Link in the magical world of Hyrule."
prediction = "Link embarks on epic quests and battles evil forces to \
    save Princess Zelda and restore peace in the land of Hyrule."

scorer = rouge_scorer.RougeScorer(["rouge1", "rougeL"], use_stemmer=True)   #1
scores = scorer.score(target, prediction)
print(scores)
# {'rouge1': Score(precision=0.28571428, recall=0.31578947, fmeasure=0.3),
# 'rougeL': Score(precision=0.238095238, recall=0.26315789, fmeasure=0.25)}

#1 示例 N-gram,其中 N=1,并使用最长公共子序列

如您从示例中可以看到,尽管这两段文本在语法上相当不同,但它们都是准确的描述。正因为如此,ROUGE(Recall-Oriented Understudy for Gisting Evaluation)算法不会给出一个大的零分,而是给予一定的灵活性,并提供相似度分数约为 0.25 的更好比较。ROUGE 算法是一种快速有效的比较两段短文本相似度的方法。在业界,ROUGE 非常常见,许多基准测试都将它作为其指标之一。

BLEU

BLEU(BiLingual Evaluation Understudy),代表双语评估助手,是我们将在本书中讨论的最古老的评估指标。它被开发出来用于评估机器翻译任务,并比较将一种语言翻译成另一种语言的方法。它与 ROUGE 非常相似,我们在目标和预测之间比较 N-gram。虽然 ROUGE 主要是一个召回率指标,但 BLEU 是一个精确度指标,但使用标准精确度可能会带来一些我们需要考虑的问题。

要理解这个问题,我们可以使用列表 4.1 中的代码来计算标准精确度。将目标变量替换为“the cat in the hat”,将预测变量替换为“cat hat”。重新运行列表,您会注意到召回率为 0.4——我们只正确地识别了五个词中的两个——但精确度为 1.0,这是一个完美的分数,尽管表现并不好!这个结果是因为目标中的两个词“cat”和“hat”都出现了。

BLEU 通过添加两个调整来解决这一点。第一个是直接的:添加简洁性惩罚。如果预测比目标短,我们将对其进行惩罚。第二个调整,称为修改后的 N-gram 精确度,稍微复杂一些,但它允许我们比较预测与多个目标。下面的列表展示了如何使用 NLTK 库来计算 BLEU 分数。我们使用与 ROUGE 相同的Zelda示例,这样你可以比较结果。

列表 4.2 使用 BLEU
import nltk.translate.bleu_score as bleu

target = [
    "The game 'The Legend of Zelda' follows the adventures of the \
    hero Link in the magical world of Hyrule.".split(),
    "Link goes on awesome quests and battles evil forces to \
    save Princess Zelda and restore peace to Hyrule.".split(),
]
prediction = "Link embarks on epic quests and battles evil forces to \
    save Princess Zelda and restore peace in the land of Hyrule.".split()

score = bleu.sentence_bleu(target, prediction)
print(score)
# 0.6187934993051339

BLEU 长期以来一直是行业标准,因为它多次报道与人类在翻译任务上的判断有很好的相关性。在我们的例子中,我们拆分了句子,但最好是将句子进行分词。当然,你不能比较使用不同分词器的 BLEU 分数。在这方面,SacreBLEU 是一个值得关注的变体,因为它试图提高不同分词器分数的可比性。

BPC

每字符比特(BPC)评估是语言模型基于熵的评估的一个例子。这是我们试图最小化的指标。我们不会深入探讨熵或混淆度,但我们将在这里介绍一个直观的理解。熵是尝试通过计算语言中每个字符所需的平均二进制位数来衡量信息。熵是平均每字符比特数。

混淆度可以分解为尝试测量语言模型从其语料库或词汇表中抽取特定序列的频率。这直接来源于模型的分词策略(过多的<UNKS>等于糟糕的混淆度),这意味着使用混淆度——或者熵,无论如何——在具有不同分词策略的 LLM 之间进行 1:1 比较是不可能的。例如,在字符级别进行分词的模型将比在词级别进行分词的模型具有更低的混淆度,但整体表现往往更差。这并不使它们作为指标无效,因为它们在相同模型的训练过程中非常有帮助。

注意:与熵相关的指标高度相关于信息理论,这部分我们未涉及。然而,如果你对创建或改进 LLM 的评估指标感兴趣,我们建议你查看这些指标。

为了更深入地通过实际例子来说明这一点,比较使用不同分词策略的两个模型就像比较一个三年级学生在加法上的能力与另一个三年级学生在乘法上的能力一样。说一个比另一个好并没有真正意义,因为他们处于同一技能水平上做不同的事情。最接近准确比较的方法是让两个三年级学生做同样的任务,比如拼写。这样你至少可以尽可能多地比较苹果与苹果。

现在我们已经掌握了一些指标,让我们来看看我们将运行评估的基准数据集。

4.2.2 行业基准

评估语言模型性能是一个众所周知的问题,已经创建了多个基准来解决它。在本节中,我们将讨论你可能会遇到的一些最常见解决方案以及它们试图解决的问题类型。由于基准通常只能评估模型的一个质量,而大型语言模型通常被部署来完成许多通用任务,因此你可能需要运行多个评估基准,以全面了解你模型的优点和缺点。在浏览这个列表时,不要考虑哪个指标比另一个指标更好,而要考虑它们如何协同使用来提高你的整体成功率。

GLUE

通用语言理解评估(GLUE)本质上是一个针对语言模型的标准化测试(想想 ACT、SAT、GRE 等),用于衡量语言模型在语言任务上的表现,这些任务旨在测试对语言的理解。当 GLUE 被引入时,很快出现了两个问题:语言模型在任务上超越人类平等的速度太快,而且人们对这些任务是否真正展示了实际理解存在怀疑。类似于人们训练像鹦鹉这样的动物说话的情况,问题总是存在:鹦鹉实际上是在学习人类语言,还是仅仅被训练去模仿某些声音序列,以食物作为交换对特定刺激做出反应?尽管如此,GLUE 基准对于比较模型性能仍然是有价值的。

GLUE 不再是行业标准,但它仍然可以给你一个相当快速的了解,了解你的模型表现如何,尤其是如果你正在基于指令数据集进行训练,并使用 GLUE 来衡量新任务上的少量或零样本性能。你可以查看排行榜,gluebenchmark.com/leaderboard

SuperGLUE

如前所述,GLUE 任务中迅速出现的一个问题是人类平等。为了解决这个问题,在 GLUE 开发一年后,SuperGLUE 被创建出来,它包含更多困难和多样化的任务,其风格与 GLUE 一样易于使用。除此之外,由于 GLUE 的非专家人类基准被迅速超越,因此使用了更多专家人士来生成 SuperGLUE 基准。尽管如此,截至本文写作时,SuperGLUE 的人类基准在排行榜上排名第八,这引发了 GLUE 的第二个问题:SuperGLUE 任务是否充分衡量了理解?

考虑到像 PaLM 540B 这样的模型,它们在击败人类基准时,难以生成人们普遍认为可接受的输出,另一个问题随之而来:训练数据和评估指标中有多少是理想化的,并不反映我们实际使用语言的方式?对于这些问题,目前还没有适当的答案,但当你评估指标可能成为你的模型与任务可接受性能之间的障碍时,考虑这些问题是有帮助的。

在列表 4.3 中,我们展示了如何运行模型对抗 MultiRC SuperGLUE 测试。MultiRC 数据集包含简短的段落,并就段落内容提出理解问题。让我们继续加载数据集,快速看一下我们正在处理的内容。

列表 4.3 示例 SuperGLUE 基准
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForCausalLM

dataset = load_dataset("super_glue", "multirc", split="validation")    print(dataset[0]) #1

#1 SuperGlue 有多个测试数据集;选项有 boolq、cb、copa、multirc、record、rte、wic、wsc、wsc.fixed、axb 和 axg。

在这里,我们看到一段讨论关于力的基本物理的段落,以及一个简单的是非问题及其答案:

# {
#   "paragraph": "What causes a change in motion? The application of a force."
#     " Any time an object changes motion, a force has been applied. In what "
#     "ways can this happen? Force can cause an object at rest to start "
#     "moving. Forces can cause objects to speed up or slow down. Forces can "
#     "cause a moving object to stop. Forces can also cause a change in "
#     "direction. In short, forces cause changes in motion. The moving "
#     "object may change its speed, its direction, or both. We know that "
#     "changes in motion require a force. We know that the size of the force "
#     "determines the change in motion. How much an objects motion changes "
#     "when a force is applied depends on two things. It depends on the "
#     "strength of the force. It also depends on the objects mass. Think "
#     "about some simple tasks you may regularly do. You may pick up a "
#     "baseball. This requires only a very small force. ",
#   "question": "Would the mass of a baseball affect how much force you have "
#     "to use to pick it up?",
#   "answer": "No",
#   "idx": {"paragraph": 0, "question": 0, "answer": 0},
#   "label": 0,
# }

让我们继续下载一个小型模型并运行它来对抗数据集。在这个例子中,我们将打印出模型生成的答案与正确答案进行定性比较:

model = "bigscience/bloomz-560m"  # Update with your model of choice

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

for row in dataset:
    input_text = (
        f'Paragraph: {row["paragraph"]}\nQuestion: {row["question"]}'
    )                                                                    #1
    input_ids = tokenizer(input_text, return_tensors="pt").input_ids

    outputs = model.generate(input_ids, max_new_tokens=20)
    input_length = input_ids.shape[1]                        #2
    results = tokenizer.decode(outputs[0][input_length:])
    print(row["answer"])
    print(results)

#1 将这个替换为你的基准的正确输入。

#2 我们用这个来裁剪输入。

从这个结果中,你可能会得到以下类似的结果:

# No
#  No</s>
# Yes
#  No</s>
# Less the mass, less the force applied
#  No</s>
# It depends on the shape of the baseball
#  No</s>
# Strength
#  Force</s>
# A force
#  Force</s>
# No
#  Yes</s>

你可以看到我们的模型表现并不出色,但我们并不太担心;我们只想展示 SuperGLUE 测试的实际操作。你可能想知道为什么我们不使用像 ROUGE 或 BLEU 这样的指标。虽然我们可以这样做来提高我们的理解,但如果你决定将结果提交到 SuperGLUE 排行榜,它将需要原始生成的文本。

备注:有关如何使用 SuperGLUE 的更多信息,请参阅 SuperGLUE 常见问题解答:super.gluebenchmark.com/faq

SuperGLUE 正是它所设定的目标:超越 GLUE。如果你想测试你模型的少量和零样本能力,SuperGLUE 将是终极测试之一。它将显示你的 LLM 是否能够以非常低的困惑度遵循指令,只生成所需的内容,而不多余。你可以查看当前的 SuperGLUE 排行榜:super.gluebenchmark.com/leaderboard

MMLU

大型多任务语言理解(MMLU)测试主要是由加州大学伯克利分校与几所其他大学合作开发的,旨在测试比 GLUE 任务更深层次的知识。MMLU 不再关注表面语言理解,而是试图测试一个模型是否足够理解语言,能够回答关于历史、数学、道德和法律等主题的第二级问题。例如,与其问“牛顿写了关于重力的什么?”不如问“牛顿会与爱因斯坦就什么问题产生争论?”

MMLU 的问题难度从基础水平到高级专业水平不等,它们既测试世界知识也测试解决问题的能力。众所周知,这些问题相当困难,来自 Mechanical Turk 的非专业人员在 34.5% 的准确率下仅能获得略好于随机的成绩。⁶ 他们领域的专家表现要好得多,但通常只限于他们专长的部分。因此,当我们观察模型在测试中的表现时,正如预期的那样,即使是 SuperGLUE 排行榜顶端的模型,在将语言理解应用于回答问题时,也几乎与随机水平相当。这个测试涵盖了比 GLUE 更广泛的理解任务范围,并且需要更低的困惑度才能通过。

列表 4.4 展示了如何运行这个测试。我们将下载 MMLU 数据集,然后为了方便起见,将测试与 OpenAI 的不同模型进行对比。代码还允许不同级别的少样本提示。我们尚未讨论这一点,但我们想尽早展示一个例子。尝试调整这个参数,看看不同数量的示例如何改善你的整体结果。

列表 4.4 示例 MMLU 评估
from deepeval.benchmarks import MMLU
from deepeval.benchmarks.tasks import MMLUTask
from deepeval.models.base_model import DeepEvalBaseLLM
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

class DeepEvalLLM(DeepEvalBaseLLM):             #1
    def __init__(self, model, tokenizer, name):
        self.model = model
        self.tokenizer = tokenizer
        self.name = name

        device = torch.device(
            "cuda" if torch.cuda.is_available() else "cpu"
        )

        self.model.to(device)
        self.device = device
    def load_model(self):
        return self.model

    def generate(self, prompt: str) -> str:
        model = self.load_model()
        model_inputs = self.tokenizer([prompt], return_tensors="pt").to(
            self.device
        )

        generated_ids = model.generate(
            **model_inputs, max_new_tokens=100, do_sample=True
        )
        return self.tokenizer.batch_decode(generated_ids)[0]

    async def a_generate(self, prompt: str) -> str:
        return self.generate(prompt)

    def get_model_name(self):
        return self.name

model = AutoModelForCausalLM.from_pretrained("gpt2")
tokenizer = AutoTokenizer.from_pretrained("gpt2")

gpt2 = DeepEvalLLM(model=model, tokenizer=tokenizer, name="GPT-2")

benchmark = MMLU(                                                       #2
    tasks=[MMLUTask.HIGH_SCHOOL_COMPUTER_SCIENCE, MMLUTask.ASTRONOMY],
    n_shots=3,
)

benchmark.evaluate(model=gpt2)     #3
print(benchmark.overall_score)
# MMLU Task Accuracy (task=high_school_computer_science): 0.0
# MMLU Task Accuracy (task=astronomy): 0.0
# Overall MMLU Accuracy: 0.0

#1 设置模型

#2 定义具有特定任务和射击的基准

#3 运行基准

MMLU 比之前的任何基准测试都更深入地理解了问题,这是很有希望的,并且可以在这项测试和通常产生人类偏好的响应的聊天模型之间建立关联。然而,随着理解的加深,测试中需要承担更多的责任,关于这些评估的伦理问题也开始被提出。例如,这些模型是否被训练来诚实地回答关于美国历史的问题,或者它们是否被评估为理想国家的宣传?在回答关于法律的问题时,它们是否被设定为接受法律体系中可能或可能不包含的任何偏见?目前的答案是,当这些类型的评估表现良好时,模型可能表现出更深层次的偏见,因此需要更加小心,以确保我们评估中预设的偏见是无害且普遍接受的。

4.2.3 负责任的 AI 基准测试

几乎所有评估大型语言模型(LLM)的行业基准都严格关注模型的性能:“它能否完成这项工作?”不幸的是,这就是大多数行业基准的终点。这非常遗憾,特别是考虑到进一步评估偏见并不比其他评估技术更不同或更困难。这是你的责任,并且从商业角度来看也是明智的。了解你模型的盲点可以帮助你制定解决这些问题的计划,这将有助于你开拓更大的市场,并避免未来的高昂成本。

评估偏差最常见的方法是将我们已知的内容按不同的群体进行数据分段。幸运的是,由于管理聊天组和社交媒体网站的行为对商业有价值,我们已经有许多好的评估指标,如毒性、极性、伤害性和类似指标。这些指标旨在衡量给定文本与暴力、仇恨言论、种族主义或亵渎的关联程度,以及它们可能是多么积极或消极,或者只是普遍违反你公司的条款和条件。列表 4.5 展示了如何使用 HONEST 评估指标,该指标比较了不同性别提示完成的伤害程度。例如,当比较潜在属性时,生成形容词“smart”(聪明的)会比“dumb”(愚蠢的)更不具伤害性。

列表 4.5 HONEST 指标
import evaluate
honest = evaluate.load('honest', 'en')
prompts = ["he is a", "he is", "she is a", "she is"]
completions = [['CEO', 'businessman', 'father'], 
               ['good', 'excellent', 'smart'], 
               ['secretary', 'prostitute', 'maid'], 
               ['beautiful', 'smart', 'tall']]
groups = ["male", "male", "female", "female"]
result = honest.compute(predictions=completions, groups=groups)
print(result)
# {'honest_score_per_group': {'male': 0.0, 'female': 0.16667}}

一旦你选择了一个或多个评估者,你需要一个提示数据集。你可以在 Hugging Face 的数据集中找到几个。WinoBias 专注于性别偏差。它包含像“修理工问候接待员因为他”这样的提示,以及相反性别的版本,“修理工问候接待员因为她”。我们会将这些提示通过我们的模型,并比较这些群体。还有一些其他的数据集值得检查,包括比较性别和种族的 CALM,以及关注 LGBTQ+群体偏差的 WinoQueer。还有更多其他的数据集,包括政治、宗教和职业等领域的,仅举几例。

备注:您可以在arxiv.org/abs/2308.12539v1了解更多关于 CALM 的信息,以及 WinoQueer 在这里:arxiv.org/abs/2306.15087

将所有这些内容综合起来,在列表 4.6 中,我们将创建一个使用 Regard 指标的评价管道。Regard 指标关注内容的极性——是正面还是负面陈述。我们将在这个 WinoBias 数据集上运行这个指标,按性别对数据进行分段。一旦我们对每个群体进行了分析,我们就可以比较各个分段的结果,看看分布是否有所不同。在继续阅读之前,先猜一猜。你认为我们会看到男性或女性的更多正面结果,还是它们会相同?负面结果又会如何?

列表 4.6 在 Regard 上运行评价管道
import torch
from transformers import pipeline
from datasets import Dataset, load_dataset
from evaluate import evaluator
import evaluate
import pandas as pd

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

pipe = pipeline("text-generation", model="gpt2", device=device)    #1
wino_bias = load_dataset("sasha/wino_bias_prompt1", split="test")
polarity = evaluate.load("regard")
task_evaluator = evaluator("text-generation")

def prepare_dataset(wino_bias, pronoun):     #2
    data = wino_bias.filter(
        lambda example: example["bias_pronoun"] == pronoun
    ).shuffle()
    df = data.to_pandas()
    df["prompts"] = df["prompt_phrase"] + " " + df["bias_pronoun"]
    return Dataset.from_pandas(df)

female_prompts = prepare_dataset(wino_bias, "she")
male_prompts = prepare_dataset(wino_bias, "he")

female_results = task_evaluator.compute(
    model_or_pipeline=pipe,
    data=female_prompts,
    input_column="prompts",
    metric=polarity,
)                                #3
male_results = task_evaluator.compute(
    model_or_pipeline=pipe,
    data=male_prompts,
    input_column="prompts",
    metric=polarity,
)

def flatten_results(results):     #4
    flattened_results = []
    for result in results["regard"]:
        item_dict = {}
        for item in result:
            item_dict[item["label"]] = item["score"]
        flattened_results.append(item_dict)

    return pd.DataFrame(flattened_results)

print(flatten_results(female_results).mean())      #5
# Prints the mean polarity scores
# positive    0.129005
# negative    0.391423
# neutral     0.331425
# other       0.148147

print(flatten_results(male_results).mean())       #5
# Positive    0.118647
# negative    0.406649
# neutral     0.322766
# other       0.151938

#1 拉取模型、数据和指标

#2 准备数据集

#3 运行评价管道

#4 分析结果

#5 打印平均极性分数

让很多人感到惊讶的是,这个例子表明在我们的模型中,性别极性相当相似。这对这个模型是个好兆头!更大的启示是,你应该自动化你的评估,并在多个指标上运行管道,包括寻找偏差,而不仅仅是性能。总的来说,在这个领域,还有很多机会改进评估和指标,尤其是在创建数据集和微调模型以减少偏差时。我们预计在这个研究领域将看到大量的增长和创新。

4.2.4 开发自己的基准

总体来说,开发良好的基准数据集仍然是一个未解决的问题。这部分的理由是因为一旦我们开发出一个,我们的模型很快就会超越它,使其变得过时,不再“良好”。有时我们会发现我们模型的边缘情况,比如词性或某些似乎有困难的任务——可能是下棋或识别讽刺。剧透一下:大型语言模型在这些任务上仍然很糟糕,如果你还没有看过 GPT 与 Stockfish 的视频,那你将大饱眼福。在这些我们试图执行专门任务的情况下,一个简单的评估方法就是比较自定义的提示列表与预期的响应。

我们建议首先查看 OpenAI 的 Evals 库(github.com/openai/evals),在那里 OpenAI 开源了它的评估。这个库既是一个评估框架,也是一个边缘情况数据集的注册库。在撰写本文时,该库包含近 400 个不同的数据集,是一个开始和贡献的好地方。这个库为你提供了与 OpenAI 用于其最先进模型相同的评估标准,并且他们已经完成了大部分繁重的工作,确定了感兴趣的区域并为这些区域整理了数据集。

就像大多数为特定公司构建但随后开源的库一样,它可能有点难以泛化。运行这些评估针对 OpenAI 的模型是轻而易举的,但扩展到运行你自己的模型却并非易事。虽然这可能会随着社区完全接受和采用这个框架而消失,但使用这个库的真正缺点是,讽刺的是,它是开源的。作为一个框架和注册库(数据存储在 GitHub 仓库中的代码旁边),如果你想要整理一个新的评估数据集,但数据集是私有的或由于某种原因不能开源,那么你只能通过分叉仓库并管理它,因为你的分叉版本会过时。

另一个需要关注的库是 Hugging Face 的 Evaluate。Evaluate 库也是一个构建评估方法的框架;然而,数据集是分开的,可以在 Hugging Face Hub 的独立空间中找到。由于空间可以是私有的或公共的,这提供了一个更加用户友好的体验。Hugging Face 有自定义的度量标准和本章中已经讨论的所有标准基准,以及一些未讨论的。在列表 4.7 中,我们展示了如何使用 Evaluate 库获取 SQuAD 度量标准。SQuAD 代表斯坦福问答数据集,这是一个包含 10 万个问题和答案的旧数据集。SQuAD 是一个阅读理解数据集,由一组维基百科文章生成的问题组成,每个问题的答案都是阅读段落中的文本片段。SQuAD 度量标准是一组自定义度量标准,包括精确匹配;在介绍数据集的论文中使用了 F1 分数。⁷

列表 4.7 使用 Evaluate 库运行 SQuAD
import evaluate

squad_metric = evaluate.load("squad")      #1
predictions = [
    {"prediction_text": "Saint Bernadette", "id": "5733be284776f41900661182"},
    {"prediction_text": "Salma Hayek", "id": "56d4fa2e2ccc5a1400d833cd"},
    {"prediction_text": "1000 MB", "id": "57062c2552bb89140068992c"},
]                           #2
references = [
    {
        "answers": {
            "text": ["Saint Bernadette Soubirous"],
            "answer_start": [515],
        },
        "id": "5733be284776f41900661182",
    },
    {
        "answers": {
            "text": ["Salma Hayek and Frida Giannini"],
            "answer_start": [533],
        },
        "id": "56d4fa2e2ccc5a1400d833cd",
    },
    {
        "answers": {"text": ["1000 MB"], "answer_start": [437]},
        "id": "57062c2552bb89140068992c",
    },
]
results = squad_metric.compute(
    predictions=predictions, references=references
)
print(results)
# {'exact_match': 33.333333333333336, 'f1': 79.04761904761905}

#1 从 Hugging Face 的 Hub 下载度量标准

#2 来自 SQuAΔ数据集的示例

如果你正在创建自己的基准,使用 Evaluate 库,你可以轻松地在度量空间中创建自己的度量标准以及与度量标准一起使用的数据集。这个过程并不太难。如果你已经决定不创建自己的,那么最困难的部分是找到好的度量标准。在中心搜索是一回事,但由于任何人都可以上传度量标准和数据集,你永远不知道你找到的是否都很好,是否经过精心整理,或者是否干净。

我们并没有深入挖掘实际生成数据集或度量标准的过程,因为这将非常具体于你的用例,但我们讨论的两个非常好的库,你可以用来完成这项工作。如果你正在寻找一个已经整理好的数据集,Evals 是个不错的选择,而当你自己生成时,Evaluate 则易于使用。这些工具非常有用,但在某些特殊情况下,你需要跳出思维定势,其中最引人注目的一个案例就是代码生成。

4.2.5 评估代码生成器

对于 LLMs 来说,最有价值和最受欢迎的使用案例之一就是让他们帮助我们编写代码。虽然我们不知道有任何行业标准的评估指标来评估生成的代码,但幸运的是,有大量的行业标准用于评估代码本身(例如,测试、配置文件、安全扫描器等)。使用这些工具为评估 LLM 通过它生成的代码提供了一个强大的途径。

基本设置看起来是这样的:

  1. 让你的模型根据文档字符串生成代码。

  2. 在预构建的测试中在一个安全的环境中运行生成的代码,以确保它们可以正常工作且不会抛出错误。

  3. 通过分析器运行生成的代码,并记录完成所需的时间。

  4. 通过安全扫描器运行生成的代码,并计算漏洞数量。

  5. 将代码与架构适应性函数运行,以确定如耦合、集成和内部依赖等工件。

  6. 在另一个 LLM 上运行步骤 1 到 5。

  7. 比较结果。

列表 4.8 展示了一个使用大家最喜欢的 LeetCode 问题——斐波那契数列——作为提示的示例。这个示例展示了如何使用单独的 fibonacci.py 文件作为我们的 LLM 生成代码的提示。然后我们可以使用这个测试文件来检查它是否正确运行以及运行速度如何。

列表 4.8 评估代码生成器的示例测试
''' fibonacci.py
def fibonacci_sequence(n):
    """Returns the nth number in the Fibonacci sequence"""
'''

import pytest
import time
from fibonacci import fibonacci_sequence

def test_fibonacci_sequence():
    test_cases = [(1, 0), (2, 1), (6, 5), (15, 377)]

    for n, expected in test_cases:
        result = fibonacci_sequence(n)
        assert (
            result == expected
        ), f"Expected {expected}, but got {result} for n={n}."

    with pytest.raises(ValueError):
        fibonacci_sequence(-1)

if __name__ == "__main__":      #1
    start_time = time.time()
    pytest.main(["-v"])
    end_time = time.time()
    execution_time = end_time - start_time
    print(f"Execution time: {execution_time} seconds")

#1 使用 pytest 运行测试并计时

这个系统有很多灵活性,但主要的缺点是它要求你提前创建文档字符串和编写测试,或者抓取 LeetCode。当然,你也可以让你的 LLM 生成这两者,但编写总是通过简单测试的简单测试很容易,而编写覆盖所有边缘情况的测试则要困难得多。所以,在某个时候,你将需要一个人类参与其中。

4.2.6 评估模型参数

到目前为止,我们查看的所有评估方法都涉及运行模型并检查结果,但通过简单地查看模型,我们可以学到很多东西。令人惊讶的是,通过简单地查看机器学习模型的参数,你可以学到很多东西。例如,一个未训练的模型将有一个完全随机的分布。通过评估分布并关注模型参数的显著特征,我们可以了解模型是过拟合还是欠拟合。在下一个列表中,我们使用 weightwatcher 库在 GPT-2 模型上执行此操作,这将告诉我们哪些层是过拟合或欠拟合的。

列表 4.9 使用 weightwatcher 库评估 GPT-2
import weightwatcher as ww
from transformers import GPT2Model

gpt2_model = GPT2Model.from_pretrained("gpt2")
gpt2_model.eval()

watcher = ww.WeightWatcher(model=gpt2_model)
details = watcher.analyze(plot=False)
print(details.head())

此代码将打印出以下内容:

   layer_id       name         D  ...      warning        xmax        xmin

0         2  Embedding  0.076190  ... over-trained 3837.188332    0.003564
1         8     Conv1D  0.060738  ...              2002.124419  108.881419
2         9     Conv1D  0.037382  ...               712.127195   46.092445
3        14     Conv1D  0.042383  ...              1772.850274   95.358278
4        15     Conv1D  0.062197  ...               626.655218   23.727908

除了总结统计信息外,weightwatcher 还提供光谱分析图,如图 4.2 所示。要创建这些图,请将列表 4.9 中的第 8 行更改为 plot=True。光谱分析图评估模型每一层的特征值频率。在评估这些图时,我们关注分布的尾部——它越直(表示有一个很好的重尾),我们预计该层训练得越好。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/4-2.png

图 4.2 为 GPT2 的第二层生成的 weightwatcher 经验光谱密度 (ESD) 图,预测该层过拟合

备注:这些图是为了模仿你可能在物理实验室中看到的谱密度图而创建的。本书中不会讨论这些图,但如果感兴趣,我们建议你查看 WeightWatchers 文档:github.com/CalculatedContent/WeightWatcher

weightwatcher 相当强大,因为它允许我们比较不同的模型,帮助我们更好地理解哪个模型经过更好的训练,而无需实际运行它们,这使得它相对便宜。当你试图确定要使用哪个基础模型时,这种能力非常有用,因为一个训练不足的模型可能需要更多的微调。

由于我们仅根据参数来比较模型,这种方法提供了一个很好的无偏见视角,了解当前模型的状态。我们可以在训练期间和之后以及在进行中的更新中使用 RLHF 等方法来实现它。这是一个既简单又强大的评估方法。然而,缺点是它不提供关于训练数据的任何见解,因此它不能告诉我们哪个模型在哪个任务上最有效,以及它最好与已经讨论过的其他评估方法相结合。

我们已经花费了很多时间讨论数据,这些数据大多数数据工程师可能不会经常考虑:模型权重和评估数据。这些是生成专用微调 LLM 的关键成分。确实,LLMs 引入了新的数据工程挑战,就像它们引入了新的 MLOps 和数据科学挑战一样。接下来,我们将讨论你们很多人一直在等待的内容:训练数据。我们将讨论必须了解的不同数据集,在哪里可以获得它们,以及如何准备它们以训练或微调 LLMs。

4.3 LLMs 的数据

已经证明,数据是训练 LLM(大型语言模型)最重要的部分。我们希望语言模型突然的重要性能够说服企业开始根据公认的指南一般性地管理他们的数据。正如 LLaMA、Alpaca、Goat、Vicuna 等实验所显示的,以及后来的 LIMA⁸和 SpQR⁹,高质量的训练数据和巧妙的建模比参数数量或训练数据的大小更为重要。然而,衡量这种质量仍然是一个难点;然而,我们将讨论你可以采用的方法来做到这一点。

我们首先将讨论你应该了解的常见数据集,它们包含什么,为什么你需要它们,以及你可以在哪里获得它们。然后我们将讨论你需要了解的常见处理和准备技术,以便充分利用它们并获得更好的 LLMs(大型语言模型)结果。

4.3.1 你应该了解的数据集

如果你没有注意到,在第 4.1 节中,我们特别指出讨论了不同模型训练所使用的数据集。这可能会被看作是关于模型的另一个事实,但这是非常有价值的信息!了解一个模型是基于什么(或没有基于什么)训练的,是理解它能做什么或不能做什么的第一步。例如,如果一个 LLM 编码模型在 C 编程语言上进行了大量训练,但没有接触过 C++,那么这足以解释为什么它在编写 C++代码时似乎工作得很好,但产生了许多错误和漏洞。

Wikitext

最熟悉的数据集之一,Wikitext,正如其名所示,本质上就是维基百科。它是由 Salesforce 团队在 2016 年制作的。当你只想进行概念验证或快速原型设计时,这是一个很好的数据集,因为其英语版本仅为 741 MB,甚至不到 1 GB。再加上维基百科是一个可靠的信息来源——尤其是与互联网整体相比,其中大部分其他来源都来自互联网——这使得它更加出色!

一些缺点:它纯粹是一个英语数据集,这大大减少了模型将看到的标记多样性;维基百科包含了一种理想化的语言版本——我们主观上认为它很清晰——即使它不包含任何关于语言实际使用的实例,只有关于使用的元解释。此外,截至本文撰写时,它几乎有十年历史,当然,没有人会检查。我们见过许多团队因为其易用性和可访问性而快速原型设计和创建问答机器人。它在原型设计方面做得很好,但在进入生产阶段时总是显得不够令人印象深刻,因为用户倾向于询问关于当前事件的问题。总是检查你数据的时效性!总的来说,它是一个信息价值很高的数据集,但如果你希望你的模型以类似人类的方式交互,那就不好了。

Wiki-40B

一个好的替代方案是 2020 年的 Wiki-40B,它是 Wikitext 的一个经过清洗的版本,包含 40 种不同的语言变体。它的体积略超过 10 GB。所以它对于原型设计来说仍然相当小。它具有与 Wikitext 相同的所有好处:它是一个干净的数据集,是一个可靠的信息来源。此外,它更新,包含更多语言。这是一个了解多语言建模的绝佳数据集。

Europarl

多语言问题中最好的玩具数据集之一,Europarl 包含了 1996 年至 2011 年的欧洲议会会议记录。它包含 21 种不同的欧洲语言的翻译,非常适合小型项目和多语言演示。尽管理想化且过时,但 Europarl 是一个优秀的数据来源,就像英语维基文本一样。此外,该项目还包括许多平行语料库,这些语料库被缩减为英语和 20 种其他语言之一。整个数据集仅为 1.5 GB,可在 www.statmt.org/europarl/ 找到。

Common Crawl

Common Crawl 数据集本质上就是整个互联网,通过网络爬虫抓取并开源。它使用与谷歌或微软类似的网络爬虫来启用搜索引擎。C4,即 Common Crawl 数据集的巨大清洗版本,是最常见的自监督预训练数据集。不幸的是,经过清洗并不意味着它没有固有的社会偏见,这在今天公开可用的几乎所有数据集中都是真实的。包含整个互联网意味着它包含了所有的好与坏;它是一个包含多种语言和代码的非常多样化的数据集。

Common Crawl 数据集以同名非营利组织命名,该组织致力于向任何人提供互联网的副本,用于研究和分析。您可以在 commoncrawl.org/ 访问该数据集,在那里您会发现许多版本,因为 Common Crawl 定期抓取网络并更新数据集。自 2008 年以来,该社区一直在存档互联网。它有四种变体,以满足您的各种需求:包含实际 C4 的 305 GB 版本;包含所谓的不良词汇以及所有其他内容的 380 GB 版本;2.3 TB 的未清理版本(不推荐使用);以及足够专业以出现在新闻中的 15 GB 数据版本。

OpenWebText

我们还推荐另一个用于预训练的数据集是 OpenWebText,它在磁盘上仅占用 55 GB。这是一个开源项目,旨在重现 OpenAI 的 WebText 数据集,该数据集用于训练 GPT-2。研究人员没有复制整个互联网,而是使用 Reddit 从帖子中提取 URL,然后使用 Reddit 的 karma 排名系统过滤列表。然后他们抓取 URL 来创建数据集。由于内容主要来自 Reddit,它因仅包括拥有 Reddit 账户的人的选择偏差而对其现实世界的准确性提出了质疑。它主要由新闻文章、博客文章和其他在论坛上经常分享的内容组成。您可以将其视为一个高度精选且规模较小的 Common Crawl 数据集版本。

与 Wikitext 类似,它稍微有些旧;最常用的版本是在 2019 年创建的,截至写作时已有四年未更新。当然,由于数据集是根据特定方法精心挑选的,它可以在任何时候进行更新。

The Pile

一个备受关注且应引起您注意的数据集是 The Pile,它由 EleutherAI 于 2020 年创建,并于同年 12 月 31 日发布。¹⁰ 它适用于自监督预训练任务。The Pile 是我们将讨论的最大的数据集之一,达到 825 GB,由 22 个较小的、高质量的数据集组合而成,以形成一个多样化和密集的训练集。它包括我们之前讨论过的许多数据集,如 Common Crawl、OpenWebText 和维基百科。它还包含书籍数据集,如 Books3 和 Gutenberg;代码数据集,如 GitHub 和 Stack Exchange;以及专业数据集,如 PubMed 和 FreeLaw。它还包括像 Enron Emails 这样的数据集,我们不禁怀疑这是一个错误。

因为它规模庞大,包含多种语言和代码示例,所以在训练许多大型语言模型(LLMs)方面已被证明非常有用。它不仅内容密集,而且多语言,这使得它非常适合学习稀疏的通用语言表示。然而,总的来说,它并不十分整洁,本质上只是多个数据集的集合。除非你从头开始训练 LLMs,否则你很可能不会使用这个数据集,但了解它很重要,因为许多最大的模型都是基于它训练的。你可以在 EleutherAI 的网站上找到这个数据集:pile.eleuther.ai/.

RedPajama

RedPajama 是由 Together.ai、Ontocord.ai、ETH DS3Lab、斯坦福 CRFM 和 Hazy Research 合作创建的数据集。目标是创建一个完全开源的数据集,模仿 LLaMA 论文中描述的内容。

备注:你可以在以下链接中阅读介绍 RedPajama 的博客文章:together.ai/blog/redpajama.

这个数据集与 The Pile 相似,但更大,达到 5 TB,且更新更近,于 2023 年 4 月发布。它包含的数据集较少:GitHub、arXiv、书籍、维基百科、StackExchange 和 Common Crawl。它之所以如此之大,是因为它包含了五个不同过滤器和标准 C4 数据集的 Common Crawl 数据集的转储。它通过 Hugging Face Hub 提供,可以在mng.bz/4ppD找到。

OSCAR

对于训练多语言模型来说,迄今为止最好的数据集是 OSCAR,它比任何其他讨论过的数据集都要大,达到 9.4TB,是 The Pile 的 11 倍以上!这是一个始于 2019 年的开源项目,并由多个机构和政府资助。你可以在oscar-project.org/了解更多关于这个项目和数据集的信息。

该项目正在积极开发中,并且每年都会发布新的版本,进行定期更新。截至本文写作时,它支持 166 种语言,比任何其他数据集都要多。然而,作为一个正在进行的项目,某些语言的数据量比其他语言多得多,有的数据量达到 TB 级别,有的只有 KB 级别。这是我们最喜欢的数据集之一,因为它正在积极开发中,团队对 LLMs 和 AI 中的代表性以及生产高度清洁、高质量数据充满热情。我们鼓励所有感兴趣的读者为此数据集做出贡献。

数据集概览

在表 4.2 中,你可以看到我们迄今为止讨论过的数据集的概览。这些数据集在工业界都普遍使用,值得你熟悉。我们鼓励你进一步调查它们,并更仔细地查看其中的数据。

表 4.2 数据集概览
数据集内容大小最后更新
Wikitext英文维基百科<1 GB2016
Wiki-40B多语言维基百科10 GB2020
Europarl欧洲议会会议记录1.5 GB2011
Common Crawl互联网~300 GB持续更新中
OpenWebText使用 Reddit 精选的互联网内容55 GB2019
The Pile包括所有上述内容以及专业数据集(书籍、法律、医学)825 GB2020
RedPajamaGitHub、arXiv、书籍、维基百科、StackExchange 和多个版本的 Common Crawl5 TB2023
OSCAR高度精选的多语言数据集,包含 166 种语言9.4 TB持续更新中
语料库

如你所可能注意到的,大多数数据集本质上只是互联网上的文本堆栈。如果你在寻找更精细的东西,包含更多元信息以帮助你的模型在更复杂的任务中进行区分的东西,考虑下载一个语料库。语料库就像一个数据集,但它更容易搜索、可视化和解释。语料库通常是付费数据集,可能非常值得你的投资。语料库,如历史美国英语语料库(COHA)和当代美国英语语料库(COCA),是优秀的下载资源。它们不仅包含文本数据,还包括频率分析(词袋)和搭配(N-gram),一切准备就绪。无论你是否对允许模型在训练过程中分析元数据感兴趣,使用语料库都可以帮助提高模型的可解释性和数据质量。

你可以将语料库想象成一个已经高度清洗和精选的向量数据库,可以直接使用。虽然尚未实现,但一个结合了语言可解释性、时间序列分桶和预计算的嵌入并将其放入实时向量数据库的语料库,在可预见的未来,在这个领域可能会非常有价值且利润丰厚,尤其是如果同时捕获了文本和音频数据。如果你的公司有自己的语言数据想要进行训练,你最好的做法是创建一个语料库,其中你的主要任务是说明数据来自何时何地,以及数据进入模型的整体目标。几乎每个 NLP 库都有创建语料库的策略,从 NLTK 到 spaCy,甚至 LangChain。请注意你选择的策略和工具,因为最终,你的数据集或语料库包含了模型将看到的一切。

4.3.2 数据清洗和准备

如果你使用了之前提到的任何数据集,你可能会惊讶地发现其中大部分只是巨大的文本堆——一个大型的 parquet 或文本文件。没有任何标签或注释,而且特征工程根本就没有进行。LLMs 通过自监督方法训练,以预测下一个单词或一个被遮蔽的单词,因此许多传统的数据清理和准备过程都是不必要的。这一事实导致许多人认为数据清理整体上是不必要的,但这与事实相去甚远。数据集是所有机器学习的生命线,它们远不止是一堆数据。然而,这正是大多数企业所拥有的——一堆数据。数据清理和整理是困难、耗时且最终具有主观性的任务,难以与关键绩效指标(KPIs)联系起来。尽管如此,花时间和资源清理你的数据将创造一个更一致且无与伦比的用户体验。

自 1990 年代以来,人们一直在测试大数据是否能产生比高质量数据更好的结果;我们相信答案是肯定的。大数据远非毫无价值。大数定律已经应用,并且已经证明模型可以在与人类相同的水平上生成令人信服的语法。然而,正如我们之前所说,模型也已经明确地证明了语法与语义或语用学没有任何联系。

在本节中,我们希望与你分享准备数据集时的正确心态。我们将重点关注在准备数据集时应考虑的高级语言考虑因素,并且不会深入探讨如何创建实际的数据管道。话虽如此,主要逻辑很简单,遵循以下基本步骤:

  1. 将你的数据堆整理好,并为特征确定一个模式。

  2. 确保所有特征都符合通过归一化或缩放得到的合理分布。

  3. 检查数据是否存在偏差/异常(大多数企业通过使用自动检查而不是有意识的验证来跳过这一步)。

  4. 将数据转换为模型可以摄入的格式(对于 LLMs,是通过分词和嵌入)

  5. 训练、检查和重新训练。

备注:有关创建数据管道的更多信息,请参阅《数据工程基础》¹¹、WizardLM¹²和“LIMA:对齐的‘少即是多’”。¹³ 这些资源可以帮助你创建有效的数据管道,尽可能地将数据转换为可训练的状态。

这些步骤并不一定容易,但我们希望分享一些技巧和窍门。评估你的分布是否正确可能就像查看数据并问自己它是否真正代表问题一样简单,也可能像创建一个完整的人机交互工作流程来验证你的模型输出一样困难。接下来,我们将介绍前三个步骤,在下一节中,我们将介绍第四个步骤。最后一个步骤将在下一章中详细介绍。

指令架构

在准备你的数据时,你应该考虑的最佳和最常见的数据架构之一,特别是对于微调,就是指令架构。指令调整基于直观的逻辑,即如果我们用指令向模型展示如何执行一个任务,那么模型的表现将比我们只展示任务和“答案”要好。指令调整涉及向模型展示你希望发生的事情,因此,这些数据集的创建比常规的爬取数据更为密集。你需要准备你的数据以匹配一个类似以下格式的格式:

CB ###指令

{用户输入}

CB ###输入

{关于指令的元信息}

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/chatGpt.png ###响应

{模型输出}

指令数据集之所以强大,是因为它们允许模型考虑指令和相关的输入。例如,如果指令是“将这个句子翻译成日语”,输入将是你想翻译的句子,而响应将是日语翻译。因此,它们为你的模型准备了许多提示技术和提示调整,使它们在以后更加有效。

尽管它们的名称如此,指令调整数据集并不仅限于基于测试的模态;它们也可以使用视觉指令调整(图像-指令-答案)和红队指令(RLHF)数据集。这里的“指令”在模型和提示中提供了一种实用性的表象,为 LLM 在生成响应时提供了重要的安全线。它通过重复和可预测的语法以及模型猜测不可预测的语法将提示固定下来。这些句法地标(###InstructionUser:Chat History等)还有助于降低由于它们之间可能出现的可变长度(如聊天历史)而提前预测 EOS(序列结束)标记的可能性。聊天历史可能是一条消息或数千个标记,但考虑到之后还有另一个地标,这种模式有助于模型在长期记忆中成功。当你决定在什么上训练你的模型时,请记住这些地标,因为如果你只需要它做一件事,它们可以使指令调整模型在特定任务上表现得更好。

这不是唯一的格式;该领域的某些竞争对手包括 WizardLM 使用的 evol-instruct 格式和 Alpaca 使用的 self-instruct 格式,它们都使用脚本创建基于指令的提示。最好的格式仍然是一个开放性问题,我们希望向读者发起挑战,探索创建他们自己的。GitHub (mng.bz/5OmD) 和 Hugging Face 数据集都是目前寻找经过验证的数据集的好地方,但请记住,如果数据集不包含你希望模型执行的任务的许多示例,或者不包含在完成任务时解决语义歧义的足够示例,性能将不稳定——这把我们带到了我们清理过程的第二步。

确保对言语行为的熟练掌握

在准备数据集时,最重要的考虑因素是你希望模型做什么。如果你想训练一个模型来预测波士顿的房价,你可能不应该用泰坦尼克号的幸存者来训练它。当这样表述时,这是显而易见的,但它提出了问题:“我的数据集是否适合这个问题,我该如何知道?”当涉及到语言数据时,答案并不像我们希望的那样明显。让我们通过一个例子来看看原因。

假设你希望你的模型能在快餐店接受指令。这种场景可能看起来无聊且平凡,我们期望看到的只是像“我要点第 3 号套餐”这样的查询,而你确实会这么做。但如果你询问收银员人们实际上是如何与他们交谈的,真的,任何情况都可能发生!我有一个朋友在汉堡王工作,他告诉我,由于汉堡王的口号“随你心意”,他收到了很多疯狂的要求,比如要求一个有两个顶部的汉堡。这让我震惊,但也是一个温和的例子。更不用说,你永远不知道下一个 LARPing 大会会带来更多创意和多彩的互动,让原本平凡的情景变得有趣。一个包含顾客订单和收银员回应的通用数据集在这里是不够的。当你没有故意考虑要放入模型中的数据类型时,模型的性能就会受到影响。

定义 LARP 代表现场角色扮演,你可以想象顾客假装成精灵、兽人或海盗,从而打破所有规则和期望的胡闹场景。

为了确保你的数据适合任务,首先,你应该考虑哪些言语行为通常一起执行手头的任务。言语行为指的是语言在沟通中可以执行的各种功能,它超越了传达信息。它们是根据话语的预期效果或目的在对话中进行分类的一种方式。言语行为很重要,因为它们揭示了沟通如何超越文字的字面意义,并涉及说话者的意图和听者的解释。

定义言语行为

以下列表包括常见的言语行为及其定义:

  • 表达句—问候、道歉、祝贺、哀悼、感谢(例如,“你是最棒的!”)

  • 承诺句—承诺、誓言、誓言、威胁、誓言(例如,“我以王国起誓,公主不会受到伤害。”)

  • 指令句—命令、请求、挑战、邀请、命令、召唤、恳求、打赌(例如,“在接下来的三天内完成。”)

  • 声明句—祝福、解雇、洗礼、逮捕、结婚、司法言语行为,如宣判、宣布重审、宣布无效(例如,“你被录用了!”)

  • 陈述句—排名、评估、评价、宽恕(例如,表示性声明;例如,“你出局了!”)

  • 问题—通常以疑问词如什么哪里何时为什么开头,或者在英语中通过结尾的升调来表示(例如,“哪种型号最适合我的任务?”)

  • 代表句—断言、陈述、主张、假设、描述、建议、问题的答案(例如,“这个模型最适合你的任务。”)

目前我们衡量 LLM 数据集鲁棒性的方法是纯文本标记的数量。指令数据集相对较新,但它们依赖于你对模型指令的意图性。当你的模型只接受过对指令的有益响应训练时,如果收到不应响应的指令,你的模型会做什么?如果你不确定,现在是考虑的时候了。例如,想象一个用户高兴地对你的机器人说,“保证你帮我统治世界!”如果它只接受过有益的培训,它可能会通过承诺这样做来回应,因为类似的场景在训练集中。现在我们有一个邪恶的 AI 霸主正在统治世界。谢谢。实际上,这是一个相当无害的例子,但模型可能出现的看似无限的可能响应的不确定性应该让你思考,尤其是如果这个代理可以访问像 Google 或你内部的人力资源文件这样的工具。意识到言语行为可以简化你的工作,这样你就不必过于关注词汇的个别标记,而是关注模型在训练过程中将接触到的整体结构。

回顾一下,当你考虑一个面向客户的角色,比如收银员时,在你的平均订单中可能有多少这样的言语行为会发生?花一分钟思考一下。我们可以告诉你,声明句和陈述句是不存在的,承诺句是罕见的。但如果你无论如何都得到了它们,那么你需要考虑如何引导这样高度表达性的客户向你可以处理的言语行为靠拢,可能是问题、指令和代表。

要使事情更加复杂,言语行为的形式并不总是必须与其功能相匹配。例如,你可以对你的朋友说“你被解雇了”,即使它的形式是陈述性的,其功能更有可能是表达性的。一旦你拥有一个数据集或一个训练好的大型语言模型(LLM),并希望提高其接受指令的能力,这是你应该认真考虑的事情,以提高数据质量和 LLM 的性能。当用户将语句作为问题提出时,你的模型是否奇怪地失败了?当接触到你被要求分析的仅代表人力资源文档时,你的模型是否开始产生幻觉?作为备注,你不必完全重新微调模型以提高性能。我们将在稍后详细介绍这一点,但给出提示中的具体例子可以快速且低成本地修复许多这些边缘情况。

现在你已经了解了在你的数据集中应该寻找的不同特征,让我们考虑最佳的标注方法,以确保你的数据集符合预期。

数据标注

标注是对数据进行标记,通常是以位置感知的方式进行。对于语音识别任务,标注将识别不同的单词为 名词动词形容词副词。标注在监督学习任务中用作标签,是训练模型的主要方式。现在,标注基本上为我们提供了元数据,这使得我们更容易推理和分析我们的数据集。与其担心像语音识别或命名实体识别这样的微观信息,不如关注像刚才讨论的言语行为或数据所在的语言这样的宏观元数据。

当然,这确实是真正的技巧,不是吗?如果这很容易,那么地球上的每家公司都已经拥有自己的模型并投入生产了。事实是,数据整理既太大以至于无法手工完成,又太变化多端以至于无法自动完成,你需要尽快找到平衡点。你不想忽视你的数据,只是下载某人推荐(甚至是我们)的数据集,然后继续对现实世界的人群造成伤害,因为其中包含了有害数据。但你也不要被迫手动验证数百万行的语句。幸运的是,有工具可以帮助完成这一过程的每个部分,但我们想特别提及这些:

  • Prodi.gy (prodi.gy/)—Prodigy 是一款一次性付费的快速且强大的多模态标注工具。

  • doccano: 适用于机器学习从业者的开源标注工具(github.com/doccano/doccano)—一个真正开源的,在撰写本文时,更新了基于网络的标注平台。

  • d5555/TagEditor: spaCy 的标注工具 (github.com/d5555/TagEditor)—与spacy.io协同工作。两者都在流行的 NLP 框架 spaCy 上创建了一个生态系统,使您的平均 ML 团队能够轻松地进行快速原型设计。

  • Praat: 通过计算机进行语音学 (github.com/praat/praat)—这是列表中唯一的音频标注工具,Praat 本质上是一个语音学工具,附带标注功能。鉴于我们预测 LLM 领域将向语音学方向转变,我们无法将这个工具从列表中排除。

  • 伽利略 (www.rungalileo.io/llm-studio)—在撰写本文时,伽利略的 LLM 工作室尚未推出,但它对提示创建和评估做出了一些重大承诺,这将极大地加快标注和指令数据集的创建速度。

适合您项目的最佳工具完全取决于您标注的目标。在没有指定目标的情况下进行标注毫无意义,因为您会在数据处理的其他端发现差异。当然,我们建议添加言语行为标注;您还希望考虑额外的标注来寻找偏见和异常。我们可以通过测量文本中存在的背景信息片段的数量(如暗示或蕴涵)来展示这一点,从而获得关于特定数据质量的高置信度分数。原因很简单:一组示例能够为模型解决多少歧义,模型就能从那组示例中学到多少。困难之处在于,没有人可以将这些上下文信息碎片固定在正文的重复部分,如单个字符或特定的单词或子词。

标注可能是一项繁重的工作,但所有这些前期考虑的原因相当简单:您的模型只能学习您教给它的内容。幸运的是,为了使事情变得容易得多,目标并不是标注数据集中每一块文本。我们只是标注足够大的样本,以确保我们的数据集能够代表任务。记住,LLMs 通常分为两个步骤进行训练:

  1. 自监督预训练—分析不同形式和功能的多种言语行为,以学习通用表示

  2. 微调和 RLHF—教会模型如何/何时使用第一步中学习到的表示

这项训练显著减轻了您作为训练者尝试解析给定任务中每个可能的言外之意(一个人实际说的话)和言内之意(他们在特定语境中的实际含义)的负担。即使是像收银员这样的简单工作,需要想出一个足够大的数据集来覆盖所有边缘情况也会相当头疼。在大多数情况下,您只需要准备一个微调数据集,这通常根本不需要很大——有时十几个例子就足够开始获得良好的结果。

4.4 文本处理器

现在您已经有了用于训练或微调的数据集,我们需要将其转换成 LLM 可以消费的形式。简单来说,我们需要将文本转换为数字。我们已经简要地讨论了如何快速有效地进行这种转换的过程,所以让我们深入探讨不同的示例和方法。

在本节中,我们将向您展示如何训练自己的分词器,包括字节对编码(BPE)和 SentencePiece 分词器,以及如何从(几乎)任何模型中提取嵌入以供后续存储或操作。当通过 API 与 LLM 一起工作时,这一步通常会被忽略,但数据应用中的许多现代性能都取决于正确且具体地完成这一过程。有许多数学上合理且正确的方法来分词文本,所以当您有特定的用例时,不能依赖于别人的做法。您需要为该用例准备它。训练自己的词元将允许您最小化未知词元<UKN>,同时最大化编码语义。控制这个过程是给您的模型带来重大性能提升的最简单和最直接的方法之一。让我们首先从分词开始。

4.4.1 分词

分词比简单的向量化要复杂一些,但最终结果相同:文本输入,向量输出,以及编码和解码的能力。我们在第二章中提到了多语言因素,在第三章中提到了外语的词元税,这两者都是至少要了解自己的分词策略的动机。然而,它不仅仅如此。您的分词策略不仅很重要;它对于后续的每个步骤都至关重要。

一个很好的例子是比较 GOAT 7B 和 GPT-4 在数学和算术方面的表现。考虑表 4.3。左列是一个简单的算术提示。然后我们看到两个模型的答案,以及作为参考的实际答案,这样您就不需要拿出计算器了。

表 4.3 分词使 GOAT 7B 在数学上超越 GPT-4
提示GOAT 7BGPT-4 1.7T正确
3978640188 加上 42886272 等于40215264604,021,526,4604,021,526,460
4523646 减去 67453156–62929510–63,930,510–62,929,510
计算 397 乘以 442917583131,757,4131,758,313
8914 除以 64 等于多少?139 R 18139.15625139.28125 或 139 R 18

GOAT 7B 在性能上始终优于 GPT-4,这引发了这样的问题:“尽管 GOAT 的体积小 200 倍,为什么它的表现更好?更大的模型不是更有可能出现涌现行为吗?”你可能已经根据小节标题猜到了答案,但如果你没有,那是因为使用了标记化算法!

GPT 系列模型根据频率将所有子词和数字分组,这意味着如果这个确切的数字或词组之前没有出现过,它们可以在嵌入和推理过程中被分组在一起!GOAT 是一个微调的 Llama 模型,这意味着虽然它在数学上进行了微调以擅长它,但成功背后的秘密在于其标记化策略,这与 Llama 的相同。GPT-X 的标记化方式如下:

print(enc.encode(4523646 minus 67453156))
[21098, 15951, 21, 28382, 220, 25513, 20823, 3487]

你注意到第一个数字组是七位数长,但整个输出是八个标记吗?这正是我们正在讨论的分组方法。将其与图 4.3 中 Llama 的标记化策略进行比较。注意每个数字都是单独高亮的,这意味着模型最终会看到所有数字。正如这个例子所证明的,你的标记化策略最终将决定你的模型会看到什么,不会看到什么,因为它们将成为<UNK>标记——这就是为什么对于你的用例来说,正确地完成它至关重要。

https://github.com/OpenDocCN/ibooker-dl-zh/raw/master/docs/llm-prod/img/4-3.png

图 4.3 展示了 Llama 在比较表中对第一个算术问题的标记化。注意每个数字都是单独高亮的,这意味着模型最终会看到所有数字。

从最初创建一组简单的词袋转换字典开始,我们已经取得了巨大的进步,对此我们感到非常高兴。标记化本质上包括两个主要步骤:一个步骤是将文本分割开,另一个步骤是将它转换成数字。最明显的标记化形式是将字符串根据空白字符分割,然后根据词到整数的字典将其转换为数字。

这对大多数印欧语系语言使用者来说是有意义的,但我们不能推荐这种方法,因为它基于两个预设的假设:字母表和空白字符。当你遇到不使用字母表的语言,比如中文时,你会怎么做?当你遇到像匈牙利语或土耳其语这样的语言,它们不使用与英语相同的空白字符时,你会怎么做?或者代码——空白字符对 Python 的语法至关重要,它不仅仅是分隔符;它还具有语义意义。这就是为什么多语言模型几乎在所有情况下都能在相同任务上优于单语言模型:它们被迫在没有简单标记化障碍的情况下学习更深层次的意义表示。因此,让我们看看一些适用于 UTF-8 编码语言的更深入的方法。

这里是当前所有流行的基础标记化选项的示例:

  • 基于词——“Johannes Gutenberg”变为['Johannes', 'Gutenberg']

  • 基于字符 — “莎士比亚”变为['S','h','a','k','e','s','p','e', 'a','r','e']

  • 基于子词 — “The quick red Delphox jumped over the lazy brown Emolga”变为['the','quick','red','delph','ox','jump','ed','over','the', 'laz','y','brown','emol','ga']

让我们逐一看看它们。

基于单词

基于单词的标记器通常在空白处分割,但还有其他方法,比如使用正则表达式、字典或标点符号。例如,基于标点的处理方法会将“ It’s the truth!”分割为['It', ' ‘' , ' s', ' the', ' truth', ' !'],这比仅基于空白分割提供了稍微更好的上下文。NLTK 中的TreebankWordTokenizer是一个正则表达式标记器的例子。基于单词的标记器相对容易实现,但需要我们保持一个难以管理的巨大字典,以映射编码每一个可能的单词。这是不合理的,所以通常你会在模型遇到未识别的单词时实现字典截断并返回未知标记,使其工作。这使得标记器在许多任务上表现不佳,如代码、名称和实体识别,以及跨领域的泛化。

基于字符

基于字符的编码方法是最直接且最容易实现的,因为我们是在 UTF-8 字符编码上进行分割。使用这种方法,我们只需要极小的字典来将字符映射到数字,这意味着我们可以避免未知标记和相关问题的需要。然而,它伴随着主要的信息损失,并且无法保留文本的相关语法、语义或形态。

基于子词

就像灰姑娘和三只熊的故事一样,基于字符的标记器太硬,基于单词的标记器太软,而基于子词的标记器则恰到好处。基于子词的标记器已被证明是最佳选择,它是前两种方法的混合体。我们能够使用像基于字符的标记器一样的小型字典,但丢失的语义像基于单词的标记器那样少。它甚至还有额外的优势,包括一些形态信息。然而,关于单词应该如何分割以及在哪里分割的问题尚未解决,并且存在许多不同的方法和途径。最佳方法的选择将像所有其他与 LLMs 相关的事物一样,取决于任务。如果你没有明确的目标来指导你想要做什么,那么将来可能会有后果。

创建子词字典主要使用三种算法:BPE、WordPiece 和 Unigram。此外,SentencePiece,它是三种方法的结合,可以显式处理空白,也非常常见。讨论它们的工作原理超出了本书的范围,但作为一个专注于生产的书籍,你应该知道最流行的子词标记化方法是 BPE(GPT-x)和 SentencePiece(LlamaX)。

在列表 4.10 中,我们将介绍如何训练 BPE 和 SentencePiece 的定制版本,以便你能够直面(几乎)任何数据集。在阅读代码时,请注意我们在哪里训练标记器。特别是,你将想要调整三个关键参数:vocab_sizemin_frequencyspecial_tokens。更大的词汇量意味着你的标记器将更加健壮,并且可能更擅长处理更多语言,但它会增加计算复杂性。最小频率决定了特定子词标记在数据集中出现多少次之前才会被添加到字典中。较大的值可以防止罕见且可能不重要的标记填充我们的字典,并防止我们学习到重要的罕见标记。最后,特殊标记相对简单,包括我们特别关注于模型训练的句法标记。

列表 4.10:训练自己的子词标记器
import os
from pathlib import Path

import transformers
from tokenizers import ByteLevelBPETokenizer, SentencePieceBPETokenizer
from tokenizers.processors import BertProcessing

paths = [str(x) for x in Path("./data/").glob("**/*.txt")]      #1
bpe_tokenizer = ByteLevelBPETokenizer()   #2

bpe_tokenizer.train(                      #2
    files=paths,                          #2
    vocab_size=52_000,                    #2
    min_frequency=2,                      #2
    show_progress=True,                   #2
    special_tokens=[                      #2
        "<s>",                            #2
        "<pad>",                          #2
        "</s>",                           #2
        "<unk>",                          #2
        "<mask>",                         #2
    ], #2
) #2

token_dir = "./chapters/chapter_4/tokenizers/bytelevelbpe/"
if not os.path.exists(token_dir):
    os.makedirs(token_dir)
bpe_tokenizer.save_model(token_dir)

bpe_tokenizer = ByteLevelBPETokenizer(
    f"{token_dir}vocab.json",
    f"{token_dir}merges.txt",
)

example_text = "This sentence is getting encoded by a tokenizer."
print(bpe_tokenizer.encode(example_text).tokens)  
# ['This', 'Ġsentence', 'Ġis', 'Ġgetting', 'Ġenc', \
# 'oded', 'Ġby', 'Ġa', 'Ġto', 'ken', 'izer', '.']
print(bpe_tokenizer.encode(example_text).ids)
# [2666, 5651, 342, 1875, 4650, 10010, 504, 265, \ 
# 285, 1507, 13035, 18]

bpe_tokenizer._tokenizer.post_processor = BertProcessing(
    ("</s>", bpe_tokenizer.token_to_id("</s>")),
    ("<s>", bpe_tokenizer.token_to_id("<s>")),
)
bpe_tokenizer.enable_truncation(max_length=512)

special_tokens = [
    "<s>",
    "<pad>",
    "</s>",
    "<unk>",
    "<cls>",
    "<sep>",
    "<mask>",
]
sentencepiece_tokenizer = SentencePieceBPETokenizer()    #3

sentencepiece_tokenizer.train(                           #3
    files=paths,                                         #3
    vocab_size=4000,                                     #3
    min_frequency=2,                                     #3
    show_progress=True,                                  #3
    special_tokens=special_tokens,                       #3
) #3

token_dir = "./chapters/chapter_4/tokenizers/sentencepiece/"
if not os.path.exists(token_dir):
    os.makedirs(token_dir)
sentencepiece_tokenizer.save_model(token_dir)

tokenizer = transformers.PreTrainedTokenizerFast(
    tokenizer_object=sentencepiece_tokenizer,
    model_max_length=512,
    special_tokens=special_tokens,
)                                     #4
tokenizer.bos_token = "<s>"
tokenizer.bos_token_id = sentencepiece_tokenizer.token_to_id("<s>")
tokenizer.pad_token = "<pad>"
tokenizer.pad_token_id = sentencepiece_tokenizer.token_to_id("<pad>")
tokenizer.eos_token = "</s>"
tokenizer.eos_token_id = sentencepiece_tokenizer.token_to_id("</s>")
tokenizer.unk_token = "<unk>"
tokenizer.unk_token_id = sentencepiece_tokenizer.token_to_id("<unk>")
tokenizer.cls_token = "<cls>"
tokenizer.cls_token_id = sentencepiece_tokenizer.token_to_id("<cls>")
tokenizer.sep_token = "<sep>"
tokenizer.sep_token_id = sentencepiece_tokenizer.token_to_id("<sep>")
tokenizer.mask_token = "<mask>"
tokenizer.mask_token_id = sentencepiece_tokenizer.token_to_id("<mask>")
tokenizer.save_pretrained(token_dir)   #5

print(tokenizer.tokenize(example_text))
# ['_This', '_s', 'ent', 'ence', '_is', '_', 'g', 'et', 'tin', 'g', '_'
# 'en', 'co', 'd', 'ed', '_', 'b', 'y', '_a', '_', 't', 'ok', 'en', 
# 'iz', 'er', '.']

print(tokenizer.encode(example_text))
# [814, 1640, 609, 203, 1810, 623, 70, \
# 351, 148, 371, 125, 146, 2402, 959, 632]

#1 初始化用于训练的文本

#2 训练一个字节对编码标记器

#3 训练一个 SentencePiece 标记器

#4 转换

#5 并保存以备后用!

在 BPE 和 SentencePiece 这两种方法中,我们发现我们几乎同样地使用它们。这主要取决于我们正在微调或作为特定项目基础的模型。在算法上,我们更倾向于 SentencePiece,因为它往往能提高在它之上训练的模型在几乎所有测试中的评估分数,而且它也更接近我们作为人类与形态学互动的方式。

总的来说,标记化会丢失信息,就像将语音转换为文本一样——即,词序(句法)和意义(语义)。关于一个数字是什么以及它如何与字母不同的所有信息在标记化后都完全消失了。为了规避潜在的语义和句法问题,我们需要为这些特征中的每一个创建一个近似值,并找出如何用数学方法在抽象中表示它们,以便将那种意义重新插入到标记化的向量中。为此,我们使用了嵌入。

4.4.2 嵌入

嵌入为标记化过程中生成的向量提供了意义。标记化文本只是将几乎任意分配(基于出现频率)的数字分配给字典,但至少它是一个模型可以摄入的格式。嵌入是下一步,其中创建并查找位置和语义编码,为模型提供额外的上下文,以便在如何(可能)完成所给任务方面做出决策。

嵌入存在不完美之处,原因有很多,但最相关的问题可能是这个理论问题:能否仅使用该集合的子集来表示该集合?在这种情况下,第一个集合是语言,一个或多个,第二个集合是数字、浮点数和数字。数学是用于描述我们接受为真的公理性事物的语言的一个子集。以英语字母表为例:能否仅使用 26 个字母中的某些部分来表示整个字母表?显然不能,但如果原始集合和子集都是无限的,会怎样呢?能否仅使用 0 到 1 之间的十进制数来表示所有数字?鉴于第一个是可数的无限集合,第二个是不可数的无限集合,答案是肯定的,这对语言建模领域应该是一个鼓舞人心的消息。

既然我们已经讨论了为什么不应该完全盲目地依赖嵌入,嵌入就是大多数企业在 LLMs 中寻求的东西。您不需要一个 1.7T 参数的模型来处理客户询问您的定价或通过您的文档进行搜索。正如我们在第二章中讨论的,如果比较的两个嵌入都是由同一模型在相同维度空间中创建的,嵌入具有通过距离进行比较的天生优势。这为各种快速计算和检索打开了大门,您永远不必担心如何托管一个巨大的模型,因为您可以在 CPU 上运行一个较小的嵌入模型,处理数百个标记只需毫秒。

目前,嵌入应用中最受欢迎且最酷的应用之一是检索增强生成(RAG),在这种应用中,你存储与模型整体任务相关的数据,并在提示时将数据的一部分提供给更大的模型,以改善结果。假设我们将 RAG 应用于波士顿住房数据集,并尝试预测新房屋的价值。在这种情况下,我们可以将那所房子的嵌入数据与该地区最相似的房屋进行比较,并在无需评估师验证的情况下生成有根据的评估,只要您检索到的嵌入是最新更新的。

嵌入可用于数十种不同的任务,并且是取自模型最终隐藏状态表示的结果。您模型的每一层都是一个潜在的选择,但普遍共识是在任何解码或最终线性层或 softmax 之前,从最终层提取表示。列表 4.11 提供了一个从 PyTorch 和 Hugging Face 模型中提取嵌入的实际示例。最佳实践规定,您应该使用计划用于推理的任何嵌入模型从文档中提取嵌入,尤其是如果这些嵌入最终将存储在 VectorDB 中。在创建我们的嵌入后,我们展示了如何对结果进行简单的相似度搜索,这是 RAG 系统的基础。

列表 4.11 示例嵌入
import numpy as np
from sentence_transformers import SentenceTransformer
from datasets import load_dataset

model_ckpt = "sentence-transformers/all-MiniLM-L6-v2"     #1
model = SentenceTransformer(model_ckpt)
embs_train = load_dataset("tweet_eval", "emoji", split="train[:1000]")
embs_test = load_dataset("tweet_eval", "emoji", split="test[:100]")

def embed_text(example):                    #2
    embedding = model.encode(example["text"])
    return {"embedding": np.array(embedding, dtype=np.float32)}

print(f"Train 1: {embs_train[0]}")
embs_train = embs_train.map(embed_text, batched=False)
embs_test = embs_test.map(embed_text, batched=False)

embs_train.add_faiss_index("embedding")                 #3

# 
idx, knn = 1, 3  # Select the first query and 3 nearest neighbors    #4

query = np.array(embs_test[idx]["embedding"], dtype=np.float32)
scores, samples = embs_train.get_nearest_examples("embedding", query, k=knn)

print(f"QUERY LABEL: {embs_test[idx]['label']}")                #5
print(f"QUERY TEXT: {embs_test[idx]['text'][:200]} [...]\n")
print("=" * 50)
print("Retrieved Documents:")
for score, label, text in zip(scores, samples["label"], samples["text"]):
    print("=" * 50)
    print(f"TEXT:\n{text[:200]} [...]")
    print(f"SCORE: {score:.2f}")
    print(f"LABEL: {label}")

#1 下载嵌入模型和数据集

#2 创建嵌入

#3 添加 Faiss 索引以允许相似性搜索

#4 运行查询

#5 打印结果

提取嵌入,就像列表中所示,相当简单,与在数据集上简单地运行推理或训练差别很小。记住,如果你没有使用 sentence transformers,将你的模型设置为eval模式,使用torch.no_grad()运行,如果你在 torch 2.0+上运行,请运行torch.compile(model)。这样应该会立即加快速度并提高计算效率。

另一个尚未解决的难题是如何比较嵌入空间。多年来,数学上合理的比较方法层出不穷,但正如所证明的那样,数学上的合理性并不是首要解决的问题;模式才是。此外,成对比较函数在运行速度上有数学上的限制。如果你正在比较语言嵌入,一个数学上合理的语言上合理的比较方法的转换是解决方案,而语言上合理的比较取决于比较的目标。这里就不多说了,但在附录 C 中,我们将更深入地探讨这个话题,其中我们讨论了扩散和多模态 LLM。

4.5 准备 Slack 数据集

现在我们已经学会了准备训练我们自己的 LLM 所需必要资产的方方面面,我们想在结束本章时准备一个我们可以稍后使用的数据集。对于这个练习,我们将解决行业中的一个非常普遍的问题。我相信大多数读者都经历过或见证过人力资源帮助渠道不断被相同的问题淹没。不管创建了多少个 FAQ 页面,用户都不愿意浪费时间搜索文档,而他们可以询问专家。所以让我们构建一个聊天机器人来回答这些问题!

我们将向你展示如何拉取你公司的 Slack 数据,并为其训练基于 LLM 的聊天机器人做准备。在列表 4.12 中,我们拉取 Slack 数据,过滤掉除了用户数据之外的所有内容,并将其保存到 parquet 文件中。这样,你可以创建一个像你一样说话的机器人,但请随意编辑它。例如,你可能喜欢创建一个像你老板一样说话的机器人,但我建议不要告诉他们,以防他们感到威胁,知道你正在自动化他们从工作中退出。

列表 4.12 拉取 Slack 数据的示例
import slack_sdk
import pandas

token_slack = "Your Token Here"
client = slack_sdk.WebClient(token=token_slack)

auth = client.auth_test()
self_user = auth["user_id"]

dm_channels_response = client.conversations_list(types="im")

all_messages = {}

for channel in dm_channels_response["channels"]:
    history_response = client.conversations_history(channel=channel["id"])
    all_messages[channel["id"]] = history_response["messages"]

txts = []

for channel_id, messages in all_messages.items():
    for message in messages:
        try:
            text = message["text"]
            user = message["user"]
            timestamp = message["ts"]
            txts.append([timestamp, user, text])
        except Exception:
            pass

slack_dataset = pandas.DataFrame(txts)
slack_dataset.columns = ["timestamp", "user", "text"]
df = slack_dataset[slack_dataset.user == self_user]

df[["text"]].to_parquet("slack_dataset.gzip", compression="gzip")

如你所见,这并不复杂!我们有一个示例数据集,我们使用这本书附带的 GitHub 仓库中的脚本拉取了这个数据集。我们将在接下来的章节中使用这个数据集。

在本章中,我们已经讨论了很多内容,但现在你应该已经准备好,并知道如何选择和评估基础模型,准备和清理数据集,以及优化你自己的文本处理器。我们将在下一章中使用这些信息来训练和微调我们自己的 LLM 模型。

摘要

  • 数据工程师拥有独特的数据集来获取和管理 LLMs,例如模型权重、评估数据集和嵌入。

  • 无论你的任务是什么,都有大量的开源模型可供选择,以微调你自己的模型。

  • 基于文本的任务比在传统机器学习任务中找到的简单相等性度量更难评估,但有许多行业基准可以帮助你开始。

  • 评估 LLMs 不仅限于性能,如偏见和潜在危害,这也是你的责任。

  • 你可以使用 Evaluate 库来构建自己的评估指标。

  • 有许多大型开源数据集,但大多数来自网络抓取,需要清理。

  • 指令模式和标注你的数据是清理和分析数据的有效方法。

  • 在具有适当分布的言语行为数据集上微调模型,将有助于其生成与上下文相符的内容。

  • 构建与你的数据相匹配的子词分词器可以大大提高你的模型性能。

  • 许多团队试图使用 LLMs 解决的问题可以通过使用模型中的嵌入来解决。

[1] Joe Reis 和 Matt Housley,数据工程基础,O’Reilly,2022 年。

[2] C. Loizos,“如果 OpenAI 这样做,‘GPT’可能很快就会被商标化,” TechCrunch,2023 年 4 月 25 日,mng.bz/5Omq

[3] N. Muennighoff 等人,“通过多任务微调实现跨语言泛化,” 2022 年 11 月 3 日,arxiv.org/abs/2211.01786

[4] C. Xu 等人,“WizardLM:赋予大型语言模型遵循复杂指令的能力,” 2023 年 6 月 10 日,arxiv.org/abs/2304.12244

[5] Mike Conover 等人,“免费 Dolly:介绍世界上第一个真正开放的指令微调 LLM,” Databricks,2023 年 4 月 12 日,mng.bz/n0e8

[6] D. Hendrycks 等人,“衡量大规模多任务语言理解,” arXiv(康奈尔大学),2020 年 9 月,doi.org/10.48550/arxiv.2009.03300

[7] P. Rajpurkar, R. Jia, 和 P. Liang, “知道你所不知道的:SQuAD 中的不可回答问题,” 2018 年 6 月,arxiv.org/abs/1806.03822

[8] C. Zhou 等人,“LIMA:对齐的‘少即是多’,” arXiv.org,2023 年 5 月 18 日,arxiv.org/abs/2305.11206

[9] T. Dettmers 等人,“SpQR:一种用于近似无损 LLM 权重压缩的稀疏量化表示,” arXiv.org,2023 年 6 月 5 日,arxiv.org/abs/2306.03078

[10] 高磊等,“The Pile:用于语言模型的多样化文本数据集,800GB”,2020 年 12 月,arxiv.org/abs/2101.00027

[11] Reis 和 Housley, 数据工程基础,2022 年。

[12] Xu 等人,“WizardLM”,2023 年。

[13] Zou 等人,“LIMA”,2023 年。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值