重现一下第一名的方案
这里先介绍第一部分的工作——pretrain一个bert
1.像是为数据集微调一个LM,在问答网站用StackExchange data训练是个不错的方案。
这里的数据都用xml形式存储,可利用的信息在“user.xml”和“Posts.xml”这里抓取数据的步骤就取消了,可以通过xml_to_pandas直接转换成csv的形式。得到的csv如下,选取我们需要的列。
question里面,body和title当然是很重要的点。但一般来说会将更多的信息加入进去,比如用来表征问题的更多特征,比如说浏览次数,问题的分数,提问次数等,但是还会加入一些我认为是噪音的特征比如说id...
answer里面,answer的score,answer的评论当然是比较感觉比较相关的特征了。
制造噪音的特征,考虑为生成一个比较有鲁棒性的模型。
上述过程是预处理的第一阶段,预处理的第二阶段主要是数据的清理工作:
1.直接dropna掉空值行
2.增加新的行,is_answer_accepted
最终选出的数据如下:
answer_max_score和answer_mean_score可作为quetion的特征,也可以作为answer的指标表征。
数据预处理包括:progress_apply。
html_pattern = re.compile(r'<.*?>')
question_body = all_questions['Body'].astype(str).progress_apply(lambda s: html_pattern.sub('', s))
question_title = all_questions['Title'].astype(str).progress_apply(lambda s: html_pattern.sub('', s))
用compile 设置pattern,用pattern.sub进行正则化替换
最终除了question_username、title、body、answer_username、answer其他都是float,再进行标准化处理
接下来导入tokenizer,导入词库,得到编号表,得到train和val的类。
构建我们的prerain model(用的词库是较大的词库),导入我们的config,对transformer参数的设置,从预训练模型中导出。
这里设置模型的embedding层用的是一个更小模型的embedding,然后扩充成一个大模型的embedding。因为这里用的词库是一个非常大的词库11w的词,bert-base只有28000,bert-large只有3w,所以我们需要扩充embedding层中的vacab。
最终形成我们的trainer:
其中model是在base的基础上对embeddding层进行扩充维度。
loss_func:
其中loss 除了预训练中用到的mlm loss和下一句预测loss,还包括了target预测的loss,由三个部分组成。
这里训练还是挺难的,理解了半天才明白:
这是我们设置的trainer,在Model类里面放着我们的模型,优化器,损失,评价指标。
trainer_to(device)
第三步就是最重要的!
我们需要读取两个Dataloader,对Dataloader而言,我们需要在它下面放dataset。
对fit_generator而言,每次读取dataloader的一个batch的数据
这里的__getitem__(self,index)是来自:class QuestMLMDataset(QuestDataset):
而Dataset是QuestDataset的父类,因此QuestMLMDataset类有方法__getitem__,可以取出某个index(也就是批次)下的数据。
1.再取出文本数据后,首先我们需要进行分词,编号化。
2.经过encode,得到的是这个batch下的3个encode,我们计算得到它们的长度,并且计算出了均衡化的answer长度和question长度(title+body),我们希望它们的长度尽可能均衡。
接下来我们需要加入bert可识别的令牌,我们将question的title和body用SEP分开
input_ids 长度为201(question_ids的长度+answer_ids的长度+3) 因为 build_inputs_with_special_tokens 如果输入为两个ids的话,自动加入令牌CLS+A+SEP+B+SEP,因此长度多了3,因此我们一个句子的token令牌分布是CLS+title+SEP+body+SEP+answer+SEP,这里主要强调的是answer和question的ids需要分成两个句子,作用是可以用来做句子对分类任务。
等于是训练的时候输入的是两个句子,用SEP分开。
padding成bert的词嵌入维度
制造masked ids 选出15%的词为masked,masked词中80%的词被替换成masked的ids,10%的masked词被替换成别的词,10%的masked的词保留成原词。
我们只计算masked的词的损失
让我们关注模型的输入输出:
我们在Dataloader中准备了input_ids、token_type_ids、attention_mask
outputs:
对于outputs,首先我们需要得到embeddings,经过embedding层
embedding层如下:
包括word_embedding,position_embedding,token_type_embedding
其中word_embedding:num_embeddings为vocab-size(110000),embedding_dim为hidden-size(768)
position_embedding:max_position_size,hidden_size
type_vocab_size:2
最终的embedding将三者进行相加
得到embedding后再经过encoder层:
输入为embedding层的输出,和attention_mask
encoder 执行的核心在于以上两行代码,关键模型为layer_module,每一个layer_module都是属于Bertlayer类的。hidden_states是上一层的输出,对应第一层的就是embedding的输出。attention_mask指的是extended_attention_mask。想要的词上是0,被masked的词是-10000。
encoder的理论基础如下:
首先我们需要经过连接层得到QKV,代码实现如下:
我们的一个batch的size为(1,512,768),生成的QKV的size为(1,512,768),计算为(512,768)*(768,768)。再将(1,512,768)进行拆分成(1,512,12,64),再交换位置得到:(1,12,512,64) 对应batch,层数,(512,64)为输出size。
因为有12个encoder,每个encoder有64维度的hidden_size,这里是将12层concat计算了。
得到的是三个向量空间,可以认为是3个特征表征。
接下来就是做运算了,并且使用我们的attention_mask了:
先做矩阵乘法, 得到一个(512,512)的attention,再加上我们的attention_mask,再经过softmax,根据softmax函数,对于mask掉的词它与自己、其他的词的“关联性”为0。我们对masked掉的词不赋予注意力,最后经过dropout。再将关系矩阵*value,此时的size回到(512,768)
ok,那这样我们就得到了我们的attention_outputs。这里attention_outputs==attention_output
在经过BertIntermediate,非常简单,经过一个dense,经过dropout,再x+attention,再LN,得到输出。
再将得到的x+attention经过dense,dropout,再与attention相加做LN,感觉这里重复做是为了增加更多的参数,提高模型的拟合能力。自此我们的encoder就结束了。
最终来到我们的BertPooler:
觉得非常简单的做法,把每个句子的第一个token的hidden_states拿出来,作为pooled_output。
所以最终self.bert的输出就是 :outputs = (sequence_output, pooled_output,)
也就是12层之后输出的hidden_states和pooled_output
接下来做剩余的操作:
执行self.cls()
先做predictions:把最高层的输出作为输入,经过两个步骤:1.transform和decoder,transform包括dense、gelu、LN,decoder是将transfor的输出变成(512,vocab-size)的形式,最终的输出为prediction_score。
seq_relationship是将每个句子的第一个token的state_states辨认是否属于next_sentence。
到这里self.cls就做好了,输出为:outputs = (prediction_scores, seq_relationship_score,)
这里还有个改进,我们在预训练模型的时候不仅考虑了MLM的训练loss,和isnextsentence的loss,还考虑了最终输出和target的loss,我们只需要将最高层的states进行求平均,再输出成一个5维的向量即可。
最后的最后我们再回到Dataset的__getitem__:
非常关键 !
我们return的是两个turple,第一个是训练的输入,第二个是作为targte。
首先,第一个target是labels,最终计算的损失函数是交叉熵损失函数。
其次token_type_ids里面answer和question被设置成0和1。sop_lable被初始化为0,但是其中如果title、body、answer的对应关系不对的话,sop_label被设置为1。因为我们在输入的时候有一半的数据对应关系是不对的。我们同样以交叉熵损失来计算损失。
其中numeric_targets就是5维的指标向量。用的是BCEwithLogitLoss,和bce类似,不过在输出的后面接了一个sigmoid,相当于省略了加上sigmoid的步骤。
自此一个完整的pretraining bert就搭建好了!
最终的训练时间训练一个完整的LM,大概需要60万条数据,大概训练20个epoch,3-4个batchsize。