目 录CONTENT

文章目录

使用PyTorch从零开始构建Transformer模型:10天迷你课程

Administrator
2025-10-19 / 0 评论 / 0 点赞 / 0 阅读 / 0 字

📢 转载信息

原文链接:https://machinelearningmastery.com/building-transformer-models-from-scratch-with-pytorch-10-day-mini-course/

原文作者: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+全球最新模型,以及全球各种生图生视频模型,无需翻墙高速稳定,文档丰富,小白也可以简单操作。

0

评论区