目录
1. 写在前面
在自然语言处理(NLP)领域,特别是在使用大型预训练模型(如BERT、GPT等)时,我们经常会遇到“token”这个词。理解什么是 token 以及如何生成它们对于理解和应用这些模型至关重要。
2. Token 的定义和作用
2.1 Token 的基本概念
在 NLP 中,一个 token 可以被定义为文本中的最小单位,可以是:
- 单词(如 “hello”)
- 子词(如 “unhappiness” → “un”, “happiness”)
- 字符(如 “h”, “e”, “l”, “l”, “o”)
例如,Hello, world!
这句话可以被分割成两个tokens:Hello,
和 world!
。
2.2 Token 在大模型中的作用
在大规模的语言模型中,比如 Transformer 架构的 BERT 或 GPT 系列,每个输入序列首先会被转换成一系列 tokens。这是因为模型的内部结构是基于对序列的处理来构建的,而不仅仅是单个字符或者单词。通过这种方式,模型可以更好地捕捉到句子之间的语义关系和上下文信息。
3. 如何生成 Tokens?
以 Transformer 模型中(如 BERT)为例,通常使用 WordPiece 算法进行子词划分(Subword Tokenization),这是一种将单词拆分成更小单元的方法。例如:
- 输入句子:
Hello, world!
Tokenization(子词划分): 可能拆分为:
["Hello", ",", "world", "!"]
- 转为 ID:
- 假设词表:
{"Hello": 5, ",": 6, "world": 7, "!": 8}
- 结果:
[5, 6, 7, 8]
- 假设词表:
添加特殊 Token(如 BERT):
[CLS]
+ Tokens +[SEP]
→[101, 5, 6, 7, 8, 102]
- 最终张量: 直接输入 ID 或通过嵌入层转为向量。
4. Token 如何转为张量?
需要两步:
- step1: Token 转 ID:用词表(Vocabulary)将 Token 映射为
整数
- step2: ID 转张量:将
整数
转换为向量
(如词嵌入)或直接作为标量输入模型
下面以一个具体例子说明:
示例1:
假设有一个 prompt: I love NLP
,有一个词表:{"<PAD>": 0, "<UNK>": 1, "I": 2, "love": 3, "NLP": 4}
。
其中:
- <PAD>:填充符号
- <UNK>:未知词
- Tokenization: 将 句子拆分为 tokens:
["I", "love", "NLP"]
- Token -> ID: 用词表映射每个 token 到一个整数:
[2, 3, 4]
ID -> 张量
- 标题形式:直接作为整数输入(形状 [3])
tensor([2, 3, 4])
- One-Hot 编码:
- “I” (ID=2) → [0, 0, 1, 0, 0]
- “love” (ID=3) → [0, 0, 0, 1, 0]
- 最终张量(形状 [3, 5],5 是词表大小):
tensor([ [0, 0, 1, 0, 0], # "I" [0, 0, 0, 1, 0], # "love" [0, 0, 0, 0, 1] # "NLP" ])
- 词嵌入(Embedding):
- 通过嵌入层将 ID 映射为低维度向量(如:维度=3)
# 假设嵌入矩阵: embedding_matrix = [ [0.0, 0.0, 0.0], # <PAD> [0.1, 0.2, 0.3], # <UNK> [0.4, 0.5, 0.6], # "I" [0.7, 0.8, 0.9], # "love" [1.0, 1.1, 1.2] # "NLP" ]
- 输入 [2, 3, 4] 会转换为(形状 [3, 3]):
tensor([ [0.4, 0.5, 0.6], # "I" [0.7, 0.8, 0.9], # "love" [1.0, 1.1, 1.2] # "NLP" ])
示例2:
使用 HuggingFace Tokenizer transformers 库的示例: 使用
BertTokenizer
分词器对I love NLP but bugs
进行分词:
- 通过嵌入层将 ID 映射为低维度向量(如:维度=3)
- 标题形式:直接作为整数输入(形状 [3])
from transformers import AutoTokenizer
# 加载预训练分词器
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
# 分词并转为张量
inputs = tokenizer("I love NLP but bugs", return_tensors="pt")
print("BERT Tokenizer 输出:")
print("Input IDs:", inputs["input_ids"]) # Token IDs
print("Attention Mask:", inputs["attention_mask"]) # 非填充部分标记为1
输出:
BERT Tokenizer 输出:
Input IDs: tensor([[ 101, 1045, 2293, 17953, 2021, 12471, 102]]) # 包含[CLS]和[SEP]
Attention Mask: tensor([[1, 1, 1, 1, 1, 1, 1]])
5. 词表
词表是通过统计训练数据中所有 Token 的频率生成的,下面使用 HuggingFace 的 transformers 库加载 GPT-2 分词器并查看词表:
- 代码
from transformers import GPT2Tokenizer
# 加载 GPT-2 分词器
tokenizer = GPT2Tokenizer.from_pretrained("gpt2")
# 获取词表(字典格式:token → id)
vocab = tokenizer.get_vocab()
# 词表大小
print("词表大小:", len(vocab)) # 输出: 50257
# 打印前10个token和ID
print("\n前10个token示例:")
for idx, (token, token_id) in enumerate(vocab.items()):
if idx >= 10:
break
print(f"Token: {token:<20} ID: {token_id}")
# 打印一些特殊token
special_tokens = {
"bos_token": tokenizer.bos_token, # 句子开始(GPT-2实际未显式使用)
"eos_token": tokenizer.eos_token, # 句子结束
"unk_token": tokenizer.unk_token, # 未知词
"pad_token": tokenizer.pad_token, # 填充符
}
print("\n特殊Token:")
for key, value in special_tokens.items():
print(f"{key}: {value}")
# 随机采样一些有趣的token
random_tokens = ["!", "Hello", "ĠNLP", "ĠðŁĺ", "Ġtokenization", "ĠĠĠ", "512", "Ġ[", "Ġ]"]
print("\n随机token示例:")
for token in random_tokens:
if token in vocab:
print(f"Token: {token:<20} ID: {vocab[token]}")
- 输出
词表大小: 50257
前10个token示例:
Token: ! ID: 0
Token: " ID: 1
Token: # ID: 2
Token: $ ID: 3
Token: % ID: 4
Token: & ID: 5
Token: ' ID: 6
Token: ( ID: 7
Token: ) ID: 8
Token: * ID: 9
特殊Token:
bos_token: None
eos_token: <|endoftext|>
unk_token: <|endoftext|>
pad_token: None
随机token示例:
Token: ! ID: 0
Token: Hello ID: 15496
Token: ĠNLP ID: 4234
Token: ĠðŁĺ ID: 12736
Token: Ġtokenization ID: 38325
Token: ĠĠĠ ID: 220
Token: 512 ID: 40237
Token: Ġ[ ID: 27
Token: Ġ] ID: 28
- 词表结构:
- 普通字符:如
!
,"
,#
等符号。 - 子词:以
Ġ
开头的 token(表示前面有空格,例如ĠNLP
对应 “ NLP”)。 - 数字和字母:如
512
,Hello
。 - 特殊符号:如
<|endoftext|>
(GPT-2 的句子结束符)。
- 普通字符:如
- 特殊标记:
- GPT-2 没有显式的
bos_token
(句子开始符),但用<|endoftext|>
作为eos_token
。 - 空格会被编码为
Ġ
(例如 “ Hello” →ĠHello
)。
- GPT-2 没有显式的
- 编码特性:
- 使用
字节级 BPE(Byte Pair Encoding)
分词,可以处理任意文本。 - 生僻字符(如表情符号)会被拆分为多个子词。
- 使用
另外,也可以通过直接查询指定 token 的 ID:
text = "NLP"
tokens = tokenizer.tokenize(text)
ids = tokenizer.encode(text)
print(f"文本: {text}")
print(f"Token化: {tokens}") # 输出: ['ĠN', 'LP']
print(f"Token ID: {ids}") # 输出: [4234, 2271]
6. 参考
- https://huggingface.co/docs/tokenizers/index