# 前言

在做 MLM 实验的时候,我经常对 BERT 中 AttentionMask 的值表示疑惑。在之前的实验操作中,如果一个 Token 被设置为 [MASK],其 AttentionMask 就被设置为 0 以将其注意力忽略,但是细细思考又觉得这样的做法其实有问题 —— 如果注意力忽略,那么 [MASK] 和 [PAD] 又有什么区别?

# 区别测试

检测很简单,一般来说预训练模型都做过 NSP 和 MLM 任务,我们只需要构造一个通用的语句然后让其还原即可:

测试 1:

测试语句:

北京欢迎你!

MLM 任务:

北京[MASK][MASK]你

测试代码:

# load model
from transformers import BertLMHeadModel, BertTokenizer
model_path = "hfl/chinese-bert-wwm-ext"
model = BertLMHeadModel.from_pretrained(
    model_path
)
tokenizer = BertTokenizer.from_pretrained(model_path)
# predict
test_text = "北京[MASK][MASK]你!"
# preprocess
inputs = tokenizer(test_text, return_tensors="pt")
# inputs["attention_mask"][0,3:5] = 0
print(inputs)
output = model(**inputs)
output_ids = output.logits.argmax(dim=-1)
print(output_ids.tolist())
print(tokenizer.convert_ids_to_tokens(output_ids.tolist()[0]))

由于模型 chinese-bert-wwm-ext 使用了全词掩码的形式,则其预测是 欢迎 这个词的概率应该是更大的。以上代码为标准代码,执行之后得到的结果为:

{'input_ids': tensor([[ 101, 1266,  776,  103,  103,  872,  106,  102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 1, 1, 1, 1, 1]])} 
[[4638, 1266, 776, 3614, 6816, 872, 106, 776]] 
['的', '北', '京', '欢', '迎', '你', '!', '京']

对比代码则是取消 16 行的注释 inputs["attention_mask"][0,3:5] = 0 ,将欢迎这两个位置的 mask 设置为忽略。

得到的结果为:

{'input_ids': tensor([[ 101, 1266,  776,  103,  103,  872,  106,  102]]), 'token_type_ids': tensor([[0, 0, 0, 0, 0, 0, 0, 0]]), 'attention_mask': tensor([[1, 1, 1, 0, 0, 1, 1, 1]])}
[[4638, 1266, 776, 3300, 3300, 872, 106, 776]]
['的', '北', '京', '有', '有', '你', '!', '京']

可以看到这里并没有正确预测出 欢迎 这个词

# 结论

MLM 任务中(至少 chinese-wwm-ext 类的模型),其 [mask] 对应的 attentionmask 应该设置为 1 而不应该设置为 0(忽略注意力)。

# 勘误(2023-9-14 更新)

在实际上代码中发现在 huggingFace 里的 BertLMHeadModel 中 Label 是偏移一位的(即为 GPT 那样的预测下一个 Token 的方式):

lm_loss = None
if labels is not None:
    # we are doing next-token prediction; shift prediction scores and input ids by one
    shifted_prediction_scores = prediction_scores[:, :-1, :].contiguous()
    labels = labels[:, 1:].contiguous()
    loss_fct = CrossEntropyLoss()
    lm_loss = loss_fct(shifted_prediction_scores.view(-1, self.config.vocab_size), labels.view(-1))

而实际上做 MLM 的模型应该是 BertForMaskedLM ,其计算 loss 代码才是标准的交叉殇计算:

masked_lm_loss = None
if labels is not None:
    loss_fct = CrossEntropyLoss()  # -100 index = padding token
    masked_lm_loss = loss_fct(prediction_scores.view(-1, self.config.vocab_size), labels.view(-1))

不过由于实验只是做了预测,并没有用到 loss,所以修正后并不影响实验结果。

更新于 阅读次数