12_masked_LM
import torchfrom torch import nnfrom transformers.models.bert import BertModel, BertTokenizer, BertForMaskedLM1. model load and data processing
model_type = 'bert-base-uncased'tokenizer = BertTokenizer.from_pretrained(model_type)bert = BertModel.from_pretrained(model_type)mlm = BertForMaskedLM.from_pretrained(model_type, output_hidden_states=True)bert
mlm输出为:
# BertModel 的结构BertModel( (embeddings): BertEmbeddings( (word_embeddings): Embedding(30522, 768, padding_idx=0) (position_embeddings): Embedding(512, 768) (token_type_embeddings): Embedding(2, 768) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (encoder): BertEncoder( (layer): ModuleList( (0-11): 12 x BertLayer( (attention): BertAttention( (self): BertSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (intermediate): BertIntermediate( (dense): Linear(in_features=768, out_features=3072, bias=True) (intermediate_act_fn): GELUActivation() ) (output): BertOutput( (dense): Linear(in_features=3072, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) ) ) ) (pooler): BertPooler( (dense): Linear(in_features=768, out_features=768, bias=True) (activation): Tanh() ))
# BertForMaskedLM 的结构BertForMaskedLM( (bert): BertModel( (embeddings): BertEmbeddings( (word_embeddings): Embedding(30522, 768, padding_idx=0) (position_embeddings): Embedding(512, 768) (token_type_embeddings): Embedding(2, 768) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (encoder): BertEncoder( (layer): ModuleList( (0-11): 12 x BertLayer( (attention): BertAttention( (self): BertSelfAttention( (query): Linear(in_features=768, out_features=768, bias=True) (key): Linear(in_features=768, out_features=768, bias=True) (value): Linear(in_features=768, out_features=768, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) (output): BertSelfOutput( (dense): Linear(in_features=768, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) ) (intermediate): BertIntermediate( (dense): Linear(in_features=768, out_features=3072, bias=True) (intermediate_act_fn): GELUActivation() ) (output): BertOutput( (dense): Linear(in_features=3072, out_features=768, bias=True) (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) (dropout): Dropout(p=0.1, inplace=False) ) ) ) ) ) (cls): BertOnlyMLMHead( (predictions): BertLMPredictionHead( (transform): BertPredictionHeadTransform( (dense): Linear(in_features=768, out_features=768, bias=True) (transform_act_fn): GELUActivation() (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) ) (decoder): Linear(in_features=768, out_features=30522, bias=True) ) ))我们从输出中可以看出,实际上 BertModel 与 BertForMaskedLM 只是在最后结尾的部分结构有所不同:
| 组件 | BertModel | BertForMaskedLM |
|---|---|---|
embeddings | ✅ 有 | ✅ 有(在 .bert 里) |
encoder (12层) | ✅ 有 | ✅ 有(在 .bert 里) |
pooler | ✅ 有 | ✅ 有(在 .bert 里) |
cls (MLM Head) | ❌ 没有 | ✅ 独有的 |
这是 BertForMaskedLM 比 BertModel 多出来的唯一模块:
(cls): BertOnlyMLMHead( (predictions): BertLMPredictionHead( (transform): BertPredictionHeadTransform( (dense): Linear(768 → 768) (transform_act_fn): GELU (LayerNorm) ) (decoder): Linear(768 → 30522) ← 关键! ))它做了两件事:
- transform:把 encoder 输出的 768 维隐藏向量,通过一个全连接 + GELU 激活 + LayerNorm 做一次非线性变换
- decoder:把 768 维映射到 30522 维(= BERT 词表大小),每个位置对应一个词的预测分数
1.1 为什么会有这些区别?
1.1.1 分工不同
- BertModel 是”通用编码器”——输入文本 → 输出每个 token 的上下文向量。至于拿到向量后做什么,它不管。
- BertForMaskedLM 是”专用任务模型”——在 BertModel 的基础上,加了一个预测头,专门做完形填空:输入
"The [MASK] is blue"→ 预测[MASK]是"sky"。
1.1.2 设计模式:骨干网络 + 任务头
这是 HuggingFace Transformers 的统一设计模式:
BertForMaskedLM = BertModel (骨干) + MLM Head (任务头)BertForSequenceClassification = BertModel (骨干) + 分类头BertForQuestionAnswering = BertModel (骨干) + QA头同一个 BertModel 被不同任务模型复用,只需换不同的”头”即可。
1.1.3 为什么 decoder 是 768 → 30522?
BERT 的词表大小是 30522,所以 MLM 做的是一个 30522 分类问题——对每个被 mask 的位置,从 30522 个词中选出最可能的那个。
1.1.4 Pooler 虽然在,但 MLM 不用它
BertForMaskedLM 内部的 BertModel 仍然包含 pooler,这是因为 HuggingFace 直接复用了整个 BertModel 类。但 MLM 任务实际上只用 encoder 最后一层的隐藏状态,pooler 在这里是”冗余但无害”的。
一句话总结:
BertModel 只负责”理解”,BertForMaskedLM 在理解的基础上多了一步”预测被遮住的词”。
text = ("After Abraham Lincoln won the November 1860 presidential " "election on an anti-slavery platform, an initial seven " "slave states declared their secession from the country " "to form the Confederacy. War broke out in April 1861 " "when secessionist forces attacked Fort Sumter in South " "Carolina, just over a month after Lincoln's " "inauguration.")textinputs = tokenizer(text, return_tensors='pt')inputs['input_ids'].shape # torch.Size([1, 62])inputs输出为:
{'input_ids': tensor([[ 101, 2044, 8181, 5367, 2180, 1996, 2281, 7313, 4883, 2602, 2006, 2019, 3424, 1011, 8864, 4132, 1010, 2019, 3988, 2698, 6658, 2163, 4161, 2037, 22965, 2013, 1996, 2406, 2000, 2433, 1996, 18179, 1012, 2162, 3631, 2041, 1999, 2258, 6863, 2043, 22965, 2923, 2749, 4457, 3481, 7680, 3334, 1999, 2148, 3792, 1010, 2074, 2058, 1037, 3204, 2044, 5367, 1005, 1055, 17331, 1012, 102]]), 'token_type_ids': tensor([[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': tensor([[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]])}这里 inputs['input_ids'] 的形状为 (1, 64),它代表的是 (batch_size, sequence_length). 注意这里的 62 个 token 包含了开头自动加的 [CLS] 和结尾的 [SEP].
' '.join(tokenizer.convert_ids_to_tokens(inputs['input_ids'][0]))输出为:
"[CLS] after abraham lincoln won the november 1860 presidential election on an anti - slavery platform , an initial seven slave states declared their secession from the country to form the confederacy . war broke out in april 1861 when secession ##ist forces attacked fort sum ##ter in south carolina , just over a month after lincoln ' s inauguration . [SEP]"2. masking
inputs['labels'] = inputs['input_ids'].detach().clone()这行代码是我们在 MLM(Masked Language Model)任务中准备标签数据的一步:
.detach():切断计算图链接,防止梯度传播。input_ids本身是模型输入的整数张量,按理说是”数据”不需要梯度- 但
.detach()是 PyTorch 的安全习惯,明确告诉框架:“这个张量只是作为标签使用,不需要反向传播”
.clone():创建一个独立的副本。- 如果不
.clone(),新变量和原变量会共享同一块内存。之后我们会在input_ids上随机把某些 token 换成[MASK](变成模型的输入),但labels需要保留原始未 mask 的值作为正确答案,所以必须复制一份。
- 如果不
inputs['labels']:这就是 MLM 的”标准答案”。
整体流程如下图:
我们总体的步骤将是:
- 先把原始
input_ids备份一份到labels - 然后把
input_ids中的部分 token 替换成[MASK] - 模型预测
[MASK]位置应该是什么词 - 拿预测结果和
labels里的原始词对比,计算损失
所以这行代码的核心作用就是:在把句子弄乱之前,先保存一份”正确答案”。
inputs['labels']输出为:
tensor([[ 101, 2044, 8181, 5367, 2180, 1996, 2281, 7313, 4883, 2602, 2006, 2019, 3424, 1011, 8864, 4132, 1010, 2019, 3988, 2698, 6658, 2163, 4161, 2037, 22965, 2013, 1996, 2406, 2000, 2433, 1996, 18179, 1012, 2162, 3631, 2041, 1999, 2258, 6863, 2043, 22965, 2923, 2749, 4457, 3481, 7680, 3334, 1999, 2148, 3792, 1010, 2074, 2058, 1037, 3204, 2044, 5367, 1005, 1055, 17331, 1012, 102]])mask = torch.rand(inputs['input_ids'].shape) < 0.15mask输出为:
tensor([[False, False, True, False, False, True, True, False, False, False, False, False, False, False, False, False, False, False, False, False, True, False, False, True, False, False, False, False, False, False, False, False, False, False, True, False, False, False, False, False, False, True, False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False]])torch.rand(inputs['input_ids'].shape):生成一个形状和input_ids相同的随机数矩阵,每个值在[0, 1)之间均匀分布。
因为 input_ids 的形状是 (1, 62),所以生成的就是 1 行 × 62 列 的随机小数,例如:
tensor([[0.73, 0.02, 0.91, 0.14, ..., 0.88]])< 0.15:阈值判断——将每个随机数与 0.15 比较,小于 0.15 的位置为True,否则为False。
结果就是一个布尔矩阵(Boolean mask):
tensor([[ True, False, False, ..., False]])True≈ 这个位置要被遮住(概率 ≈ 15%)False≈ 这个位置保持不变(概率 ≈ 85%)
为什么是 15%?
这是 BERT 原始论文中的设定——随机选择 15% 的 token 进行掩码。
从 notebook 的输出可以验证,总共 62 个 token,被选中的数量约为 62 × 15% ≈ 9.3,实际输出正好是 14 个(随机波动正常):
完整步骤回顾:
这行代码就是上图中的第二步——决定把哪些词藏起来让模型猜。
sum(mask[0]) # tensor(8)mask_arr = (torch.rand(inputs['input_ids'].shape) < 0.15) \ * (inputs['input_ids'] != 101) \ * (inputs['input_ids'] != 102)mask_arr输出为:
tensor([[False, False, False, False, False, False, False, False, False, False, False, False, True, False, False, False, False, False, False, True, False, False, False, False, False, True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, True, False, False, False, False, False, False, False, True, False, True, False, False, True, False, False, True, False, True, False, False, False]])这段代码相比上一个 mask 加了两个额外条件:
mask_arr = (torch.rand(inputs['input_ids'].shape) < 0.15) # ① 随机选 15% * (inputs['input_ids'] != 101) # ② 排除 [CLS] * (inputs['input_ids'] != 102) # ③ 排除 [SEP]torch.rand(...) < 0.15:随机选 15%,和之前一样,随机选择 15% 的 token 位置标记为True。inputs['input_ids'] != 101:排除[CLS],即每个句子的第一个 token。- 这行代码生成一个布尔张量:
[CLS]的位置为False,其他位置为True。
- 这行代码生成一个布尔张量:
inputs['input_ids'] != 102— 排除[SEP]. 同理,102是[SEP](句子的最后一个 token),这个位置也设为False。*(乘法):逻辑与(AND)。三个布尔张量逐元素相乘(True = 1,False = 0),相当于做逻辑与运算——只有三个条件都为True的位置,结果才是True。
2.1 为什么要排除 [CLS] 和 [SEP]?
这是 BERT 论文中的要求——特殊 token 不应该被 mask:
[CLS]是分类标记,不包含有意义的词义[SEP]是分隔符,也不包含有意义的词义- mask 它们没有意义,模型也学不到什么
sum(mask_arr[0]) # tensor(9)selection = torch.flatten(mask_arr[0].nonzero()).tolist()selection输出为:
[12, 19, 25, 40, 48, 50, 53, 56, 58]这里我们就是把布尔掩码中 True 的位置提取出来,变成一个 easy-to-use 的 Python 列表。
mask_arr[0]:取出mask_arr的第一行(因为只有 1 句话,batch size = 1)。
之前 mask_arr 的形状是 (1, 62),所以 mask_arr[0] 变成一维的 (62,):
tensor([False, False, ..., True, ..., False]).nonzero():找到所有值为True的索引位置。
输出是一个 2D 张量,每个 True 对应一行:
tensor([[12], [19], [25], ... [58]])torch.flatten(...):把上面的 2D 张量展平成一维:
tensor([[12, 19, 25, 40, 48, 50, 53, 56, 58]]).tolist():把 PyTorch 张量转成普通的 Python 列表:
[12, 19, 25, 40, 48, 50, 53, 56, 58]我们可以对比一下数据流转:
| 步骤 | 形状 | 内容 |
|---|---|---|
mask_arr | (1, 62) | [[False, False, True, ...]] |
mask_arr[0] | (62,) | [False, False, True, ...] |
.nonzero() | (12, 1) | [[12], [19], ...] |
flatten | (12,) | [12, 19, 25, ...] |
.tolist() | Python list | [12, 19, 25, 40, 48, 50, 53, 56, 58] |
为什么要转成列表?
后续代码需要逐个处理这些被选中的位置(把对应的 token 换成 [MASK])。Python 列表比 PyTorch 张量更容易做循环遍历:
for i in selection: # 把第 i 个位置的 token 替换成 [MASK]最终输出 [12, 19, 25, 40, 48, 50, 53, 56, 58] 就是这 9 个 token 的位置索引,后续我们要对它们进行 masking 操作。
tokenizer.special_tokens_map输出为:
{'unk_token': '[UNK]', 'sep_token': '[SEP]', 'pad_token': '[PAD]', 'cls_token': '[CLS]', 'mask_token': '[MASK]'}tokenizer.vocab['[MASK]'] # 103inputs['input_ids'][0, selection] = 103inputs输出为:
{'input_ids': tensor([[ 101, 2044, 8181, 5367, 2180, 1996, 2281, 7313, 4883, 2602, 2006, 2019, 103, 1011, 8864, 4132, 1010, 2019, 3988, 103, 6658, 2163, 4161, 2037, 22965, 103, 1996, 2406, 2000, 2433, 1996, 18179, 1012, 2162, 3631, 2041, 1999, 2258, 6863, 2043, 103, 2923, 2749, 4457, 3481, 7680, 3334, 1999, 103, 3792, 103, 2074, 2058, 103, 3204, 2044, 103, 1005, 103, 17331, 1012, 102]]), 'token_type_ids': tensor([[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': tensor([[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]]), 'labels': tensor([[ 101, 2044, 8181, 5367, 2180, 1996, 2281, 7313, 4883, 2602, 2006, 2019, 3424, 1011, 8864, 4132, 1010, 2019, 3988, 2698, 6658, 2163, 4161, 2037, 22965, 2013, 1996, 2406, 2000, 2433, 1996, 18179, 1012, 2162, 3631, 2041, 1999, 2258, 6863, 2043, 22965, 2923, 2749, 4457, 3481, 7680, 3334, 1999, 2148, 3792, 1010, 2074, 2058, 1037, 3204, 2044, 5367, 1005, 1055, 17331, 1012, 102]])}这里就是我们 MLM 任务数据准备中 最关键的一步:我们真正把被选中的 token 替换为 [MASK].
代码解析:
inputs['input_ids']:形状为 (1, 62) 的 token 矩阵;[0, selection]:高级索引(fancy indexing):0表示取第 0 句话,在这里,我们的 batch 中也只有一句;selection就是我们之前得到的列表[12, 19, 25, 40, 48, 50, 53, 56, 58],即所有要遮挡的位置。
=103:我们把这 9 个位置的值全部替换为 103,即[MASK]的 id.
例如替换前:
位置: ... 18 19 20 21 ...值: ... 3988 2698 6658 2163 ... (anti) (slavery) (platform) (,)替换后:
位置: ... 18 19 20 21 ...值: ... 103 2698 103 2163 ... ([MASK]) (slavery) ([MASK]) (,)别忘了,我们的 labels 还是备份好的 原始值。所以,我们的下一步就是:模型看到 [MASK],需要预测出 anti 和 platform,再和 labels 对比计算 loss.
' '.join(tokenizer.convert_ids_to_tokens(inputs['input_ids'][0]))输出为:
"[CLS] after abraham lincoln won the november 1860 presidential election on an [MASK] - slavery platform , an initial [MASK] slave states declared their secession [MASK] the country to form the confederacy . war broke out in april 1861 when [MASK] ##ist forces attacked fort sum ##ter in [MASK] carolina [MASK] just over [MASK] month after [MASK] ' [MASK] inauguration . [SEP]"我们可以很清晰地从上面的输出中看到,这 9 个位置都被 MASK 掉了。
' '.join(tokenizer.convert_ids_to_tokens(inputs['labels'][0]))输出为:
"[CLS] after abraham lincoln won the november 1860 presidential election on an anti - slavery platform , an initial seven slave states declared their secession from the country to form the confederacy . war broke out in april 1861 when secession ##ist forces attacked fort sum ##ter in south carolina , just over a month after lincoln ' s inauguration . [SEP]"可以看到,我们保存好的 labels 是完整的,这是我们的 ground truth.
3. forward and calculate loss
mlm.eval()with torch.no_grad(): outputs = mlm(**inputs)
outputs.keys() # odict_keys(['loss', 'logits', 'hidden_states'])如果你还记得我们在 11 节中看到的 outputs.keys() 的话,你会发现 11 节中的输出是 odict_keys(['last_hidden_state', 'pooler_output']),和这里的 odict_keys(['loss', 'logits', 'hidden_states']) 不一样。
这主要是因为模型不同。在 11 节中,outputs.keys() 来自 BertModel,而这里我们使用的是 BertForMaskedLM. 它的输出结果中:
loss:预测结果与labels之间的损失值(标量);logits:每个 token 位置对词表中 30522 个子的预测分数 (1, 62, 30522);hidden_states:各层的隐藏状态,因为我们加载时传入了output_hidden_states=True的参数。
3.1 loss 是怎么来的
首先,前向传播时,BertModel 先输出 last_hidden_state 和 pooler_output,然后 MLM Head 接过 last_hidden_state 做预测,loss 和 logits 都是在这个阶段算出来的。
具体到 loss,它的出现是因为我们在 inputs 里传入了 labels:
inputs['labels'] = inputs['input_ids'].detach().clone()BertForMaskedLM 的前向代码中有一个内置的损失函数,当检测到 labels 存在时,会自动计算 CrossEntropyLoss(logits, labels),把结果放在 outputs['loss'] 中。
而 BertModel 没有这个逻辑——它甚至不知道什么是 MLM 任务,所以永远不会有 loss 和 logits.
outputs.logits输出为:
tensor([[[ -7.2043, -7.1276, -7.1729, ..., -6.3316, -6.3432, -4.3540], [-12.1506, -12.0077, -12.1967, ..., -11.4132, -10.8127, -8.9152], [ -6.2586, -6.4102, -5.8441, ..., -6.1903, -6.3061, -5.1038], ..., [ -3.5310, -3.7281, -3.4160, ..., -2.7291, -2.5730, -6.5398], [-14.4257, -14.3249, -14.3869, ..., -11.1762, -11.3302, -10.0136], [-11.3686, -11.7058, -11.4880, ..., -11.0921, -9.7356, -8.3027]]])outputs.loss # tensor(0.6281)type(outputs.hidden_states) # tuple
len(outputs.hidden_states) # 13
outputs['hidden_states'][-1]输出为:
tensor([[[-4.3005e-01, 3.4016e-02, -3.8321e-01, ..., -3.4883e-01, -1.0174e-01, 4.8761e-01], [-6.9344e-01, -3.2490e-04, 2.5159e-01, ..., -4.7717e-01, 1.6140e-01, 6.2177e-01], [-6.0726e-01, 1.0112e+00, -6.3583e-01, ..., -6.0174e-01, -3.2421e-01, 4.3809e-01], ..., [-4.3882e-01, 2.9114e-01, -8.9229e-01, ..., -3.3631e-01, -8.5137e-01, 9.9881e-01], [ 5.0888e-01, 1.2994e-02, -3.8768e-01, ..., 3.2210e-02, -6.3586e-01, 1.6127e-02], [-3.6704e-03, 1.7164e-01, -7.1432e-01, ..., -4.6810e-01, -8.0063e-01, 2.6163e-02]]])关于 outputs.hidden_states 的长度和类型,我们在 07 节中做了详尽的探讨,这里的 13 包含了第 0 层 embedding 层和 12 个 transformer 层(attention layers).
4. from scratch
mlm.cls(outputs['hidden_states'][-1])输出为:
tensor([[[ -7.2043, -7.1276, -7.1729, ..., -6.3316, -6.3432, -4.3540], [-12.1506, -12.0077, -12.1967, ..., -11.4132, -10.8127, -8.9152], [ -6.2586, -6.4102, -5.8441, ..., -6.1903, -6.3061, -5.1038], ..., [ -3.5310, -3.7281, -3.4160, ..., -2.7291, -2.5730, -6.5398], [-14.4257, -14.3249, -14.3869, ..., -11.1762, -11.3302, -10.0136], [-11.3686, -11.7058, -11.4880, ..., -11.0921, -9.7356, -8.3027]]], grad_fn=<ViewBackward0>)可以看到,这里的输出与之前的 outputs.logits 是完全一致的。这是因为 outputs.logits 本身就是由 mlm.cls(outputs['hidden_states'][-1]) 计算出来的。当我们执行 mlm(**inputs) 时,内部的数据流是这样的:
inputs │ ▼mlm.bert(inputs) ← 12层 Transformer │ ├── outputs['last_hidden_state'] = 最后一层输出 (1, 62, 768) └── outputs['hidden_states'] = 全部 13 层输出 │ hidden_states[-1] ← 还是最后一层 │ ▼ mlm.cls(...) ← BertOnlyMLMHead │ ┌─────┴──────┐ │ │ transform decoder (Linear+GELU (Linear +LayerNorm) 768→30522) │ │ └──────┬──────┘ ▼ outputs['logits'] (1, 62, 30522) │ CrossEntropyLoss(logits, labels) │ outputs['loss']需要补充的一点是,这里的 cls 指的就是 分类器。在 HuggingFace 中,各类任务模型的”任务头”统一命名为 cls,因为本质上它们都是在做分类(classification):
| 模型 | 任务头 | 做什么分类 |
|---|---|---|
BertForMaskedLM | cls | 对 30522 个词分类 → 预测被 mask 的词 |
BertForSequenceClassification | cls | 对情感/类别分类 → 判断句子是正面/负面 |
BertForNextSentencePrediction | cls | 二分类 → 判断两句话是否连续 |
而在 MLM 任务中,cls = MLM Head.
我们可以从模型结构看到 mlm.cls 的类型:
mlm.cls输出为:
BertOnlyMLMHead( (predictions): BertLMPredictionHead( (transform): BertPredictionHeadTransform( (dense): Linear(in_features=768, out_features=768, bias=True) (transform_act_fn): GELUActivation() (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True, bias=True) ) (decoder): Linear(in_features=768, out_features=30522, bias=True) <- 真正的分类层 ))它做的就是对一个 mask 位置的上下文向量,从 30522 个候选词中 分类出 最可能是哪一个词。
last_hidden_state = outputs['hidden_states'][-1] # 最后一个隐藏层的输出mlm.eval()with torch.no_grad(): transformed = mlm.cls.predictions.transform(last_hidden_state) print(transformed.shape) # torch.Size([1, 62, 768]) logits = mlm.cls.predictions.decoder(transformed) print(logits.shape) # torch.Size([1, 62, 30522])logits输出为:
tensor([[[ -7.2043, -7.1276, -7.1729, ..., -6.3316, -6.3432, -4.3540], [-12.1506, -12.0077, -12.1967, ..., -11.4132, -10.8127, -8.9152], [ -6.2586, -6.4102, -5.8441, ..., -6.1903, -6.3061, -5.1038], ..., [ -3.5310, -3.7281, -3.4160, ..., -2.7291, -2.5730, -6.5398], [-14.4257, -14.3249, -14.3869, ..., -11.1762, -11.3302, -10.0136], [-11.3686, -11.7058, -11.4880, ..., -11.0921, -9.7356, -8.3027]]])这里,我们就是把 hidden_states[-1](最后一层的输出,768 维)转换成最终的 logits:
- 第一步是
transform,它对应我们前面mlm.cls结构中的:
(transform): BertPredictionHeadTransform( (dense): Linear(768 → 768) ← 全连接 (transform_act_fn): GELUActivation() ← GELU 激活 (LayerNorm): LayerNorm((768,)) ← LayerNorm)做的事情:对每个 token 的 768 维向量做一次非线性变换(Linear → GELU → LayerNorm),输出仍是 (1, 62, 768). 你可以理解为,在输出最终的分类结果前,我们先对特征做一道“精加工”。
- 第二步是
decoder,它对应模型结构中的:
(decoder): Linear(in_features=768, out_features=30522, bias=True)做的事情:把 768 维映射到 30522 维,输出 (1, 62, 30522).
整个的数据流如下:
last_hidden_state (1, 62, 768) │ ▼ transform (Linear + GELU + LayerNorm)transformed (1, 62, 768) ← 维度不变,特征被精加工 │ ▼ decoder (Linear 768 → 30522)logits (1, 62, 30522) ← 每个 token 对 30522 个词的预测分数而两者合起来就是完整的 mlm.cls().
5. loss and translate
cross_entropy = nn.CrossEntropyLoss()logits.shape # torch.Size([1, 62, 30522])
inputs['labels'].shape # torch.Size([1, 62])
inputs['labels'][0].view(-1).shape # torch.Size([62])这里我们主要关注最后一行,它输出的是 labels 展平后的一维形状:
inputs['labels']:形状是 (1, 62),这是之前备份的原始input_ids,所以形状就是(batch_size, seq_len);[0]:取第 0 句话,也就是去掉 batch 维度,形状变为 (62,);.view(-1):这是 Pytorch 中“自动推断该维度大小”的展平操作。由于原来的[0]已经是 (62,) 一维了,所以view(-1)并没有改变形状。
因此,最终的形状就是 62 个 token ID,一个简单的一维序列。
torch.argmax(logits[0], dim=-1) # torch.Size([1, 62])输出为:
tensor([ 1012, 2044, 8181, 5367, 2180, 1996, 2281, 7313, 4883, 2602, 2006, 2019, 3424, 1011, 8864, 4132, 1010, 2019, 3988, 2698, 6658, 2163, 4161, 2037, 22965, 2013, 1996, 2406, 2000, 2433, 1996, 18179, 1012, 2162, 3631, 2041, 1999, 2258, 6863, 2043, 22965, 8055, 2749, 4457, 3481, 7680, 3334, 1999, 2148, 3792, 1010, 2074, 2058, 1037, 3204, 2044, 5367, 1005, 1055, 17331, 1012, 1055])这里我们就是 把模型的预测分数转换成具体的预测词 ID:
logits:形状 (1, 62, 30522),表示logits中每个 token 的 30522 个分数,代表这个词是词表中每个词的可能性;logits[0]:去掉 batch 维度,形状变为 (62, 30522);argmax(logits[0], dim=-1):取最后一个维度的最大值索引。dim=-1表示沿着最后一个维度(即 30522 词表维度),找分数最高的那个位置;- 对每个 token,从 30522 个候选词中挑出分数最高的那个词的 ID;
- 输出形状为 (62,),即 62 个位置各自的预测词 ID.
logits[0] (62, 30522)┌──────────────────────────────────────┐│ 位置0: [-6.97, -6.90, ..., -4.24] │ → argmax → ID=1996│ 位置1: [-12.18, -12.09, ..., -9.73] │ → argmax → ID=2044│ 位置2: [-6.56, -6.69, ..., -5.23] │ → argmax → ID=1055│ ... ││ 位置61: [-12.45, -12.79, ..., -8.94] │ → argmax → ID=1055└──────────────────────────────────────┘ ↓ tensor([1996, 2044, 1055, ..., 1055]) (62,)' '.join(tokenizer.convert_ids_to_tokens(inputs['input_ids'][0]))输出为:
"[CLS] after abraham lincoln won the november 1860 presidential election on an [MASK] - slavery platform , an initial [MASK] slave states declared their secession [MASK] the country to form the confederacy . war broke out in april 1861 when [MASK] ##ist forces attacked fort sum ##ter in [MASK] carolina [MASK] just over [MASK] month after [MASK] ' [MASK] inauguration . [SEP]"这是我们之前看过的输出,也就是我们要进行“完形填空”的对象。
' '.join(tokenizer.convert_ids_to_tokens(torch.argmax(logits[0], dim=1)))输出为:
". after abraham lincoln won the november 1860 presidential election on an anti - slavery platform , an initial seven slave states declared their secession from the country to form the confederacy . war broke out in april 1861 when secession confederate forces attacked fort sum ##ter in south carolina , just over a month after lincoln ' s inauguration . s"这里就是我们模型对整个句子的预测结果,相当于“做完完形填空后的答卷”。
我们可以看到,大部分 [MASK] 的位置都填对了,只有个别位置,比如 ##ist 前预测了 confederate 而不是 secession.
不过我们要特别注意的是,预测结果的开头和结尾:
| 位置 | 预测 | 期望 | 原因 |
|---|---|---|---|
| 位置 0 | .(句号) | [CLS] | 模型不知道 [CLS] 是特殊 token,它只是在 30522 个词中选了分数最高的 |
| 位置 61 | s | [SEP] | 同理 |
这不是模型”猜错了”,而是 convert_ids_to_tokens 会把任何 ID 都转成对应的词——包括非 mask 位置。模型对 [CLS] 位置预测了 .,因为 [CLS] 本身不在词表中,模型只能输出词表中存在的 token。
' '.join(tokenizer.convert_ids_to_tokens(inputs['labels'][0]))输出为:
"[CLS] after abraham lincoln won the november 1860 presidential election on an anti - slavery platform , an initial seven slave states declared their secession from the country to form the confederacy . war broke out in april 1861 when secession ##ist forces attacked fort sum ##ter in south carolina , just over a month after lincoln ' s inauguration . [SEP]"文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!