1:为什么要做分词(Tokenizer)?
神经网络不能直接读文字 ------ 它们只读数字(vectors)。所以必须把「一句话」变成「一连串数字」。分词器就是完成这一步的工具。
md-end-block
简单流程:
原始文本(Hello 世界)
⟶ Tokenizer(分词)
⟶ token / 子词(['▁Hello','▁世','界'] 或 ['Hello','世界'])
⟶ 映射到数字 ID([12, 345])
⟶ 送入模型(训练/推理)
通过分词,可以决定词表大小、决定序列长度、决定稀有词,未登录得词怎么处理、影响模型对语言的理解能力。
2:分词的三种基本粒度
-
word(词级)
-
优点:保留完整语义、直观。
-
缺点:词表巨大,遇到新词(OOV)会出问题。
-
-
character(字符级)
-
优点:词表非常小(英语 26 个字母,中文常用字 ~5k)
-
缺点:语义丢失、序列变长(对模型负担大)。
-
-
subword(子词) ------ 大多数现代模型采用
-
用字/子词作为基本单元,折中词表大小与表达能力。
-
常见算法:BPE / BBPE / WordPiece / Unigram(SentencePiece 支持其中几种)
-
3. 主流子词算法直观对比
-
BPE(Byte-Pair Encoding) :从字符开始,按频次合并最常见的相邻对,直到达到词表大小。好理解、常用。
-
BBPE(Byte-level BPE) :把**最小单位改成字节(0--255),跨语言更通用(**HuggingFace 的 GPT-2/ChatGLM/BLOOM 常用),但对多字节语言(中文)会使序列变长。
-
WordPiece:类似 BPE,但选择合并基于让语言模型概率最大化(BERT 用)。
-
Unigram LM:先准备一个大候选词表,再用语言模型概率逐步删减,得到目标词表(SentencePiece 支持)。
-
SentencePiece:不是新算法,而是一个工具包,支持 Unigram/BPE/char/word 实现,并把空格当作 token(用
▁符号表示)。 -
很多大模型用 SentencePiece / BBPE:
-
跨语言支持:可以不用预分词(尤其中文、日文没有空格),直接从原始句子训练。
-
固定词表大小(训练时指定):便于资源规划(8k、32k 等)。
-
空格作为 token :用
▁表示空格,detokenize(复原文本)可以无损。 -
支持子词正则化(BPE-dropout):训练/推理时可随机采样不同分词,增强鲁棒性。
-
支持多种算法(BPE/Unigram),灵活。
-
4.sentencepiece简介
在自然语言处理(NLP)中,分词(Tokenization) 是将文本分解为最小的有意义单位(即"词元")的过程。这一过程对于许多NLP任务(如文本分类、机器翻译、语言模型训练等)至关重要。在传统的分词方法中,常见的分词工具(如 jieba 、NLTK)将文本分解为常见的词汇单位,但这些方法存在一些局限性,特别是在处理低频词、未登录词(OOV, Out-Of-Vocabulary)时表现不佳。
SentencePiece 是一种基于无监督学习的子词(subword)分词器,能够处理这种情况。它通过对语料库进行自适应学习,生成一个子词级别的词汇表,可以很好地解决未登录词问题,并且在训练大型语言模型(如 BERT 、GPT )时广泛使用。SentencePiece 是 Google 提出的通用文本分词工具,它支持多种子词分词算法(如 BPE 和 Unigram LM) 。它的关键特点是:不依赖语言的预先分词,也不需要人工词典,而是直接用无监督方式从原始文本中训练出子词词表。
-
把所有文本当成纯字符流(raw text)处理 它完全不关心有没有空格、是不是中文日文泰文,一律当作 Unicode 字符序列。
-
完全数据驱动的无监督分词 不需要任何词典、规则、人工标注,完全靠统计从语料里自己学出最优的 subword 单元。
-
100% 可逆(lossless) 分完词后再拼回去,和原始文本一模一样,连空格都完全恢复。这对多语言和代码特别重要。
-
词汇表固定 训练一次得到一个固定大小的 vocab(常见 32k~250k),之后永远用这套 vocab,分词确定性 100%。
SentencePiece 是一个从原始文本中学习子词(subword)词表的分词器。它的核心思想是:用可变长度的子词来表示文本,而不用预先人工分词。整个过程如下:
-
**读取原始文本(保留空格与标点)**SentencePiece 将空格视为特殊符号 "▁",因此可以直接从未预处理的文本中学习分词模型。
-
训练分词模型 使用 BPE 或 Unigram LM 等算法,通过频率统计与概率模型学习最优的子词集合(vocabulary)。
-
应用分词模型将输入文本转换为子词序列或对应的 ID 序列,供神经网络使用。
SentencePiece 其实是一个框架,里面实现了四种 subword 算法:
算法 代表模型 特点 BPE GPT-2、GPT-3、LLaMA 从单字符开始,贪心合并频率最高的相邻 pair,最常用 Unigram T5、Albert、LLaMA-2/3、Qwen2 基于子词概率的全局优化,能自动删减低频 subword,比 BPE 更优 WordPiece BERT、Electra 和 BPE 很像,但合并时用 likelihood 而不是频率,基本被 Unigram 取代 Char 纯字符级别 几乎不用,vocab 太小
- BPE(Byte Pair Encoding) -通过合并出现频率最高的字符对来构建新的"词"
原始语料统计字符对频率:
md-end-blockl o w : 5 l o w e r : 3 n e w e s t : 6 w i d e s t : 3最常见的 pair 是 "e" + "s" → 合并成新 token "es"。继续合并 "es" + "t" → "est"。最终可能学到:low、lowest、newest、widest 都能用更少的 token 表示
优点:简单、贪心、速度快 缺点:纯粹频率驱动,有时候会学出不合理的合并(比如把 "the" 拆成 "t"+"he")
- Unigram(更现代的算法) -通过建立概率模型来评估每个子词组合的概率。
Unigram 不是贪心合并,而是从一个超大种子词汇表(比如所有单字符 + 常见 subword)开始,用一个语言模型的思路不断剪枝,保留对整体语料 loss 贡献最大的 subword。核心是一个基于概率的 loss:
md-end-blockLoss = -Σ log P(x_i | vocab)每次删除一个低贡献的 subword,重新计算整个语料的 likelihood,直到 vocab 达到目标大小。
优点:
全局最优,而不是贪心的局部最优
能自动删除无用的 subword
分词结果更合理(比如更倾向保留整词)
Tokenizer 算法 是否语言无关 是否可逆 是否有UNK 代表模型 Jieba 词典+规则 仅中文 否 有 传统中文NLP WordPiece (BERT) WordPiece 基本英文 否 有 BERT SentencePiece BPE BPE 是 是 可无 GPT-2、LLaMA1 SentencePiece Unigram Unigram 是 是 可无 T5、LLaMA3、Qwen2 tiktoken (OpenAI) Byte BPE 是 是 无 GPT-3.5、GPT-4
5.用一下SentencePiece -简单实战
md-end-block
原始文本(raw text)
↓
SentencePieceTrainer.train()
↓
子词词典(.model + .vocab)
↓
SentencePieceProcessor()
↓
文本 → 子词(encode_as_pieces)
文本 → ID(encode_as_ids)
↑
ID → 子词(decode_ids)
子词 → 文本(decode)
5.1:先创建一个案例
md-end-block
我 爱 自然语言处理
我 喜欢 吃 苹果
自然语言处理 是 人工智能 的 一部分
这里测试,一下标点符号,的一个使用。。。试试看看梦;不饿能处理。、
5.2:训练SentencePiece 模型
补充一下各个参数的含义:
md-end-blockspm.SentencePieceTrainer.train( input='train_5GB.txt', # 1 model_prefix='zh_en_32k', # 2 vocab_size=32000, # 3 character_coverage=0.99995, # 4 ★ 非常重要 model_type='unigram', # 5 ★ 2025 绝对主流 max_sentence_length=16384, # 6 byte_fallback=True, # 7 ★ 零 OOV 的核心 split_digits=True, # 8 split_by_unicode_script=True, # 9 ★ 中文必开 bos_piece='[BOS]', # 10 eos_piece='[EOS]', # 11 pad_piece='[PAD]', # 12 unk_piece='[UNK]', # 13 user_defined_symbols='[CLS],[SEP],[MASK],<|eot_id|>,<|endoftext|>', # 14 add_dummy_prefix=False, # 15 ★ 重要 remove_extra_whitespaces=False, # 16 normalization_rule_name='identity', # 17 ★ 重要 train_extremely_large_corpus=True, # 18 shuffle_input_sentence=True # 19 )
序号 参数名 作用 / 用途 示例 推荐值 说明 / 坑点 1 input指定训练用语料路径,SentencePiece 会从这些文本学习子词分词规则 'train.txt,valid.txt'你的语料路径 支持多个文件,用逗号分隔;语料必须 UTF-8 编码;坑点:文件不存在、编码错误会报错;语料太少模型效果差。 2 model_prefix模型输出前缀,会生成 .model和.vocab文件。同样如果指定路径就会生成在对应的路径下,如model_prefix= '/workspace/model/zh_en_32k''zh_en_32k'模型前缀,例如 'zh_en_32k'不要带路径,否则在某些系统下权限可能报错;生成文件名为 zh_en_32k.model/zh_en_32k.vocab。3 vocab_size生成的子词词表大小。SentencePiece 的词表是从语料统计出来的,如果语料本身就很小,那么 vocab_size 再大,模型也"统计不出"更多子词,因此: vocab_size 增加 ≠ 让模型 magically 学到更多词 3200032000~128000 词表太小 → 训练的时候,某些很常见的词没有被收入词表,导致变 <unk>,太大 → 训练慢、显存占用高;中文/中英混合一般 32k;多语模型可 50k~100k。4 character_coverage控制语料字符覆盖比例,影响 rare character 的处理 0.99995中文/多语 0.99995,纯英文 1.0 中文/多语种建议 0.99995,英文可 1.0;太小 → 稀有字被拆成 byte 序列,可能出现乱码;太大 → 无需覆盖稀有符号,浪费词表空间。 5 model_type分词算法类型 'unigram''unigram' 可选 'bpe'/'unigram'/'word'/'char';2025 主流大模型都用'unigram';BPE 老旧,WordPiece 依赖语言预处理。6 max_sentence_length每行语料最大长度,超过会截断 1638416384 一行太长会被截断;现代长上下文模型建议 16k~32k;坑点:开超大上下文时需要增加内存。 7 byte_fallback遇到未登录字符时回退到 byte 序列,避免 <unk>TrueTrue 核心参数!中文、生僻字、emoji 必开;不开 → 出现 <unk>,模型无法学习稀有字。8 split_digits数字拆分为单个 token,提高数字序列处理效率 TrueTrue 好处:1234567890 拆成 10 个 token,而不是一个大 token;不开 → 长数字 token 占用太多位置。 9 split_by_unicode_script不同 Unicode 脚本之间拆分 TrueTrue 中文、日文、韩文、emoji 必开;不开 → "你好Hello"被拆成"你好H" "ello",非常蠢。10 bos_piece文本开始 token '[BOS]''[BOS]' 与模型一致即可;很多 Transformers 默认 <s>;坑点:不同模型不一致可能报错。11 eos_piece文本结束 token '[EOS]''[EOS]' 与模型一致即可;默认 </s>;训练时保证与模型逻辑一致。12 pad_piece填充 token '[PAD]''[PAD]' 用于对齐 batch;默认 3;坑点:注意和 id 顺序一致。 13 unk_piece未登录 token '[UNK]''[UNK]' 默认 <unk>;很多代码固定<unk>=0,不要随意改。14 user_defined_symbols自定义控制符 / 特殊 token `'[CLS],[SEP],[MASK],< 根据任务自定义 eot_id 15 add_dummy_prefix是否在每行前加 _前缀FalseFalse 老版本 LLaMA 会加 _;新版 LLaMA-3/Qwen2 全关,空格处理更自然。16 remove_extra_whitespaces是否去掉多余空格 FalseFalse 保留原始空格和制表符;对代码、markdown 特别重要。 17 normalization_rule_name字符归一化规则 'identity''identity' 'identity'保留原字符;老版本默认nfkc会把 ①②③ → 123,全角 → 半角;坑点:多语混合需 identity,否则中文符号被改变。18 train_extremely_large_corpus优化大语料训练 TrueTrue 大于 2GB 必开;开启后用更高效采样策略,节省时间和内存。 19 shuffle_input_sentence是否打乱语料顺序 TrueTrue 建议开;防止语料前几行特殊导致初始迭代偏差;训练更稳。
SentencePieceTrainer不是单一算法,而是一个训练工具箱 。它的主要职责是用已有的分词算法(BPE、Unigram 等)从语料训练出一个"专用的子词分词模型"。换句话说:
底层算法:BPE / Unigram / char / word
上层工具:SentencePieceTrainer
输出:一个可直接使用的分词模型文件(
.model)+ 词表文件(.vocab)
观点 说明 SentencePieceTrainer 是训练器 它本身不是算法,而是调用算法训练模型 算法是底层核心 Unigram / BPE 决定了子词拆分方式 输出模型是分词器 .model文件就是最终可以直接使用的"算法模型",可以 encode/decode参数控制训练策略 vocab_size、character_coverage、byte_fallback 等直接影响分词质量和 OOV 率
md-end-block
#训练SentencePiece模型
import sentencepiece as spm #注意python文件的名字不能和这个一样,否则会先去这个python文件中找,导致报错:AttributeError: partially initialized module 'sentencepiece' has no attribute 'SentencePieceTrainer'
#(most likely due to a circular import)
spm.SentencePieceTrainer.train(
input='./dataaaaa/cropus.txt',
vocab_size=100,
model_prefix='toy_spm',
model_type='bpe',
character_coverage=1.0
)
print("okok")

md-end-block
import sentencepiece as spm
sp = spm.SentencePieceProcessor(model_file='toy_spm.model')
text = "我想学习自然处理啊。你想不想?"
token = sp.encode_as_pieces(text) #encode_as_pieces 把文本切分成子词(subword)序列,也就是分词操作。分词结果是一个列表。
print(token)
#['▁我', '想学习', '自', '然', '处理', '啊', '。', '你想', '不', '想?']
'''
▁ 表示词的开头(用于代替空格)
subword 可以是:
一个完整的词(自然)
一个词的片段(语言、处理)
一个字符(很罕见时)
'''
之后映射为ID:把子词转为 ID,这也是神经网络真正要的输入
md-end-block
ids = sp.encode_as_ids(text)
print(ids)
#结果
[4, 0, 69, 65, 3, 0, 58, 0, 75, 0]
'''
每个 subword → 词表中的一个数字 ID
神经网络只认识数字,不认识文字。
'''
还可以把ID还原回原句
md-end-block
decoded = sp.decode_ids(ids)
print(decoded)
#输出
我 ⁇ 自然处理 ⁇ 。 ⁇ 不 ⁇
'''
遇到上边的情况的主要原因是:
训练模型时 vocab 太小(比如 vocab_size=8000 甚至更小),导致一些子词根本不在词表里,被映射成 UNK(未知词)。
一旦一个词被编码成 UNK,那么decode 的时候 SentencePiece 就无法恢复原文,只能输出一个占位符("⁇")。不是用法错了,是模型"没背够词"。
分析一下:
分词结果:
['▁我', '想学习', '自', '然', '处理', '啊', '。', '你想', '不', '想?']
然后得到ids:
[4, 0, 69, 65, 3, 0, 58, 0, 75, 0]
这个时候就可以发现,有很多的0,0 在 SentencePiece 的默认定义:是 <unk>(未知词)的 ID
也就是说:
想学习 不在词表 → 变成 <unk>
啊 不在词表 → <unk>
你想 不在词表 → <unk>
想? 不在词表 → <unk>
所以在decode的时候:
<unk> → ⁇ # 这不是 unicode 问题,这是"未知子词"无法还原
导致出现??,每一个"⁇"都是 <unk> 恢复失败的结果。
其根本原因就是------SentencePiece 模型训练语料太少了!
'''
#如果 decode 的逻辑搞错,会得到空格奇怪的文本,但 SentencePiece 可以完美还原。
如果训练数据不够,SentencePiece 在遇到没见过的词还能分词吗?
答案是:能分词,但效果取决于算法(BPE / Unigram)和训练语料覆盖率。
不过绝大多数情况下 ------ SentencePiece 很抗 OOV(未登录词)问题,即使数据少,也不会完全分不出来。
一:SentencePiece 为什么能处理没见过的词?
因为 SentencePiece 是子词级别分词,而不是词级分词。它不会把整个词当成一个整体,它的词表里有:
字符(a, b, c ... 或 中文的 "我"、"爱")
高频组合(th, ing, tion, 自然, 语言 ...)
最坏最坏的情况,它也能把一个没见过的词拆成最小单位(字符)来切分。
举例:假设你的词表里没有 "深度学习模型" SentencePiece 可能会切成:
md-end-block▁深, 度, 学, 习, 模, 型**它不会报错,也不会说找不到词,它就拆小一点继续分。**所以 SentencePiece 是天然的 OOV killer。
第二部分:训练数据少,会有什么影响?
会影响分词质量,但不会导致"不能分"。
(1)BPE 的情况
BPE 会通过合并高频字符来构造子词。 如果训练数据少,一些真正应该合并的词(例如"自然语言")可能没能学到。
结果就是:原本:
▁自然, 语言在数据少时可能变成:
▁自, 然, 语, 言分词更碎,但不是不能分。
(2)Unigram 的情况
Unigram 是概率模型,即使数据少,也会选择概率最高的拆分方式。更稳一些,通常在低数据下效果比 BPE 好。
第三部分:一个非常形象的比喻
把 SentencePiece 想成拼乐高:
数据多:你能训练出很多"组合块"(自然、语言、学习、机器学习...)
数据少:组合块很少,但总有最小的"单独积木块"(字符)
所以你永远能用小积木把一句话拼出来,只不过组合块越少,拼得越碎。不会出现拼不出来(OOV)的情况。
第四部分:实战验证(非常直观)
我们来模拟一个非常极端的情况:
- 训练文本只包含一句话:
"我 爱 自然 语言"那模型只看过这四个词。但我们拿它去分一个完全没见过的句子:
md-end-block"深度学习非常重要"SentencePiece 会输出类似:
md-end-block▁深, 度, 学, 习, 非, 常, 重, 要就是拆到最小单位。
第五部分:所以总结一句话就是:SentencePiece 即使在数据极少时:
分词绝不会失败
不会出现未登录词(no OOV)
最坏情况是变成"字符级分词"
语义会变弱,但功能不受影响
1 分词(Tokenization)
用 SentencePiece 的
encode_as_pieces或encode_as_ids,把文本变成 子词序列 或 ID序列:
md-end-blocktext = "我爱自然语言处理" tokens = sp.encode_as_pieces(text) # ['▁我', '▁爱', '▁自然', '语言', '处理'] ids = sp.encode_as_ids(text) # [48, 10, 124, 112, 101] 主要还是通过ids然后用pytorch的nn.Embedding转为tensor
tokens可读
ids模型可直接用
2 查词表(Vocab) → Embedding
神经网络里一般 每个 ID 对应一个向量(embedding vector):
md-end-block词表: 0 1 2 3 4 ... 子词: <unk> <s> </s> ▁我 ▁爱 ... embedding: [[0.12, ...], [0.33, ...], ...]
Embedding 表是一个二维矩阵
行索引 = 子词 ID
列 = embedding 维度(如 512、768、1024...)
所以你有了 ID 后,可以直接查 embedding:
md-end-blockimport torch import torch.nn as nn # 假设词表大小 10000,embedding 维度 512 embedding_layer = nn.Embedding(num_embeddings=10000, embedding_dim=512) # ids 转 tensor ids_tensor = torch.tensor([ids]) # shape: [1, seq_len] # 查 embedding embeddings = embedding_layer(ids_tensor) # shape: [1, seq_len, 512]例如用bert的Embedding
md-end-block#需要先安装一下transformers库 #导入 BERT 模型和 tokenizer from transformers import BertTokenizer, BertModel import torch #BertTokenizer:负责分词 + 转 ID(包含 WordPiece 子词分词器 + vocab 表) #BertModel:完整的 BERT 模型(12 层 Transformer),包含 embedding 层 + 12 个 encoder 层 #加载预训练模型-以中文bert为例 # 加载 tokenizer tokenizer = BertTokenizer.from_pretrained('bert-base-chinese') ''' 第一次运行会自动下载约 400MB 文件到 ~/.cache/huggingface/hub,包含: vocab.txt(21128 行,WordPiece 词表) config.json pytorch_model.bin(权重) ''' # 加载 BERT 模型(只要 embedding 层就够了,也可以加载整个模型) model = BertModel.from_pretrained('bert-base-chinese') ''' bert-base-chinese 是 HuggingFace 提供的中文 BERT 基础模型。 当然,也可以用英文 BERT:bert-base-uncased。 下载约 390MB 的权重文件,加载完整的 12 层 BERT(总参数 102M)。 很多人误以为这行只加载了 embedding 层,其实加载了整个模型,只是后面我们只取输出当 embedding 用。 ''' #将文本转成 token id--BERT 的 tokenizer 会把文本分词并转换成对应的 ID: text = "我爱自然语言处理" # encode_plus 会返回字典,包含 input_ids, attention_mask 等 inputs = tokenizer.encode_plus( #这里就不用单独分词了,用bert的tokenizer已经把输入文本切分成子词(subword),并映射成 ID text, add_special_tokens=True, # 会加 [CLS] 和 [SEP] return_tensors='pt' # 转成 pytorch tensor ) print(inputs) #是一个字典
md-end-block{'input_ids': tensor([[ 101, 2769, 4263, 5632, 4197, 6427, 6241, 1905, 4415, 102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]])}
键名 shape(单句时) 每个位置放什么 为什么要这个东西? 如果写错会发生什么 input_ids [1, seq_len] 词汇表里的数字 ID 模型真正读的"文字",每个数字对应 vocab 表里的一个 token(字/子词/特殊符号) 乱码、错码,模型完全看不懂 attention_mask [1, seq_len] 1 或 0 告诉模型哪些位置是真实 token,哪些是 padding(填充)。 1=要看,0=完全忽略 写错会导致模型"看见"了本来不该看见的 padding,输出彻底错乱 token_type_ids [1, seq_len] 0 或 1 告诉模型当前 token 属于第几句话(sentence A=0,sentence B=1)。 只在双句任务(如问答、NLI)时有用,单句全 0 单句任务写错没大事;双句任务写错会导致模型分不清哪句是问题哪句是答案,准确率暴跌
md-end-blockinput_ids = inputs['input_ids'] # [1, seq_len] #从字典中取出来 print(inputs_ids) #shape: [1, 10],因为只说了一句话,所以是1 #tensor([[ 101, 2769, 4263, 5632, 4197, 6427, 6241, 1905, 4415, 102]]) attention_mask = inputs['attention_mask'] print(attention_mask) ## shape: [1, 10],这句话被分成了9个token,这 9 个位置都是真的内容,没有 padding,要全部看 #tensor([[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]) 1表示看,0表示掩 ''' 得到10个token,现在的 bert-base-chinese(2024~2025 版本)在处理纯中文时,会把每一个汉字前自动加一个空格(add_dummy_prefix=False 没开),导致: 旧版本(2019~2020)行为: [CLS] 我 爱 自 然 语 言 处 理 [SEP] → 9 个 token 新版本(2021 年之后到现在)行为: [CLS] [UNK] 我 爱 自 然 语 言 处 理 [SEP] → 10 个 token ''' #--------------------------------输出------------- #输出示例(ID 序列): #tensor([[ 101, 2769, 4263, 5632, 4197, 6427, 6241, 1905, 4415, 102]]) ''' 101 → [CLS] 102 → [SEP] 中间的数字就是 token 对应的 ID ''' #之后就可以获取bert的embedding # 不训练,只做前向传播 with torch.no_grad(): #不训练,关掉计算图,梯度,反向传播 outputs = model(input_ids=input_ids, attention_mask=attention_mask) #把input_ids(文字编号))和 attention_mask(哪些要看哪些别看)传给bert,让bert读文章。然后把结果打包给outputs ''' BaseModelOutputWithPoolingAndCrossAttentions( last_hidden_state=tensor([[[-3.1871e-01, -1.4260e-01, -2.1352e-01, ..., 2.9723e-01, 2.2833e-01, -2.9380e-01], [ 6.7322e-01, 1.2139e-01, 3.0185e-01, ..., -1.1036e+00, -4.9976e-02, 8.7920e-02], [ 3.6870e-01, -5.3108e-01, -6.8611e-01, ..., 1.9168e-01, -1.7675e-02, -2.7852e-01], ..., [-4.4909e-04, -1.3693e-01, 1.2886e+00, ..., -6.4701e-02, -3.4146e-01, -1.6366e-01], [ 6.7370e-01, 7.9813e-01, 6.0916e-02, ..., -3.2120e-01, 2.8577e-01, -1.1626e-01], [ 3.5677e-01, -3.3870e-01, 1.3571e+00, ..., 4.6651e-01, -7.7462e-03, 1.9304e-01]]]), pooler_output=tensor([[ 0.9990, 0.9994, 0.9955, 0.6542, 0.5782, 0.9662, 0.1993, -0.7223, 0.9975, -0.9774, 1.0000, 0.9997, 0.2967, -0.9889, 0.9998, -0.9993, 0.0553, 0.9859, 0.9895, -0.1134, 0.9990, -1.0000, -0.9475, -0.9437, 0.6193, 0.8795, 0.9063, -0.8848, -0.9994, 0.9918, 0.9598, 0.9996, 0.9818, -0.9997, -0.9707, -0.1856, -0.5481, 0.9897, -0.9543, -0.8882, -0.9894, -0.7792, -0.2958, -0.9959, -0.9966, 0.7130, -1.0000, -1.0000, -0.7482, 0.9992, -0.8175, -1.0000, 0.9299, -0.9366, -0.4949, 0.9613, -0.9987, 0.8572, 1.0000, 0.3646, 0.9998, -0.8453, -0.4993, -0.9977, 1.0000, -0.9999, -0.9380, -0.6345, 0.9997, 1.0000, -0.9844, 0.9891, 1.0000, 0.1596, 0.4219, 0.9994, -0.9884, 0.4874, -0.9999, -0.3404, 1.0000, 0.9977, -0.8997, 0.3249, -0.9087, -0.9999, -0.9982, 0.9995, -0.2233, 0.9983, 0.9893, -0.9890, -1.0000, 0.9888, -0.9959, -0.9617, -0.1566, 0.9889, -0.4927, -0.8335, -0.6995, 0.8586, -0.9989, -0.9976, 0.9891, 0.9990, 0.8502, -0.9957, 0.9995, 0.6962, -1.0000, -0.9505, -0.9999, -0.6469, -0.9928, 0.9998, 0.7769, 0.3680, 0.9993, -0.9975, 0.8339, -0.9995, -0.7823, -0.8749, 0.9994, 0.9998, 0.9964, -0.9971, 0.9996, 1.0000, 0.9129, 0.9762, -0.9914, 0.9878, 0.3500, -0.9685, 0.6507, -0.4985, 1.0000, 0.9729, 0.8890, -0.9952, 0.9978, -0.9972, 0.9998, -1.0000, 0.9978, -1.0000, -0.9940, 0.9986, 0.9969, 1.0000, -0.8818, 0.9998, -0.9921, -0.9996, 0.9996, -0.4299, 0.9146, -0.9999, 0.8315, 0.0364, -0.7976, 0.0031, -1.0000, 0.9999, -0.8389, 1.0000, 0.9969, -0.9696, -0.9821, -0.9938, 0.5275, -0.9969, -0.8006, 0.9271, 0.8504, 0.9751, -0.4175, -0.9865, 0.9424, -0.6139, -0.9995, 0.9890, -0.6234, 0.1437, -0.6950, 0.7324, 0.9610, 0.9366, -0.6396, 1.0000, 0.4866, 0.9982, 0.9847, -0.0273, -0.2899, -0.9855, -0.9999, 0.6397, 0.9999, -0.8703, -0.9963, 0.5146, -0.9999, 0.9511, 0.8914, 0.4573, -0.9995, -0.9998, 0.9999, -0.9557, -0.9930, 0.9694, -0.7924, 0.0365, -1.0000, 0.9604, 0.9947, -0.3961, 0.3055, -0.1778, -0.9841, 0.9995, -0.9873, 0.9489, 0.7719, 1.0000, 0.9473, -0.5751, -0.8062, 0.9999, -0.2670, -1.0000, 0.9899, -0.9786, -0.3255, 0.9999, -0.9989, 0.9020, 1.0000, 0.9199, 1.0000, 0.5958, -0.9998, -0.9971, 1.0000, 0.9954, 0.9999, -0.9991, -0.9940, 0.1052, -0.9250, -1.0000, -0.9930, -0.3352, 0.9946, 1.0000, -0.3226, -0.9999, -0.1957, -0.9988, 1.0000, -0.9292, 1.0000, 0.9925, -0.9990, -0.9850, 0.8100, -0.4185, -0.9997, 0.4111, -0.9995, -0.9601, -0.9998, 0.8892, -0.9968, -1.0000, 0.9708, 0.9999, 0.9552, -0.9996, 0.9987, 0.9890, -0.9415, -0.9983, 0.9665, -1.0000, 1.0000, -0.9941, 0.7786, -0.8434, -0.9945, 0.6330, 0.9962, 0.9992, -0.9975, -0.8205, -0.9191, -0.9932, -0.2312, 0.8392, -0.5658, 0.2604, -0.9354, -0.9289, 0.8212, -0.9773, -1.0000, 0.6296, 1.0000, 0.3149, 1.0000, 0.4595, 1.0000, 0.8509, -0.9654, 0.9876, -0.5134, -0.9713, -0.8866, -0.9908, 0.9033, -0.1172, -0.1291, -0.9991, 0.9996, 0.9964, 0.9346, 0.8582, -0.2581, -0.5363, 0.9775, -0.9979, 0.9990, -0.9996, -0.8134, 0.9986, 0.9998, 0.9995, 0.7047, -0.9511, 0.9811, -0.9952, 0.9989, -0.9960, 0.9843, -0.9947, 0.8994, -0.5026, -0.9926, 1.0000, 0.9204, -0.5673, 0.9999, -0.8706, 0.9710, 0.9789, 0.9579, 0.9738, 0.8307, 0.9995, -0.9979, -0.9926, -0.9717, -0.9971, -0.9991, -1.0000, 0.4608, -0.9610, -0.9625, -0.3321, 0.8014, 0.8732, -0.9679, -0.1824, 0.4361, 0.6450, -0.1147, 0.3580, 0.8029, -0.9989, -0.9292, -1.0000, -0.9979, 0.1667, 0.9992, -0.9992, 0.9987, -1.0000, -0.9924, 0.9930, -0.3573, -0.5991, 0.9999, -0.9999, 0.9609, 0.9998, 1.0000, 0.9987, 0.9993, -0.9231, -0.9993, -0.9989, -0.9998, -1.0000, -0.9995, 0.8217, 0.2156, -1.0000, -0.9546, 0.9320, 1.0000, 0.9787, -0.9997, -0.5998, -0.9978, -0.9988, 0.9984, -0.9686, -0.9995, 0.9955, -0.1974, 0.9999, -0.8583, 0.7634, 0.5766, 0.9926, 0.8973, -1.0000, 0.8602, 1.0000, 0.7305, -1.0000, -0.8749, -0.8335, -1.0000, -0.4368, 0.8896, 0.9998, -0.9999, -0.6215, -0.9973, 0.7621, 0.9137, 0.9996, 0.9998, 0.9542, 0.7737, 0.9919, 0.1902, 0.9996, 0.4898, -0.9991, 0.9955, -0.4584, 0.6374, -0.9999, 0.9897, 0.9962, 0.9997, 0.8636, 0.5291, -0.9089, -0.5041, 0.9949, 1.0000, -0.9752, -0.0326, -0.9997, -0.9998, -0.9966, -0.6903, -0.7607, -0.9831, -0.9995, 0.6905, 0.4463, 1.0000, 0.9999, 0.9874, -0.7310, -0.9817, 0.9953, 0.6114, 0.9932, -0.5878, -1.0000, -0.9940, -0.9994, 0.9993, -0.8695, -0.8623, -0.9642, -0.2951, 0.8379, -0.9992, -0.9723, -0.9786, 0.1709, 1.0000, -0.9967, 0.9952, -0.9983, 0.8571, 0.4528, 0.7883, 0.9998, -0.6045, -0.5038, -0.7671, 0.9952, 0.9742, 0.9980, -0.9079, 0.6509, 0.9981, -0.8302, 0.9997, 0.5249, 0.8883, 0.9139, 0.9999, -0.0477, 0.9977, 0.8694, 0.9998, 0.9999, -0.9866, -0.2708, 0.5830, -0.6242, -0.2885, 0.9119, 1.0000, 0.7911, -0.9922, -0.9999, 0.9853, 0.9994, 1.0000, 0.5292, 0.9993, -0.4219, 0.9193, 0.7783, 0.7442, 0.4045, 0.7158, 0.9959, 0.9992, -0.9995, -1.0000, -1.0000, 1.0000, 1.0000, -0.7450, -1.0000, 0.9988, -0.8713, 0.9755, 0.9925, -0.2027, -0.8299, 0.9044, -0.9997, 0.2687, 0.9563, 0.5424, 0.6390, 0.9981, -0.9994, 0.1943, 1.0000, -0.0036, 0.9998, 0.5797, -0.9971, 0.9978, -0.9959, -0.9999, -0.8854, 0.9997, 0.9997, -0.6329, 0.1520, 0.9996, -0.9992, 1.0000, -0.9999, 0.9561, -0.9995, 0.9999, -0.6466, -0.9975, -0.3826, 0.6095, 0.9607, -0.9876, 0.9998, -0.2239, -0.9743, -0.6722, -0.9867, -0.9997, -0.9976, 0.7843, -0.9999, 0.8044, -0.0420, -0.6296, -0.9204, -0.9998, 1.0000, -0.1639, -0.9714, 0.9996, -0.9902, -1.0000, 0.9809, -0.9902, -0.0035, 0.9651, 0.4965, -0.1940, -1.0000, 0.5034, 0.9994, -0.9991, -0.8527, -0.9069, -0.9980, 0.9869, 0.9806, -0.0290, -0.3901, 0.8789, 0.8893, 0.6797, 0.4357, 0.8359, -0.9998, -0.9902, -0.9447, -0.9985, -0.9995, -0.9999, 1.0000, 0.9966, 0.9998, 0.5454, -0.1505, 0.6635, 0.9918, -0.9986, 0.0673, 0.8569, 0.9365, -0.9129, -0.9980, -0.7126, -1.0000, -0.4439, 0.3994, -0.9617, -0.5834, 1.0000, 0.9999, -0.9987, -0.9990, -0.9962, -0.9934, 0.9999, 0.9914, 0.9983, 0.2091, -0.6956, 0.9884, -0.8879, 0.4414, -0.9900, -0.9825, -0.9998, 0.8720, -0.9978, -0.9993, 0.9967, 0.9998, 0.5962, -0.9999, -0.8496, 0.9998, 0.9976, 1.0000, 0.3361, 0.9998, -0.9984, 0.9913, -0.9999, 1.0000, -1.0000, 1.0000, 0.9999, 0.9405, 0.9981, -0.9972, 0.7860, 0.1710, -0.7074, 0.9304, -0.7626, -0.9981, -0.5751, 0.9971, -0.8386, 1.0000, 0.9089, 0.6421, 0.8799, 0.8599, 0.9977, -0.9454, -0.9995, 0.9988, 0.9978, 0.9978, 1.0000, 0.9293, 0.9998, -0.9637, -0.9987, 0.9396, -0.8117, 0.5826, -0.9996, 0.9999, 0.9999, -0.9994, -0.8777, 0.0710, -0.3275, 0.9998, 0.9973, 0.9765, 0.9392, -0.4346, 0.9998, -0.9986, 0.9635, -0.8929, -0.8638, 0.9998, -0.8782, 0.9997, -0.9874, 0.9998, -0.9928, 0.9623, 0.9927, 0.9954, -0.9852, 1.0000, 0.2159, -0.9962, -0.9685, -0.9990, -0.9827, 0.9323]]), hidden_states=None, past_key_values=None, attentions=None, cross_attentions=None) last_hidden_state: 每一个字(包括 [CLS][SEP])的最终 768 维"理解向量" ← 就是我们要的 BERT embedding! pooler_output: 只取了 [CLS] 再过一层小网络的结果(分类任务常用) ''' # outputs[0] 是 token embedding, shape=[batch_size, seq_len, hidden_size] token_embeddings = outputs.last_hidden_state print(token_embeddings.shape) #torch.Size([1, 10, 768]) ''' 对于 bert-base-chinese,hidden_size=768 每个 token 对应一个 768 维向量 ''' #使用bert_Embedding-- ''' token_embeddings 就是一块 1 × 10 × 768 的大巧克力(或者 1 × 9 × 768) 现在需要干的事情是:只切出最前面那一小块 [CLS] 的巧克力! Python 的切片语法 [:, 0, :] 三个位置的意思是: 位置 写法 中文意思 你的例子 第1个 : : "第1层我全部都要" 也就是取所有的batch 取第1句话 第2个0 0 "第2层我只要第0块" → 就是 [CLS] 这个 token取第1个位置([CLS]) 第3个 : : "第3层我全部都要" → 768个数字一个不落 取完整的768维 tensor[ <取哪个 batch> , <取哪个 token> , <取 token 的哪些维度> ] 位置 针对的维度 示例中的写法 含义 第 1 个 batch_size 维度 : 取所有 batch(所有句子) 第 2 个 token 维度 0 取第 0 个 token(即 [CLS]) 第 3 个 hidden_size 维度 : 取 token 的全部 768 维 BERT 作者在论文里说:"我们特意训练了这个 [CLS] token,让它来总结整句话的含义"。所以直接取第1个位置(索引0)就完事了,超级方便! ''' # 举例:取 CLS token 作为句子向量--现在就可以把 token_embeddings 当成输入送给你自己的模型,例如 Transformer、LSTM、分类器等。 cls_embedding = token_embeddings[:, 0, :] # [batch_size, hidden_size]1. 先说一个基本事实:BERT 输出的是 token 的 embedding,不是句子的 embedding
假设输入:
md-end-block[CLS] 我 爱 自然语言 处理 [SEP] #根据分词器切割出来的就是一个tokenBERT 输出 shape:
md-end-block[batch, seq_len, 768]也就是:
每一个 token(包括 CLS、我、爱...)都会得到一个 768 维向量
BERT 本质上是"给每个 token 编码"
但是任务需要"句子级"信息(例如分类、情感分析、句子匹配)。怎么办?
2. BERT 在训练的时候故意把 [CLS] 当成 "句子代表"
**[CLS] 不是一个普通 token,是 BERT 自己设计出来的特殊符号。**它的作用不是"词义",而是:
让整个句子的语义流向它,使它成为句子的整体表示(summary representation)。
因为在训练 BERT 的预训练任务时(NSP + MLM): NSP(Next Sentence Prediction)任务
BERT 会把两句话放进去:
md-end-block[CLS] 句子A [SEP] 句子B [SEP]然后让模型判断:
"B 是否是 A 的下一句?"
这个判断,就是在 [CLS] 的 embedding 上面接一个分类器做的。
**也就是说:**整个句子组合(A + B)的所有语义都被 Transformer"聚合"到 CLS embedding 里。
这就像是:
多层 Transformer 把两句话互相交互编码,最终将句子整体的信息沉淀到 CLS 这个"桶"里。
3. 为什么 Transformer 会把信息"汇聚"到 CLS 上?
因为 self-attention 会让每个 token 能看到所有 token。而我强调一句:
CLS 本身也会和所有 token 进行 attention。
也就是说:
CLS 看整个句子
整个句子也能影响 CLS
Transformer 多层迭代后:
CLS embedding = 整句的语义压缩结果
所以它天然成为 sentence embedding。这不是"巧合",是设计出来的。
4. 类比一下你就更懂了
把句子想象成一群人:
"我"
"爱"
"自然"
"语言"
"处理"
他们都在说自己相关的信息。
CLS 就是:
会议主持人(主持人永远坐在最前排第 0 位)
每一层 Transformer,CLS 都会听所有人的发言(attention),然后做一条总结。网络训练久了以后:
CLS 已经被训练成"总结全句语义的专家"。
所以最后我们取:
md-end-blockcls_embedding = token_embeddings[:, 0, :]5. 那为什么不取"平均所有 token"呢?
这是可以的,举例:
**① 早期模型(如 FastText)就是平均所有 token 的 embedding。**但效果不如 CLS。
② RoBERTa/BERT 的论文说明 CLS 表现最好,因为 CLS 专门被训练来做"句子级任务"。
③ Sentence-BERT(SBERT)也证明 CLS 更适合作为句子 embedding,(尽管 SBERT 会做额外的 pooling)
取 CLS 是因为 BERT 在训练阶段就是把 CLS 训练成整句话的语义摘要器。
换句话说:
CLS 是为了句子而生的
它的 embedding = 整句话的含义
所以句子向量就用 CLS
一、BERT Embedding 到底包含哪几部分?
层级 名称 是否属于 "BERT Embedding" 说明 1 分词器(Tokenizer) 算,但只是预处理 WordPiece 子词分词 + [CLS](#层级 名称 是否属于 “BERT Embedding” 说明 1 分词器(Tokenizer) 算,但只是预处理 WordPiece 子词分词 + CLS + vocab 映射成 ID 2 静态词表嵌入(Token Embedding) 768维 是,最底层 3 位置编码(Position Embedding) 768维 是 学出来的或者用 sin/cos,告诉 BERT 第几个位置 4 段落编码(Segment Embedding) 768维 是 区分 sentence A / sentence B(单句任务基本全是 0) → 上面 2+3+4 相加 → 得到 输入嵌入(Input Embedding) 很多人说的“BERT Embedding”指的就是这个 这就是喂进 Transformer 前的初始向量 5 12/24 层 Transformer 编码后的上下文向量 真正最值钱的 “BERT Embedding” 每个 token 的最终 768/1024 维表示,包含了完整上下文) + vocab 映射成 ID 2 静态词表嵌入(Token Embedding) 768维 是,最底层 3 位置编码(Position Embedding) 768维 是 学出来的或者用 sin/cos,告诉 BERT 第几个位置 4 段落编码(Segment Embedding) 768维 是 区分 sentence A / sentence B(单句任务基本全是 0) → 上面 2+3+4 相加 → 得到 输入嵌入(Input Embedding) 很多人说的"BERT Embedding"指的就是这个 这就是喂进 Transformer 前的初始向量 5 12/24 层 Transformer 编码后的上下文向量 真正最值钱的 "BERT Embedding" 每个 token 的最终 768/1024 维表示,包含了完整上下文
在论文里看到 "We use BERT embeddings as features" 99% 指的是第 5 步的输出(上下文相关的动态向量),不是第 2 步那个死的词表。
但第 2 步那个静态词表也是 BERT 的一部分,所以也有人把它叫 BERT Embedding(容易混淆)。
3 总结流程
步骤 描述 原始文本 "我爱自然语言处理" 分词 SentencePiece → 子词 ['▁我', '▁爱', '▁自然', '语言', '处理'] 转 ID SentencePiece → ID [48, 10, 124, 112, 101] Embedding ID → 向量 [[0.12,...], ...] → 可输入模型 结论:分词只是第一步,得到子词或 ID 后,才能查 embedding,最终喂给神经网络模型。
