第 2 章 · Token 与 Token 嵌入
本章目标:理解 Token 与嵌入作为构建 LLM 不可或缺的组成部分,动手对比不同分词器,并体验从词嵌入到歌曲推荐的应用。
2.1 下载并运行一个 LLM
第一步是把模型加载到 GPU 上以获得更快的推理速度。注意我们分别加载模型和分词器,并保持它们相互独立,以便后续分别探索。
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")下面用一个道歉邮件的提示词跑一次完整生成:
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:
print(input_ids)逐个解码每个 token ID,观察文本如何被切分:
for id in input_ids[0]:
print(tokenizer.decode(id))检查生成结果的原始结构:
generation_output一个有趣的实验:单个 token 与 token 组合的解码并不总是等价的——
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)。
# %%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.42.3 对比不同 LLM 的训练分词器
不同模型使用不同的分词器,切分粒度差异巨大。定义一个可视化函数,用彩色背景打印每个 token:
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、中文、标点、数字运算与空白符的测试文本:
text = """
English and CAPITALIZATION
🎵 鸟
show_tokens False None elif == >= else: two tabs:" " Three tabs: " "
12.0*50=600
"""依次用不同模型的分词器对比切分结果——bert-base-uncased(不区分大小写):
show_tokens(text, "bert-base-uncased")bert-base-cased(区分大小写):
show_tokens(text, "bert-base-cased")gpt2:
show_tokens(text, "gpt2")google/flan-t5-small:
show_tokens(text, "google/flan-t5-small")GPT-4 使用的分词器(官方实现是 tiktoken,这里是 HF 平台上的同一分词器移植版):
# The official is `tiktoken` but this the same tokenizer on the HF platform
show_tokens(text, "Xenova/gpt-4")StarCoder2(需要先申请访问权限):
# You need to request access before being able to use this tokenizer
show_tokens(text, "bigcode/starcoder2-15b")facebook/galactica-1.3b:
show_tokens(text, "facebook/galactica-1.3b")Phi-3 的分词器:
show_tokens(text, "microsoft/Phi-3-mini-4k-instruct")2.4 来自语言模型的上下文化词嵌入(以 BERT 类模型为例)
语言模型输出的嵌入是上下文相关的——同一个词在不同句子里向量不同。这里用 DeBERTa 演示:
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]查看输出张量的形状:
output.shape把输入拆开来看每个 token:
for token in tokens['input_ids'][0]:
print(tokenizer.decode(token))完整的输出张量(每个 token 一个上下文化向量):
output2.5 文本嵌入(面向句子和整篇文档)
如果要为整个句子或文档生成一个固定长度向量,sentence-transformers 是最常用的工具:
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!")得到的向量维度:
vector.shape2.6 LLM 之外的词嵌入
在 LLM 出现之前,Word2Vec、GloVe 等静态词嵌入就已广泛应用。用 gensim 加载 GloVe 向量:
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 关系):
model.most_similar([model['king']], topn=11)2.7 用嵌入做歌曲推荐
最后是一个完整应用:基于播放列表数据训练 Word2Vec,为歌曲推荐相似歌曲。先下载并解析数据集:
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')看看前两个播放列表的内容:
print( 'Playlist #1:\n ', playlists[0], '\n')
print( 'Playlist #2:\n ', playlists[1])每个播放列表被视为一个"句子",其中的歌曲 ID 就像"单词",据此训练 Word2Vec:
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 号歌曲相似的歌曲:
song_id = 2172
# Ask the model for songs similar to song #2172
model.wv.most_similar(positive=str(song_id))查看这首歌的元信息:
print(songs_df.iloc[2172])封装一个推荐函数,输出带歌名的推荐列表:
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)再试几次不同的种子歌曲:
print_recommendations(2172)print_recommendations(842)2.8 本章小结
- 文本先经分词器切成 token 再映射为 token ID;
tokenizer(prompt).input_ids得到 ID 序列,tokenizer.decode反向还原; - 单个 token 的解码与组合解码可能不等价:
decode([3323, 622])与分别 decode 再拼接的结果可能不同; - 不同模型的分词器切分粒度差异巨大(
bert-base-uncased、gpt2、Xenova/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 的"句子"和"单词"分别对应什么?
🛠️ 动手实践
- 用
show_tokens对比"bert-base-cased"与"Xenova/gpt-4"对同一段中文+emoji 测试文本(2.3 节的text)的切分,数一数各自的 token 数量并解释差异来源。 - 在 2.5 节基础上,用
all-mpnet-base-v2分别编码"Best movie ever!"和"Worst film I have seen.",用 NumPy 计算两个向量的余弦相似度。 - 在 2.7 节的歌曲推荐中,把
model.wv.most_similar(positive=str(song_id), topn=5)的song_id改成你喜欢的任意 ID,打印songs_df.iloc[song_id]并评价推荐结果是否合理。