当前位置:   article > 正文

BERT代码实现_bert实现

bert实现

前段时间实现了transformer,用李沐老师的话来讲其实bert可以简单理解为缩水版的transformer,transformer有encoder和decoder,bert去掉了decoder,改为用N个encoder堆叠在一起,拿最后一个encoder的输出直接做预训练任务。老规矩,先把大体框架列出来,然后根据框架一个一个去实现。

目录

架构

 数据预处理

NSP

MLM:

BERT

Embedding

Encoder

NSP任务

MLM任务


 

架构

Bert的架构很简单,包括词向量输入,encoder层,NSP(下一句预测任务)和MLM(掩码词预测任务),如下图

 其中,bert的embedding由三个部分组成,分别是词向量,位置向量,句向量(主要用于NSP任务),所以Embedding的架构如下

bert的encoder和transformer的 encoder一样,由多头注意力和前馈神经网络组成

 数据预处理

首先对数据建立两个词表,分别是词对应序号的word_2_id和序号对应词的id_2_word,BERT使用[CLS]作为一个句子的开头,用[SEP]作为句子的结尾,[MASK]主要为了MLM任务中进行掩码,[PAD]用作填充,所以词表初始化如下

  1. word_2_id = {'PAD': 0, 'CLS': 1, 'SEP': 2, 'MASK':3}
  2. id_2_word = {0: 'PAD', 1: 'CLS', 2: 'SEP', 3:'MASK'}

打乱数据的词语,再将数据里的词语添加到词表中

  1. shuffle(sentences)
  2. for i in range(len(sentences)):
  3. if sentences[i] not in word_2_id:
  4. word_2_id[sentences[i]] = len(word_2_id)
  5. id_2_word[len(id_2_word)] = sentences[i]
  6. return word_2_id, id_2_word

 接下来为NSP和MLM任务要用到的数据作准备

NSP

在数据中随机取出两个句子(注意这两个句子不能是同一个),然后使用[CLS]和[SEP]对两个句子拼接成一个句子对

  1. num_a = randint(0, len(sentences)-1)
  2. while True:
  3. num_b = randint(0, len(sentences)-1)
  4. if num_b != num_a:
  5. break
  6. a_and_b = 'CLS ' + ' '.join(sentences[num_a]) + ' SEP ' + ' '.join(sentences[num_b]) + ' SEP'

拼接后的句子应该是:CLS 句子1 SEP 句子2 SEP

num_a代表句子1在数据中的位置,num_b代表句子2在数据中的位置,如果num_a + 1 ==num_b,即说明句子1是句子2的上一句,此时将这个句子对设为True标志

  1. if num_a == num_b + 1 and positive != batch_size/2:
  2. IsNext.append(True)
  3. elif num_a != num_b and negative != batch_size/2:
  4. IsNext.append(False)
  5. else:
  6. continue

拼接好后,句子对中的句子1用0表示,句子2用1表示,这便是句向量

seg_ids = [0] * (len(sentences[num_a]) + 2) + [1] * (len(sentences[num_b]) + 1)

MLM:

MLM任务中,对于一个句子对中的单词,有80%的概率将其替换为[MASK]标志,10%的概率使用该句子对的其他单词替换,10%的概率保持不变。这里注意mask_num表示一个句子最多能有多少个词被替换,并且在选择句子中的词和选择要替换的词时,不能是特殊符号[CLS],[SEP],和[MASK]

  1. for i in range(mask_num):
  2. while True:
  3. num = randint(0,len(token_ids)-1)
  4. if token_ids[num] != 0 and token_ids[num] != 1 and token_ids[num] != 2 and token_ids[num] != 3:
  5. break
  6. mask_token.append(token_ids[num])
  7. if randint(0,1)<0.8:
  8. token_ids[num] = 3
  9. elif randint(0,1)<0.5:
  10. while True:
  11. num1 = randint(0, len(token_ids) - 1)
  12. if token_ids[num1] != 0 and token_ids[num1] != 1 and token_ids[num1] != 2 and token_ids[num] != 3:
  13. break
  14. token_ids[num] = token_ids[num1]
  15. mask_position.append(num)

以上是对数据进行预处理,接下来实现BERT的框架。

BERT

根据框架,可以初步列出所需模块

  1. class BERT(nn.Module):
  2. def __init__(self):
  3. super(BERT, self).__init__()
  4. self.embed = Embedding()
  5. self.encoder = nn.ModuleList(Encoder() for _ in range(n_layers))
  6. self.cls = CLS()
  7. self.mlm = MLM()

Embedding

BERT的输入词向量包括三个部分,词向量,位置向量,句向量。在数据预处理的时候已经实现了词向量和句向量,只需实现位置向量,位置向量就是句子从头到尾遍历一遍,生成一个[0,1,2...max_seq-1]的张量。

  1. position = np.arange(0, len(input_ids[0]))
  2. position = torch.LongTensor(position)

然后将词向量,位置向量,句向量加起来做归一化便得到BERT的输入词向量

input_embed = self.norm(token_embed + position_embed + seg_embed)

Encoder

Encoder层包括多头注意力层和前向反馈层,在上一篇博客“Transformer的实现”中讲的很详细了,这里不作过多阐述。

NSP任务

  1. class CLS(nn.Module):
  2. def __init__(self):
  3. super(CLS, self).__init__()
  4. self.linear_a = nn.Linear(Embedding_size, Embedding_size)
  5. self.tanh = nn.Tanh()
  6. self.linear_b = nn.Linear(Embedding_size, 2)
  7. def forward(self,en_output):
  8. cls_output = self.linear_a(en_output[:, 0])
  9. cls_output = self.tanh(cls_output)
  10. cls_output =self.linear_b(cls_output)
  11. return cls_output

经过一个线性层,然后经过tanh激活函数,最后经过一个将原本Embedding_size维度映射为2维的线性层,为什么是2维,因为NSP其实就是一个二分类任务,对下一句是否是下一句,模型只需判断是正确还是错误即可。

MLM任务

  1. class MLM(nn.Module):
  2. def __init__(self):
  3. super(MLM, self).__init__()
  4. self.linear_a = nn.Linear(Embedding_size,Embedding_size)
  5. self.norm = nn.LayerNorm(Embedding_size)
  6. self.linear_b = nn.Linear(Embedding_size, len_vocab, bias=False)
  7. self.softmax = nn.Softmax(dim=2)
  8. def forward(self,en_output,masked_position):
  9. masked_position = masked_position.unsqueeze(1).expand(batch_size, Embedding_size, max_mask).transpose(1,2) #[6,5,768]
  10. mask = torch.gather(en_output, 1, masked_position) #[6,5,768]
  11. mlm_output = self.linear_a(mask)
  12. mlm_output = gelu(mlm_output)
  13. mlm_output = self.norm(mlm_output)
  14. mlm_output = self.linear_b(mlm_output)
  15. mlm_output = self.softmax(mlm_output)
  16. return mlm_output

这里需要注意的是BERT使用了gelu作为激活函数,gelu其实是dropout、zoneout、Relus的综合,论文里的计算公式如下

 因为这里做的是预测任务,是多分类任务,所以最后一个线性层将Embedding_size维度映射为词表长度的维度(因为预测词要在词表中选出),然后加上softmax进行分类。但其实去掉softmax模型收敛更快,可能是已经有了gelu作为激活函数了。

以上,BERT框架所需模块已经全部实现了,接下来只需调用即可

  1. def forward(self, input_ids, segment_ids, masked_position):
  2. embed = self.embed(input_ids,segment_ids) #[6,30,768]
  3. attn_mask = get_attn_mask(input_ids) #[6,30,30]
  4. for encoder in self.encoder:
  5. en_outpput = encoder(embed, attn_mask) #[6,30,768]
  6. cls_output = self.cls(en_outpput) #[6,2]
  7. mlm_output = self.mlm(en_outpput, masked_position) #[6,5,29]
  8. return cls_output, mlm_output

BERT的代码和Transformer的很像,基本就是直接将Transformer的encoder和注意力的代码全搬过来。BERT主要是两个预训练任务NSP和MLM,如何处理数据其实就是实现BERT最大的难点了。

完整代码:GitHub - bowspider-man/bert-

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/笔触狂放9/article/detail/616636
推荐阅读
  

闽ICP备14008679号