理解大语言模型(二)——从零开始实现GPT-2

相关说明

这篇文章的大部分内容参考自我的新书《解构大语言模型:从线性回归到通用人工智能》,欢迎有兴趣的读者多多支持。
本文涉及到的代码链接如下:regression2chatgpt/ch11_llm/char_gpt.ipynb1

本文将讨论如何利用PyTorch从零开始搭建GPT-2。虽然GPT-2已经是该领域非常前沿的内容(现在市面上使用比较多的开源大模型从结构上来说,跟GPT-2大同小异),但实现它并不困难。由于大语言模型在结构上有一些相似之处,在掌握了GPT-2的实现方法后,也就具备了实现其他大语言模型的能力。

在阅读本文之前,推荐先参考如下的文章获取一些背景知识:

  • 利用神经网络学习语言(一)——自然语言处理的基本要素
  • 利用神经网络学习语言(四)——深度循环神经网络
  • 理解大语言模型(一)——什么是注意力机制

内容大纲

  • 相关说明
  • 一、概述
  • 二、模型结构
  • 三、多头单向注意力
  • 四、解码块
  • 五、GPT-2的完整结构与重现
  • 六、Python语言学习任务

一、概述

大语言模型这个商业术语正如其名,强调了这类模型的一个共同特点,那就是“大”。这主要体现在三个方面:首先,这类模型拥有大规模的模型参数,其数量级通常在数十亿到数千亿之间;其次,为了训练这些模型,需要大规模的数据集,语料库的总长度常常达到万亿级别;最后,由于前两个因素的影响,训练这些模型的成本也相当巨大。2023年,从零开始训练一个最先进的大语言模型需要数千台专业服务器,花费高达数百万美元。

从技术角度看,大语言模型并没有一个明确的定义。通常,它指的是包含注意力机制且用于自然语言处理的神经网络模型。尽管不同的大语言模型在结构上存在较大差异,但从发展历史来看,它们都有一个共同的祖先:Transformer。图1左侧展示了模型的详细结构,这是从Transformer模型的原始论文中摘取的,因此在相关文献中被广泛引用。这个图示中包含大量细节,可能会让读者迷失方向。因此,本书更倾向于使用图1右侧的简化示意图,以便更清晰地理解模型的整体架构。

图1

图1

Transformer模型具备完整的编码器和解码器结构,因此通常应用于序列到序列模式2。从注意力的角度来看,它包含3种不同类型的注意力机制,分别是双向注意力,用于编码器;单向注意力,用于解码器;以及交叉注意力,用于编码器和解码器的协同工作。

复杂的结构提高了模型在翻译等任务中的性能,也使它的应用范围受到限制。为了更广泛地应用这一架构,出现了两种不同的改进和简化方式:一种是仅使用图1中的编码器部分(只包含双向注意力),通常用于自编码模式,最著名的代表是BERT;另一种是只包含图1中的解码器部分(只包含单向注意力),通常用于自回归模式,其中最著名的是GPT3

就结构而言,以GPT为代表的单向注意力模型是最简单的,在工程处理和训练数据准备方面也最为便捷。也许正因如此,这类模型取得了最引人瞩目的成就。因此,本章的讨论重点是这类模型的经典代表:GPT-2。从实用角度来看,尽管存在更卓越的单向注意力模型,但它们通常规模巨大,难以在普通的家用计算机上运行,更不用说训练了。相比之下,GPT-2的规模适中,适合在家用计算机上运行,我们可以下载、使用或修改该模型,以便更好地理解其原理。(但要注意,最好在配备GPU的服务器上进行模型训练,在家用计算机上训练模型可能需要非常长的时间)。

二、模型结构

总体来说,GPT-2的结构可以分为3个主要部分,自下而上分别是嵌入层、多次重复的解码块,以及语言建模头,如图11-7右侧所示。其中,解码块(Decoder Block)是至关重要的组成部分,包含4个核心元素:多头单向自注意力(Masked Multi-Head Attention)、残差连接、层归一化和多层感知器4。多层感知器是一个相对被人熟知的概念,残差连接和层归一化是提高模型训练效果的关键技术,具体的细节可以参考其他文章[TODO],这里不再详述。或许会让读者感到困惑的是多头单向自注意力,它只是自注意力机制的改进版本,在初步理解时,可以将其等同于普通的注意力。基于上述内容,在深入细节之前,再从宏观上讨论一下这个模型的独特之处。

GPT-2的图示与神经网络图示有一些显著不同,特别是解码块。在神经网络发展的早期,研究人员通常从仿生学的角度来构建模型,因此引入了神经元、全连接和隐藏层等概念。随着研究的深入,学术界发现神经网络的核心本质是线性计算和非线性变换的多层叠加。因此,研究人员突破神经元连接方式的限制,设计了卷积神经网络和循环神经网络。不仅如此,他们还改进了神经元的内部结构,设计了长短期记忆网络。

GPT-2的解码块延续了这一创新思路。尽管它的内部结构难以用传统的图示表示,但这并不重要,因为它的设计承载了神经网络的核心理念。如这篇文章所述,注意力机制只涉及线性计算,层归一化和残差连接也同样如此。因此,整个解码块实际上是线性计算和非线性变换的叠加。其中,非线性变换来自多层感知器,这也是解码块中包含多层感知器的原因之一。

从类比的角度来看,注意力机制是对循环神经网络的改进。尽管在图示上无法准确表示,但解码块实际上是循环神经网络和多层感知器的组合。在理解复杂神经网络时,读者不应固守传统的图示,而应从计算意义的角度理解各个运算步骤的作用,这有助于更深刻地理解模型。

图2

图2

在其他文献中,GPT-2的结构经常被表示为图2左侧的形式,它与Transformer的图示相似,更易于理解,然而它并不是模型的精确表示。对比图2左右两侧的图示可以发现,在左侧的图示中,层归一化分别放置在多层感知器(对应Feed Forward)和多头单向自注意力(对应Masked Multi-Head Attention)之后,与模型的实际设计不相符。然而这无伤大雅,这种差异并不会对整体效果产生重大影响。这个例子再次强调了在理解神经网络时,应重点关注关键组件,而对非关键部分的理解应具有一定的灵活性,完全没必要死记特定模型的结构。

三、多头单向注意力

多头单向注意力是多个单向注意力的组合。为了更清晰地表述,可以将这篇文章中讨论的注意力机制称为单头注意力。程序清单1是单头单向注意力组件的代码实现,其中有两个关键点需要注意。

  1. 注意力的计算需要3个关键参数,分别是query、key和value。在模型中,采用3个独立的线性回归模型生成这些向量,具体实现请参考第8—10行和第17—20行。由于模型中使用了层归一化,这3个线性模型都不需要截距项。
  2. 单向注意力的实现需要依赖一个上三角矩阵,也就是掩码(mask)。具体的实现细节见第12、13和21行。由于GPT-2对模型能够处理的文本长度有限制(关于这一点的详细原因请参考后文),为了提高计算效率,在创建模型时使用参数sequence_len提前生成能够覆盖最长文本的矩阵tril。值得注意的是,上三角矩阵的作用是辅助注意力计算,它并不需要参与模型的训练。为了实现这一点,使用register_buffer来记录生成的矩阵。
程序清单1 单头单向注意力
 1 |  def attention(query, key, value, dropout, mask=None):2 |      ......3 |  4 |  class MaskedAttention(nn.Module):5 |  6 |      def __init__(self, emb_size, head_size):7 |          super().__init__()8 |          self.key = nn.Linear(emb_size, head_size, bias=False)9 |          self.query = nn.Linear(emb_size, head_size, bias=False)
10 |          self.value = nn.Linear(emb_size, head_size, bias=False)
11 |          # 这个上三角矩阵不参与模型训练
12 |          self.register_buffer(
13 |              'tril', torch.tril(torch.ones(sequence_len, sequence_len)))
14 |          self.dropout = nn.Dropout(0.4)
15 |  
16 |      def forward(self, x):
17 |          B, T, C = x.shape  # C = emb_size
18 |          q = self.query(x)  # (B, T, H)
19 |          k = self.key(x)    # (B, T, H)
20 |          v = self.value(x)  # (B, T, H)
21 |          mask = self.tril[:T, :T]
22 |          out, _ = attention(q, k, v, self.dropout, mask)
23 |          return out         # (B, T, H)

从组件的功能角度来看,单头单向注意力的主要任务是进行特征提取。为了尽可能全面地提取特征信息,多头单向注意力采用了反复提取的策略。简而言之,对于相同的输入,它会使用多个结构相同但具有不同模型参数的单头单向注意力组件来提取特征5,并对得到的多个特征进行张量拼接6和映射。

从张量形状的角度来看,由于在模型中使用了残差连接,因此注意力组件的输入形状和输出形状最好相同。然而,单头注意力组件并没有这种保证,通常情况下,单头注意力的输出形状会小于输入形状。为了确保张量形状的一致性,可以使用多头注意力,通过对多个张量进行拼接的方式来实现这一目标。

基于上述讨论,多头单向注意力的实现如图3所示,其实现相对简单且易于理解,但它并不是最高效的实现方式。细心的读者可能会注意到红色框内包含循环操作,生成self.heads时也使用了循环操作。这些循环操作不利于并行计算,会影响模型的运算效率。更高效的实现方式是将“多头”操作设计为张量运算的形式,具体细节可以借鉴长短期记忆网络(LSTM)中的程序清单2。

图3

图3

四、解码块

与传统的多层感知器略有不同,解码块中的多层感知器[^ 7]包括两层线性计算和一层非线性变换7,如程序清单2所示,而传统的多层感知器通常采用一层线性计算和一层非线性变换的配对结构。实际上,解码块中的多层感知器可以分为两个部分:第一部分是经典的单层感知器,第二部分是一个线性映射层。组件的第二部分不仅可以完成一次线性学习,还保证了组件输入和输出的张量形状相同8

程序清单2 解码块中的多层感知器
 1 |  class FeedForward(nn.Module):2 |      3 |      def __init__(self, emb_size):4 |          super().__init__()5 |          self.l1 = nn.Linear(emb_size, 4 * emb_size)6 |          self.l2 = nn.Linear(4 * emb_size, emb_size)7 |          self.dropout = nn.Dropout(0.4)8 |  9 |      def forward(self, x):
10 |          x = F.gelu(self.l1(x))
11 |          out = self.dropout(self.l2(x))
12 |          return out

按照模型的图示,将上述的组件组合在一起,就得到了解码块的实现,如图4所示。

图4

图4

五、GPT-2的完整结构与重现

在设计GPT-2的模型结构时,还有最后一个关键细节需要考虑,那就是如何捕捉词元在文本中的位置信息。尽管注意力机制成功地捕捉了词元之间的相关关系,但它却顾此失彼,忽略了词元的位置。回顾一下注意力机制中的内容,可以发现:双向注意力只包含不受位置影响的张量计算。这意味着打乱词元在文本中的位置不会影响双向注意力的计算结果。类似地,对于单向注意力,更改左侧文本中的词元顺序也不会影响计算结果。然而,对于自然语言处理来说,词语在文本中的位置通常至关重要,因此需要想办法让模型能够捕捉词元的位置信息。

有一种非常简单的方法可以实现这一点。在使用循环神经网络进行自然语言处理时,在模型的开头使用了文本嵌入技术。文本嵌入层的输入是词元在字典中的位置,而输出是词元的语义特征,该特征将用于后续的模型计算。对于位置信息,我们完全可以“依葫芦画瓢”,在模型的开头引入一个位置嵌入层。这一层的输入是词元在文本中的位置,输出是与语义特征具有相同形状的位置特征。位置特征和语义特征将被结合在一起,参与后续的模型处理。从人类的角度来看,文本嵌入层学习了词元的语义特征,位置嵌入层学习了词元的位置信息;从模型的角度来看,它们几乎是相同的,都是基于位置信息(字典位置和文本位置)的学习。因此,这个设计虽然简单,却能够有效地捕获词元的位置信息。

将上述内容转化为代码,如程序清单3所示。其中,第6行定义了位置嵌入层。在生成位置嵌入层时,需要确定嵌入层的大小,即最大的文本长度,这也解释了为什么模型只能处理有限长度的文本。除了GPT-2,其他大语言模型也存在类似的限制,这是由注意力机制和位置嵌入导致的,也是这些模型的明显不足。因此,如何克服或放宽这一限制是当前的热门研究方向。

程序清单3 GPT-2
 1 |  class CharGPT(nn.Module):2 |  3 |      def __init__(self, vs):4 |          super().__init__()5 |          self.token_embedding = nn.Embedding(vs, emb_size)6 |          self.position_embedding = nn.Embedding(sequence_len, emb_size)7 |          blocks = [Block(emb_size, head_size) for _ in range(n_layer)]8 |          self.blocks = nn.Sequential(*blocks)9 |          self.ln = nn.LayerNorm(emb_size)
10 |          self.lm_head = nn.Linear(emb_size, vs)
11 |  
12 |      def forward(self, x):
13 |          B, T = x.shape
14 |          pos = torch.arange(0, T, dtype=torch.long, device=x.device)
15 |          tok_emb = self.token_embedding(x)       # (B, T,  C)
16 |          pos_emb = self.position_embedding(pos)  # (   T,  C)
17 |          x = tok_emb + pos_emb                   # (B, T,  C)
18 |          x = self.blocks(x)                      # (B, T,  C)
19 |          x = self.ln(x)                          # (B, T,  C)
20 |          logits = self.lm_head(x)                # (B, T, vs)
21 |          return logits

与其他模型类似,要在自然语言处理任务中应用该模型,需要完成两个额外的步骤:定义分词器和准备训练数据。

  • GPT-2采用的分词器是字节级字节对编码分词器。有关此分词器的详细算法和缺陷,请参考利用神经网络学习语言(一)——自然语言处理的基本要素。
  • GPT-2的训练数据是OpenWebText9。在准备训练数据时,采用的方法是在文本的末尾添加一个特殊字符来表示文本结束,然后将所有文本拼接成一个长字符串。在这个长字符串上,截取长度等于sequence_len的训练数据。这种方法有效解决了文本长度不一致的问题,提高了训练效率。

至此,我们终于完成了重现GPT-2所需的一切准备工作。模型的训练过程需要耗费一定的计算资源和时间,根据Andrej Karpathy的实验10,为了复现最小版本的GPT-2(拥有1.24亿个参数),我们需要一台配备8块A100 40GB显卡的计算机和大约4天的训练时间。

六、Python语言学习任务

尽管没有资源来复现GPT-2,但是可以利用类似的模型来解决较小的自然语言处理任务,比如前文中反复提到的Python语言学习。这将使我们有机会亲身体验该模型的优点。
在Python语言学习任务中,无须改动模型结构,只需调整分词器和训练数据。具体来说,模型将使用字母级别的分词器,训练数据的准备方法与GPT-2非常相似。更多细节可以参考完整代码和这里。

模型的具体结果如图5所示。该模型的规模相对较小,只包含大约240万个参数,训练时间较短,但取得了令人满意的效果。如果进行更长时间的训练或者增加模型规模,能够获得更出色的模型效果。

图5

图5


  1. 模型的实现过程参考了OpenAI提供的GPT-2开源版本,Harvard NLP提供的Transformer开源实现,以及Andrej Karpathy的课程“Neural Networks: Zero to Hero”。 ↩︎

  2. 经过精心的设计和调整,Transformer模型已经成功应用于自回归和自编码模式。此外,仅包含编码器的模型也能在序列到序列模式和自回归模式下使用(解码器也类似)。 ↩︎

  3. BERT的全称是Bidirectional Encoder Representation from Transformer。GPT的全称是Generative Pre-trained Transformer。 ↩︎

  4. 解码块中的多层感知器与传统的多层感知器略有不同,细节请见后文。 ↩︎

  5. 这里的设计受到了卷积神经网络中卷积层的启发,多头机制对应卷积层中的通道概念。 ↩︎

  6. 为了更好地理解这一方法,可以参考循环神经网络中的隐藏状态。 ↩︎

  7. 模型中的非线性变换是GeLU(Gaussian Error Linear Unit),这是对ReLU的一种改进。 ↩︎

  8. 在多头单向注意力组件中,最后一个计算步骤也是线性映射。不同的是,多头单向注意力的线性映射并没有改变张量的形状,而这里的线性映射对张量进行了压缩。这个设计使得解码块中的多层感知器呈现出两头细、中间粗的形状,既有助于特征提取(中间越宽,模型可以提取的特征就越多),又能兼顾模型后续的残差连接。 ↩︎

  9. OpenWebText是由OpenAI创建的数据集,用于训练GPT-2模型。尽管它的名字中包含“Open”,但实际上它本身并不是一个开源的数据集。不过,我们可以使用工具datasets来获取由其他研究者创建的开源版本。根据论文“Language Contamination Helps Explain the Cross-lingual Capabilities of English Pretrained Models”的研究,该数据集以英文为主,包含少量中文,这也解释了为什么GPT-2能够理解中文。 ↩︎

  10. 具体结果请查阅Andrej Karpathy的GitHub页面。 ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/14127.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

VMware VCP VCAP认证已经不需要培训记录了

之前,VMware的VCP、VCAP认证,必需花上万银子参加培训才能参加考试拿证书;但从今年5月6号开始,只需要参加考试就可以了,不再需要这个培训记录了。 而且,VCTA、VCP、VCAP各等级的考试费统一了,都…

【Qt常用控件】—— 布局管理器

目录 前言 (一)垂直布局 (二)水平布局 (三)网格布局 (四)表单布局 (五)分组布局 (六)Spacer 总结 前言 之前使⽤Qt在界⾯上…

申请公众号数量达标

一般可以申请多少个公众号?目前企业主体只能申请2个公众号,这也意味着想做矩阵公众号的难度提升了。有些公司靠着诸多不同分工的公众号形成一个个矩阵,获取不同领域的粉丝。比如,目前主体为xx旗下公众号,共有30个&…

3.1 掌握RDD的创建

在Apache Spark中,RDD(Resilient Distributed Dataset)是一个基本的、不可变的、分布式的和可分区的数据集。它能够自动进行容错处理,并支持在大规模集群上的并行操作。RDD之间存在依赖关系,可以实现管道化&#xff0c…

Mysql-存储引擎、索引、SQL优化和视图

存储引擎 mysql体系结构 连接层 最上层是一些客户端和链接服务,主要完成一些类似于连接处理、授权认证、及相关的安全方案。服务器也会为安全接入的每个客户端验证它所具有的操作权限。服务层 第二层架构主要完成大多数的核心服务功能,如SQL接口&#…

倪海夏的思维逻辑总结

1《天纪》是自然法则,自然法则是个《真理》, 《真理》不需要再证实,《真理》没有二元对立。 《真理》没有例外。 2研究任何学问(事物),批判去看,假设--验证--结果。 以果决其行&#xff0…

10个顶级的论文降重指令,让你的论文降重至1.9%

10个顶级的论文降重指令,本硕博写论文必备! 在ChatGPT4o对话框中输入:写一个Spring BootVue实现的车位管理系统的论文大纲,并对其具体章节进行详细描述。 几小时即可完成一份1万字论文的编写 在GPTS中搜索论文降重,使…

Milvus 使用过程中的常见问题集锦

引言 在使用Milvus的过程中,可能会遇到一些常见问题。这些问题可能涉及到配置、查询、数据同步等方面。 常见问题 以下是一些可能遇到的常见问题及其解决方法: 查询结果不正确: 可能原因:Milvus内部缓存与数据不一致&#xff0…

2024 电工杯高校数学建模竞赛(B题)数学建模完整思路+完整代码全解全析

你是否在寻找数学建模比赛的突破点?数学建模进阶思路! 作为经验丰富的数学建模团队,我们将为你带来2024电工杯数学建模竞赛(B题)的全面解析。这个解决方案包不仅包括完整的代码实现,还有详尽的建模过程和解…

Aware接口作用

介绍 Aware(感知)接口是一个标记,里面没有任何方法,实际方法定义都是子接口确定(相当于定义了一套规则,并建议子接口中应该只有一个无返回值的方法)。 我们知道spring已经定义好了很多对象,如…

2024 电工杯高校数学建模竞赛(A题)| 储能配置 |建模秘籍文章代码思路大全

铛铛!小秘籍来咯! 小秘籍团队独辟蹊径,运用负载均衡,多目标规划等强大工具,构建了这一题的详细解答哦! 为大家量身打造创新解决方案。小秘籍团队,始终引领着建模问题求解的风潮。 抓紧小秘籍&am…

微信小程序uniapp+django洗脚按摩足浴城消费系统springboot

原生wxml开发对Node、预编译器、webpack支持不好,影响开发效率和工程构建。所以都会用uniapp框架开发 前后端分离,后端给接口和API文档,注重前端,接近原生系统 使用Navicat或者其它工具,在mysql中创建对应名称的数据库&#xff0…

cn.hutool.poi.excel 实现excel导出效果 首行高度,行样式,颜色,合并单元格,例子样式

需求 接了需求,下载excel模版,本来看着还是简单的,然后实现起来一把泪,首先是使用poi,我查了好久,才实现,然后是我用easyexcel又实现了一遍,用了一个周多才实现。 这是需求&#x…

Python使用virtualenv创建虚拟环境

目录 第一步:安装virtualenv 第二步:选择一个文件夹用来放所创建的虚拟环境 第三步:创建虚拟环境 第四步:激活虚拟环境 第五步:退出虚拟环境 第六步:测试安装django 前提:你得有个python环…

【STL专题】深入探索C++之std::string:不止于字符串【万字详解】

欢迎来到CILMY23的博客 🏆本篇主题为:深入探索C之std::string:不止于字符串 🏆个人主页:CILMY23-CSDN博客 🏆系列专栏:Python | C | C语言 | 数据结构与算法 | 贪心算法 | Linux &#x1f3…

aardio - godking.vlistEx虚表点击表头全选、排序

新版虚表内置了名称为 DefaultCheckedImg 和 DefaultUnCheckedImg 的两张图片,分别为 【选择框勾选状态默认图片】 和 【选择框未勾选状态默认图片】 以下代码调用了这两张图片,所以请将虚表库升级为最新版。 如果使用旧版库,可以自行添加这…

【Python自动化测试】:Unittest单元测试与HTMLTestRunner自动生成测试用例的好帮手

读者大大们好呀!!!☀️☀️☀️ 🔥 欢迎来到我的博客 👀期待大大的关注哦❗️❗️❗️ 🚀欢迎收看我的主页文章➡️寻至善的主页 文章目录 🔥前言🚀unittest编写测试用例🚀unittest测…

六种常用设计模式

单例设计模式 单例模式指在整个系统生命周期里,保证一个类只能产生一个实例,确保该类的唯一性。 单例模式分类 单例模式可以分为懒汉式和饿汉式,两者之间的区别在于创建实例的时间不同: 懒汉式:指系统运行中&#…

SpringBootWeb 篇-深入了解 Mybatis 删除、新增、更新、查询的基础操作与 SQL 预编译解决 SQL 注入问题

🔥博客主页: 【小扳_-CSDN博客】 ❤感谢大家点赞👍收藏⭐评论✍ 文章目录 1.0 Mybatis 的基础操作 2.0 基础操作 - 环境准备 3.0 基础操作 - 删除操作 3.1 SQL 预编译 3.2 SQL 预编译的优势 3.3 参数占位符 4.0 基础操作 - 新增 4.1 主键返回…

Python图像处理:从基础到高级的全方位指南

目录 第一部分:Python图像处理基础 1.1 图像处理概念 1.2 Python图像处理常用库 1.3 实战案例:图像显示与保存 1.4 注意事项 第二部分:Python图像处理高级技巧 2.1 图像变换 2.2 图像增强 2.3 图像复原 第三部分:Python…