分词器(Tokenizer)-sentencepiece(把训练语料中的字符自动组合成一个最优的子词(subword)集合。)

1:为什么要做分词(Tokenizer)?

神经网络不能直接读文字 ------ 它们只读数字(vectors)。所以必须把「一句话」变成「一连串数字」。分词器就是完成这一步的工具。

md-end-block 复制代码
简单流程:
原始文本(Hello 世界) 
  ⟶ Tokenizer(分词) 
    ⟶ token / 子词(['▁Hello','▁世','界'] 或 ['Hello','世界']) 
      ⟶ 映射到数字 ID([12, 345]) 
        ⟶ 送入模型(训练/推理)

通过分词,可以决定词表大小、决定序列长度、决定稀有词,未登录得词怎么处理、影响模型对语言的理解能力。

2:分词的三种基本粒度

  1. word(词级)

    • 优点:保留完整语义、直观。

    • 缺点:词表巨大,遇到新词(OOV)会出问题。

  2. character(字符级)

    • 优点:词表非常小(英语 26 个字母,中文常用字 ~5k)

    • 缺点:语义丢失、序列变长(对模型负担大)。

  3. 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任务(如文本分类、机器翻译、语言模型训练等)至关重要。在传统的分词方法中,常见的分词工具(如 jiebaNLTK)将文本分解为常见的词汇单位,但这些方法存在一些局限性,特别是在处理低频词、未登录词(OOV, Out-Of-Vocabulary)时表现不佳。

SentencePiece 是一种基于无监督学习的子词(subword)分词器,能够处理这种情况。它通过对语料库进行自适应学习,生成一个子词级别的词汇表,可以很好地解决未登录词问题,并且在训练大型语言模型(如 BERTGPT )时广泛使用。SentencePiece 是 Google 提出的通用文本分词工具,它支持多种子词分词算法(如 BPE 和 Unigram LM) 。它的关键特点是:不依赖语言的预先分词,也不需要人工词典,而是直接用无监督方式从原始文本中训练出子词词表

  1. 把所有文本当成纯字符流(raw text)处理 它完全不关心有没有空格、是不是中文日文泰文,一律当作 Unicode 字符序列。

  2. 完全数据驱动的无监督分词 不需要任何词典、规则、人工标注,完全靠统计从语料里自己学出最优的 subword 单元。

  3. 100% 可逆(lossless) 分完词后再拼回去,和原始文本一模一样,连空格都完全恢复。这对多语言和代码特别重要。

  4. 词汇表固定 训练一次得到一个固定大小的 vocab(常见 32k~250k),之后永远用这套 vocab,分词确定性 100%。

SentencePiece 是一个从原始文本中学习子词(subword)词表的分词器。它的核心思想是:用可变长度的子词来表示文本,而不用预先人工分词。整个过程如下:

  1. **读取原始文本(保留空格与标点)**SentencePiece 将空格视为特殊符号 "▁",因此可以直接从未预处理的文本中学习分词模型。

  2. 训练分词模型 使用 BPEUnigram LM 等算法,通过频率统计与概率模型学习最优的子词集合(vocabulary)。

  3. 应用分词模型将输入文本转换为子词序列或对应的 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 太小
  1. BPE(Byte Pair Encoding) -通过合并出现频率最高的字符对来构建新的"词"

原始语料统计字符对频率:

md-end-block 复制代码
l 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")

  1. Unigram(更现代的算法) -通过建立概率模型来评估每个子词组合的概率。

Unigram 不是贪心合并,而是从一个超大种子词汇表(比如所有单字符 + 常见 subword)开始,用一个语言模型的思路不断剪枝,保留对整体语料 loss 贡献最大的 subword。核心是一个基于概率的 loss:

md-end-block 复制代码
Loss = -Σ 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-block 复制代码
spm.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 学到更多词 32000 32000~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 每行语料最大长度,超过会截断 16384 16384 一行太长会被截断;现代长上下文模型建议 16k~32k;坑点:开超大上下文时需要增加内存。
7 byte_fallback 遇到未登录字符时回退到 byte 序列,避免 <unk> True True 核心参数!中文、生僻字、emoji 必开;不开 → 出现 <unk>,模型无法学习稀有字。
8 split_digits 数字拆分为单个 token,提高数字序列处理效率 True True 好处:1234567890 拆成 10 个 token,而不是一个大 token;不开 → 长数字 token 占用太多位置。
9 split_by_unicode_script 不同 Unicode 脚本之间拆分 True True 中文、日文、韩文、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 是否在每行前加 _ 前缀 False False 老版本 LLaMA 会加 _;新版 LLaMA-3/Qwen2 全关,空格处理更自然。
16 remove_extra_whitespaces 是否去掉多余空格 False False 保留原始空格和制表符;对代码、markdown 特别重要。
17 normalization_rule_name 字符归一化规则 'identity' 'identity' 'identity' 保留原字符;老版本默认 nfkc 会把 ①②③ → 123,全角 → 半角;坑点:多语混合需 identity,否则中文符号被改变。
18 train_extremely_large_corpus 优化大语料训练 True True 大于 2GB 必开;开启后用更高效采样策略,节省时间和内存。
19 shuffle_input_sentence 是否打乱语料顺序 True True 建议开;防止语料前几行特殊导致初始迭代偏差;训练更稳。

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_piecesencode_as_ids,把文本变成 子词序列ID序列

md-end-block 复制代码
text = "我爱自然语言处理"
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-block 复制代码
import 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-block 复制代码
input_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]   #根据分词器切割出来的就是一个token

BERT 输出 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-block 复制代码
cls_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,最终喂给神经网络模型。

相关推荐
咖啡の猫1 小时前
Python列表的查询操作
开发语言·python
Chiandra_Leong1 小时前
Python-Pandas、Numpy
python·pandas
BoBoZz191 小时前
ParametricObjectsDemo多种参数曲面展示及面上部分点法线展示
python·vtk·图形渲染·图形处理
quikai19812 小时前
python练习第三组
开发语言·python
JIngJaneIL2 小时前
基于Java非遗传承文化管理系统(源码+数据库+文档)
java·开发语言·数据库·vue.js·spring boot
吃西瓜的年年3 小时前
1. 初识C语言
c语言·开发语言
ULTRA??3 小时前
初学protobuf,C++应用例子(AI辅助)
c++·python
CHANG_THE_WORLD3 小时前
Python 字符串全面解析
开发语言·python
不会c嘎嘎3 小时前
深入理解 C++ 异常机制:从原理到工程实践
开发语言·c++