五、BERT的应用
目录
来源
Datewhle29期__NLP之transformer :
- erenup(多多笔记),北京大学,负责人
- 张帆,Datawhale,天津大学,篇章4
- 张贤,哈尔滨工业大学,篇章2
- 李泺秋,浙江大学,篇章3
- 蔡杰,北京大学,篇章4
- hlzhang,麦吉尔大学,篇章4
- 台运鹏 篇章2
- 张红旭 篇章2
学习资料地址:
https://datawhalechina.github.io/learn-nlp-with-transformers/#/
github地址:
https://github.com/datawhalechina/learn-nlp-with-transformers
1.1 BERT 的模型
- 基于 BERT 的模型都写在/models/bert/modeling_bert.py里面,包括 BERT 预训练模型和 BERT 分类等模型。
1.1.1 BertPreTrainedModel
- 预训练任务: Masked Language Model(MLM和 Masked Language Model(MLM)
- MLM--------------句子中随机用[MASK]替换一部分单词,然后将句子传入 BERT 中编码每一个单词的信息,最终用[MASK]的编码信息预测该位置的正确单词,-----------旨在训练模型根据上下文理解单词的意思;
- NSP---------------------将句子对 A 和 B 输入 BERT,使用[CLS]的编码信息进行预测 B 是否 A 的下一句----------------------------旨在训练模型理解预测句子间的关系。
- BertForPreTraining,包含两个组件:
class BertForPreTraining(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.bert = BertModel(config) #默认add_pooling_layer=True,即会提取[CLS]对应的输出用于 NSP 任务
self.cls = BertPreTrainingHeads(config) # 负责两个预测模块
self.init_weights()
# ...
- 以上
BertPreTrainingHeads
细节-----LMPredictionHead
以及代表 NSP 任务的线性层
class BertPreTrainingHeads(nn.Module):
def __init__(self, config):
super().__init__()
self.predictions = BertLMPredictionHead(config)
self.seq_relationship = nn.Linear(config.hidden_size, 2)
def forward(self, sequence_output, pooled_output): #代表 NSP 任务的线性层
prediction_scores = self.predictions(sequence_output)
seq_relationship_score = self.seq_relationship(pooled_output)
return prediction_scores, seq_relationship_score
- 第一层 :
LMPredictionHead
-----用于预测[MASK]位置的输出在每个词作为类别的分类输出-----MLM
class BertLMPredictionHead(nn.Module):
def __init__(self, config):
super().__init__()
self.transform = BertPredictionHeadTransform(config) #完成线性变换
# The output weights are the same as the input embeddings, but there is
# an output-only bias for each token.
self.decoder = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
self.bias = nn.Parameter(torch.zeros(config.vocab_size)) #全0向量作为权重
# Need a link between the two variables so that the bias is correctly resized with `resize_token_embeddings`
self.decoder.bias = self.bias
def forward(self, hidden_states):
hidden_states = self.transform(hidden_states)
hidden_states = self.decoder(hidden_states)
return hidden_states
注意:
- 该类重新初始化了一个全 0 向量作为预测权重的 bias;
- 该类的输出形状为[batch_size, seq_length, vocab_size],即预测每个句子每个词是什么类别的概率值(注意这里没有做 softmax);
- 又一个封装的类:BertPredictionHeadTransform,用来完成一些线性变换:
- 两类预测loss处理方式:
- 它的前向传播和BertModel的有所不同,多了labels和next_sentence_label 两个输入:
-
labels:形状为[batch_size, seq_length] ,代表 MLM 任务的标签,注意这里对于原本未被遮盖的词设置为 -100,被遮盖词才会有它们对应的 id,和任务设置是反过来的。
例如,原始句子是I want to [MASK] an apple,这里我把单词eat给遮住了输入模型,对应的label设置为[-100, -100, -100, 【eat对应的id】, -100, -100];
为什么要设置为 -100 而不是其他数?因为torch.nn.CrossEntropyLoss默认的ignore_index=-100,也就是说对于标签为 100 的类别输入不会计算 loss。 -
next_sentence_label:这一个输入很简单,就是 0 和 1 的二分类标签。
-
- 它的前向传播和BertModel的有所不同,多了labels和next_sentence_label 两个输入:
两部分 loss 的组合-------直接相加
# ...
total_loss = None
if labels is not None and next_sentence_label is not None:
loss_fct = CrossEntropyLoss()
masked_lm_loss = loss_fct(prediction_scores.view(-1, self.config.vocab_size), labels.view(-1))
next_sentence_loss = loss_fct(seq_relationship_score.view(-1, 2), next_sentence_label.view(-1))
total_loss = masked_lm_loss + next_sentence_loss
# ...
- BertForMaskedLM:只进行 MLM 任务的预训练;
基于BertOnlyMLMHead,而后者也是对BertLMPredictionHead的另一层封装; - BertLMHeadModel:这个和上一个的区别在于,这一模型是作为 decoder 运行的版本;
同样基于BertOnlyMLMHead; - BertForNextSentencePrediction:只进行 NSP 任务的预训练。
基于BertOnlyNSPHead,内容就是一个线性层。
1.1.2 BertForSequenceClassification
-
这一模型用于句子分类(也可以是回归)任务,比如 GLUE benchmark 的各个任务。
-
句子分类的输入为句子(对),输出为单个分类标签。
结构上很简单,就是BertModel(有 pooling)过一个 dropout 后接一个线性层输出分类:
class BertForSequenceClassification(BertPreTrainedModel):
def __init__(self, config):
super().__init__(config)
self.num_labels = config.num_labels
self.bert = BertModel(config)
self.dropout = nn.Dropout(config.hidden_dropout_prob)
self.classifier = nn.Linear(config.hidden_size, config.num_labels)
self.init_weights()
# ...
在前向传播时,和上面预训练模型一样需要传入labels输入。
-
如果初始化的num_labels=1,那么就默认为回归任务,使用 MSELoss;
-
否则认为是分类任务。
1.1.3 BertForMultiplechoice
这一模型用于多项选择,如 RocStories/SWAG 任务。
- 线性层输出维度为 1,即每次需要将每个样本的多个句子的输出拼接起来作为每个样本的预测分数。
- 实际上,具体操作时是把每个 batch 的多个句子一同放入的,所以一次处理的输入为[batch_size, num_choices]数量的句子,因此相同 batch 大小时,比句子分类等任务需要更多的显存,在训练时需要小心。
1.1.4 BertForTokenClassification
这一模型用于序列标注(词分类),如 NER 任务。
- 序列标注任务的输入为单个句子文本,输出为每个 token 对应的类别标签。 由于需要用到每个 token对应的输出而不只是某几个,所以这里的BertModel不用加入 pooling 层;
- 同时,这里将_keys_to_ignore_on_load_unexpected这一个类参数设置为[r"pooler"],也就是在加载模型时对于出现不需要的权重不发生报错。
1.1.5 BertForQuestionAnswering
这一模型用于解决问答任务,例如 SQuAD 任务。
- 问答任务的输入为问题 +(对于 BERT 只能是一个)回答组成的句子对,输出为起始位置和结束位置用于标出回答中的具体文本。 这里需要两个输出,即对起始位置的预测和对结束位置的预测,两个输出的长度都和句子长度一样,从其中挑出最大的预测值对应的下标作为预测的位置。
- 对超出句子长度的非法 label,会将其压缩(torch.clamp_)到合理范围。
以上就是关于 BERT 源码的介绍,下面介绍一些关于 BERT 模型实用的训练细节。
1.2 训练和优化
1.2.1 预训练
- 预训练阶段,除了众所周知的 15%、80% mask 比例,有一个值得注意的地方就是参数共享。 不止 BERT,所有 huggingface 实现的 PLM 的 word embedding 和 masked language model 的预测权重在初始化过程中都是共享的------因为 word_embedding 和 prediction 权重太大了,以 bert-base 为例,其尺寸为(30522, 768),降低训练难度。
1.2.2 微调----AdamW和Warmup
- AdamW 是在 Adam+L2 正则化的基础上进行改进的算法,与一般的 Adam+L2 的区别如下:
BERT 的训练中另一个特点在于 Warmup,其含义为:
在训练初期使用较小的学习率(从 0 开始),在一定步数(比如 1000 步)内逐渐提高到正常大小(比如上面的 2e-5),避免模型过早进入局部最优而过拟合;
- 在训练后期再慢慢将学习率降低到 0,避免后期训练还出现较大的参数变化。
- 在 Huggingface 的实现中,可以使用多种 warmup 策略: