📢 转载信息
原文作者:Adrian Tam
你很可能使用过ChatGPT、Gemini或Grok,这些都展示了大型语言模型如何展现出类人智能。虽然在家中克隆这些大型语言模型既不现实也无必要,但了解它们的工作原理有助于揭开其能力的神秘面纱,并认识到它们的局限性。
所有这些现代大型语言模型都是仅解码器的Transformer模型。令人惊讶的是,它们的架构并非过于复杂。即使你没有大量的计算能力和内存,你仍然可以创建一个较小的语言模型,模仿大型模型的一些能力。通过设计、构建和训练这样一个缩减规模的版本,你将更好地理解模型在做什么,而不是简单地将其视为一个贴着“AI”标签的黑匣子。
在本10部分的速成课程中,你将通过示例学习如何使用PyTorch从零开始构建和训练一个Transformer模型。本迷你课程侧重于模型架构,而高级优化技术虽然重要,但超出了我们的范围。我们将指导你完成从数据收集到运行训练模型的整个过程。每节课都涵盖一个特定的Transformer组件,解释其作用、设计参数以及PyTorch实现。到最后,你将探索模型的方方面面,并获得关于Transformer模型如何工作的全面理解。
让我们开始吧。

使用PyTorch从零开始构建Transformer模型(10天迷你课程)
图片来源:Caleb Jack。保留部分权利。
本迷你课程面向谁?
在我们开始之前,让我们确保你来对了地方。下面的列表提供了本课程设计对象的总体指导方针。如果你不完全符合这些要点,也别担心——你可能只需要复习某些领域才能跟上。
- 具备一定编码经验的开发者。 你应该熟悉编写Python代码并设置开发环境(这是先决条件)。你不需要成为专家级编码员,但你应该能够毫不犹豫地安装包和编写脚本。
- 具备基础机器学习知识的开发者。 你应该对机器学习模型有大致了解,并乐于使用它们。你不需要成为专家,但你不应该害怕了解更多关于它们的信息。
- 熟悉PyTorch的开发者。本项目基于PyTorch。为保持简洁,我们将不涵盖PyTorch的基础知识。你不必成为PyTorch专家,但我们期望你能够阅读和理解PyTorch代码,更重要的是,知道如何在遇到不熟悉的函数时查阅PyTorch文档。
本迷你课程不是一本关于Transformer或LLM的教科书。相反,它是一个基于项目的指南,将你从一个经验最少的开发者逐步带到能够自信地演示如何创建Transformer模型的人。
迷你课程概述
本迷你课程分为10个部分。
每节课设计为普通开发者约30分钟完成。虽然有些课程可能完成得更快,但如果你选择深入探索,其他课程可能需要更多时间。
你可以按自己的进度进行。我们建议在十天内以每天一课的舒适节奏进行,以便充分吸收材料。
在接下来的10节课中,你将涵盖以下主题:
- 第1课:获取数据
- 第2课:为你的语言模型训练一个分词器
- 第3课:位置编码
- 第4课:分组查询注意力(Grouped Query Attention)
- 第5课:因果掩码(Causal Mask)
- 第6课:专家模型混合(Mixture of Expert Models)
- 第7课:RMS范数与残差连接(RMS Norm and Skip Connection)
- 第8课:完整的Transformer模型
- 第9课:训练模型
- 第10课:使用模型
这段旅程既充满挑战又收获颇丰。
虽然它需要通过阅读、研究和编程付出努力,但你在构建Transformer模型中获得的实践经验将是无价的。
在评论区发布你的成果;我将为你加油鼓劲!
坚持住;不要放弃。
你可以点此下载本帖的代码。
第01课:获取数据
我们正在使用Transformer架构构建一个语言模型。语言模型是对人类语言的概率表示,用于预测序列中词语出现的可能性。这些概率不是手动构建的,而是从数据中学习的。因此,构建语言模型的第一步是收集大量的文本语料库,以捕捉自然语言使用的模式。
有许多文本数据源可用。古腾堡计划(Project Gutenberg)是一个获取免费文本数据的绝佳来源,提供了跨越不同体裁的大量书籍。以下是如何将文本数据从古腾堡计划下载到本地目录的方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
import os
import requests
DATASOURCE = {
"memoirs_of_grant": "https://www.gutenberg.org/ebooks/4367.txt.utf-8",
"frankenstein": "https://www.gutenberg.org/ebooks/84.txt.utf-8",
"sleepy_hollow": "https://www.gutenberg.org/ebooks/41.txt.utf-8",
"origin_of_species": "https://www.gutenberg.org/ebooks/2009.txt.utf-8",
"makers_of_many_things": "https://www.gutenberg.org/ebooks/28569.txt.utf-8",
"common_sense": "https://www.gutenberg.org/ebooks/147.txt.utf-8",
"economic_peace": "https://www.gutenberg.org/ebooks/15776.txt.utf-8",
"the_great_war_3": "https://www.gutenberg.org/ebooks/29265.txt.utf-8",
"elements_of_style": "https://www.gutenberg.org/ebooks/37134.txt.utf-8",
"problem_of_philosophy": "https://www.gutenberg.org/ebooks/5827.txt.utf-8",
"nights_in_london": "https://www.gutenberg.org/ebooks/23605.txt.utf-8",
}
for filename, url in DATASOURCE.items():
if not os.path.exists(f"{filename}.txt"):
response = requests.get(url)
with open(f"{filename}.txt", "wb") as f:
f.write(response.content)
|
此代码将每本书下载为一个单独的文本文件。由于古腾堡计划提供的是预清理的文本,我们只需提取书本内容并将其存储为Python中的字符串列表即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
# Read and preprocess the text
def preprocess_gutenberg(filename):
with open(filename, "r", encoding="utf-8") as f:
text = f.read()
# Find the start and end of the actual content
start = text.find("*** START OF THE PROJECT GUTENBERG EBOOK")
start = text.find("\n", start) + 1
end = text.find("*** END OF THE PROJECT GUTENBERG EBOOK")
# Extract the main content
text = text[start:end].strip()
# Basic preprocessing
# Remove multiple newlines and spaces
text = "\n".join(line.strip() for line in text.split("\n") if line.strip())
return text
def get_dataset_text():
all_text = []
for filename in DATASOURCE:
text = preprocess_gutenberg(f"{filename}.txt")
all_text.append(text)
return all_text
text = get_dataset_text()
|
preprocess_gutenberg()
函数会从每本书中移除古腾堡计划的页眉和页脚,并将行连接成一个单一的字符串。get_dataset_text()
函数将此预处理应用于所有书籍,并返回一个字符串列表,其中每个字符串代表一本书的完整内容。
你的任务
尝试运行上述代码!虽然这个小型书籍集合通常不足以训练一个可投入生产的语言模型,但它是学习的一个极好起点。请注意,DATASOURCE
字典中的书籍跨越了不同的体裁。你认为拥有多样化的体裁对于构建语言模型为什么很重要?
在下一课中,你将学习如何将文本数据转换为数字。
第02课:为你的语言模型训练一个分词器
计算机处理的是数字,因此文本必须转换为数字形式才能进行处理。在语言模型中,我们将数字分配给“tokens”(标记),这些数以千计的独特标记构成了模型的词汇表。
一个简单的方法是打开一个字典,为每个单词分配一个数字。然而,这种天真的方法无法有效地处理未见过的词语。一个更好的方法是训练一个算法,该算法处理输入文本并将其分解为标记。这个算法被称为分词器(tokenizer),它可以高效地分割文本并处理未见过的词语。
有几种训练分词器的方法。字节对编码(BPE)是现代LLM中最流行的方法之一。让我们使用tokenizer
库,利用我们上节课收集的文本来训练一个BPE分词器:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
tokenizer = tokenizers.Tokenizer(tokenizers.models.BPE())
tokenizer.pre_tokenizer = tokenizers.pre_tokenizers.ByteLevel(add_prefix_space=True)
tokenizer.decoder = tokenizers.decoders.ByteLevel()
VOCAB_SIZE = 10000
trainer = tokenizers.trainers.BpeTrainer(
vocab_size=VOCAB_SIZE,
special_tokens=["[pad]", "[eos]"],
show_progress=True
)
text = get_dataset_text()
tokenizer.train_from_iterator(text, trainer=trainer)
tokenizer.enable_padding(pad_id=tokenizer.token_to_id("[pad]"), pad_token="[pad]")
# Save the trained tokenizer
tokenizer.save("gutenberg_tokenizer.json", pretty=True)
|
此示例创建了一个词汇表大小为10,000的BPE分词器。生产级LLM通常使用大得多的词汇表以获得更好的语言覆盖范围。即使对于这个玩具项目,训练分词器也需要时间,因为它需要分析字符组合以形成词语。如上所示,最好将分词器保存为JSON文件,以便以后轻松重新加载:
1
|
tokenizer = tokenizers.Tokenizer.from_file("gutenberg_tokenizer.json")
|
你的任务
除了BPE,WordPiece也是一种常见的词法分析算法。尝试使用上面代码中的文本创建一个WordPiece版本的tokenizer。
为什么10,000的词汇量对于一个好的语言模型来说是不足够的?研究一个典型英语词典中的词汇量,并解释这对语言建模有何影响。
在下一课中,你将了解位置编码。
第03课:位置编码
与循环神经网络不同,Transformer模型同时处理整个序列。然而,这种并行处理意味着它们缺乏对标记顺序的内在理解。由于标记位置对于理解上下文至关重要,Transformer模型在其输入处理中加入了位置编码,以捕获这种顺序信息。
虽然存在几种位置编码方法,但旋转位置编码(RoPE)已成为最广泛使用的方法。RoPE通过对嵌入的标记向量应用旋转变换来工作。每个标记都被表示为一个向量,编码过程涉及将向量元素的成对部分乘以一个 $2 imes 2$ 旋转矩阵:
$$\mathbf{\hat{x}}_m = \mathbf{R}_m\mathbf{x}_m = \begin{bmatrix}\cos(m\theta_i) & -\sin(m\theta_i) \\ \sin(m\theta_i) & \cos(m\theta_i)\end{bmatrix} \mathbf{x}_m$$
为了实现RoPE,你可以使用以下PyTorch代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
def rotate_half(x):
x1, x2 = x.chunk(2, dim=-1)
return torch.cat((-x2, x1), dim=-1)
def apply_rotary_pos_emb(x, cos, sin):
return (x * cos) + (rotate_half(x) * sin)
class RotaryPositionalEncoding(nn.Module):
def __init__(self, dim, max_seq_len=1024):
super().__init__()
N = 10000
inv_freq = 1. / (N ** (torch.arange(0, dim, 2).float() / dim))
position = torch.arange(max_seq_len).float()
inv_freq = torch.cat((inv_freq, inv_freq), dim=-1)
sinusoid_inp = torch.outer(position, inv_freq)
self.register_buffer("cos", sinusoid_inp.cos())
self.register_buffer("sin", sinusoid_inp.sin())
def forward(self, x, seq_len=None):
if seq_len is None:
seq_len = x.size(1)
cos = self.cos[:seq_len].view(1, seq_len, 1, -1)
sin = self.sin[:seq_len].view(1, seq_len, 1, -1)
return apply_rotary_pos_emb(x, cos, sin)
sequence = torch.randn(1, 10, 4, 128)
rope = RotaryPositionalEncoding(128)
new_sequence = rope(sequence)
|
在下一课中,你将学习分组查询注意力。
🚀 想要体验更好更全面的AI调用?
欢迎使用青云聚合API,约为官网价格的十分之一,支持300+全球最新模型,以及全球各种生图生视频模型,无需翻墙高速稳定,文档丰富,小白也可以简单操作。
评论区