02_tokenizer_encode_plus_token_type_ids
上一节我们通过情感分析的例子初探了 BERT 模型的一次完整推理。这一节,我们来看 BERT 的两个核心预训练任务:MLM 与 NSP. 我们同时也介绍 token_type_ids 的设计来源。
1. 加载模型
from transformers import BertTokenizermodel_name = 'bert-base-uncased'tokenizer = BertTokenizer.from_pretrained(model_name)tokenizer输出为:
BertTokenizer(name_or_path='bert-base-uncased', vocab_size=30522, model_max_length=512, padding_side='right', truncation_side='right', special_tokens={'unk_token': '[UNK]', 'sep_token': '[SEP]', 'pad_token': '[PAD]', 'cls_token': '[CLS]', 'mask_token': '[MASK]'}, added_tokens_decoder={ 0: AddedToken("[PAD]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True), 100: AddedToken("[UNK]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True), 101: AddedToken("[CLS]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True), 102: AddedToken("[SEP]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True), 103: AddedToken("[MASK]", rstrip=False, lstrip=False, single_word=False, normalized=False, special=True),})输出解析:
打印 tokenizer 可以查看 tokenizer 的配置摘要:
| 字段 | 值 | 含义 |
|---|---|---|
name_or_path | 'bert-base-uncased' | 加载自哪个 checkpoint |
vocab_size | 30522 | 词表大小 |
model_max_length | 512 | 模型能接受的最大 token 数 |
padding_side | 'right' | padding 加在句子右侧 |
truncation_side | 'right' | 超长时从右边截断 |
special_tokens 特殊标记及其对应的 token ID:
| token | ID | 作用 |
|---|---|---|
[UNK] | 100 | 遇到不认识的字时替换 |
[SEP] | 102 | 句子分隔符/结束符 |
[PAD] | 0 | 填充符,补齐长度用 |
[CLS] | 101 | 分类标记,放在句首 |
[MASK] | 103 | 掩码标记,BERT 预训练时遮住词用 |
added_tokens_decoder 特殊标记的详细注册信息:
每个特殊标记被包装成 AddedToken 对象,属性含义如下:
rstrip=False/lstrip=False:在标记前后不自动去除空格single_word=False:不视为独立单词normalized=False:不做归一化(如小写化)special=True:标记为特殊 token(不会被拆分成子词)
2. 认识文本语料
newsgroups_train.DESCR:数据集描述,一段长文本,介绍这个数据集的来源、内容、划分方式等。类似你看到的 README 文档。newsgroups_train.data:原始文本数据,包含类型、长度和内容示例。newsgroups_train.target:标签(数字)。newsgroups_train.target_names:标签名(文本)。
from sklearn.datasets import fetch_20newsgroupsnewsgroups_train = fetch_20newsgroups(subset='train')fetch_20newsgroups 返回一个 Bunch 对象,这是一种在 Python(特别是 Scikit-learn 机器学习库)中广泛使用的容器对象。它的本质是一个字典(dict),但额外允许像访问对象属性一样来获取和修改里面的值。
特性:
- 字典式访问:你可以像普通字典一样使用键来访问数据,例如:
bunch["data"]. - 属性式访问:你可以像调用对象属性一样直接获取值,例如:
bunch.data. - 动态扩展:支持随时通过属性赋值的方式添加新键值对(如
bunch.new_key = "value").
from collections import CounterCounter(newsgroups_train.target)输出为:
Counter({np.int64(10): 600, np.int64(15): 599, np.int64(8): 598, np.int64(9): 597, np.int64(11): 595, np.int64(7): 594, np.int64(13): 594, np.int64(14): 593, np.int64(5): 593, np.int64(2): 591, np.int64(12): 591, np.int64(3): 590, np.int64(6): 585, np.int64(1): 584, np.int64(4): 578, np.int64(17): 564, np.int64(16): 546, np.int64(0): 480, np.int64(18): 465, np.int64(19): 377})我们可以看到数据集中每个类别的样本数量有多少,比如:
np.int64(10): 600 ← 类别10(棒球)有 600 篇新闻,最多np.int64(15): 599 ← 类别15(基督教)有 599 篇...np.int64(19): 377 ← 类别19(宗教杂谈)只有 377 篇,最少其中:
np.int64():NumPy 的 64 位整数类型。newsgroups_train.target是 NumPy 数组,它的元素类型是int64,所以 Counter 统计时 key 保留了这个类型。np.int64(10):(10)表示具体的类别编号,对应 20 个新闻组中的第 10 个。
一些其他的调用:
type(newsgroups_train.data) # list
len(newsgroups_train.data) # 11314
len(newsgroups_train.target) # 11314
len(newsgroups_train.target_names) # 20
newsgroups_train.target_names输出为:
list # data 的类型为 list
11314 # 总共 11314 条数据
11314 # 总共 11314 个数字标签
20 # 总共 20 类
['alt.atheism', 'comp.graphics', 'comp.os.ms-windows.misc', 'comp.sys.ibm.pc.hardware', 'comp.sys.mac.hardware', 'comp.windows.x', 'misc.forsale', 'rec.autos', 'rec.motorcycles', 'rec.sport.baseball', 'rec.sport.hockey', 'sci.crypt', 'sci.electronics', 'sci.med', 'sci.space', 'soc.religion.christian', 'talk.politics.guns', 'talk.politics.mideast', 'talk.politics.misc', 'talk.religion.misc'] # 每一类的类别名称3. tokenizer 补充
input_idsattention_masksencode_plustoken_type_ids
test_news = newsgroups_train.data[:3]len(test_news[2])输出为:
1981这个代表的是字节(字母)的数量。
tokenizer(test_news, truncation=True, max_length=32)输出为:
{'input_ids': [[101, 2013, 1024, 3393, 2099, 2595, 3367, 1030, 11333, 2213, 1012, 8529, 2094, 1012, 3968, 2226, 1006, 2073, 1005, 1055, 2026, 2518, 1007, 3395, 1024, 2054, 2482, 2003, 2023, 999, 1029, 102], [101, 2013, 1024, 3124, 5283, 2080, 1030, 9806, 1012, 1057, 1012, 2899, 1012, 3968, 2226, 1006, 3124, 13970, 2080, 1007, 3395, 1024, 9033, 5119, 8554, 1011, 2345, 2655, 12654, 1024, 2345, 102], [101, 2013, 1024, 1056, 29602, 6856, 1030, 14925, 1012, 14925, 2078, 1012, 19749, 1012, 3968, 2226, 1006, 2726, 1041, 12688, 1007, 3395, 1024, 1052, 2497, 3980, 1012, 1012, 1012, 3029, 1024, 102]], 'token_type_ids': [[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]], 'attention_mask': [[1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]]}输出解析:
input_ids:是文本被 tokenizer 转化后的数字 ID 序列,也就是模型实际接收的”输入信号”(这里我们设置了truncation=True, max_length=32,所以每个序列都被截断/补齐到了固定长度 32)。
input_ids: [ [101, 2013, 1024, 3393, ..., 102], ← 第1篇新闻,32个token [101, 2013, 1024, 3124, ..., 102], ← 第2篇新闻,32个token [101, 2013, 1024, 1056, ..., 102] ← 第3篇新闻,32个token]token_type_ids:可以看到token_type_ids全是 0. 这是因为我们的输入是单句分类任务。token_type_ids是用来区分两个句子的:- 单句分类(你的情况):全为 0.
- 句子对任务(如问答):第一个句子全 0,第二个句子全 1.
attention_mask:可以看到attention_mask全是 1. 因为这三篇新闻都没有被 padding(没有补 0)。1 表示”这个位置是真实 token,模型要关注它”。- 如果 batch 里有不同长度的句子,短的句子补了
[PAD](ID=0),对应的attention_mask就会是 0.
- 如果 batch 里有不同长度的句子,短的句子补了
tokenizer(test_news[0], test_news[1], truncation=True, padding=True, max_length=32)
tokenizer.decode([101, 2013, 1024, 3393, 2099, 2595, 3367, 1030, 11333, 2213, 1012, 8529, 2094, 1012, 3968, 102, 2013, 1024, 3124, 5283, 2080, 1030, 9806, 1012, 1057, 1012, 2899, 1012, 3968, 2226, 1006, 102])输出为:
{'input_ids': [101, 2013, 1024, 3393, 2099, 2595, 3367, 1030, 11333, 2213, 1012, 8529, 2094, 1012, 3968, 102, 2013, 1024, 3124, 5283, 2080, 1030, 9806, 1012, 1057, 1012, 2899, 1012, 3968, 2226, 1006, 102], 'token_type_ids': [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], 'attention_mask': [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]}
# 可以看到第一个句子和第二个句子'[CLS] from : lerxst @ wam. umd. ed [SEP] from : guykuo @ carson. u. washington. edu ( [SEP]'这里我们就能看出,对于句子对人物,第一个句子的 token_type_ids 全 0,第二个句子全 1.
关于句子对任务,其一般是用在 NSP(next sentence prediction) 任务中,也就是 BERT 的预训练任务。
顺带一提,BERT 的两大预训练任务分别是 NSP 和 MLM,其中 MLM 任务用的就是 mask.
3.1 MLM(Masked Language Model) 掩码语言模型
是什么:随机遮住句子中 15% 的词,让 BERT 根据上下文去猜被遮住的是什么词。
输入: 我今天去 [MASK] 了,天气真好 ↓ BERT ↓预测: 我今天去 [公园] 了,天气真好和笔记的联系:你之前看到的 tokenizer.vocab 中的 [MASK](ID=103)就是为此而生。MLM 让 BERT 学会了双向上下文理解——不仅要看左边的词,还要看右边的词,才能猜出被遮住的是什么。这是 BERT 的核心创新。
3.2 NSP(Next Sentence Prediction)— 下一句预测
是什么:给 BERT 两个句子,让它判断第二句是不是第一句的”下一句”。
句子A: 我今天去了公园 ← 正例(是下一句)句子B: 公园里有很多花和树 → 预测: IsNext
句子A: 我今天去了公园 ← 负例(不是下一句)句子B: 苹果是一种很好吃的水果 → 预测: NotNext和笔记的联系:这就是你刚在代码中看到的!
tokenizer(test_news[0], test_news[1], ...)传入了两个句子(text + text_pair),所以输出中的 token_type_ids 变成了:
[0, 0, 0, ..., 0, 1, 1, 1, ..., 1] ~~~~句子A~~~~ [SEP] ~~~~句子B~~~~- 全 0 = 第一个句子的 token
- 全 1 = 第二个句子的 token
这个 token_type_ids 就是 BERT 用来区分两个句子的”标记”,让模型知道哪些 token 属于句子 A、哪些属于句子 B。
3.3 两者与笔记的对应关系总结
| 预训练任务 | 笔记中的关联 |
|---|---|
| MLM | [MASK] token(ID=103)—— 用来遮住词让模型预测 |
| NSP | token_type_ids(全 0 / 全 1)—— 用来区分两个句子 |
[SEP] token(ID=102)—— 分隔两个句子 | |
[CLS] token(ID=101)—— NSP 的分类信号就取自它的输出 |
BERT 先用 MLM 和 NSP 在大量无标签文本上预训练,然后再在你的情感分析任务上微调(即之前 distilbert-base-uncased-finetuned-sst-2-english 做的事情)。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!