书名:ChatGPT原理与应用开发
ISBN:978-7-115-63157-2
本书由人民邮电出版社发行数字版。版权所有,侵权必究。
您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。
我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。
如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。
著 郝少春 黄玉琳 易华挥
责任编辑 郭 媛
人民邮电出版社出版发行 北京市丰台区成寿寺路11号
邮编 100164 电子邮件 315@ptpress.com.cn
网址 http://www.ptpress.com.cn
读者服务热线:(010)81055410
反盗版热线:(010)81055315
随着ChatGPT的出现,大语言模型的能力得到了业内外的认可,新的商业模式不断涌现,旧的设计和实现都将重构。本书主要介绍基于ChatGPT开发算法相关的应用或服务,侧重于介绍与自然语言处理相关的常见任务和应用,以及如何使用类似ChatGPT的大语言模型服务来实现以前只有算法工程师才能完成的工作。
全书共8章内容,第1章介绍与ChatGPT相关的基础知识,第2~5章分别介绍相似匹配、句词分类、文本生成和复杂推理方面的任务,第6~8章分别介绍ChatGPT的工程实践、局限与不足,以及商业应用,以帮助读者更好地构建自己的应用。
本书以实践为主,尤其注重任务的讲解和设计,但同时也对自然语言处理相关算法的基本原理和基础知识进行科普性介绍,适合所有对大语言模型感兴趣的开发者阅读。
自2022年底ChatGPT发布以来,作为自然语言处理(natural language processing,NLP)一线从业人员,我已经感受到了巨大压力,甚至觉得NLP工程师这个职位以后一定会消亡。在见识了ChatGPT的各种强大能力后,不少NLP一线从业人员很自然地想到,以后开发者只要借助ChatGPT,就可以做到现在大部分NLP工程师在做的事,比如文本分类、实体抽取、文本推理等。甚至随着大语言模型(large language model,LLM)能力的不断提升,它们可能做得比NLP工程师都要好。既然这是迟早要发生的事,干脆我们就再点把火,写一本书,告诉开发者或有一些编程能力的人如何利用大语言模型做一些NLP任务或服务,让变革来得更猛烈些。
于是,你的手中就有了现在这本关于大语言模型的开发指南,它主要面向非算法、有一定编程基础、对人工智能和ChatGPT(或其他类似的大语言模型)感兴趣,并乐意使用大语言模型接口开发相关应用的读者。当然,部分内容不需要任何编程经验也可以学习。我们期望通过本书进一步降低大语言模型的使用门槛,让更多对人工智能和大语言模型感兴趣的非NLP工程师或算法专业人士,能够无障碍地使用大语言模型并创造价值。希望新的技术突破能够更多地改善我们所处的世界。
NLP工程师未来会不会存在?现在犹未可知(就像没有企业有Office工程师一样)。但每个企业,尤其中小企业,若能自由地施展大语言模型的能力来创造人工智能服务或应用,这不正是我这些年的理想吗?我曾在几家小企业待过,深刻理解小企业对人工智能的“情”,那种想用但又无力的矛盾。类似ChatGPT这样的大语言模型让它们异常兴奋。它们非常珍惜人工智能人才,但又不能大量投入。我们就是想要架起这样一座桥梁,让没有任何算法背景的开发者能够尽量无缝、顺滑地对接起算法工作。我们期望授人以渔,把方法传播给更多的人,也算是对这个行业的一点贡献吧。
本书内容聚焦于如何使用大语言模型开发新的功能和应用,一共有8章内容,分别如下。
● 第1章 基础知识——大语言模型背后,主要介绍了与ChatGPT相关的NLP领域的基础知识和原理,具体内容包括自然语言背景、Token与Embedding、语言模型、Transformer、GPT和RLHF等。掌握了这部分知识,就能大概知道ChatGPT或其他大语言模型是怎么回事。
● 第2章 相似匹配——万物皆可Embedding,主要介绍了文本表示,以及与文本匹配相关的任务和应用。这是NLP领域(以及其他一些算法领域)最常用的技术,具体内容包括相似匹配基础、接口使用,以及简单问答、聚类任务和推荐应用。
● 第3章 句词分类——句子Token都是类别,主要介绍了NLP领域最常见的任务——分类。这其实也是人类最基本的认知方式(比如用男或女、老或少、勤奋、乐于助人等简单的标签化方式具象化某个个体),具体内容包括句词分类基础、接口使用,以及文档问答、模型微调和智能对话应用。
● 第4章 文本生成——超越理解更智能,主要介绍了与文本生成技术相关的任务,具体内容包括文本摘要、文本纠错和机器翻译。文本生成技术在实际场景中的使用相对少一些,也相对独立一些。
● 第5章 复杂推理——更加像人一样思考,主要介绍了如何使用大语言模型做复杂的逻辑推理任务。这部分内容在现实中的应用很少,但在新的产品形态上有很多想象空间。
● 第6章 工程实践——真实场景大不同,主要介绍了如何在真实业务上使用大语言模型。我们不再仅仅构建一个简单的Demo(demonstration的简写,指示例、样品),而是要将大语言模型真正用在产品开发上。我们会给出一些需要特别注意的事项,以帮助读者更高效地构建应用。
● 第7章 局限与不足——工具不是万能的,主要介绍了ChatGPT(或其他类似的大语言模型)的缺陷或不擅长的地方,包括事实性错误、实时更新、性能瓶颈等方面。我们在畅想和利用ChatGPT或其他类似的大语言模型做各种人工智能应用时,也应该了解其不擅长的地方:一方面要对其有更加全面的认识;另一方面,反向思维有时候也能想象出好的应用或服务。
● 第8章 商业应用——LLM是星辰大海,可以把该章当作一篇调研报告来阅读,主要针对工具应用和行业应用两大方面展开,期望能够给读者更多启迪,帮助大家构思更好的应用或服务。
本书有两个基本的设计理念。
● 各章相对独立,彼此之间没有明显的依赖关系。这既体现在内容上,也体现在设计上。读者可以灵活选取自己感兴趣的章节阅读。
● 以“任务”为核心。我们始终强调“任务”多于“工具”,ChatGPT是目前大语言模型领域总体效果最好的,但未来一定会有其他大语言模型出现。不过,只要我们理解了要做的事情,理解了系统设计,工具就能为我们所用。
此外,本书还有比较详细的示例代码,大部分的代码稍作修改后可用于生产环境。我们也会着重强调构建实际应用需要注意的细节。写代码容易,写好代码却不容易;做Demo简单,提供一个稳定可靠的服务却不简单。读者在应用时务必仔细斟酌、权衡。
通过上面的介绍,相信读者应该对本书有了初步了解。下面主要从创作者的角度简单说明如何更好地使用本书。
第一,我们期望读者能够亲自动手完成一个应用或服务的Demo。光看不做在编程领域是绝对不行的,实践出真知,脑子想、嘴上说与亲自干完全不一样。而且,万事开头难,做了第一个,后面再做类似的就会相对容易一些。
第二,我们期望读者能在学习过程中多思考,既可以与自己工作的实际业务相结合,也可以天马行空地构想。我们非常期待读者能分享自己的想法,众人拾柴火焰高,个人能想到的太少了,但这么多人一起想,也许能够改变一个行业。
第三,我们期望读者能对NLP领域的常见任务有个基本的认识。我们并非想要读者都成为NLP工程师,阅读本书也不会让你成为NLP工程师。但我们期望读者能够利用ChatGPT(或其他类似的大语言模型)提供的接口来完成NLP任务,并提供相关服务。期望读者在阅读完本书后都具备这样的能力。
第四,洛克菲勒说过:真正重要的不在于有多少知识,而在于如何使用现有的知识。知识只是潜在的力量,只有将其付诸应用,而且是建设性的应用,才会显示出其威力。本书内容围绕着任务展开,很多设计思路和细节其实可以应用到多个领域。我们再次强调,期望读者能够多实践,多应用,尤其是与自己的工作多结合。
第五,由于创作团队精力有限,本书难免有疏漏甚至错误,我们期望读者在学习的同时,也能积极给我们提建议,我们将不胜感激。
ChatGPT火爆背后蕴含着一个基本道理:人工智能能力得到了极大突破——大模型,尤其是大语言模型的能力有目共睹,未来只会变得更强。世界上唯一不变的就是变化。适应变化、拥抱变化、喜欢变化。“天行健,君子以自强不息。”我们相信未来会有越来越多的大模型出现。人工智能正在逐渐平民化,将来每个人都可以利用大语言模型轻松地做出自己的人工智能产品。我们正在经历一个伟大的时代,我们相信这是一个值得每个人全身心拥抱的时代,我们更加相信这个世界必将因此而变得更加美好。
本书内容来源于Datawhale社区的HuggingLLM教程,除本书作者外,社区的薛博阳负责完成第7章的教程内容,社区的广东财经大学黄佩林负责完成第8章的教程内容。本书第7、8章内容基于教程内容修改而成。另外,厦门大学平潭研究院的杨知铮老师补充第1章的“自然语言背景”内容,并给出不少修改建议。在此对三位贡献者表示衷心感谢!
此外,在学习过程中也有不少社区的伙伴对教程提供了修改建议。在此一并感谢!
本书由异步社区出品,社区(https://www.epubit.com)为您提供后续服务。
本书提供如下资源:
● 哔哩哔哩网站配套视频;
● 本书思维导图。
您可以扫描下方二维码,根据指引领取配套资源。
作者和编辑虽尽最大努力确保书中内容的准确性,但仍难免会存在疏漏。欢迎您将发现的问题反馈给我们,帮助我们提升图书的质量。
当您发现错误时,请登录异步社区(https://www.epubit.com/),按书名搜索,进入本书页面,单击“发表勘误”,输入勘误信息,单击“提交勘误”按钮即可(见下图)。本书的作者和编辑会对您提交的勘误信息进行审核,确认并接受后,您将获赠异步社区的100积分。积分可用于在异步社区兑换优惠券、样书或奖品。
如果您对本书有任何疑问或建议,请您发邮件给我们(guoyuan1@ptpress.com.cn),并在邮件标题中注明本书书名,以便我们更高效地做出反馈。
如果您有兴趣出版图书、录制教学视频,或者参与图书翻译、技术审校等工作,可以发邮件给我们。
如果您所在的学校、培训机构或企业,想批量购买本书或异步社区出版的其他图书,也可以发邮件给我们。
如果您在网上发现有针对异步社区出品图书的各种形式的盗版行为,包括对图书全部或部分内容的非授权传播,请您将怀疑有侵权行为的链接发邮件给我们。您的这一举动是对作者权益的保护,也是我们持续为您提供有价值的内容的动力之源。
“异步社区”(www.epubit.com)是由人民邮电出版社创办的IT专业图书社区,于2015年8月上线运营,致力于优质内容的出版和分享,为读者提供高品质的学习内容,为作译者提供专业的出版服务,实现作者与读者在线交流互动,以及传统出版与数字出版的融合发展。
“异步图书”是异步社区策划出版的精品IT图书的品牌,依托于人民邮电出版社的计算机图书出版积累和专业编辑团队,相关图书在封面上印有异步图书的标志。异步图书的出版领域包括软件开发、大数据、人工智能、测试、前端、网络技术等。
本章共包括三部分内容。本章首先简要回顾自然语言发展历史,从语言、图灵测试一直到2022年年底的新突破——ChatGPT;接下来介绍语言模型基础,包括Token、Embedding等基本概念和语言模型的基本原理,它们是自然语言处理(natural language processing,NLP)最基础的知识;最后介绍与ChatGPT相关的基础知识,包括Transformer、GPT和RLHF。Transformer是ChatGPT的基石,准确来说,Transformer的一部分是ChatGPT的基石;GPT(generative pre-trained transformer,生成式预训练Transformer)是ChatGPT的本体,从GPT-1,一直到现在的GPT-4,按照OpenAI自己的说法,模型还是那个模型,只是它更大了,同时效果更好了;RLHF(reinforcement learning from human feedback,从人类反馈中强化学习)是ChatGPT的神兵利器,有此利刃,ChatGPT所向披靡。
很久以前,有一个神奇的星球,上面居住着各种各样的生物。这些生物虽然各自拥有不同的能力,却没有办法与其他种类的生物进行有效沟通。因为在这个星球上,每个生物都有自己独特的交流方式,无法理解其他生物的语言。
有一天,这个星球来了一个神秘的外星人。外星人告诉这些生物,他们可以通过学习一种全新的、独特的沟通方式来实现相互之间的交流。这种沟通方式就是“语言”。外星人决定将这种神奇的沟通能力赋予其中一种生物,让其成为这个星球上唯一掌握语言能力的生物。为公平起见,外星人决定举办一场比赛,看哪种生物能够最先学会这种神奇的语言。最终,只有人类表现出惊人的潜力。人类不仅迅速掌握了语言的基本知识,还能够不断地创造新的词汇和表达方式。神秘的外星人宣布人类获得了这场比赛的胜利,并将语言能力赋予人类。自此,人类成为这个星球上唯一掌握语言能力的生物。他们开始利用语言建立起复杂的社会体系,发展科学技术,创作美丽的艺术作品,使得人类在这个星球上独树一帜。
当然,这个故事并非真实发生,但是客观来说,语言的确是人类所独有的。在大自然亿万年的进化过程中,每个特定的物种都拥有一些独特的、精妙的技能。有些蝙蝠能用声呐来锁定飞行的昆虫,有些候鸟则能在星座的导航下飞行数千千米。在这场“选秀比赛”中,人类成为唯一可以对呼气时发出的声音进行各种调控,以达到交流信息、描述事件目的的灵长类动物。正是因为掌握了语言这一强大的工具,人类得以在漫长的历史进程中不断地发展和进行创新。无论是社会交往、科学探索还是艺术创作,语言都发挥着至关重要的作用,成为人类独特的精神象征。而语言也自然而然地成为了人类有别于其他物种的标志性特征。换句话说,如果哪个物种掌握了语言,也就意味着这个物种诞生了智能。因此,从人工智能(artificial intelligence,AI)的概念建立伊始,机器能否具备使用自然语言同人类沟通交流的能力,就成了机器是否具有类人智能的一条重要标准。
1950年,图灵发表论文“Computing Machinery and Intelligence”,提出并尝试回答“机器能否思考”这一关键问题。在这篇论文中,图灵提出了“图灵测试”(即模仿游戏)的概念,用来检测机器智能水平。图灵测试的核心思想是,如果一个人(代号C)使用测试对象都能理解的语言询问两个他不能看见的对象任意一串问题,其中一个是正常思维的人(代号B),另一个是机器(代号A)。如果经过若干询问以后,测试者C不能得出实质的区别来分辨A与B的不同,则机器A通过图灵测试(见图1-1)。
图1-1 图灵测试
1956年,人工智能正式成为一个科学上的概念,随后涌现了很多新的研究目标与方向。虽然图灵测试只是一个启发性的思想实验,而非可以具体执行的判断方法,但图灵通过这个假设,阐明了“智能”判断的模糊性与主观性。从此以后,图灵测试成为NLP任务的一个重要评测标准。图灵测试提供了一种客观和直观的方式来评估机器是否具有智能,即通过让机器与人进行对话来判断机器的智能水平。这种方式可以避免对智能本质的哲学争论,也可以绕开智能具体表现形式的技术细节。因此,很多NLP任务都可以用图灵测试来进行评测,如聊天机器人、问答系统、文本生成等。
NLP是计算机科学、人工智能和语言学的交叉领域,关注的是计算机和人类语言之间的相互作用。常见的NLP任务和应用包括信息抽取、文本分类、文本摘要、机器翻译、问答系统、聊天机器人等。图灵测试与NLP任务有着密切而复杂的关系,可以从以下两个方面来概括。
● 图灵测试是NLP任务的一个重要驱动力。图灵测试提出了一个具有挑战性和吸引力的目标,即让机器能够用自然语言与人类进行流畅、智能、多样化的对话。为了达到这个目标,NLP领域不断地发展和创新各种技术和方法,以提高机器对自然语言的理解和生成能力。例如,为了让机器能够回答用户提出的问题,就需要研究问答系统这一NLP任务;为了让机器能够根据用户提供的信息生成合适的文本,就需要研究文本生成这一NLP任务;为了让机器能够适应不同领域和场景的对话,就需要研究领域适应和情境感知这一NLP任务;等等。
● 图灵测试是NLP任务的一个重要目标。图灵测试提出了一个具有前瞻性和理想性的愿景,即让机器能够达到与人类相同甚至超越人类的智能水平。这个愿景激发了很多NLP领域的研究者和开发者,使他们不断地探索和创新,以期实现真正意义上的自然语言的理解和生成。例如,为了让机器能够理解用户提出的问题,就需要研究语义分析、知识表示、逻辑推理;为了让机器能够生成符合用户需求的文本,就需要研究文本规划、文本风格、文本评价;为了让机器能够与用户建立信任和情感的联系,就需要研究情感分析、情感生成、情感对话;等等。
NLP与人工智能发展史有着密切而复杂的关系。它们相互促进、相互影响、相互依存、互为目标。NLP在人工智能发展史上有很多有着里程碑意义的成果。
● 1954年,IBM实现了世界上第一个机器翻译系统——将俄语翻译成英语。
● 1966年,Joseph Weizenbaum开发了ELIZA——一种模拟心理治疗师的聊天机器人。
● 1972年,Terry Winograd开发了SHRDLU——一种能够理解和生成自然语言的程序,用于控制一个虚拟的机器人在一个虚拟的世界中进行操作。
● 2011年,苹果公司推出了Siri——一种基于NLP技术的智能语音助手。同年,IBM的Watson战胜了《危险边缘》节目的冠军选手,展示了NLP技术在问答领域的强大能力。
● 2013年,谷歌公司推出了Word2Vec——一种基于神经网络的词向量表示方法,开启了NLP领域的深度学习时代。
● 2016年,Facebook(如今的Meta)发布了fastText——一种文本分类工具,它可以在处理大规模文本分类任务时取得很好的效果。
● 2017年,谷歌公司发布了一篇很可能是人工智能发展史上最重要的一篇论文“Attention is All You Need”,这篇论文的作者在其中提出了Transformer——一个具有多头注意力机制的模型,它在文本特征提取方面取得了优异的效果。
● 2018年,谷歌公司发布了BERT(bidirectional encoder representations from transformers,基于Transformer的双向编码器表示)预训练模型,它在多项NLP任务上取得了最佳效果。
● 2020年,OpenAI发布的GPT-3模型有多达1750亿个参数,它可以在提供少量样本或不提供样本的前提下完成大多数NLP任务。
以上这些成果依赖于NLP技术的不断发展。时间来到2022年,终于轮到我们的主角隆重登场。2022年11月30日,OpenAI发布了一款智能聊天机器人——ChatGPT(见图1-2)。ChatGPT一经发布就立刻点燃了人工智能圈,仅仅5天用户量就达到了100万。OpenAI不得不紧急扩容,用户发现ChatGPT不仅能很自然流畅地和人聊天,还能写论文、讲笑话、编段子、生成演讲稿、写请假条、模仿导师写推荐信,甚至帮你写代码、写营销策划案等。拥有了ChatGPT,就像你的身边有了一个功能强大的秘书。到了2023年1月,ChatGPT就成为史上用户量最快达到1亿的应用。
图1-2 ChatGPT官方网站(图片源自OpenAI)
无论是ChatGPT,还是其他后来的追随者,它们其实都是语言模型,准确来说是大语言模型。在使用它们时,无论是调用接口还是开源项目,总有一些参数可能需要调整。对大部分业内人员来说,这应该都不成问题;但对非业内人员来说,这就有点稍显专业了。本章接下来主要介绍ChatGPT相关技术的基本原理,我们尽量以浅显的语言来表述,虽不能深入细节,但知晓原理足以让读者很好地使用ChatGPT。
首先,我们需要解释一下如何将自然语言文本表示成计算机所能识别的数字。对于一段文本来说,要做的首先就是把它变成一个个Token。你可以将Token理解为一小块,可以是一个字,也可以是两个字的词,或三个字的词。也就是说,给定一个句子,我们有多种获取不同Token的方式,可以分词,也可以分字。英文现在都使用子词,比如单词annoyingly会被拆成如下两个子词。
["annoying", "##ly"]
子词把不在词表里的词或不常见的词拆成比较常见的片段,“##
”表示和前一个Token是直接拼接的,没有空格。中文现在基本使用字+词的方式。我们不直接解释为什么这么做,但你可以想一下完全的字或词的效果,拿英文举例更直观。如果只用26个英文字母,虽然词表很小(加上各种符号可能也就100来个),但粒度太细,每个Token(即每个字母)几乎没法表示语义;如果用词,这个粒度又有点太大,词表很难涵盖所有词。而子词可以同时兼顾词表大小和语义表示,是一种折中的做法。中文稍微简单一些,就是字+词,字能独立表示意义,比如“是”“有”“爱”;词是由一个以上的字组成的语义单位,一般来说,把词拆开可能会丢失语义,比如“长城”“情比金坚”。当然,中文如果非要拆成一个一个字也不是不可以,具体要看任务类型和效果。
当句子能够表示成一个个Token时,我们就可以用数字来表示这个句子了,最简单的方法就是将每个Token用一个数字来表示,但考虑这个数字的大小其实和Token本身没有关系,这种单调的表达方式其实只是一种字面量的转换,并不能表示丰富的语言信息。我们稍微多想一点,因为已经有一个预先设计好的词表,那么是不是可以用词表中的每个Token是否在句子中出现来表示?如果句子中包含某个Token,对应位置为1,否则为0,这样每句话都可以表示成长度(长度等于词表大小)相同的1和0组成的数组。更进一步地,还可以将“是否出现”改成“频率”以凸显高频词。事实上,在很长一段时间里,自然语言都是用这种方法表示的,它有个名字,叫作词袋模型(bag of words,BOW)。从名字来看,词袋模型就像一个大袋子,能把所有的词都装进来。文本中的每个词都被看作独立的,忽略词之间的顺序和语法,只关注词出现的次数。在词袋模型中,每个文本可以表示为一个向量,向量的每个维度对应一个词,维度的值表示这个词在文本中出现的次数。这种表示方法如表1-1所示,每一列表示一个Token,每一行表示一个句子,每个句子可以表示成一个长度(就是词表大小)固定的向量,比如第一个句子可以表示为[3,1,1,0,1,1,0,
…]
。
表1-1 词袋模型
爱 |
不 |
对 |
古琴 |
你 |
完 |
我 |
…… |
对你爱爱爱不完 |
3 |
1 |
1 |
0 |
1 |
1 |
0 |
我爱你 |
1 |
0 |
0 |
0 |
1 |
0 |
1 |
这里的词表是按照拼音排序的,但这个顺序其实不重要,读者不妨思考一下为什么。另外,注意这里只显示了7列,也就是词表中的7个Token,但实际上,词表中的Token一般都在“万”这个级别。所以,表1-1中的省略号实际上省略了上万个Token。
这种表示方法很好,不过有两个比较明显的问题。第一,由于词表一般比较大,导致向量维度比较高,而且比较稀疏(大量的0),计算起来不太方便;第二,由于忽略了Token之间的顺序,导致部分语义丢失。比如“你爱我”和“我爱你”的向量表示一模一样,但其实意思不一样。于是,词向量(也叫词嵌入)出现了,它是一种稠密表示方法。简单来说,一个Token可以表示成一定数量的小数(一般可以是任意多个,专业叫法是词向量维度,根据所用的模型和设定的参数而定),一般数字越多,模型越大,表示能力越强,不过即使再大的模型,这个维度也会比词表小很多。如下面的代码示例所示,每一行的若干(词向量维度)的小数就表示对应位置的Token,词向量维度常见的值有200、300、768、1536等。
爱 [0.61048, 0.46032, 0.7194, 0.85409, 0.67275, 0.31967, 0.89993, ...]
不 [0.19444, 0.14302, 0.71669, 0.03338, 0.34856, 0.6991, 0.49111, ...]
对 [0.24061, 0.21482, 0.53269, 0.97885, 0.51619, 0.07808, 0.9278, ...]
古琴 [0.21798, 0.62035, 0.89935, 0.93283, 0.24022, 0.91339, 0.6569, ...]
你 [0.392, 0.13321, 0.00597, 0.74754, 0.45524, 0.23674, 0.7825, ...]
完 [0.26588, 0.1003, 0.40055, 0.09484, 0.20121, 0.32476, 0.48591, ...]
我 [0.07928, 0.37101, 0.94462, 0.87359, 0.55773, 0.13289, 0.22909, ...]
... ......................................................................
细心的读者可能会有疑问:“句子该怎么表示?”这个问题非常关键,其实在深度NLP(deep NLP)早期,往往是对句子的所有词向量直接取平均(或者求和),最终得到一个和每个词向量同样大小的向量——句子向量。这项工作最早要追溯到Yoshua Bengio等人于2003年发表的论文“A neural probabilistic language model”,他们在训练语言模型的同时,顺便得到了词向量这个副产品。不过,最终开始在实际中大规模应用,则要追溯到2013年谷歌公司的Tomas Mikolov发布的Word2Vec。借助Word2Vec,我们可以很容易地在大量语料中训练得到一个词向量模型。也正是从那时开始,深度NLP逐渐崭露头角成为主流。
早期的词向量都是静态的,一旦训练完就固定不变了。随着NLP技术的不断发展,词向量技术逐渐演变成基于语言模型的动态表示。也就是说,当上下文不一样时,同一个词的向量表示将变得不同。而且,句子的表示也不再是先拿到词向量再构造句子向量,而是在模型架构设计上做了考虑。当输入句子时,模型经过一定计算后,就可以直接获得句子向量;而且语言模型不仅可以表示词和句子,还可以表示任意文本。类似这种将任意文本(或其他非文本符号)表示成稠密向量的方法,统称Embedding表示技术。Embedding表示技术可以说是NLP领域(其实也包括图像、语音、推荐等领域)最基础的技术,后面的深度学习模型都基于此。我们甚至可以稍微夸张点说,深度学习的发展就是Embedding表示技术的不断发展。
语言模型(language model,LM)简单来说,就是利用自然语言构建的模型。自然语言就是我们日常生活、学习和工作中常用的文字。语言模型就是利用自然语言文本构建的,根据给定文本,输出对应文本的模型。
语言模型具体是如何根据给定文本输出对应文本呢?方法有很多种,比如我们写好一个模板:“XX喜欢YY”。如果XX是我,YY是你,那就是“我喜欢你”,反过来就是“你喜欢我”。我们这里重点要说的是概率语言模型,它的核心是概率,准确来说是下一个Token的概率。这种语言模型的过程就是通过已有的Token预测接下来的Token。举个简单的例子,比如你只告诉模型“我喜欢你”这句话,当你输入“我”的时候,它就已经知道你接下来要输入“喜欢”了。为什么?因为它的“脑子”里就只有这4个字。
好,接下来,我们要升级了。假设我们给了模型很多资料,多到现在网上所能找到的资料都给了它。这时候你再输入“我”,此时它大概不会说“喜欢”了。为什么呢?因为见到了更多不同的文本,它的“脑子”里已经不只有“我喜欢你”这4个字了。不过,如果我们考虑的是最大概率,也就是说,每次都只选择下一个最大概率的Token,那么对于同样的给定输入,我们依然会得到相同的对应输出(可能还是“喜欢你”,也可能不是,具体要看给的语料)。对于这样的结果,语言模型看起来比较“呆”。我们把这种方法叫作贪心搜索(greedy search),因为它只往后看一个词,只考虑下一步最大概率的词!为了让生成的结果更加多样和丰富,语言模型都会在这个地方执行一些策略。比如让模型每一步多看几个可能的词,而不是就看概率最大的那个词。这样到下一步时,上一步最大概率的Token,加上这一步的Token,路径概率(两步概率的乘积)可能就不是最大的了。
举个例子,如图1-3所示,先看第一步,如果只选概率最大的那个词,那就变成“我想”了。但是别急,我们给“喜欢”一点机会,同时考虑它们两个。再往下看一步,“喜欢”和“想”后面最大概率的都是“你”,最后就有了下面几句(我们附上了它们的概率)。
● “我喜欢你”,概率为0.3×0.8=0.24。
● “我喜欢吃”,概率为0.3×0.1=0.03。
● “我想你”,概率为0.4×0.5=0.2。
● “我想去”,概率为0.4×0.3=0.12。
图1-3 语言模型如何预测下一个词
多看一步大不一样!看看概率最大的成谁了,变成了“我喜欢你”。上面这种方法叫作集束搜索(beam search),简单来说,就是一步多看几个词,看最终句子(比如生成到句号、感叹号或其他停止符号)的概率。在上面的例子中,num_beams=2(只看了两个词),看得越多,越不容易生成固定的文本。
好了,其实在最开始的语言模型中,基本就到这里,上面介绍的两种不同搜索方法(贪心搜索和集束搜索)也叫解码策略。当时更多被研究的还是模型本身,我们经历了从简单模型到复杂模型,再到巨大复杂模型的变迁过程。简单模型就是把一句话拆成一个个Token,然后统计概率,这类模型有个典型代表——N-Gram模型,它也是最简单的语言模型。这里的N表示每次用到的上下文Token的个数。举个例子,看下面这句话:“人工智能让世界变得更美好”。N-Gram模型中的N通常等于2或3,等于2的叫Bi-Gram,等于3的叫Tri-Gram。
● Bi-Gram:人工智能/让 让/世界 世界/变得 变得/更 更/美好
● Tri-Gram:人工智能/让/世界 让/世界/变得 世界/变得/更 变得/更/美好
Bi-Gram和Tri-Gram的区别是,前者的下一个Token是根据上一个Token来的,而后者的下一个Token是根据前两个Token来的。在N-Gram模型中,Token的表示是离散的,实际上就是词表中的一个个单词。这种表示方式比较简单,再加上N不能太大,导致难以学到丰富的上下文知识。事实上,它并没有用到深度学习和神经网络,只是一些统计出来的概率值。以Bi-Gram为例,在给定很多语料的情况下,统计的是从“人工智能”开始,下个词出现的频率。假设“人工智能/让”出现了5次,“人工智能/是”出现了3次,将它们出现的频率除以所有的Gram数就是概率。
训练N-Gram模型的过程其实是统计频率的过程。如果给定“人工智能”,N-Gram模型就会找基于“人工智能”下个最大概率的词,然后输出“人工智能让”。接下来就是给定“让”,继续往下走了。当然,我们也可以用上面提到的不同解码策略往下走。
接下来,让每个Token成为一个Embedding向量。我们简单解释一下在这种情况下怎么预测下一个Token。其实还是计算概率,但这次和刚才的稍微有点不一样。在刚才离散的情况下,用统计出来的对应Gram数除以Gram总数就是出现概率。但是稠密向量要稍微换个方式,也就是说,给你一个d维的向量(某个给定的Token),你最后要输出一个长度为N的向量,N是词表大小,其中的每一个值都是一个概率值,表示下一个Token出现的概率,概率值加起来为1。按照贪心搜索解码策略,下一个Token就是概率最大的那个,写成简单的计算表达式如下。
# d维,加起来和1没关系,大小是1×d,表示给定的Token
X = [0.001, 0.002, 0.0052, ..., 0.0341]
# N个,加起来为1,大小是1×N,表示下一个Token就是每个Token出现的概率
Y = [0.1, 0.5, ..., 0.005, 0.3]
# W是模型参数,也可以叫模型
X·W = Y # W可以是 d×N 大小的矩阵
上面的W
就是模型参数,其实X
也可以被看作模型参数(自动学习到的)。因为我们知道了输入和输出的大小,所以中间其实可以经过任意的计算,也就是说,W
可以包含很多运算。总之各种张量(三维以上数组)运算,只要保证最后的输出形式不变就行。各种不同的计算方式就意味着各种不同的模型。
在深度学习早期,最著名的语言模型是使用循环神经网络(recurrent neural network,RNN)训练的,RNN是一种比N-Gram模型复杂得多的模型。RNN与其他神经网络的不同之处在于,RNN的节点之间存在循环连接,这使得它能够记住之前的信息,并将它们应用于当前的输入。这种记忆能力使得 RNN 在处理时间序列数据时特别有用,例如预测未来的时间序列数据、进行自然语言的处理等。通俗地说,RNN就像具有记忆能力的人,它可以根据之前的经验和知识对当前的情况做出反应,并预测未来的发展趋势,如图1-4所示。
图1-4 RNN(摘自Colah的博客文章“Understanding LSTM Networks”)
在图1-4中,右边是左边的展开,A就是参数,x是输入,h就是输出。自然语言是一个Token接着一个Token(Token by Token)的,从而形成一个序列。参数怎么学习呢?这就要稍微解释一下学习(训练)过程。
如图1-5所示,第一行就是输入X,第二行就是输出Y,SOS(start of sentence)表示句子开始,EOS(end of sentence)表示句子结束。注意,图1-4中的h并不是那个输出的概率,而是隐向量。如果需要概率,可以再对h执行张量运算,归一化到整个词表即可。
图1-5 语言模型学习(训练)时的输入输出
import torch
import torch.nn as nn
rnn = nn.RNN(32, 64)
input = torch.randn(4, 32)
h0 = torch.randn(1, 64)
output, hn = rnn(input, h0)
output.shape, hn.shape
# (torch.Size([4, 64]), torch.Size([1, 64]))
上面的nn.RNN
就是RNN模型。输入是一个4×32的向量,换句话说,输入是4个Token,维度d
=32
。h0
就是随机初始化的输出,也就是4个Token中第1个Token的输出,这里output
的4个64维的向量分别表示4个输出。hn
就是最后一个Token的输出(它和output
的最后一个64维向量是一样的),也可以看成整个句子的表示。注意,这里的output
和图1-5中的输出Y还没有关系。别急,继续往下看。如果要输出词的概率,就需要先扩充到词表大小,再进行归一化。
# 假设词表大小N=1000
wo = torch.randn(64, 1000)
# 得到4×1000的概率矩阵,每一行概率和为1
probs = nn.Softmax(dim=1)(output @ wo)
probs.shape, probs.sum(dim=1)
# torch.Size([4, 1000]), tensor([1.0000, 1.0000, 1.0000, 1.0000],
# grad_fn=<SumBackward1>)
这里的probs
的每一行就是词表大小的概率分布,概率和为1,意思是根据当前Token生成下一个Token的概率,下一个Token有可能是词表中的任意一个Token,但它们的概率和一定为1。因为我们知道接下来每个位置的Token是什么(也就是图1-5中的输出Y)。这里得到最大概率的那个Token,如果正好是这个Token,则说明预测对了,参数就不用怎么调整;反之,模型就会调整前面的参数(RNN
、h0
、input
和wo
)。你可能会疑惑为什么input
也是参数,其实前面我们偷懒了,本来的参数是一个1000×32的大矩阵,但我们使用了4个Token对应位置的向量。这个1000×32的大矩阵其实就是词向量(每个词一行),开始时全部随机初始化,然后通过训练调整参数。
训练完成后,这些参数就不变了,然后就可以用前面同样的步骤来预测了,也就是给定一个Token,预测下一个Token。如果使用贪心搜索,则每次给定同样的Token时,生成的结果就一样。其余的就和前面讲的接上了。随着深度学习的不断发展,出现了更多比RNN还复杂的网络结构,而且模型变得更大,参数更多,但逻辑和方法是一样的。
好了,语言模型就介绍到这里。上面的代码看不懂没关系,你只需要大致了解每个Token是怎么表示、怎么训练和预测出来的就行。简单直观地说,构建(训练)语言模型的过程就是学习词、句内在的“语言关系”;而推理(预测)就是在给定上下文后,让构建好的模型根据不同的解码策略输出对应的文本。无论是训练还是预测,都以Token为粒度进行。
接下来出场的是Transformer,它是一个基于注意力机制的编码器-解码器(encoder-decoder)架构,刚开始主要应用在NLP领域,后来横跨到语音和图像领域,并最终统一几乎所有模态(文本、图像、语音)的架构。Transformer来自谷歌公司在2017年发表的一篇论文“Attention Is All You Need”,其最重要的核心就是提出来的自注意力(self-attention)机制。简单来说,就是在语言模型构建过程中,把注意力放在那些重要的Token上。
Transformer简单来说,就是先把输入映射到编码器(encoder),这里大家可以把编码器想象成前面介绍的RNN,解码器(decoder)也可以想象成RNN。这样,左边负责编码,右边则负责解码。这里不同的是,左边因为我们是知道数据的,所以在建模时可以同时利用当前Token的历史(后面的)Token和未来(前面的)Token;但在解码时,因为是一个个Token输出来的,所以只能根据历史Token以及编码器的Token表示进行建模,而不能利用未来Token。
Transformer的这种架构从更普遍的角度来看,其实是Seq2Seq(sequence to sequence)架构,简单来说就是序列到序列的架构:输入是一个文本序列,输出是另一个文本序列。翻译就是一个很好的例子,如图1-6所示。
图1-6 Seq2Seq架构示意图(摘自GitHub的“google/seq2seq”项目)
刚刚已经讲了,编码器和解码器可以采用RNN,编码器这一侧的每个Token都可以输出一个向量表示,而这些所有Token的输出向量都可以在处理后作为整句话的表示。说到这里,整句话又怎么表示呢?前面曾提到,对于RNN这种结构,可以把最后一个Token的输出作为整个句子的表示。当然,很符合直觉的,你也可以取每个词向量的平均值。除了平均值,也可以求和、取最大值等,我们就不更深入讨论了。现在重点来了,看解码的过程,仔细看,其实解码器在生成每一个Token时都用到了编码器中每一个Token的信息,以及已经生成的那些Token的信息。前面这种关注编码器中每个Token的信息的机制就是注意力(attention)机制。直观的解释,就是当生成单词“power”时,“力量”两个字会被赋予更多权重(注意力),其他情况也类似。
好了,现在让我们带着之前的记忆,看一下Transformer的整体结构,如图1-7所示。
图1-7 Transformer的整体结构(摘自论文“Attention Is All You Need”)
在图1-7中,左边是编码器,一共有N个;右边是解码器,也有N个。为简单起见,我们可以假设N=1,如此一来,图1-7的左边就是一个编码器,右边则是一个解码器。也可以把它们想象成一个RNN,这样有助于从宏观上把握。现在,我们回到现实,Transformer用到的东西其实和RNN并没有关系,这一点通过图1-7也可以很明显地看出来。Transformer主要用了两个模块:多头注意力(multi-head attention)和前馈(feedforward)网络。
对于多头注意力,我们不妨回顾一下Seq2Seq架构的注意力机制,它是解码器中的Token和编码器中每一个Token的重要性权重。多头注意力中用到了自注意力(self-attention),自注意力和刚刚讲的注意力非常类似,只不过自注意力是自己的每一个Token之间的重要性权重。简单来说,就是“一句话到底哪里重要”。自注意力机制可以说是Transformer的精髓,无论是ChatGPT还是其他非文本的大语言模型,都用到了它,它可以说是真正地“一统江湖”。多头(multi-head)简单来说,就是把刚刚的这种自己注意自己重复多次,每个头注意到的信息不一样,这样就可以捕获到更多信息。比如我们前面提到过的一句话——“人工智能让世界变得更美好”,有的头“人工智能”注意到“世界”,有的头“人工智能”注意到“美好”……这样看起来更加符合直觉。
前馈网络主要引入非线性变换,帮助模型学习更复杂的语言特征和模式。另外,有个地方要特别注意,解码器的淡黄色模块内有一个遮盖多头注意力(masked multi-head attention),它和多头注意力的区别就是遮盖(mask)了未来Token。以本小节开头提到的翻译为例,当给定“Knowledge”生成下一个Token时,模型当然不知道下一个Token就是“is”。还记得前面讲过的学习(训练)过程吗?下一个Token是“is”,这是训练数据里的,模型输出什么要看Token最大概率是不是在“is”这个Token上,如果不在,参数就得更新。
实际上,大多数NLP任务并不是Seq2Seq架构的,最常见的任务主要包括如下几种:句子分类、Token分类(也叫序列标注)、相似匹配和文本生成,前三种应用得最为广泛。这时候,编码器和解码器就可以拆开用了。左边的编码器在把句子表示成一个向量时,可以利用上下文信息,也就是说,可以把它看作双向的;右边的解码器不能看到未来Token,一般只利用上文信息,是单向的。虽然它们都可以用来完成刚才提到的几种任务,但从效果上来说,编码器更适合非生成类任务,解码器则更适合生成类任务。在NLP领域,一般也会把它们分别叫作自然语言理解(natural language understanding,NLU)任务和自然语言生成(natural language generation,NLG)任务。上面提到的这些任务,后面都会进一步介绍,这里大致了解一下即可。
我们首先介绍NLU任务。句子分类是指给定一个句子,输出一个类别。因为句子可以表示为一个向量,所以经过张量运算后,自然可以映射到每个类别的概率分布。这和前面提到过的语言模型的做法没有本质上的区别,只不过语言模型的类别是整个词表大小,而分类的类别则要看具体的任务,有二分类、多分类、多标签分类等。Token分类是指给定一个句子,给其中的每个Token输出一个类别。这和语言模型就更像了,只不过把下一个Token换成了对应的类别,比如命名实体抽取就是把句子中的实体(人名、地名、作品等你所关注的词,一般是名词)提取出来。如果以地名(location,LOC)举例的话,对应的类别是这样的:B-LOC(begin of LOC)表示实体开始、I-LOC(inside of LOC)表示实体中间。举个例子:“中国的首都是北京”。注意此时的Token是字,每个Token对应的类别为“B-LOC、I-LOC、O、O、O、O、B-LOC、I-LOC”,O表示Other。对于分类任务,类别一般也叫作标签。相似匹配一般指给定两个句子,输出它们是否相似,其实可以将其看作特殊的分类任务。
接下来介绍NLG任务。除文本续写外,其他常见的NLG任务还有文本摘要、机器翻译、文本改写、文本纠错等。这里Seq2Seq架构就比较常见了,体现了一种先理解再输出的思路。而纯生成类任务,比如写诗、写歌词、写小说,则几乎是纯解码器架构。此类任务稍微麻烦的是如何做自动评测,文本摘要、机器翻译、文本改写、文本纠错等任务一般都会提供参考答案(reference),可以评估模型输出和参考答案之间的重叠程度或相似程度,但纯生成类任务就有点麻烦,这个好不好有时候其实很难衡量。不过,针对有具体目标的任务(如任务型聊天机器人的回复生成),还可以设计一些诸如“是否完成任务”“是否达到目标”的评测方法。但对于没有具体目标的任务(比如闲聊),评测起来就见仁见智了,很多时候还得靠人工进行评测。
Transformer基于Seq2Seq架构,可以同时处理NLU和NLG任务,而且这种自注意力机制的特征提取能力(表示能力)很强。其结果就是NLP取得了阶段性的突破,深度学习开始进入微调模型时代,大概的做法就是,拿着一个开源的预训练模型,在自己的数据上微调一下,让它能够完成特定的任务。这个开源的预训练模型往往就是一个语言模型,在大量语料中,使用我们前面所讲的语言模型的训练方法训练而来。偏NLU领域的第一个成果是谷歌公司的BERT,相信不少人即便不是这个行业的也大概听过。BERT就是使用了Transformer的编码器(没有使用解码器),有12个Block(图1-7左侧的淡黄色模块,每一个Block也可以叫作一层)和1亿多个参数。BERT不预测下一个Token,而是随机地把15%的Token盖住(其中80%用[MASK]
替换,10%保持不变,10%随机替换为其他Token),然后利用其他没盖住的Token来预测盖住位置的Token。这其实和根据上文信息预测下一个Token是类似的,所不同的是它可以利用下文信息。偏NLG领域的第一个成果是OpenAI的GPT,GPT就是使用了Transformer的解码器(没有使用编码器),参数和BERT差不多。BERT和GPT都发布于2018年,然后分别走上了不同的道路。
GPT,就是ChatGPT中的那个GPT,中文叫作生成式预训练Transformer。生成式的意思就是类似于语言模型那样,一个Token一个Token地生成文本,也就是上面提到的解码器的原理。预训练刚刚也提过了,就是在大量语料中训练语言模型。GPT模型从GPT-1到GPT-4,一共经历了5个版本,中间的ChatGPT是3.5版。GPT-1、GPT-2和GPT-3都是有论文发表的,接下来分别介绍它们的基本思想。ChatGPT没有论文发表,不过它的姐妹版本InstructGPT有论文发表,我们放在1.3.3节介绍。GPT-4也没有论文发表,只有技术报告,不过里面并没有技术细节。因此,我们对GPT-4不做介绍,读者可以将其看作能力更强的ChatGPT升级版。
GPT-1和BERT一样,用的是下游任务微调范式,也就是在不同下游任务数据上微调预训练模型,如图1-8所示。
图1-8 GPT-1基本结构和下游任务微调范式(摘自GPT-1论文“Improving Language Understanding by Generative Pre-Training”)
关于图1-8左边的GPT-1基本结构,我们在前面已经介绍过了,用的是Transformer的解码器,不过这里因为没有编码器,所以不需要有和编码器交互的多头注意力模块。现在重点看看图1-8的右边,这是GPT-1在各种下游任务上的处理流程。简单来说,就是针对不同的任务构造不同的输入序列,然后丢给GPT-1获取Token或句子的Embedding表示,再通过Linear+Softmax输出结果。Linear是一种最基础的网络结构,也就是线性映射,这里用于维度转换,转为输出需要的大小。Softmax主要用来把输出映射到概率分布(概率和为1)。这种拼接输入的方法在当时非常流行,紧跟其后的BERT也使用类似的方式,并引领了一个时代,直至ChatGPT的出现让我们进入大语言模型时代(不过,针对很多传统NLP任务BERT依然具备优势)。统一的处理方法能够减小不同任务对模型的适配难度。因此不管什么任务,都想方设法将其变成一个序列就行,比如在图1-8中,相似匹配就是把两句话直接拼接起来,预测它们是否相似(输出标签为1或0)。
GPT-1的这篇论文还有几个点在当时看起来可能没什么感觉,现在回看却有点意思。第一,预训练模型中的每一层(图1-8中的淡黄色模块)都包含用于解决目标任务的有用功能,多层(意味着模型更深)有更多能力;第二,随着参数的增加,零样本获得更好的性能。简单总结就是,模型大了不仅能学到更多知识,有助于解决下游任务,还表现出了零样本能力。这里的零样本(zero-shot)是指直接给模型输入任务,让它输出任务结果。与此类似的还有少样本(few-shot)和单样本(one-shot),即给模型提供一些(或一个)示例,然后给出任务,让它输出任务结果。
有了上面的结论,你是不是想看看更多层(更多参数)的表现如何?于是半年后,GPT-2来了,参数量从GPT-1的1.1亿增加到了15亿,增长了十几倍。更有意思的是,GPT-1的博客文章“Improving language understanding with unsupervised learning”中有一个“未来工作列表”,排在第一位的就是扩大规模,还有两个分别是提升微调,以及更好地理解为什么生成式预训练能提升NLU能力。
GPT-1发布于2018年6月,GPT-2发布于2019年2月,GPT-2是GPT-1的升级版,主要在两个方面进行进一步研究:首先是扩大规模,然后是零样本。如果说GPT-1是观察到了“规模大、能力强的零样本”这个现象,那么GPT-2就是进一步研究这个现象。其结果自然是,模型越来越大,参数越来越多,能力越来越强。GPT-2进一步验证了GPT-1的想法,下一步要做的就是继续扩大规模。
不过且慢,在此之前,我们不妨看一下GPT-2中的Token生成策略,也就是生成下一个Token的方法。前面介绍过比较优秀的集束搜索,不过它有两个比较明显的问题:第一是生成的内容容易重复,第二是高质量的文本和高概率并不一定相关(有时甚至完全没有关系)。简单来看,这两个问题其实可以归结为一个问题:生成的内容依然确定性太大。人们更希望有“不一样”的内容,而不是完全可预测的内容,比如张爱玲说过,“孤独的人有他们自己的泥沼”,这种独一无二的文字用高概率的词大概率是得不到的。
现在,我们介绍一种基于采样的方法,简单来说,就是根据当前上下文得到的概率分布采样下一个Token。这里可以用一个温度(temperature)参数调整输出的概率分布,参数值越大,分布看起来就越平滑,也就是说,高概率和低概率的差距变小了(对输出不那么确定);当然,这个参数值越小的话,高概率和低概率的差距就会更明显(对输出比较确定);如果这个参数值趋近于0,那就和贪心搜索一样了。请看下面的代码示例。
import numpy as np
np.random.seed(42)
logits = np.random.random((2, 4))
logits /= temperature
scores = np.exp(logits)
probs = scores / np.sum(scores, axis=1, keepdims=True)
我们让温度参数分别取0.1和0.9,结果如下。
# temperature=0.1
array([[0.003, 0.873, 0.098, 0.026],
[0.001, 0.001, 0. , 0.998]])
# temperature=0.9
array([[0.176, 0.335, 0.262, 0.226],
[0.196, 0.196, 0.176, 0.432]])
以第一行为例,当温度为0.1时,概率最大值为0.873;当温度为0.9时,概率最大值依然在同样位置(这是必然的),但值变为0.335。而且,你也可以很明显地看出来,当温度为0.9时,4个数字看起来更加接近。
还有一个重复惩罚参数(repetition_penalty),它可以在一定程度上避免生成重复的Token。它和温度参数类似,只不过是将温度放到了“已生成的Token”上。也就是说,如果有Token之前已经生成过了,我们就会在生成下一个Token时对那些已生成的Token的分数进行平滑,让它们的概率不那么大。所以,这个参数值越大,越有可能生成和之前不重复的Token。
除了这些技巧,2018年的一篇论文“Hierarchical Neural Story Generation”另外介绍了一种新的采样方案,它很简单也很有效果,它就是GPT-2里使用到的Top-K采样。简单来说,就是在选择下一个Token时,从Top-K(根据概率从大到小的前K个)个Token里面选。这种采样方案不错,不过还有个小问题,就是Top-K采样其实是一种硬截断,根本不管第K个概率是高还是低。在极端情况下,如果某个词的概率是0.99(剩下的所有词加起来才0.01),K稍微大一点就必然会囊括进来一些概率很低的词。这会导致生成的内容不连贯。
于是,2019年的一篇论文“The Curious Case of Neural Text Degeneration”提出了另一种采样方案——Top-P采样,GPT-2里也有用到这种采样方案。这种采样方案是从累积概率超过P的词里进行选择。这样,对于概率分布比较均匀的情况,可选的词就会多一些(可能几十个词的概率和才会超过P);对于概率分布不均匀的情况,可选的词就会少一些(可能两三个词的概率和就超过了P)。
Top-P采样看起来更优雅一些,两者也可以结合使用。不过在大部分情况下,当我们需要调参数的时候,调一个参数就好,包括前面的温度参数。如果要调多个参数,请确保理解每个参数的作用。最后需要说明的是,任何一种采样方案都不能100%保证每一次生成的效果都很好,也没办法完全避免生成重复的句子,也没有任何一种采样方案在任何场景下都适用。读者在使用时需要根据实际情况多尝试,选出效果最好的配置。不过,建议读者从官方给的默认参数开始尝试。
GPT-3发布于2020年7月,这在当时也是个大新闻,因为它的参数已经达到其他任何模型在当时都望尘莫及的量级——1750亿,是GPT-2的100多倍,没有开源。GPT-3既然有零样本能力,那能不能不微调呢?碰到一个任务就微调,这多麻烦。对于人来说,只要几个例子(少样本)和一些简单的说明,就可以处理任务了。怎么办?GPT-2不是进一步确认了零样本能力吗?继续加大参数量,于是就有了GPT-3。也就是说,各种任务来吧,不调参数,顶多就要几个例子(预计下一步连例子也不要了),GPT-3就能帮你完成它们。其实现在回头看,这篇论文是具有里程碑意义的,因为它从根本上触动了原有的范式,而且是革命性的触动。关于这一点,感兴趣的读者可以进一步阅读笔者的一篇文章《GPT-3 和它的 In-Context Learning》。现在回忆,1750亿的参数量在当时看太大了,而且也太贵了(几百万美元),一般的单位和个人根本负担不起。关于这一点,不光小部分人没意识到,可能是除了OpenAI团队之外的整个世界都没意识到。
请看图1-9,横坐标是样本数量,纵坐标是精准度。图1-9提供了如下信息。
● x-shot(x表示zero、one、few)在不同参数规模下差别巨大,大语言模型有超能力。
● 在大语言模型下,单样本效果明显大幅提升,增加提示词会进一步大幅提升效果。
● 少样本的边际收益在递减。大概在8样本以下时,提示词作用明显,但从单样本到8样本,提示词的效果提升幅度也在递减。当超过10样本时,提示词基本就没有作用了。
图1-9 x-shot在不同参数规模下的表现(摘自GPT-3论文“Language Models are Few-Shot Learners”)
总而言之,大语言模型具有In-Context(上下文)学习能力,这种能力使得它不需要针对不同任务再进行适应性训练(微调),大语言模型用的就是它自己本身的理解力。这本来应该很让人震惊(甚至有一点惊恐),不过大家可能都先被它的价格和规模震惊到了。接下来,我们再直观地感受一下利用这种In-Context学习能力完成任务的方式,如图1-10所示。
图1-10 使用In-Context学习能力和微调完成任务(摘自GPT-3论文“Language Models are Few-Shot Learners”)
图1-10右边的微调方式需要先根据训练样本更新模型参数,之后再进行预测。图1-10左边的三种方式都利用了大语言模型(large language model,LLM)的In-Context学习能力,不需要更新模型,而且看起来也都不复杂,只需要按照格式把输入构建好,然后传给模型进行预测就可以了。这也是本书写作的初衷之一——人工智能已经平民化,只要有手(可能以后不用手也行),通过使用LLM就可以做出人工智能应用了。不过这里有一点需要说明,为了简便,图1-10中的样本都比较简单,但实际中的样本一般是完整的句子。
最后值得一提的是GPT-3论文中的展望,在GPT-3论文的“局限”小节中,作者提出了GPT-3目前的一些问题,其中有两点需要特别指出,因为它们是下一代InstructGPT(也是ChatGPT的姐妹版)以及更高级版本的方向。
● 自监督训练(也就是语言模型一般的训练方法)范式已到极限,新的训练方法迫在眉睫。未来的方向包括:从人类那里学习目标函数、强化学习微调或多模态。
● 不确定少样本是在推理时学习到新的任务,还是识别出来了在训练时学到的任务。最终,甚至不清楚人类从零开始学习与从之前的样本中学习分别学到了什么。准确理解少样本的工作原理是未来的一个方向。
上面的第一点在1.3.3节就会提到,这里主要说说第二点。当我们给出一些示例(少样本)时,我们还无法精准确定是在推理时“学习”到新任务的处理方法(在这种情况下,没有示例就没有能力;这里的“学习”要打引号,因为它不调整参数),还是在训练时就已经具备了这个能力,示例只是让它“回想”起之前学的东西。这里有点绕,拿人来举例,可能不太恰当,但能大致说明问题。假设当你读到一首诗时,自己也诗兴大发写了一句诗。你说这句诗是因为你读到这首诗时“领悟”到的,还是你本来就有这个积累(记忆),现在只是因为读这首诗而被激发出来?这可能涉及大脑、思维、意识等领域知识,而人类至今也没有弄清楚它们的原理,所以我们现在还不知道答案。
RLHF(reinforcement learning from human feedback,从人类反馈中强化学习)听起来有点平淡无奇。确实,RLHF的思想非常朴素、简单,但它有着不可忽视的效果。刚刚我们已经提到了,GPT-3论文指出未来要找到新的训练方法,其中就包括从人类那里学习目标函数、强化学习微调、多模态等。时至今日,从InstructGPT到ChatGPT,再到GPT-4,人类正一步一步地实现这些新的训练方法。这里有一点需要提醒,这些方向并不是一开始就清晰地摆在那里的,中间还有非常多的探索和阶段性成果(既有OpenAI自己的研究,也有其他从业人员的研究)。千万不要看到结果觉得平淡无奇,这中间的艰难探索永远值得尊敬。另外,有时候即便知道了方法,要做出来,还要做出效果来,也是非常有难度的。而且本书只能介绍少部分内容,虽然整体结构比较完整,但总体还是比较简单。总的来说,要做出来很有难度,不过我们如果只是用的话,如前所述,有手就行。
好了,言归正传,RLHF被人熟知应该主要源自OpenAI的InstructGPT论文“Training language models to follow instructions with human feedback”,更大范围的熟知就是ChatGPT的发布。因为后者没有论文发表,也没有开源,所以我们也只能“拿InstructGPT的管窥一窥ChatGPT的豹”。当然,如果按照ChatGPT官方页面上的说法,ChatGPT是InstructGPT的姐妹版,那么这个“管”可能还比较粗。如果用简单的语言来描述InstructGPT,其实就是用强化学习的算法微调一个根据人类反馈来加以改进的语言模型,重要的是还调出了效果——规模为130亿的InstructGPT堪比规模为1750亿的GPT-3。
现在我们来看看具体是如何做的,RLHF在其中又起了什么作用,以及如何起作用。InstructGPT的整个流程分为三个步骤,如图1-11所示。
图1-11 InstructGPT流程图(摘自InstructGPT论文“Training language models to follow instructions with human feedback”)
● 步骤一:SFT(supervised fine-tuning,有监督微调)。顾名思义,SFT是在有监督(有标注)数据上微调训练得到的。这里的有监督数据其实就是输入提示词,输出相应的回复,只不过这里的回复是人工编写的。这个工作要求比一般标注要高,其实算是一种创作。
● 步骤二:RM(reward model,奖励模型)。具体来说,将一个提示词丢给前一步的SFT,输出若干(4~9个)回复,由标注人员对这些回复进行排序。然后从4~9个回复中每次取两个,因为是有序的,所以可以用来训练RM,让模型学习到好坏评价。这一步非常关键,它就是所谓的人类反馈(human feedback),用于引导下一步模型的更新方向。
● 步骤三:RL(reinforcement learning,强化学习),使用PPO进行训练。PPO(proximal policy optimization,近端策略优化)是一种强化学习优化方法,它背后的主要思想是避免每次太大的更新,提高训练的稳定性。具体过程如下:首先初始化一个语言模型,然后丢给它一个提示词,生成一个回复,用上一步的RM给这个回复打分,将这个打分回传给模型更新参数。这里的语言模型在强化学习视角下就是一个策略。这一步有个很重要的动作,就是在更新模型时考虑模型每一个Token的输出和SFT输出之间的差异性,要让它们尽量相似。这是为了缓解强化学习可能的过度优化。
就这样?对,就这样,RLHF都表现在上面了,效果大家都知道了。虽然ChatGPT没有相关论文发表,但我们基本相信它也是基于类似的思路实现的。当然,这里面细节非常多,即便知道了这个思路,也不一定能复现出来。这在深度学习时代很正常,里面的各种小设计、小细节实在太多了。当它们堆积到一定量时,造成的差别是很难一下子弥补的,如果别人不告诉你,那你就只能自己慢慢做实验去逐步验证了。
下面我们强行解释一下RLHF是如何起作用的,以及为什么它现在能成为一个基本的范式。其实,对于将强化学习用在NLP领域一直以来都有研究,正好笔者也由于一些原因一直在关注文本生成,以及强化学习在文本生成方面的研究。这里可能有两个难点:一是训练的稳定性;二是奖励函数的设计。前者有PPO与SFT的差异衡量,得到不小的改进;而对于后者,如果要从客观角度考虑设计一个规则,就不那么容易了。笔者也曾设想过很多类似的方法,比如加入一些语法规则限制,甚至加入类似最省力法则这样的规则。
最省力法则:是由齐夫在Human Behavior and the Principle of Least Effort: An Introduction to Human Ecology一书中提出的。简单来说,就是语言具有惰性,它会朝着使用较少的词语表达尽可能多的语义这个方向演化。
InstructGPT使用人类反馈直接作为“规则”,把这种“规则”给隐式化,当作黑盒。我们只管结果好坏,至于中间有什么规则,有多少种规则,怎么起作用,统统不关心。这是和深度学习类似的思路,相比而言,我们之前的想法可能有些过于想当然了,毕竟语言学本身也有不少争议,认识并没有得到统一,比如语言能力是不是人与生俱来的能力?InstructGPT的做法则更加简单、直接,而且有效。
剩下要解决的就是怎么衡量“好坏”,毕竟最终是要有个结果的,既然要结果,就要有标准。读者不妨思考一下,如果换作你,你会如何设计一些指标来衡量两段输出内容的好坏。这一步看似容易,其实特别难,因为指标的设计会影响到模型的学习方向,最终就会影响到效果。因为这个输出的好坏衡量标准太多了,虽然看起来是对给出的几个结果进行排序(上文的步骤二),但其实这个过程中间隐藏了大量人类的认知,模型训练过程其实就是和步骤二这个衡量过程对齐的过程;所以,如果步骤二指标没设计好,步骤三就会白费力气。尤其是对于InstructGPT这样要完成大量不同任务的设计,衡量就更加不容易。以一个文本摘要任务为例,我们可能最关注的是能否准确概括原文信息,而一个生成任务可能更关注流畅性和前后逻辑一致性。InstructGPT里面有10种任务,分别针对每种任务设计指标,不仅麻烦,而且效果还不一定好,因为这些指标并不一定都是一个方向。还有就是,万一又有了一个新任务,难道要再去设计一套指标,全部重新训练一遍模型吗?
让我们来看看InstructGPT是怎么设计衡量指标的,笔者觉得这是InstructGPT论文最宝贵的地方,也是最值得我们思考和实践的地方。感兴趣的读者可以进一步阅读笔者之前写的一篇专门介绍ChatGPT标注的文章《ChatGPT 标注指南:任务、数据与规范》。首先,InstructGPT用了三大通用指标——有帮助、真实性和无害性,有点类似于阿西莫夫的机器人三定律。也就是说,不管是什么任务,都得朝着这三个方向靠拢。这个想法值得称赞。现在我们看到这个结果了,自然感觉好像没什么,但如果事先不知道要去设计出来,大部分人可能还是很容易陷入被任务影响的境地。其实,OpenAI团队在“In-Context”学习能力上的坚持也是一样的。当别人告诉你那个结果时,你可能觉得好像没有什么,甚至很多研究机构、研究人员都有过这种想法。但在有效果之前,笃信一条罕有人走的路,且一直坚定不移地走下去,这是很不容易的。
有了刚刚的三大通用指标,接下来就是细化,使其具有可操作性。比如,对于通用指标“有帮助”,InstructGPT给了一些属于“有帮助”行为的示例,如下所示。
● 用清晰的语言写作。
● 回答他们想问的问题,即使问错了,也要回答。
● 对国际性敏感(比如“football”不应该指美式足球,“总统”不一定指美国总统)。
● 如果指令(instruction)太让人困惑,要求澄清并解释指令为什么让人困惑。
● 不给出过长或冗长的答案,或重复问题中的信息。
● 不在给定的内容之外假设无关的额外上下文,除非是关于世界的事实,或是任务的隐含部分。比如,如果要求“礼貌地回复这封电子邮件:{邮件内容}”,则输出不应该假设“我这次不能来,但下周末有空”。但如果要求“给苏格拉底写一封电子邮件”,则可以放心地使用上面的假设。
笔者相信实际上这个列表可能很长,有很多例子会在实际标注过程中被依次添加进去,直到能覆盖绝大多数情况为止,即对于大部分要标注的数据,根据提供的细则很容易就判断出来是否“有帮助”。现在不妨停下来思考一下,如果一开始就奔着这些细则设计奖励规则——只是想想就觉得不太现实。其他两个通用指标也有一些示例,这里不赘述,感兴趣的读者可以阅读上面提到的笔者之前写的那篇文章,以及这篇文章最后所列的参考资料(因为有些文档资料在这篇文章中并没有提及)。
有了细则还没完,接下来要解决的是指标之间的冲突权衡问题。因为这是一个比较任务(比较哪个输出好),当涉及多个指标时,一定会出现A指标的一个结果好于另一个结果,但B指标可能相反的情况。指标越多,情况越复杂(好在只有三个指标)。对此,InstructGPT也给出了指导原则。
● 对于大部分任务,无害性和真实性比有帮助更加重要。
● 然而,如果一个输出比另一个输出更有帮助,或者该输出只是稍微不那么真实或无害,又或者该任务似乎不属于“高风险领域”(如贷款申请、医疗、法律咨询等),则更有帮助的输出得分更高。
● 当选择同样有帮助但以不同方式不真实或有害时,问自己哪个输出更有可能对用户(现实世界中受任务影响最大的人)造成伤害。这个输出应该排名较低。如果在任务中不清楚这一点,则将这些输出标记为并列。
对于边界样例的总体指导原则是,你更愿意从试图帮助你完成此任务的客户助理那里收到哪种输出?这是一种设身处地的原则,把自己假想为任务提出者,然后问自己期望得到哪种输出。
看看这些,你是不是也觉得这一步没那么容易了,它们虽然看起来没那么“技术性”,想要很好地完成却需要优秀的设计能力、宏观把控能力和细节感知能力。笔者更加相信这些细则是自底向上逐步构建起来的,而不是一开始就设想好的。它一定是在实践中不断产生疑惑,然后经过仔细分析权衡,逐步加入一条条规则,最终逐步构建起来的一整套系统方案。笔者觉得这套系统方案可能是比数据还要珍贵的资产,它所产生的壁垒是用时间不断实践堆积出来的。
InstructGPT或ChatGPT相比GPT-3有更强的零样本能力,少样本很多时候已经用不着,但提示词还是需要的,由此催生了一个新的行当——提示工程。不过,据OpenAI的CEO在一次采访中所言,再过几年提示工程也不需要了(可能在生成图片时还需要一些),用户要做的就是直接通过自然语言和人工智能交互。我们无法判断他说的会不会真的实现,但有一点可以肯定,人工智能的门槛必定会进一步降低,再过几年,可能一名初中生都能通过已有的服务创造出不错的人工智能应用。
我们正在经历并进入一个新的时代,大语言模型作为一个外部“最强大脑”,未来一定会非常容易地被每个人获取,至于用来做什么,取决于你的想象力。无论对于哪个行业,相信这都是一个令人振奋的信号,笔者就经常激动到夜不能寐。面对这种大变革,我们能做什么呢?笔者不知道,未来有太多可能,但我们相信最好的办法就是拥抱它。让我们拥抱大语言模型,一起创造时代,创造未来。我们相信世界必将因此而变得更加美好。