数据准备阶段流程解析

代码仓库: rasbt/LLMs-from-scratch

本章节主要讲解了数据准备阶段的完整流程:
原始文本 → token → token ID → 向量
以下将逐步拆解每个阶段的核心内容。


1. 原始文本 → token 阶段

描述

在这一阶段,通过正则表达式将原始文本分割为 token(词或符号)。随后,使用 set 去重并通过 sorted() 排序,构建完整的词汇列表。


2. token → token ID 阶段

描述

通过 enumerate 结合字典推导式生成词表:

vocab = {token: integer for integer, token in enumerate(all_words)}

注意事项

手动构建词表时,通常需要加入特殊 token,例如:

  • <|unk|>:表示不在词表中的未知词。
  • <|endoftext|>:标记文本结束。
    这些特殊 token 用于避免模型在遇到词表外的 OOV(out-of-vocabulary)token 时出错。

而像 BPE(Byte Pair Encoding) 这样的分词算法则不需要 <|unk|>,因为它会将不认识的词拆分为更小的子词单位,确保所有内容都能映射到词表中。


2.1 BPE 简介

描述

BPE 是一种子词级别的分词算法。其核心思想是将不认识的 token 拆分为更小、更常见的子 token,从而全部映射到词表中。在项目中,我们通过以下代码使用 GPT-2 的 BPE 分词器:

tokenizer = tiktoken.get_encoding("gpt2")

这里的 gpt2 是基于 BPE 算法构建的分词器。


2.2 构建 Dataset 和 DataLoader

描述

  • Dataset:负责组织样本数据结构(token ID 的片段 + 目标)。
  • DataLoader:负责按批次从 Dataset 中提取数据(可控制 batch size、是否打乱等)。

示例代码:

from torch.utils.data import Dataset, DataLoader

class GPTDatasetV1(Dataset):
    def __init__(self, txt, tokenizer, max_length, stride):
        self.input_ids = []
        self.target_ids = []

        token_ids = tokenizer.encode(txt, allowed_special={"<|endoftext|>"})

        for i in range(0, len(token_ids) - max_length, stride):
            input_chunk = token_ids[i:i + max_length]
            target_chunk = token_ids[i + 1: i + max_length + 1]
            self.input_ids.append(torch.tensor(input_chunk))
            self.target_ids.append(torch.tensor(target_chunk))

    def __len__(self):
        return len(self.input_ids)

    def __getitem__(self, idx):
        return self.input_ids[idx], self.target_ids[idx]

def create_dataloader_v1(txt, batch_size=4, max_length=256, 
                         stride=128, shuffle=True, drop_last=True,
                         num_workers=0):
    tokenizer = tiktoken.get_encoding("gpt2")
    dataset = GPTDatasetV1(txt, tokenizer, max_length, stride)

    dataloader = DataLoader(
        dataset,
        batch_size=batch_size,
        shuffle=shuffle,
        drop_last=drop_last,
        num_workers=num_workers
    )

    return dataloader

2.2.1 滑动窗口

描述

滑动窗口是一种将长序列数据分割为多个小段的方式。例如,如果一段文本有 1000 个字符,而模型最多支持 200 个字符输入,可以设置:

max_length = 200
stride = 200

这样可以将文本切分为 5 个非重叠片段,每段作为一条训练数据。在此阶段,DataLoader 仅负责读取数据,尚未涉及将数据输入 LLM 进行推理。

若希望片段之间保留上下文信息,可设置 stride < max_length 来实现重叠窗口。


3. token ID → 向量阶段

Token ID to Vector Stage

3.1 Token 嵌入

描述

使用嵌入层将每个 token ID 映射为一个稠密向量:

embedding_layer = torch.nn.Embedding(vocab_size, output_dim)
max_length = 4
dataloader = create_dataloader_v1(
    raw_text, batch_size=8, max_length=max_length,
    stride=max_length, shuffle=False
)
data_iter = iter(dataloader)
inputs, targets = next(data_iter)

token_embeddings = embedding_layer(inputs)

3.2 位置嵌入

描述

如果序列中包含多个相同的 token,它们的 token embedding 会完全相同。但在自然语言中,同一词在不同位置的语义往往不同。为解决此问题,引入了位置嵌入:将每个 token 的 embedding 与对应位置的向量相加,得到最终的输入向量。最终的 input embedding 维度与 token embedding 一致。


4. 总结流程图

描述

原始文本  
  ↓ 分词 + 去重 + 编号  
Tokens → 词表 → Token IDs  
  ↓ 传入 embedding 层  
Token Embedding + Position Embedding  
  ↓  
最终输入向量