第一章:數據預處理與分詞
想象你是一位廚師,目標是烤制美味的蛋糕。
不能直接把生雞蛋、麪粉和糖扔進烤箱。首先需要準備食材:打散雞蛋、稱量麪粉、甚至過篩去除結塊。
這些準備工作確保食材以正確的形態和比例進入烘焙流程。
在GPT這類大語言模型(LLM)的世界裏,情況非常相似
我們的"廚師"是GPT模型,"食材"則是海量的人類書寫文本。nanoGPT項目旨在構建一個迷你GPT模型,而在模型開始學習之前,我們需要專門的"廚房團隊"來預處理原始文本數據——這正是數據預處理與分詞的核心任務。
本章將完整展示nanoGPT如何將原始文本切分並轉化為模型可消化的數字"餐點"。我們的核心目標是理解
如何將"Hello world!"這樣的文本轉換為訓練所需的數字序列。
為什麼需要數據預處理?
根本問題在於計算機(尤其是GPT這類神經網絡)無法直接理解單詞或字符,它們只認識數字。
因此,我們的首要任務是將所有文本(故事、文章、詩歌等)轉換為整數序列。
預處理還包含其他關鍵步驟:
- 獲取數據:尋找並下載大規模文本集合
- 數據分割:將文本劃分為"訓練集"(模型學習素材)和"驗證集"(用於檢查學習效果的"模擬考試")
- 分詞:將文本轉化為數字標記的核心過程
- 高效存儲:以模型能快速加載的方式保存這些數字
讓我們通過實例瞭解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’對應1itos(整數到字符)是逆向映射
vocab_size表示數據集中獨特字符(即獨特標記ID)的數量。微型莎士比亞數據集通常約65個。
現在可以用encode函數將train_data和val_data轉為數字列表:
# 將訓練集和驗證集編碼為整數
train_ids = encode(train_data)
val_ids = encode(val_data)
print(f"訓練集標記數: {len(train_ids):,}")
print(f"驗證集標記數: {len(val_ids):,}")
此時train_ids和val_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):,}")
這裏不再手動創建stoi和itos,而是使用tiktoken.get_encoding("gpt2")加載OpenAI為GPT-2預訓練的BPE分詞器。該分詞器已掌握將文本拆分為常見片段(子詞)並分配ID的方法。
encode_ordinary()方法用這個強大的BPE分詞器將train_data和val_data轉為整數列表。
這些標記ID通常更大(GPT-2最多50256),因為它們代表比單個字符更復雜的標記。
本質其實還是我們一層不夠高,那就再套一層實現的思想
對於超大數據集(如網絡文本集合"OpenWebText"),nanoGPT在data/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()方法保持不變
datasets的map函數幫助高效處理數百萬文檔。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'))
這段代碼用numpy將train_ids和val_ids列表轉為高效數組。
dtype=np.uint16指定每個數字存為16位無符號整數(GPT-2的標記ID最大值50256在此範圍內,因2^16=65536)。.tofile()生成緊湊的二進制文件(train.bin和val.bin),訓練時可快速加載。
字符級分詞還會額外保存meta.pkl文件存儲stoi和itos映射,便於後續將標記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.bin、val.bin和meta.pkl。
處理BPE版微型莎士比亞數據集:
python data/shakespeare/prepare.py
將在data/shakespeare/生成train.bin和val.bin。
處理更大的OpenWebText數據集:
python data/openwebtext/prepare.py
注意:這將下載超大數據集(54GB),耗時耗空間
最終在data/openwebtext/生成train.bin(17GB)和`val.bin`(8.5MB)。
技術原理:數據預處理流程
用序列圖展示字符級莎士比亞數據處理的完整流程:
該圖展示了原始文本從網絡到Python處理,最終存儲為高效二進制文件的完整旅程。
分詞方法對比
以下是nanoGPT使用的兩種分詞方法對比:
|
特性
|
字符級分詞(如 |
字節對編碼(BPE)配合 |
|
標記單元 |
單個字符(如’H’,‘e’,‘l’,‘l’,‘o’)
|
常見子詞或完整單詞(如"Hello",“world”,“the”)
|
|
詞彙表大小 |
較小(如微型莎士比亞約65)
|
較大(如GPT-2的 |
|
複雜度 |
更簡單易懂
|
更復雜,採用最優子詞查找算法
|
|
輸出長度 |
相同文本產生更長標記序列
|
相同文本產生更短標記序列(更高效)
|
|
適用場景 |
小數據集/教學用途/需精細字符控制的特定任務
|
大語言模型標準方案,高效處理多樣化文本 |
本章小結
本章我們學習了nanoGPT如何為模型準備"食材":下載原始文本、分割為訓練驗證集、最重要的是將其轉化為數字標記。
- 我們探索了兩種分詞方法:簡單的字符級方案和高效但更復雜的BPE方案(使用
tiktoken)。 - 最終這些標記ID被存入緊湊的二進制文件(
.bin),為訓練階段的高速加載做好優化。