从零开始的 LLM 底层知识学习之旅。

本文最后更新于 2026年8月21日 下午

参考书目:《图解大模型: 生成式 AI 原理与实战》Jay Alammar Maarten Grootendorst 著;李博杰译

此为读书笔记,准确来说是原书省流版摘抄与批注。

背景来自 XHS 会拍照的崔崔cui

词元和嵌入是使用 LLM 的两个核心概念。正如我们在第 1 章所见,它们对理解语言人工智能的历史至关重要,而且如图 2-1 所示,如果没有对词元和嵌入的深入理解,我们就无法清楚地了解 LLM 的工作原理、构建方式及其未来的发展方向。

图 2-1
token 在语言模型工作中的作用

本章我们将深入探讨词元的本质以及 LLM 使用的分词方法。然后,我们将探讨著名的 word2vec 嵌入方法,它是现代 LLM 的先驱。我们将了解 word2vec 如何扩展 词元嵌入(token embedding)的概念,来构建商业推荐系统,我们使用的许多互联网应用都是由推荐系统支持的。最后,我们将从 token 嵌入过渡到 sentence 或 text 嵌入,即整个句子或文档可以用一个向量来表示——这为本书第二部分将要介绍的语义搜索和主题建模等应用奠定了基础。


2.1 LLM 分词

在与 LLM 交互过程中我们可以注意到,模型并不是一次性生成所有输出,而是一次生成一个 token。事实上,token 不仅是模型的输出单位,也是模型查看输入的方式。发送给模型的提示词首先被分解成 token,我们接下来将对此进行讨论。

2.1.1 分词器如何处理输入

从外部来看,生成式 LLM 接收输入提示词并生成响应,如图 2-2 所示。

图2
LLM 交互示意图

然而,在将提示词呈现给语言模型之前,它首先要通过分词器将其分解成片段。你可以在 OpenAI 平台上找到 GPT-4 分词器的示例。在文本框中输入文本,它会显示如图 2-3 中的输出,其中 token 以不同的颜色显示。

图3
OpenAI 分词器

让我们看一个代码示例,并亲自与这些 token 进行交互。

2.1.2 下载与运行 LLM

让我们首先像第 1 章那样,加载模型及其分词器:

1
2
3
4
5
6
7
8
9
10
from transformers import AutoModelForCausalLM, AutoTokenizer

# 加载模型及其分词器
model = AutoModelForCausalLM.from_pretrained(
"microsoft/Phi-3-mini-4k-instruct",
device_map="cuda",
torch_dtype="auto",
trust_remote_code=True,
)
tokenizer = AutoTokenizer.from_pretrained("microsoft/Phi-3-mini-4k-instruct")

首先声明提示词,然后对其进行分词,再将这些 token 传递给模型,模型随后生成输出。此例中我们要求模型只生成 20 个新 token:

1
2
3
4
5
6
7
8
9
10
11
12
prompt = "Write an email apologizing to Sarah for the tragic gardening mishap. 
Explain how it happened.<|assistant|>"

# 对输入提示词进行分词
input_ids = tokenizer(prompt, return_tensors="pt").input_ids.to("cuda")
# 生成文本
generation_output = model.generate(
input_ids=input_ids,
max_new_tokens=20
)
# 打印输出
print(tokenizer.decode(generation_output[0]))

从代码中可以看出,模型接受的输入并不是原始文本,而是经 tokenizer 处理后的 input_ids!让我们打印 input_ids 看看它包含什么:

1
2
3
tensor([[ 1, 14350, 385, 4876, 27746, 5281, 304, 19235, 363, 278, 25305, 293, 
16423, 292, 286, 728, 481, 29889, 12027, 7420, 920, 372, 9559, 29889, 32001]],
device='cuda:0')

这就是 LLM 输出的响应,如图 2-4 所示的一系列整数。每个整数都是特定 token(字符、词或词的一部分)的 唯一ID。这些 ID 是分词器内部的一张 token 表的索引,该表包含了分词器能够识别的所有 token。

图4
tokenizer 负责将输入文本处理为 token 序列

如果想查看这些 ID,可以使用分词器的 decode 方法将 ID 转换回人类可阅读的文本:

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

得到输出,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<s>
Write
an
email
apolog
izing
...(省略)
Exp
lain
how
it
happened
.
<|assistant|>
  • 第一个 token 是 ID 1(<s>),这是一个表示文本开始的特殊 token;
  • 一些 token 是完整的单词(例如 Write、an、email);
  • 一些 token 是单词的部分(例如 apolog、izing、trag、ic);
  • 标点符号是独立的 token。

2.1.3 分词器如何分解文本

决定分词器如何分解输入提示词的因素主要有三个。

  • 在模型设计时,模型创建者会选择一种分词方法。
    • 流行方法:BPE(byte pair encoding,广泛用于 GPT 模型)和 WordPiece(用于 BERT 模型)。
    • 这些方法的相似之处在于,它们都旨在 找到一组尽可能高效 的 token 来表示文本数据集,但它们采用不同的方式实现这一目标。
  • 在选择方法之后,我们需要做出一些分词器设计选择,如词表大小和使用哪些特殊 token。
  • 分词器需要在特定数据集上进行训练,以建立能最好地表示该数据集的词表。
图4
分词器不仅负责处理输入,还负责处理输出

除了把输入文本处理成语言模型的输入外,分词器还负责处理语言模型的输出,将生成的 token ID 转换为与之关联的输出词或 token,如图 2-5 所示。

2.1.4 各级别的分词

我们刚才讨论的分词方案被称为 子词级分词(subword tokenization)。这是最常用的分词方案,但并非唯一的方案。图 2-6 展示了四种主要的分词方式。

图4
常见的分词方法
  • 词级分词
    • 这种方法在早期的 word2vec 等模型中很常见,但在 NLP 中的使用越来越少。不过,由于其实用性,它在推荐系统等 NLP 以外的场景中得以应用,我们将在本章后面详细介绍。
    • 缺点:可能无法处理分词器训练完成之后,才出现在数据集中的 。这也导致词表中存在大量仅有细微差别的词元(如 apology、apologize、apologetic、apologist)。
      • 这个问题可以通过子词级分词来解决,因为它有一个 apolog 词元,还有后缀词元(如 -y、-ize、-etic、-ist),这些后缀词元与许多其他词元共用,从而形成更具表达能力的词表。
  • 子词级分词
    • 这种方法包含词和词的一部分。除了前面提到的词表更具表达能力外,这种分词方法的另一个优势是能够表示 新词,因为它可以将新词元分解成较小的字符,这些字符通常都在词表中。
    • 优点:相比字符级分词,子词级分词可以在 Transformer 模型有限的上下文长度内,容纳更多文本。
      • 因此,在上下文长度为 1024 的模型中,使用子词级分词可以容纳字符级分词约三倍的文本(对于子词级词元,平均每个词元包含三个字符)。
  • 字符级分词
    • 这是另一种能够成功处理新词的分词方法,因为它仅仅依赖最原始的字母。虽然这使得分词更容易,但建模却变得更困难。
      • 在子词级分词中,模型可以用一个词元表示 play,而使用字符级分词的模型则需要建模拼写出 p-l-a-y 的信息,此外还要建模序列的其他部分。
  • 字节级分词
    • 还有一种分词方法是将词元分解为表示 unicode(统一码)字符的单个字节。
      • 像 “CANINE: Pre-training an Efficient Tokenization-Free Encoder for Language Representation” 这样的论文概述了这类方法,它们也被称为 免分词编码
      • 其他研究成果如 “ByT5: Towards a Token-Free Future with Pre-trained Byte-to-Byte Models” 表明,字节级分词是一种颇具竞争力的方法,尤其是在多语言场景中。

2.1.5 比较训练好的 LLM 分词器

我们之前提到,分词器中出现的词元是由三个主要因素决定的:

  • 分词方法
  • 用于初始化分词器的参数和特殊词元
  • 用于训练分词器的数据集。

接下来,我们对多个实际训练好的分词器进行比较,看看这些因素如何影响它们的行为。我们会看到较新的分词器是如何改变其行为以提升模型性能的,还会看到专门的模型(如代码生成模型)通常需要专门的分词器。

我们使用多个分词器来编码以下文本:

1
2
3
4
5
6
text = """
English and CAPITALIZATION

show_tokens False None elif == >= else: two tabs:" " Three tabs: " "
12.0*50=600
"""

由此,我们能够看到每个分词器是如何处理不同类型的词元的:

  • 大小写
  • 英语以外的语言
  • 表情符号(emoji)
  • 编程代码,包括关键字和经常用于缩进的空白字符(例如在 Python 等语言中)
  • 数字
  • 特殊词元。这类词元具有特定的作用,而不仅仅表示文本。它们包括表示文本开始或结束的词元(模型用结束词元来向系统表明已完成生成),以及我们随后将看到的其他功能性词元

BERT 基座模型(大小写不敏感)2018

  • 分词方法:WordPiece 分词器
  • 词表大小:30522
  • 特殊词元:
    • unk_token:[UNK]
      • 未知词元,当分词器没有为某类字符进行特定编码时使用。
    • sep_token:[SEP]
      • 分隔符词元,用于支持需要为模型提供两段文本的特定任务——在这些情况下,模型被称为 交叉编码器(cross-encoder)。例如,我们将在第 8 章看到分隔符在重排序中的应用。
    • pad_token:[PAD]
      • 填充词元,用于填充模型输入中未使用的位置,因为模型通常要求输入数据具有固定的长度(也就是其上下文窗口)
    • cls_token:[CLS]
      • 分类词元,主要用于分类任务的特殊词元,我们将在第 4 章看到。
    • mask_token:[MASK]
      • 掩码词元,在训练过程中用于隐藏词元。
图4
BERT 1

BERT 模型(大小写敏感)2018

  • 分词方法:WordPiece 分词器
  • 词表大小:28996
  • 特殊词元:同上
图4
BERT 2

GPT-2(2019)

  • 分词方法:WordPiece 分词器
  • 词表大小:28996
  • 特殊词元:同上
图4
GPT-2

FLAN-T5(2022)

  • 分词方法:WordPiece 分词器
  • 词表大小:28996
  • 特殊词元:同上
图4
FLAN-T5

GPT-4(2023)

  • 分词方法:WordPiece 分词器
  • 词表大小:28996
  • 特殊词元:同上
图4
GPT-4

StarCoder2(2024)

  • 分词方法:WordPiece 分词器
  • 词表大小:28996
  • 特殊词元:同上
图4
StarCoder2

2.1.6 分词器的属性

分词器的分词行为是由什么决定的?

  • 分词方法
    • 例如 BPE 等等
  • 用于初始化分词器的参数
    • 词表大小、特殊词元(<s><CLS><MASK> 等等)、大小写处理策略……
  • 训练分词器的目标数据
    • 面向文本场景:可能会对缩进空格进行分词
    • 面向代码场景:可能会对 def return 等关键字进行分词

2.2 词元嵌入

语言是词元的序列,如果我们在足够大的词元集上训练一个足够好的模型,它就会开始捕捉训练数据集中出现的复杂模式:

  • 如果训练数据包含大量英语文本,通过这些模式,模型就能够表示和生成英语;
  • 如果训练数据包含事实性信息(例如维基百科),模型就会具备生成一些事实性信息的能力。

解决这个难题的下一步是为这些词元找到最佳的数值表示,使模型能够计算并正确建模文本中的模式。这些模式呈现给我们的,是模型在特定语言上的连贯性、编码能力或我们期望语言模型具备的任何其他能力。

2.2.1 为分词器的词表保存嵌入

分词器经过初始化和训练,就会在其关联的语言模型的训练过程中使用。这就是为什么预训练语言模型与其分词器绑定,在未经训练的情况下不能使用不同的分词器。

如图 2-7 所示,语言模型为分词器词表中的每个词元都保存了一个嵌入向量。当我们下载预训练语言模型时,模型的一部分就是保存所有这些向量的嵌入矩阵。

图4
图 2-7

训练开始之前,这些向量会像模型的其他权重一样被随机初始化,但训练过程会为它们分配值,使其能够执行有意义的行为。

2.2.2 创建上下文相关的词嵌入

我们已经介绍了作为语言模型输入的词元嵌入,接下来让我们看看语言模型如何创建更好的词元嵌入。这是使用语言模型进行文本表示的主要方式之一,为 命名实体识别(named entity recognition,NER)、抽取式文本摘要等应用提供了支持。

注:抽取式文本摘要是指通过突出显示文本中最重要的部分来对长文本进行摘要提炼,而不是生成新的摘要文本。

与使用静态向量表示每个词元或词不同,语言模型会创建与上下文相关(contextualized)的词嵌入(如图 2-8 所示)。所谓上下文相关,就是根据词元在上下文中的含义使用不同的表示方式。

图4
图 2-8

这些向量可以被其他系统用于各种任务。除了我们在上一段中提到的文本应用外,这些与上下文相关的向量还为 Stable Diffusion 等 AI 图像生成系统提供了支持。

图4
图 2-9

2.3 文本嵌入

虽然词元嵌入是 LLM 运作的关键,但许多 LLM 应用需要处理完整的句子、段落甚至文本文档,这催生了一些特殊的语言模型,它们能够生成 文本嵌入——用单个向量来表示长度超过一个词元的文本片段。

我们可以这样理解文本嵌入模型:它接收一段文本,最终生成单个向量,这个向量以某种形式表示该文本并捕捉其含义。图 2-10 展示了这个过程。

图4
图 2-10

2.4 LLM 之外的词嵌入

嵌入不仅在文本和语言生成方面有用。事实证明,嵌入(即为对象分配有意义的向量表示)在许多领域都很有用,包括推荐引擎和机器人技术。

在本节中,我们将学习如何使用预训练的 word2vec 嵌入,并简要介绍 word2vec 是如何创建词嵌入的。了解 word2vec 的训练方式将为第 10 章学习对比训练做好准备。在下一节中,我们将看到这些嵌入如何用于推荐系统。

2.4.1 word2vec 算法与对比训练

与 LLM 一样,word2vec 也是在从文本生成的样本上进行训练的。假设我们有 Frank Herbert 的小说 Dune 中的一段文本:“Thou shalt not make a machine in the likeness of a human mind”。该算法使用滑动窗口来生成训练样本。比如,我们可以设置窗口大小为 2,
即考虑中心词两侧各两个相邻词。

嵌入向量是通过分类任务生成的。这种任务用于训练神经网络,以预测词是否经常出现在相同的上下文中(这里的 上下文 指的是我们建模的训练数据集中的多个句子)。我们可以将其理解为一个神经网络,它接收两个词作为输入,如果这两个词倾向于出现在相同的上下文中则输出 1,否则输出 0。

在滑动窗口的第一个位置,我们可以生成四个训练样本,如图 2-11 所示。

图4
图 2-11

在每个生成的训练样本中,中心词作为第一个输入,在各个训练样本中,其相邻词分别作为第二个输入。我们期望最终训练好的模型能够对这种相邻关系进行分类,当接收到的两个输入词是相邻词时输出 1。这些训练样本如图 2-12 所示。

图4
图 2-12

然而,如果我们的数据集中只有目标值为 1 的样本(正例),那么模型可能会投机取巧,通过一直输出 1 来获得完美表现。为了解决这个问题,我们需要用非典型相邻词的样本来丰富训练数据集。这些被称为负例,如图 2-13 所示。

图4
图 2-13

事实证明,在选择负例时并不需要过于严谨。仅仅是借助从随机生成的样本中检测正例的能力,就能得到很多有用的模型 [ 这受到了噪声对比估计(noise-contrastive estimation)这一重要概念的启发,详见论文“Noise-Contrastive Estimation: A New Estimation Principle for Unnormalized Statistical Models”]。因此在这种情况下,我们随机获取一些词,将它们添加到数据集中,并将其标注为非相邻词(模型看到它们时应该输出 0)。

至此,我们已经了解了 word2vec 的两个主要概念(见图 2-14):skip-gram,选择相邻词的方法;负采样(negative sampling),通过从数据集中随机采样来添加负例。

图4
图 2-14

我们可以从连续文本中生成数百万甚至数十亿个这样的训练样本。在开始用这个数据集训练神经网络之前,我们需要做一些分词策略:包括如何处理大小写和标点符号,以及词表中包含多少 token。然后,我们为每个词元创建一个嵌入向量并随机初始化,如图 2-15 所示。在实践中,这是一个矩阵,其维度为:词表大小 × 嵌入向量的维度

图4
图 2-15

接下来,我们在每个样本上训练模型,输入两个嵌入向量并预测它们是否相邻。这个过程如图 2-16 所示。

图4
图 2-16

根据模型预测正确与否,典型的机器学习训练步骤会调整嵌入向量,以便模型在下次遇到这两个向量时,更准确地进行预测。在训练过程结束时,词表中的所有词元获得了更好的词嵌入表示。

这种接收两个向量并预测它们是否具有某种关系的模型思想,是机器学习中最强大的思想之一,并且在语言模型中屡试不爽。这就是为什么我们要在第 10 章专门讨论这个概念,以及它如何优化语言模型来完成特定任务(如句子嵌入和检索)。

这个思想也是连接文本和图像等不同模态的核心,这对 AI 图像生成模型来说至关重要,我们将在第 9 章详细讨论多模态模型。在多模态形式中,模型会接收一张图片和一段描述文本,然后预测该文本是否描述了这张图片。


2.5 推荐系统中的嵌入

在本节中,我们将使用 word2vec 算法,利用人工创建的音乐播放列表来嵌入歌曲。想象一下,我们把每首歌曲都当作一个词或词元来处理,把每个播放列表当作一个句子,这些嵌入就可以用来推荐经常出现在同一个播放列表中的歌曲。

2.5.1 基于嵌入的歌曲推荐

我们将使用的 Playlist 数据集是由康奈尔大学的 Shuo Chen 收集的。它包含了来自美国数百个广播电台的播放列表。图 2-17 展示了这个数据集的形式。

图4
图 2-17

在深入了解其构建方式之前,让我们先演示一下最终的效果。我们从 Michael Jackson 的“Billie Jean”(歌曲 ID 为 3822)开始:

1
2
# 我们将在下面详细定义和探索这个函数
print_recommendations(3822)

输出:

ID 标题 艺术家
4181 Kiss Prince & The Revolution
12749 Wanna Be Startin’ Somethin’ Michael Jackson
1506 The Way You Make Me Feel Michael Jackson
3396 Holiday Madonna
500 Don’t Stop 'Til You Get Enough Michael Jackson

看起来很合理,Madonna、Prince 的歌曲,以及 Michael Jackson 的其他歌曲是最近邻。

2.5.2 训练歌曲嵌入模型

首先加载包含歌曲播放列表的数据集,以及每首歌曲的元数据,如标题和艺术家:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pandas as pd
from urllib import request
# 获取播放列表数据集文件
data = request.urlopen('https://storage.googleapis.com/maps-premium/data set/yes_
complete/train.txt')
# 解析播放列表数据集文件。跳过前两行,因为它们只包含元数据
lines = data.read().decode("utf-8").split('\n')[2:]
# 删除只有一首歌的播放列表
playlists = [s.rstrip().split() for s in lines if len(s.split()) > 1]
# 加载歌曲元数据
songs_file = request.urlopen('https://storage.googleapis.com/maps-premium/data
set/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')

现在我们已经保存了,让我们检查一下列表 playlists。其中每个元素都是一个包含一系列歌曲 ID 的播放列表:

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

输出:

1
2
Playlist #1: ['0', '1', '2', '3', '4', '5', ..., '43']
Playlist #2: ['78', '79', '80', '3', '62', ..., '210']

让我们训练这个模型:

1
2
3
4
5
from gensim.models import Word2Vec
# 训练我们的word2vec模型
model = Word2Vec(
playlists, vector_size=32, window=20, negative=50, min_count=1, workers=4
)

训练需要一两分钟的时间,最终会为每首歌曲计算出嵌入向量。现在我们可以像之前处理词语一样,使用这些嵌入向量来寻找相似的歌曲:

1
2
3
song_id = 2172
# 让模型找出与歌曲2172相似的歌曲
model.wv.most_similar(positive=str(song_id))

输出结果为:

1
2
3
4
5
[('2976', 0.9977465271949768),
('3167', 0.9977430701255798),
('3094', 0.9975950717926025),
('2640', 0.9966474175453186),
('2849', 0.9963167905807495)]

这是与歌曲 2172 具有最相似的嵌入表示的歌曲列表。
在这个例子中,这首歌是:

1
print(songs_df.iloc[2172])

输出结果为:

1
2
3
title Fade To Black
artist Metallica
Name: 2172, dtype: object

2.6 小结

在本章中,我们介绍了 LLM 词元、分词器以及使用词元嵌入的实用方法。这为我们下一章深入研究语言模型做好了准备,同时也为我们学习如何在语言模型之外使用 embedding 打开了大门。

我们探讨了分词器如何作为处理 LLM 输入的第一步,将原始文本输入转换为词元 ID。常见的分词方案包括将文本分解为词、子词、字符或字节,具体取决于特定应用的要求。

通过对现实世界预训练分词器(从 BERT 到 GPT-2、GPT-4 和其他模型)的探索,我们了解了某些分词器在某些方面表现更好(例如,保留大小写、换行符或其他语言的词元等信息);而在其他方面,分词器之间仅存在差异(例如,它们如何分解某些词),并无优劣之分。

分词器设计中有三个主要决策点:分词器算法(如 BPE、WordPiece、SentencePiece)、分词参数(包括词表大小、特殊词元、大小写处理策略和不同语言的处理)以及用于训练分词器的数据集。

语言模型能够生成高质量与上下文相关的词元嵌入,这种嵌入改进了原始的静态嵌入。这些与上下文相关的词元嵌入可以用于命名实体识别、抽取式文本摘要和文本分类等任务。

除了生成词元嵌入,语言模型还可以生成涵盖整个句子甚至文档的文本嵌入。这为本书第二部分将要展示的众多语言模型应用提供了强大支持。在 LLM 之前,word2vec、GloVe 和 fastText 等词嵌入方法非常流行。在语言处理中,这些方法已经在很大程度上被语言模型产生的与上下文相关的词嵌入所取代。word2vec 算法依赖两个主要思想:skip-gram 模型和负采样。它还使用了与我们将在第 10 章看到的类似的对比训练方法。

从根据播放列表构建音乐推荐系统的例子中我们看到,嵌入对于创建和改进推荐系统非常有用。

在下一章中,我们将深入探讨分词后的处理过程:LLM 如何处理这些词元并生成文本?我们将探讨使用 Transformer 架构的 LLM 的主要工作原理。


Chapter 2 - 词元与嵌入
http://dbqdss.github.io/2026/08/21/个人学习笔记/AI Notes/LLM/Hands-On LLMs/HandsOn-LLMs-Ch02/
作者
失去理想的獾
发布于
2026年8月21日
许可协议