原文:
towardsdatascience.com/gpt-model-how-does-it-work-74bbcc2e97d1
图片由 Hal Gatewood 提供,Unsplash
在过去几年里,围绕人工智能的炒作非常巨大,这一切的主要触发因素显然是 GPT 基于的大型语言模型的问世。有趣的是,这种方法本身并不新颖。LSTM(长短期记忆)神经网络是在 1997 年创建的,而一篇著名的论文“Attention is All You Need”是在 2017 年发表的;这两者都是现代自然语言处理的基础。但直到 2020 年,GPT-3 的结果才足够好,不仅适用于学术论文,也适用于现实世界。
现在,每个人都可以在网页浏览器中与 GPT 进行聊天,但可能不到 1% 的人真正知道它是如何工作的。模型的聪明和机智的回答可能会让人认为他们正在与一个智能生物交谈,但真的是这样吗?好吧,最好的办法是看看它是如何工作的。在这篇文章中,我们将从 OpenAI 下载一个真实的 GPT 模型,在本地运行它,并逐步了解其内部的工作原理。
本文旨在面向初学者和对编程及数据科学感兴趣的人。我将用 Python 举例说明我的步骤,但不需要深入理解 Python。
让我们开始吧!
加载模型
在我们的测试中,我将使用 OpenAI 在 2019 年制作的 GPT-2 “大型”模型。当时这个模型是业界领先的,但如今它已经没有任何商业价值了,并且该模型可以从 HuggingFace 免费下载。下载链接。对我们来说更重要的是,GPT-2 模型的架构与较新的模型相同(但参数数量显然不同):
-
GPT-2 的“大型”模型有 0.7B 个参数(GPT-3 有 175B,而根据网络传言,GPT-4 有 1.7T 个参数)。
-
GPT-2 有 36 层堆叠,有 20 个注意力头(GPT-3 有 96 个,而根据传言,GPT-4 有 120 层)。
-
GPT-2 有 1024 个标记的上下文长度(GPT-3 有 2048,GPT-4 有 128K 的上下文长度)。
自然地,与 GPT-2 相比,GPT-3 和 -4 模型在所有基准测试中都提供了更好的结果。但首先,它们不可下载(即使可以下载,运行一个 175B 的模型可能需要一个非常昂贵的计算机),其次,为了理解其工作原理,GPT-2 已经足够好。
要在 Python 中使用该模型,我们需要两个对象:模型本身和分词器:
from transformers import GPT2LMHeadModel, GPT2Tokenizer, set_seed
tokenizer = GPT2Tokenizer.from_pretrained('gpt2-large')
model = GPT2LMHeadModel.from_pretrained('gpt2-large')
transformers 库非常智能,它将在第一次启动时自动下载模型。现在,让我们看看我们如何使用它。
分词器
标记化器是每个语言模型的关键部分。神经网络不能直接处理文本,标记化器将文本转换为数组:
print(tokenizer("Paris will", return_tensors="pt"))
#> tensor([[40313, 481]])
在这里,文本“Paris will”被转换成了一个张量(在我们的例子中,它是一个数字数组)[40313, 481]。很容易看出,对于模型来说,“Paris”只是一个标记 40313。
我们可以轻松地进行反向转换:
print(tokenizer.decode([40313]))
#> Paris
有趣的是,我们可以尝试将单词“paris”编码并得到两个标记而不是一个:
print(tokenizer.encode("paris"))
#> [1845, 271]
在这里,“paris”被转换成了两个标记,“par”和“is”。正如我们可以猜测的,只有最常用的单词被转换成单个数字标记;其他单词只是被分割成部分。原因很简单;将所有英语单词编码到一个表中是不可能的。这种方法还允许模型学习和使用新的、未知或拼写错误的单词。
读者可以在 GitHub 上找到 完整的 GPT-2 词汇表,格式为 JSON;该文件有 50,257 条记录。有趣的是,GPT-3 和 GPT-4 模型使用另一个标记化器,名为 tiktoken。它与旧版本不兼容,但总体逻辑保持不变:
import tiktoken
tokenizer = tiktoken.encoding_for_model("gpt-3.5-turbo")
tokenizer.encode("Paris")
#> [60704]
tokenizer.encode("paris")
#> [1768, 285]
然而,“GPT-2”模型仍然由 tiktoken 支持:
tokenizer.encoding_for_model("gpt-2").encode("Paris")
#> [40313]
以简单的方式运行 GPT 模型
现在,当文本被转换为标记时,我们可以使用 GPT 模型来生成输出。借助 transformers 库,我们可以在仅 10 行代码内完成:
tokenizer = GPT2Tokenizer.from_pretrained('gpt2-large')
model = GPT2LMHeadModel.from_pretrained('gpt2-large')
text = "Paris will"
model_input = tokenizer.encode(text, return_tensors="pt")
set_seed(42)
output = model.generate(model_input,
max_length=32,
pad_token_id=tokenizer.eos_token_id)[0]
print(output)
#> tensor([40313, 481, 307, 262, 717, 1748, 287, 262, 995,
#> 284, 423, 257, 3938, 16359 ... ])
print(tokenizer.decode(output))
#> Paris will be the first city in the world to have a fully
#> automated train system ...
如前所述,模型的输出也是一个张量;我们需要进行反向转换以获取文本。set_seed 方法初始化内部随机生成器。使用此方法,GPT 模型将始终对相同的提示返回相同的字符串作为响应。
以困难的方式运行 GPT 模型
我们能够使用 GPT 模型生成输出,但生成过程本身仍然隐藏在库实现中。让我们深入一层,手动逐个标记地运行这个过程。
和之前一样,首先我们需要加载模型。我还会将提示短语转换为标记:
import torch
import torch.nn.functional as F
text = "London was"
model_input = torch.tensor(tokenizer.encode(text)).unsqueeze(0)
#> tensor([[23421, 373]])
model = GPT2LMHeadModel.from_pretrained('gpt2-large')
model.eval()
现在,让我们运行生成。这是我们的 第一步:
outputs = model(model_input, labels=model_input)
loss, logits = outputs[:2]
作为输出,我们得到一个包含 logits 的数组——每个标记的未归一化概率。让我们更详细地看看:
print(logits)
#> tensor([[[ 1.1894, 4.7469, 0.0803, ..., -6.4411, -5.3999, 1.9996],
#> [-0.1938, 1.7015, -3.6939, ..., -6.9758, -3.3617, 0.0359]]])
print(logits.shape)
#> [1, 2, 50257]
我们可以从这个输出中得到什么?为了理解这一点,让我们回忆一下“Attention is all we need”论文中 Transformer 架构的原始图:
Transformer 模型架构,来源 arxiv.org/pdf/1706.03762.pdf
首先,GPT 是一个 语言模型;它是在千兆字节的文本上训练的,并且作为输出,它生成“输出概率”。例如,在短语“London was”中,下一个标记“a”的概率肯定高于单词“no”的概率。
第二,GPT 也是一个自回归模型,标记生成在每个迭代中只运行一步。如图所示,输出是“向右移动。”在我们的例子中,我们向模型发送了两个标记,并得到了两个作为输出的张量。为什么是张量?模型的输出形状为[1, 2, 50257];它不是一个单独的标记,而是一个所有标记的概率数组(50257 是 GPT-2 词汇表的大小)。在我们的例子中,我们向模型发送了一个包含两个标记的数组(我们的输入,[23421, 373]),并得到了两个长度为 50257 的数组作为输出。
实际上,我们只对最后一个标记感兴趣,因为我们已经知道了前面的标记。为了生成下一个标记,我们需要获取最后一个张量,并从中取出概率最高的标记:
logits = logits[:, -1, :]
#> tensor([[-0.1938, 1.7015, -3.6939, ..., -6.9758, -3.3617, 0.0359]])
top_k = 5
indices_to_remove = logits[0] < torch.topk(logits[0], top_k)[0][..., -1, None]
logits[:, indices_to_remove] = -float("Inf")
next_tokens = torch.multinomial(F.softmax(logits, dim=-1), num_samples=5)
print(tokenizer.decode(next_tokens.squeeze()))
#> "one", "the", "a", "also", "not"
在这里,我打印了五个最可能的标记,对应我们的提示“伦敦是。”但实际上,我们只需要一个。我们还需要向下一个输入添加一个新的标记:
next_token = torch.tensor([[next_tokens[0][0]]])
model_input = torch.cat((model_input, next_token), dim=1)
#> [23421, 373, 530]
现在,我们准备进行第二步。过程是相同的;唯一的区别是我们有三个标记作为输入:
outputs = model(model_input, labels=model_input)
loss, logits = outputs[:2]
print(logits)
#> tensor([[[ 2.1159, 4.8143, -0.3819, ..., -8.6419, -5.5092, 1.1465],
#> [ 0.4149, 1.4974, -2.9283, ..., -7.9501, -3.9587, 0.1875],
#> [-1.2257, 0.9350, -4.2245, ..., -7.4362, -4.9682, -0.8710]]]))
print(logits.shape)
#> torch.Size([1, 3, 50257])
现在,我们向模型发送了三个标记,它返回了三个长度为 50257 的标记概率数组。正如我们所见,这个过程非常低效;这就是为什么需要高端 GPU 来进行快速计算的原因。
让我们再次获取最佳 5 个标记并找到下一个单词:
indices_to_remove = logits[0] < torch.topk(logits[0], top_k)[0][..., -1, None]
logits[:, indices_to_remove] = -float("Inf")
next_tokens = torch.multinomial(F.softmax(logits, dim=-1), num_samples=5)
print(tokenizer.decode(next_tokens.squeeze()))
#> "of", "city", "such", "place", "the"
next_token = torch.tensor([[next_tokens[0][0]]])
model_input = torch.cat((model_input, next_token), dim=1)
#> [23421, 373, 530, 286]
添加标记后,序列中有四个标记。我们可以重复这个过程足够多次,这就是我们的最终短语:
print(model_input)
#> tensor([[23421, 373, 530, 286, 262, 1178, 4113, 326,
#> 550, 262, 11917, 284, 1302, 510, 284, 262, 1230, 13]])
print(tokenizer.decode(model_input.squeeze()))
#> London was one of the few places that had the courage to stand
#> up to the government.
顺便说一句,获取前 N 个最可能的标记只是可能策略之一。有不同方式来选择下一个标记,对更多细节感兴趣的读者可以阅读 2020 年 HuggingFace 的一篇博客文章:
结论
在这篇文章中,我们能够运行 GPT 模型并逐个生成输出标记。我希望,通过这篇文章,读者可以更好地理解 GPT 是如何工作的。
通过这种理解,我们也可以尝试回答另一个问题:这个模型是否可以有任何类型的意识?我认为答案很明显。一方面,GPT 模型是在数以 TB 计的数据上训练的;它记得很多事实,并且拥有大量的“百科全书”知识。另一方面,我们在生成过程中可以看到几个挑战,阻止我们说这个模型真正具有意识:
-
GPT 模型本身是冻结的。它的知识受限于其创建的日期。模型本身只是一个可以保存到文件并在 CD-ROM、硬盘或 SD 卡上存储的权重数组(这个文件自然不会有任何思想或意图)。
-
GPT 模型本身没有记忆,无法学习任何新事物。模型文件是“只读”的,在任何对话过程中都不会发生变化。每个新的请求都是从零开始计算的。读者可能会问,如果 GPT 没有记忆,他们如何与 GPT 进行对话。好吧,现代库如 LangChain 可以添加和总结之前的对话细节,并自动将它们添加到下一个提示中。聊天历史也可以由网络开发者存储在数据库中。然而,GPT 模型本身是无状态的,生成完成后不会“记住”任何对话。
-
最后但同样重要的是,我们可以看到缺少一个主要的“秘密成分”,即缺乏自我意识。GPT 是一个语言模型。它可以生成令人印象深刻的答案,这是由于我们的提示,并且它做得很好。但没有这些提示,模型本身不会生成任何内容。我们可以发送相同的提示 10 次,得到相同的响应 10 次;模型永远不会改变它的“想法”。什么是自我意识?我们作为人类认为这是理所当然的,但据我所知,关于它是如何工作的还没有明确的答案。模型本身没有内部“思考”的过程,没有“反馈循环”,没有目标,也没有意图。
这些问题能被解决吗?这实际上是一个价值十亿美元的问题。如今,人工智能的进步非常快,没有人知道未来会发生什么。关于通用人工智能(AGI)的预测各不相同,从“我们将在 5 年内实现 AGI”到“AGI 还离我们几十年(30-50 年以上)”。我们可以肯定的是,现在世界上有成千上万的团队和个人可能正在努力实现这个目标。他们何时会成功?目前尚不清楚。
感谢阅读。如果您喜欢这个故事,请随意订阅Medium,您将收到我新文章发布的通知,以及访问其他作者数千个故事的完整权限。您也可以通过LinkedIn与我建立联系。如果您想获取这篇和其他文章的完整源代码,请随意访问我的Patreon 页面。
对使用语言模型和自然语言处理感兴趣的人也可以阅读其他文章:
22万+

被折叠的 条评论
为什么被折叠?



