第一章:數據預處理與分詞

想象你是一位廚師,目標是烤制美味的蛋糕。

不能直接把生雞蛋、麪粉和糖扔進烤箱。首先需要準備食材:打散雞蛋、稱量麪粉、甚至過篩去除結塊。

這些準備工作確保食材以正確的形態和比例進入烘焙流程。

在GPT這類大語言模型(LLM)的世界裏,情況非常相似

我們的"廚師"是GPT模型,"食材"則是海量的人類書寫文本。nanoGPT項目旨在構建一個迷你GPT模型,而在模型開始學習之前,我們需要專門的"廚房團隊"來預處理原始文本數據——這正是數據預處理與分詞的核心任務。

本章將完整展示nanoGPT如何將原始文本切分並轉化為模型可消化的數字"餐點"。我們的核心目標是理解

如何將"Hello world!"這樣的文本轉換為訓練所需的數字序列。

為什麼需要數據預處理?

根本問題在於計算機(尤其是GPT這類神經網絡)無法直接理解單詞或字符,它們只認識數字

因此,我們的首要任務是將所有文本(故事、文章、詩歌等)轉換為整數序列。

預處理還包含其他關鍵步驟:

  1. 獲取數據:尋找並下載大規模文本集合
  2. 數據分割:將文本劃分為"訓練集"(模型學習素材)和"驗證集"(用於檢查學習效果的"模擬考試")
  3. 分詞:將文本轉化為數字標記的核心過程
  4. 高效存儲:以模型能快速加載的方式保存這些數字

讓我們通過實例瞭解nanoGPT如何處理這些步驟。

獲取與分割文本數據

在文本轉數字之前,我們需要原始文本

nanoGPT提供從網絡下載或使用本地文件的腳本

data/shakespeare_char/prepare.py腳本為例,該腳本設計用於在字符級別處理莎士比亞作品小數據集:

import os
import requests  # 用於文件下載

# 定義文本文件保存路徑
input_file_path = os.path.join(os.path.dirname(__file__), 'input.txt')

# 若文件不存在則下載
if not os.path.exists(input_file_path):
    data_url = 'https://raw.githubusercontent.com/karpathy/char-rnn/master/data/tinyshakespeare/input.txt'
    with open(input_file_path, 'w') as f:
        f.write(requests.get(data_url).text)

# 讀取全部文本內容
with open(input_file_path, 'r') as f:
    data = f.read()
print(f"數據集字符長度: {len(data):,}")

這種腳本獲取原數據操作在前文[項目詳解][boost搜索引擎#1] 概述 | 去標籤 | 數據清洗 | scp亦有用到

這段代碼首先檢查input.txt(微型莎士比亞數據集)是否存在,若不存在則從GitHub下載保存

隨後將全部內容讀入data變量,此時data即保存了待處理的原始文本。

接下來將data分割為兩部分:大部分用於訓練模型,小部分用於驗證

# 創建訓練集和驗證集
n = len(data)
train_data = data[:int(n*0.9)]  # 90%訓練集
val_data = data[int(n*0.9):]    # 10%驗證集

防過擬合小tip

這裏90%的莎士比亞文本進入train_data供模型學習,剩餘10%進val_data用於定期檢查模型在未見文本上的表現——這對防止模型死記硬背訓練數據至關重要。

分詞:文本轉數字

這是最關鍵的步驟。分詞即將文本拆分為稱為**標記(token)**的小單元,併為每個標記分配唯一ID

nanoGPT演示了兩種主要分詞方法:

方法1:字符級分詞(簡單版)

字符級分詞將每個獨立字符(如’a’、‘b’、’ ‘、’!')視為一個標記。這是最基礎的方法,適合理解核心概念。

繼續看data/shakespeare_char/prepare.py。加載數據後,腳本識別所有獨特字符:

# 獲取文本中所有獨特字符
chars = sorted(list(set(data)))
vocab_size = len(chars)  # 獨特字符總數
print("全部獨特字符:", ''.join(chars))
print(f"詞彙表大小: {vocab_size:,}")

# 創建字符到整數的映射(stoi = string to integer)
stoi = { ch:i for i,ch in enumerate(chars) }
# 創建整數到字符的逆向映射(itos = integer to string)
itos = { i:ch for i,ch in enumerate(chars) }

# 文本與標記ID的轉換函數
def encode(s):
    return [stoi[c] for c in s]  # 編碼器:輸入字符串,輸出整數列表
def decode(l):
    return ''.join([itos[i] for i in l])  # 解碼器:輸入整數列表,輸出字符串

這部分代碼首先找出莎士比亞文本中的所有獨特字符(如’a’、‘b’、‘c’、‘.’、’ ‘、’!'),然後創建兩個"字典":

  • stoi(字符到整數)將每個字符映射為唯一數字(標記ID)。例如’a’對應0,'b’對應1
  • itos(整數到字符)是逆向映射

vocab_size表示數據集中獨特字符(即獨特標記ID)的數量。微型莎士比亞數據集通常約65個。

現在可以用encode函數將train_dataval_data轉為數字列表:

# 將訓練集和驗證集編碼為整數
train_ids = encode(train_data)
val_ids = encode(val_data)
print(f"訓練集標記數: {len(train_ids):,}")
print(f"驗證集標記數: {len(val_ids):,}")

此時train_idsval_ids就是模型所需的長整數列表

例如文本"Hello"可能變為[20, 10, 23, 23, 26](假設’H’=20,‘e’=10,‘l’=23,‘o’=26)。

方法2:字節對編碼(BPE)配合tiktoken(進階版,LLM常用)

字符級分詞雖簡單,但對大文本效率低

例如常見詞"the"會被拆為3個標記(‘t’,‘h’,‘e’),儘管它們常一起出現。BPE通過為常見字符序列(甚至完整單詞)創建標記來解決這個問題。

nanoGPT使用OpenAI的tiktoken庫實現BPE處理更真實的數據集。查看data/shakespeare/prepare.py(注意與shakespeare_char的區別):

import tiktoken
import numpy as np  # 用於高效數值處理

# ...(數據下載與分割邏輯同前)...

# 使用tiktoken的gpt2 bpe編碼器
enc = tiktoken.get_encoding("gpt2")
train_ids = enc.encode_ordinary(train_data)
val_ids = enc.encode_ordinary(val_data)
print(f"訓練集標記數: {len(train_ids):,}")
print(f"驗證集標記數: {len(val_ids):,}")

這裏不再手動創建stoiitos,而是使用tiktoken.get_encoding("gpt2")加載OpenAI為GPT-2預訓練的BPE分詞器。該分詞器已掌握將文本拆分為常見片段(子詞)並分配ID的方法

encode_ordinary()方法用這個強大的BPE分詞器將train_dataval_data轉為整數列表。

這些標記ID通常更大(GPT-2最多50256),因為它們代表比單個字符更復雜的標記。

本質其實還是我們一層不夠高,那就再套一層實現的思想

對於超大數據集(如網絡文本集合"OpenWebText"),nanoGPTdata/openwebtext/prepare.py中結合使用tiktoken和Hugging Face的datasets庫實現高效加載處理:

import tiktoken
from datasets import load_dataset  # huggingface數據集庫

enc = tiktoken.get_encoding("gpt2")

if __name__ == '__main__':
    dataset = load_dataset("openwebtext", num_proc=8)  # 加載海量數據集
    # ...(分割邏輯)...

    def process(example):
        ids = enc.encode_ordinary(example['text'])  # 用tiktoken編碼文本
        ids.append(enc.eot_token)  # 添加文本結束特殊標記
        out = {'ids': ids, 'len': len(ids)}
        return out

    tokenized = dataset["train"].map(  # 對整個數據集應用分詞
        process,
        remove_columns=['text'],
        desc="正在分詞",
        num_proc=8,
    )
    # ...(保存邏輯)...

這段代碼顯示即使對超大數據集,核心的enc.encode_ordinary()方法保持不變

datasetsmap函數幫助高效處理數百萬文檔。enc.eot_token是特殊標記(文本結束),用於區分不同文檔內容。

保存為二進制文件

所有文本轉為整數ID列表後,nanoGPT需要高效保存它們。相比純文本文件(體積大加載慢),它們被存儲為二進制文件.bin)。

import numpy as np  # 數值計算庫

# ...(分詞完成,得到train_ids和val_ids)...

# 導出為bin文件
train_ids = np.array(train_ids, dtype=np.uint16)  # 列表轉NumPy數組
val_ids = np.array(val_ids, dtype=np.uint16)      # 使用uint16節省內存

# 將數組直接保存為二進制文件
train_ids.tofile(os.path.join(os.path.dirname(__file__), 'train.bin'))
val_ids.tofile(os.path.join(os.path.dirname(__file__), 'val.bin'))

這段代碼用numpytrain_idsval_ids列表轉為高效數組。

  • dtype=np.uint16指定每個數字存為16位無符號整數(GPT-2的標記ID最大值50256在此範圍內,因2^16=65536)。
  • .tofile()生成緊湊的二進制文件(train.binval.bin),訓練時可快速加載。

字符級分詞還會額外保存meta.pkl文件存儲stoiitos映射,便於後續將標記ID轉回可讀字符:(memo)

import pickle  # 用於保存Python對象

# ...(vocab_size, itos, stoi已定義)...

# 保存元信息供後續編碼/解碼使用
meta = {
    'vocab_size': vocab_size,
    'itos': itos,
    'stoi': stoi,
}
with open(os.path.join(os.path.dirname(__file__), 'meta.pkl'), 'wb') as f:
    pickle.dump(meta, f)

meta.pkl相當於小型字典供模型參考。

如何使用數據預處理腳本

可通過終端運行這些預處理腳本。例如用字符級分詞處理微型莎士比亞數據集:

python data/shakespeare_char/prepare.py

運行後將看到類似輸出:

數據集字符長度: 1,115,394
全部獨特字符:
 !$&',-.3:;?ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
詞彙表大小: 65
訓練集標記數: 1,003,854
驗證集標記數: 111,540

data/shakespeare_char/目錄會生成train.binval.binmeta.pkl

處理BPE版微型莎士比亞數據集:

python data/shakespeare/prepare.py

將在data/shakespeare/生成train.binval.bin

處理更大的OpenWebText數據集:

python data/openwebtext/prepare.py

注意:這將下載超大數據集(54GB),耗時耗空間

最終在data/openwebtext/生成train.bin(17GB)和`val.bin`(8.5MB)。

技術原理:數據預處理流程

用序列圖展示字符級莎士比亞數據處理的完整流程:

OpenAI的子詞標記化神器--tiktoken 以及 .NET 支持庫SharpToken-_數據集

該圖展示了原始文本從網絡到Python處理,最終存儲為高效二進制文件的完整旅程。

分詞方法對比

以下是nanoGPT使用的兩種分詞方法對比:

特性

字符級分詞(如shakespeare_char)

字節對編碼(BPE)配合tiktoken(如shakespeare, openwebtext)

標記單元

單個字符(如’H’,‘e’,‘l’,‘l’,‘o’)

常見子詞或完整單詞(如"Hello",“world”,“the”)

詞彙表大小

較小(如微型莎士比亞約65)

較大(如GPT-2的tiktoken約50,000)

複雜度

更簡單易懂

更復雜,採用最優子詞查找算法

輸出長度

相同文本產生更長標記序列

相同文本產生更短標記序列(更高效)

適用場景

小數據集/教學用途/需精細字符控制的特定任務

大語言模型標準方案,高效處理多樣化文本

本章小結

本章我們學習了nanoGPT如何為模型準備"食材":下載原始文本、分割為訓練驗證集、最重要的是將其轉化為數字標記。

  • 我們探索了兩種分詞方法:簡單的字符級方案和高效但更復雜的BPE方案(使用tiktoken)。
  • 最終這些標記ID被存入緊湊的二進制文件(.bin),為訓練階段的高速加載做好優化。