📢 转载信息
原文作者: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),它能高效地分割文本并处理未见过的词语。
训练分词器有几种方法。字节对编码(Byte-Pair Encoding, 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版本。
为什么10,000的词汇量对于一个好的语言模型来说是不够的?研究典型英语词典中的词汇量,并解释这对语言建模有何影响。
在下一课中,您将了解位置编码。
第03课:位置编码
与循环神经网络(RNN)不同,Transformer模型同时处理整个序列。然而,这种并行处理意味着它们缺乏对标记顺序的内在理解。由于标记位置对于理解上下文至关重要,Transformer模型在其输入处理中加入了位置编码(positional encodings)来捕获这种顺序信息。
虽然存在几种位置编码方法,但旋转位置编码(Rotary Positional Encoding, 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)
|
要实现RoPE,您可以使用以下PyTorch代码:
🚀 想要体验更好更全面的AI调用?
欢迎使用青云聚合API,约为官网价格的十分之一,支持300+全球最新模型,以及全球各种生图生视频模型,无需翻墙高速稳定,文档丰富,小白也可以简单操作。
评论区