基于GitHub項目:https://github.com/datawhalechina/llms-from-scratch-cn
工具介紹
-
tiktoken
:OpenAI開發的專業"分詞器" -
torch
:Facebook開發的強力計算引擎,相當于超級計算器
理解詞嵌入:給詞語畫"肖像"
-
傳統方法:給每個詞一個編號(就像學生學號)
-
詞嵌入:給每個詞畫一幅多維畫像(就像用顏色、形狀、紋理描述一幅畫),但是計算機理解不了這樣的描述,所以我們轉成多維向量來描述每個詞
?例如:
"國王" → [0.8, -0.2, 0.5]
"王后" → [0.7, -0.1, 0.6]
"蘋果" → [0.1, 0.9, -0.3]
計算機通過計算向量相似度(如余弦相似度)就可以知道“國王”與“王后”關系更密切,與“蘋果”沒什么關系!
文本分詞:把文章切成"詞塊"
(LLM對于長文本的識別能力限制,所以要切塊)
第一步:讀取文本
with open("sample.txt", "r", encoding="utf-8") as f:raw_text = f.read()
第二步:分詞器切分文本
舉例:“Hello?world.”
# 簡單切分:按空格切割
["Hello,", " ", "world."]
import re
# 專業切分:同時處理標點符號
text = "Hello, world. Is this-- a test?"
result = re.split(r'([,.?_!"()\']|--|\s)', text)
#正則表達式 r'([,.?_!"()\']|--|\s)' 匹配以下任意一種情況:
#單字符標點符號:,.?_!"()'
#雙連字符 --
#空白字符 \s
#括號 () 將整個模式捕獲為一個分組,確保 re.split 保留分隔符。
#空字符串 '' 出現在連續分隔符或字符串末尾時,表示相鄰分隔符之間的空匹配。切割結果:['Hello', ',', 'world', '.', 'Is', 'this', '--', 'a', 'test', '?']
第三步:清理切割結果
# 清除空白碎片
result = [item.strip() for item in result if item.strip()]
#對于單個字符串,可使用strip()方法移除首尾空白
通過這種處理,計算機從"看到一堆字母"變成"理解詞語之間的關系"。
將詞元轉換為詞元IDs?
舉例解釋這樣做的目的:
????????老師記不住所有學生名字 → 用學號點名
????????計算機記不住所有詞語 → 用數字ID處理文本
優勢:
-
計算機處理數字比處理文本快得多
-
數字形式更適合數學計算(比如詞嵌入)
-
統一格式方便存儲和傳輸
第一步:收集所有詞(去重)
all_words = sorted(list(set(preprocessed))) # 用集合去重,列表裝好所有詞,sorted排序
vocab_size = len(all_words)
第二步:創建詞語?ID的映射表
# 像單詞表一樣:單詞 → 序號
vocab = {"!":0, '"':1, "'":2, ... "He":50} # 同時也需要序號 → 單詞的反向映射
int_to_str = {0:"!", 1:'"', 2:"'", ... 50:"He"}
舉例:一個詞對應一個特殊的ID,方便查找
詞語 | ID | 詞語 | ID |
---|---|---|---|
! | 0 | HAD | 46 |
" | 1 | Had | 47 |
' | 2 | Hang | 48 |
( | 3 | ... | ... |
創建分詞器
#基礎版分詞器
class SimpleTokenizerV1:def __init__(self, vocab):self.str_to_int = vocab # 詞語→IDself.int_to_str = {i:s for s,i in vocab.items()} # ID→詞語def encode(self, text): # 文本 → 數字# 1. 分割文本為詞語# 2. 清理空白# 3. 查表轉換為IDreturn [self.str_to_int[s] for s in preprocessed]def decode(self, ids): # 數字 → 文本# 1. ID轉回詞語# 2. 拼接成句子# 3. 修復標點空格return text
使用舉例:
text = """"It's the last he painted, you know," Mrs. Gisburn said."""
ids = SimpleTokenizerV1.encode(text)
# [1, 58, 2, 872, 1013, 615, 541, 763, 5, 1155, 608, ...]decoded = SimpleTokenizerV1.decode(ids)
# ""It's the last he painted, you know," Mrs. Gisburn said."""
處理特殊詞元
(遇到某個新詞沒有在詞匯表中)
舉例:
text = "Hello, do you like tea?"
# 報錯:KeyError: 'Hello'(Hello不在詞匯表中)
解決方案:添加特殊詞元
all_tokens.extend(["<|endoftext|>", "<|unk|>"])
特殊詞元 | 作用 |
---|---|
<|unk|> | 未知詞語 |
<|endoftext|> | 文本結束標記 |
<|bos|> | 文本開始標記 |
<|pad|> | 填充對齊 |
#升級版分詞器
class SimpleTokenizerV2:def __init__(self, vocab):self.str_to_int = vocab # 詞語→IDself.int_to_str = {i:s for s,i in vocab.items()} # ID→詞語def encode(self, text):# 遇到不認識的詞就用<|unk|>代替preprocessed = [item if item in self.str_to_int else "<|unk|>" for item in preprocessed]return [self.str_to_int[s] for s in preprocessed]#def encode(self, text): # 文本 → 數字# 1. 分割文本為詞語# 2. 清理空白# 3. 查表轉換為ID#return [self.str_to_int[s] for s in preprocessed]def decode(self, ids): # 數字 → 文本# 1. ID轉回詞語# 2. 拼接成句子# 3. 修復標點空格return text
#特殊詞元的使用
text1 = "Hello, do you like tea?"
text2 = "In the sunlit terraces of the palace."# 用<|endoftext|>連接兩段文本
text = text1 + " <|endoftext|> " + text2ids = tokenizer.encode(text)
# [1160, 5, 362, ... 1159, 57, 1013, ... 1160, 7]tokenizer.decode(ids)
# "Hello, do you like tea? <|endoftext|> In the sunlit terraces of the palace."
# 注意:Hello和palace被標記為<|unk|>
過程總結:
-
用戶輸入 → 轉換為ID → 模型處理 → 生成回復ID → 轉換回文本
小思考:為什么GPT模型不需要[PAD]
和[UNK]
?
GPT使用更先進的字節對編碼(BPE),總能將詞語拆分成已知子詞
GPT用同一個<|endoftext|>
標記表示結束和填充