Skip to content

第 2 章 · Token 与 Token 嵌入

本章目标:理解 Token 与嵌入作为构建 LLM 不可或缺的组成部分,动手对比不同分词器,并体验从词嵌入到歌曲推荐的应用。

2.1 下载并运行一个 LLM

第一步是把模型加载到 GPU 上以获得更快的推理速度。注意我们分别加载模型和分词器,并保持它们相互独立,以便后续分别探索。

python
from transformers import AutoModelForCausalLM, AutoTokenizer

# Load model and tokenizer
model = AutoModelForCausalLM.from_pretrained(
    "microsoft/Phi-3-mini-4k-instruct",
    device_map="cuda",
    torch_dtype="auto",
    trust_remote_code=False,
)
tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct")

下面用一个道歉邮件的提示词跑一次完整生成:

python
prompt = "Write an email apologizing to Sarah for the tragic gardening mishap. Explain how it happened.<|assistant|>"

# Tokenize the input prompt
input_ids = tokenizer(prompt, return_tensors="pt").input_ids.to("cuda")

# Generate the text
generation_output = model.generate(
  input_ids=input_ids,
  max_new_tokens=20
)

# Print the output
print(tokenizer.decode(generation_output[0]))

查看分词后的 input_ids

python
print(input_ids)

逐个解码每个 token ID,观察文本如何被切分:

python
for id in input_ids[0]:
   print(tokenizer.decode(id))

检查生成结果的原始结构:

python
generation_output

一个有趣的实验:单个 token 与 token 组合的解码并不总是等价的——

python
print(tokenizer.decode(3323))
print(tokenizer.decode(622))
print(tokenizer.decode([3323, 622]))
print(tokenizer.decode(29901))

2.2 [可选] 在 Colab 中安装依赖

如果你在 Google Colab 上查看本笔记本,需要取消注释并运行下面的代码块安装本章依赖:

💡 注意:运行示例需要 GPU(Colab 中选择 T4)。

python
# %%capture
# !pip install --upgrade transformers==4.41.2 sentence-transformers==3.0.1 gensim==4.3.2 scikit-learn==1.5.0 accelerate==0.31.0 peft==0.11.1 scipy==1.10.1 numpy==1.26.4

2.3 对比不同 LLM 的训练分词器

不同模型使用不同的分词器,切分粒度差异巨大。定义一个可视化函数,用彩色背景打印每个 token:

python
from transformers import AutoModelForCausalLM, AutoTokenizer

colors_list = [
    '102;194;165', '252;141;98', '141;160;203',
    '231;138;195', '166;216;84', '255;217;47'
]

def show_tokens(sentence, tokenizer_name):
    tokenizer = AutoTokenizer.from_pretrained(tokenizer_name)
    token_ids = tokenizer(sentence).input_ids
    for idx, t in enumerate(token_ids):
        print(
            f'\x1b[0;30;48;2;{colors_list[idx % len(colors_list)]}m' +
            tokenizer.decode(t) +
            '\x1b[0m',
            end=' '
        )

准备一段覆盖大小写、emoji、中文、标点、数字运算与空白符的测试文本:

python
text = """
English and CAPITALIZATION
🎵 鸟
show_tokens False None elif == >= else: two tabs:"    " Three tabs: "       "
12.0*50=600
"""

依次用不同模型的分词器对比切分结果——bert-base-uncased(不区分大小写):

python
show_tokens(text, "bert-base-uncased")

bert-base-cased(区分大小写):

python
show_tokens(text, "bert-base-cased")

gpt2

python
show_tokens(text, "gpt2")

google/flan-t5-small

python
show_tokens(text, "google/flan-t5-small")

GPT-4 使用的分词器(官方实现是 tiktoken,这里是 HF 平台上的同一分词器移植版):

python
# The official is `tiktoken` but this the same tokenizer on the HF platform
show_tokens(text, "Xenova/gpt-4")

StarCoder2(需要先申请访问权限):

python
# You need to request access before being able to use this tokenizer
show_tokens(text, "bigcode/starcoder2-15b")

facebook/galactica-1.3b

python
show_tokens(text, "facebook/galactica-1.3b")

Phi-3 的分词器:

python
show_tokens(text, "microsoft/Phi-3-mini-4k-instruct")

2.4 来自语言模型的上下文化词嵌入(以 BERT 类模型为例)

语言模型输出的嵌入是上下文相关的——同一个词在不同句子里向量不同。这里用 DeBERTa 演示:

python
from transformers import AutoModel, AutoTokenizer

# Load a tokenizer
tokenizer = AutoTokenizer.from_pretrained("microsoft/deberta-base")

# Load a language model
model = AutoModel.from_pretrained("microsoft/deberta-v3-xsmall")

# Tokenize the sentence
tokens = tokenizer('Hello world', return_tensors='pt')

# Process the tokens
output = model(**tokens)[0]

查看输出张量的形状:

python
output.shape

把输入拆开来看每个 token:

python
for token in tokens['input_ids'][0]:
    print(tokenizer.decode(token))

完整的输出张量(每个 token 一个上下文化向量):

python
output

2.5 文本嵌入(面向句子和整篇文档)

如果要为整个句子或文档生成一个固定长度向量,sentence-transformers 是最常用的工具:

python
from sentence_transformers import SentenceTransformer

# Load model
model = SentenceTransformer('sentence-transformers/all-mpnet-base-v2')

# Convert text to text embeddings
vector = model.encode("Best movie ever!")

得到的向量维度:

python
vector.shape

2.6 LLM 之外的词嵌入

在 LLM 出现之前,Word2Vec、GloVe 等静态词嵌入就已广泛应用。用 gensim 加载 GloVe 向量:

python
import gensim.downloader as api

# Download embeddings (66MB, glove, trained on wikipedia, vector size: 50)
# Other options include "word2vec-google-news-300"
# More options at https://github.com/RaRe-Technologies/gensim-data
model = api.load("glove-wiki-gigaword-50")

查询与 king 最相近的词(经典的 king − man + woman ≈ queen 关系):

python
model.most_similar([model['king']], topn=11)

2.7 用嵌入做歌曲推荐

最后是一个完整应用:基于播放列表数据训练 Word2Vec,为歌曲推荐相似歌曲。先下载并解析数据集:

python
import pandas as pd
from urllib import request

# Get the playlist dataset file
data = request.urlopen('https://storage.googleapis.com/maps-premium/dataset/yes_complete/train.txt')

# Parse the playlist dataset file. Skip the first two lines as
# they only contain metadata
lines = data.read().decode("utf-8").split('\n')[2:]

# Remove playlists with only one song
playlists = [s.rstrip().split() for s in lines if len(s.split()) > 1]

# Load song metadata
songs_file = request.urlopen('https://storage.googleapis.com/maps-premium/dataset/yes_complete/song_hash.txt')
songs_file = songs_file.read().decode("utf-8").split('\n')
songs = [s.rstrip().split('\t') for s in songs_file]
songs_df = pd.DataFrame(data=songs, columns = ['id', 'title', 'artist'])
songs_df = songs_df.set_index('id')

看看前两个播放列表的内容:

python
print( 'Playlist #1:\n ', playlists[0], '\n')
print( 'Playlist #2:\n ', playlists[1])

每个播放列表被视为一个"句子",其中的歌曲 ID 就像"单词",据此训练 Word2Vec:

python
from gensim.models import Word2Vec

# Train our Word2Vec model
model = Word2Vec(
    playlists, vector_size=32, window=20, negative=50, min_count=1, workers=4
)

向模型询问与 2172 号歌曲相似的歌曲:

python
song_id = 2172

# Ask the model for songs similar to song #2172
model.wv.most_similar(positive=str(song_id))

查看这首歌的元信息:

python
print(songs_df.iloc[2172])

封装一个推荐函数,输出带歌名的推荐列表:

python
import numpy as np

def print_recommendations(song_id):
    similar_songs = np.array(
        model.wv.most_similar(positive=str(song_id),topn=5)
    )[:,0]
    return  songs_df.iloc[similar_songs]

# Extract recommendations
print_recommendations(2172)

再试几次不同的种子歌曲:

python
print_recommendations(2172)
python
print_recommendations(842)

2.8 本章小结

  • 文本先经分词器切成 token 再映射为 token ID;tokenizer(prompt).input_ids 得到 ID 序列,tokenizer.decode 反向还原;
  • 单个 token 的解码与组合解码可能不等价:decode([3323, 622]) 与分别 decode 再拼接的结果可能不同;
  • 不同模型的分词器切分粒度差异巨大(bert-base-uncasedgpt2Xenova/gpt-4、Phi-3 等),直接影响序列长度与计算成本;
  • BERT 类模型输出上下文化的词嵌入,sentence-transformers 则输出句子级固定长度向量;
  • 静态词嵌入(GloVe/Word2Vec)虽非 LLM 产物,仍能支撑"king ≈ queen"类比与歌曲推荐这类实际应用。

🧪 随堂测验

点击你认为正确的选项。答错时会展示正确答案与原因解析。

1. 本章中 tokenizer.decode([3323, 622]) 的实验说明了一个什么现象?

2. 对比 bert-base-uncased 与 bert-base-cased 的分词结果,主要区别是?

3. SentenceTransformer 模型 all-mpnet-base-v2 的 encode 方法输出的是?

4. 本章的歌曲推荐应用中,Word2Vec 的"句子"和"单词"分别对应什么?

🛠️ 动手实践

  1. show_tokens 对比 "bert-base-cased""Xenova/gpt-4" 对同一段中文+emoji 测试文本(2.3 节的 text)的切分,数一数各自的 token 数量并解释差异来源。
  2. 在 2.5 节基础上,用 all-mpnet-base-v2 分别编码 "Best movie ever!""Worst film I have seen.",用 NumPy 计算两个向量的余弦相似度。
  3. 在 2.7 节的歌曲推荐中,把 model.wv.most_similar(positive=str(song_id), topn=5)song_id 改成你喜欢的任意 ID,打印 songs_df.iloc[song_id] 并评价推荐结果是否合理。