Pytroch实现bert网络文本分类

本实验主要是用来指导用户如何使用pytorch来搭建经典的Bert网络,并在此基础上使用昇腾Npu硬件对Bert网络实现文本分类训练的代码实战过程。

实验介绍目录如下:

  • Bert网络的主要创新点介绍
  • Bert及网络搭建过程介绍
  • input embeddings层
  • Self-Attention
  • MutiHeadAttention
  • Encoder-Transformer模块
  • Bert网络模型及架构分析
  • Bert网络进行文本分类
  • Bert分词器
  • 训练任务构建
  • 基于昇腾Npu训练Bert模型
  • 参考文献

Bert网络的主要创新点介绍


  • 提出预训练加微调的思想,其中预训练的思想来源于图像领域中预训练。
  • 借鉴了Word2Vec中CBOW的思想(完形填空)双向编码。
  • 采用了transform架构作为双向结构的基础模块,使用Attentio机制作为特征提取器将任意位置的两个单词的相关性转换成数字,有效的解决了NLP中长期依赖问题,能更彻底的捕捉语句中的双向关系。
  • 在CBOW的基础之上,添加了语言掩码模型(Mask Language Model),减少了训练和推理阶段的不匹配问题,避免了过拟合现象。
  • 使用下句预测(NSP)作为无监督预训练的一部分,用于捕捉句子之间的语义关系。

Bert及网络搭建过程介绍


Pytroch实现bert网络文本分类_词向量

BERT(Bidirectional Encoder Representations from Transformers)是一种基于多层Transformer-Encoder的预训练语言模型。

它通过预训练+微调并与Tokenization、多种Embeddings和特定任务的输出层相结合,能够捕捉文本的双向上下文信息,泛化能力强,在各种自然语言处理任务中表现出色。

BERT的架构主要包括输入层编码层输出层

其中输入层负责处理原始文本,编码层由多个Transformer-Encoder模块组成,每个Transformer块包含多头自注意力层(MLA)和前馈神经网络层(FFN),输出层则根据具体任务进行微调。

下面将会对Bert中这三个主要模块的主要原理结合代码一起进行介绍。

input embeddings层

Pytroch实现bert网络文本分类_词向量_02

跟大多数NLP深度学习模型一样,BERT将输入文本中的每一个词(token)送入token embedding层从而将每一个词转换成向量形式,但不同于其他模型的是,BERT又多了两个嵌入层,即segment embeddingsposition embeddings

下面是输入Bert中Embedding层代码实现,定义了一个Embedding类继承了torch的Module模块,含'init'与'forward'两个功能函数,其中'init'用于遍历初始化,'forward'用于定义网络前向连接结构顺序。

import torch
import torch.nn as nn
import torch.optim as optim

'''
maxlen 表示句子的最大长度。
d_model表示词被Embedding成768维的向量。
n_segments表示上下文子句数值区间,为2的话表示取值为0或1表示两个子句是上下文关系,常用于问答系统中。
'''
maxlen = 30
d_model = 768
n_segments = 2

'''
tok_embed、pos_embed与seg_embed变量分别与inputEmbeddings中的token embedding、position embeddings与seg_embed相对应。
'''

class Embedding(nn.Module):
    def __init__(self):
        super(Embedding, self).__init__()
        self.tok_embed = nn.Embedding(vocab_size, d_model)
        self.pos_embed = nn.Embedding(maxlen, d_model)
        self.seg_embed = nn.Embedding(n_segments, d_model)
        self.norm = nn.LayerNorm(d_model)
        
    # x对应input_ids, seg对应segment_ids
    def forward(self, input_ids, segment_ids):
        seq_len = input_ids.size(1)
        pos = torch.arange(seq_len, dtype=torch.long)
        pos = pos.unsqueeze(0).expand_as(input_ids)  # (seq_len,) -> (batch_size, seq_len)
        embedding = self.tok_embed(input_ids) + self.pos_embed(pos) + self.seg_embed(segment_ids)
        return self.norm(embedding)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

下面详细介绍了上述Token embedding、Position Embeddings与Segment Embeddings原理及过程。

Token embedding

例如现有输入文本是:"strawberries is my favorite",输入文本在送入token embeddings 层之前要先进行tokenization处理,具体的处理方式是:将两个特殊的token会被插入到tokenization的结果的开头 (\[CLS\])和结尾 (\[SEP\]) 因此,输入文本变为"\[CLS\] strawberries is my favorite \[SEP\]"。

Bert中tokenization使用的方法是WordPiece tokenization. 这是一个数据驱动式的tokenization方法,旨在权衡词典大小和oov词的个数,这种方法可以把"strawberries"切分成"straw" 和"berries",将favorite切分为"favo"与"rite"。

因此,输入文本变为"\[CLS\] straw berries is my favo rite \[SEP\]"。

最后,Token Embeddings 层会将每一个wordpiece token转换成768维的向量。这样,例子中的8个token就被转换成了一个(8, 768) 的矩阵或者是(1, 8, 768)的张量(如果考虑batch_size的话)。

Position Embeddings

对于一个nlp任务而言,词与词之间的顺序关系通常会影响着整个句子含义甚至情感的走向,例"武松打虎"与"虎打武松"意义就完全不一样。

Bert中的Attention模块虽然能够有效的解决长距离依赖问题,但是其在计算的过程中未考虑到序列中词语的语序信息,从而导致会出现"武松打虎"与"虎打武松"这两句话通过Attention以后计算的结果完全一样。

这明显是不符合语义常识的,Transform架构中引入了位置编码这一思想,用来解决句子语序问题带来的语义不一致的现象。

Segment Embeddings

在bert预训练的数据集中会有对话场景,问答场景等数据集,像这类场景的话上下文关联对于情感及问答的走向非常重要。

segment embeddings在Bert中主要用来区分一个句子对中的两个句子,来识别上下文子句,标识方式是给第一句赋0,第二句赋1,表示先后关系。

Self-Attention

自注意力机制的其特点是Query、Key和Value都来自同一个输入序列,可以使模型能够迅速的学习到输入序列中的内在关系和依赖性。

自注意力机制中,对于输入序列中的每个位置,模型都会计算它与所有其他位置之间的关系,并得到一个权重分布。

然后,根据这个权重分布对输入序列进行加权求和,以得到每个位置的输出。这个过程也被称为"内部注意"或"自关注"。

Pytroch实现bert网络文本分类_Self_03

整个模块包含三个输入 Q、K、V,其中Q、K、V 来自输入句子 X 的词向量x的线性转化,即对于词向量x,给定三个可学习的矩阵参数

Pytroch实现bert网络文本分类_Self_04
,x 分别右乘上述矩阵得到 Q、K、V。

这也是Self-Attention 名字的来源:Q、K、V 三个矩阵由同一个词向量线性转化而得。

模型训练的过程中数据都是以Batch的形式输入到模型,一个Batch中每个句子的长度是不一样的,需要PADDING将所有的句子都补全到最长的长度,PADDING数值可以是0(也可以是其他较大的数)。

由于我们不希望该填充的位置参与到后期的反向传播过程,从而提出了在训练时将补全的位置给Mask掉的做法,也就是图中的MASK模块,当PADDING值为较大的负数时,通过softmax操作以后该位置的预测概率值接近于0。

整个Self-Attention模块具体代码实现如下:

import numpy as np
# dimension of K(=Q), V
d_k = d_v = 64 
class ScaledDotProductAttention(nn.Module):
    def __init__(self):
        super(ScaledDotProductAttention, self).__init__()
    '''
    input dim of Q: [batch_size x n_heads x len_q x d_k]
                 K: [batch_size x n_heads x len_k x d_k]
                 V: [batch_size x n_heads x len_k x d_v]
    '''
    def forward(self, Q, K, V, attn_pad):
        #scores dim : [batch_size x n_heads x len_q x len_k]
        scores = torch.matmul(Q, K.transpose(-1, -2)) / np.sqrt(d_k)
        # Fills elements of self tensor with defaule value -1e9.
        scores.masked_fill_(attn_pad, -1e9)
        attn = nn.Softmax(dim=-1)(scores)
        context = torch.matmul(attn, V)
        return context, attn
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

图形化原理展示过程如下:

Pytroch实现bert网络文本分类_Self_05

首先,

Pytroch实现bert网络文本分类_Self_06
为输入词向量,经过embedding后得到
Pytroch实现bert网络文本分类_代码实现_07
列向量,该列向量分别通过与一个矩阵参数
Pytroch实现bert网络文本分类_Self_08
,得到
Pytroch实现bert网络文本分类_词向量_09
Pytroch实现bert网络文本分类_代码实现_10

其次,每一个

Pytroch实现bert网络文本分类_Self_11
都会与
Pytroch实现bert网络文本分类_词向量_12
做attention计算得到一个score,该score经过softmax后再与
Pytroch实现bert网络文本分类_代码实现_10
相乘。

最后,将所有的

Pytroch实现bert网络文本分类_Self_14
Pytroch实现bert网络文本分类_代码实现_15
想加,即可得到第一个位置的输出。

该值运用到了sequence中的其他所有输入,经过attention加权重后,最终的结果包含了所有节点的信息,并赋予了不同的权重。

MutiHeadAttention

Pytroch实现bert网络文本分类_代码实现_16

"MutiHeadAttention"作为整个transform架构的核心组成部分,其核心模块是由多个self-attention组合而成。

区别在于:Self-Attention关注序列内每个位置对其他所有位置的重要性,而Multi-Head Attention则通过在多个子空间中并行计算注意力,使模型能够同时捕获和整合不同方面的上下文信息,从而增强了对复杂数据内在结构的建模能力。

整个MutiHeadAttention的代码实现与self-attention过程类似,定义了一个MultiHeadAttention类,类中有'init'与'forward'组成,其中init负责初始化操作,'forward'负责网络层构建。

对于MultiHeadAttention而言其输入的QKV是相等的,所以在'init'中使用映射linear做一个映射得到参数矩阵Wq, Wk,Wv作为共有变量。

此外,对于forward操作,MultiHeadAttentio中的多头分为三步:先映射、分头,然后计算atten_scores,最后计算attention_value。

映射与分头对应代码'self.W_Q(Q).view'、'self.W_K(K).view'与'self.W_K(K).view'操作。

atten_scores计算与attention_value在代码中可以明显看到,这里就不在一一对应。

#有多少个注意力头,图中的h取值,这里取12
n_heads = 12
class MultiHeadAttention(nn.Module):
    def __init__(self):
        super(MultiHeadAttention, self).__init__()
       # 共有变量W_Q、W_K、W_V
        self.W_Q、 = nn.Linear(d_model, d_k * n_heads)
        self.W_K、 = nn.Linear(d_model, d_k * n_heads)
        self.W_V = nn.Linear(d_model, d_v * n_heads)

    '''
    input dim: 
      Q: [batch_size x len_q x d_model], 
      K: [batch_size x len_k x d_model], 
      V: [batch_size x len_k x d_model]
    '''
    def forward(self, Q, K, V, attn_pad):
       
        residual, batch_size = Q, Q.size(0)
        '''
         (B, S, D) -proj-> (B, S, D) -split-> (B, S, H, W) -trans-> (B, H, S, W)
         q_s: [batch_size x n_heads x len_q x d_k]
         k_s: [batch_size x n_heads x len_k x d_k]
         v_s: [batch_size x n_heads x len_k x d_v]
        '''
        q_s = self.W_Q(Q).view(batch_size, -1, n_heads, d_k).transpose(1,2)  
        k_s = self.W_K(K).view(batch_size, -1, n_heads, d_k).transpose(1,2) 
        v_s = self.W_V(V).view(batch_size, -1, n_heads, d_v).transpose(1,2) 

        '''
        将pad信息重复了n个头上
        attn_pad input dim: batch_size x len_q x len_k
        attn_pad out dim:   batch_size x n_heads x len_q x len_k
        '''
        attn_pad = attn_pad.unsqueeze(1).repeat(1, n_heads, 1, 1)

        '''
        context: batch_size x n_heads x len_q x d_v
        attn: batch_size x n_heads x len_q(=len_k) x len_k(=len_q)
        '''
        context, attn = ScaledDotProductAttention()(q_s, k_s, v_s, attn_pad)

        ''' 
        context: batch_size x len_q x n_heads * d_v
        '''
        context = context.transpose(1, 2).contiguous().view(batch_size, -1, n_heads * d_v) 
        '''
        output: [batch_size x len_q x d_model]
        '''
        output = nn.Linear(n_heads * d_v, d_model)(context)
        return nn.LayerNorm(d_model)(output + residual), attn
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.

参照上述代码,muti-head-attention的详细计算过程如下:

Pytroch实现bert网络文本分类_Self_17

相比于self-attention模块,multi-head self-attention得到

Pytroch实现bert网络文本分类_词向量_09
Pytroch实现bert网络文本分类_代码实现_10
后并没有直接计算Attention,而是利用
Pytroch实现bert网络文本分类_词向量_09
Pytroch实现bert网络文本分类_代码实现_10
得到多组
Pytroch实现bert网络文本分类_Self_22
,
Pytroch实现bert网络文本分类_代码实现_23
Pytroch实现bert网络文本分类_Self_24
,然后将这些分组进行attention计算,得到
Pytroch实现bert网络文本分类_Self_25
Pytroch实现bert网络文本分类_Self_26
...,
Pytroch实现bert网络文本分类_代码实现_27
,对于sequence中某个输入
Pytroch实现bert网络文本分类_代码实现_07
而言,得到多个输出到
Pytroch实现bert网络文本分类_Self_25
Pytroch实现bert网络文本分类_Self_26
...,
Pytroch实现bert网络文本分类_代码实现_27
,最后把这些输出给concat到一起并通过矩阵乘法得到最终的输出
Pytroch实现bert网络文本分类_代码实现_32

Encoder-Transformer模块

BERT的全称为Bidirectional Encoder Representation from Transformers,从名字可以看出,BERT主要网络结构用到了Transformer的Encoder模块,见如下Transformer网络结构图,其中红框Encoder部分即BERT用到的网络结构:

Pytroch实现bert网络文本分类_代码实现_33

每个encoder块有3个主要层,即多头注意(MHA),规范层和mlp。

因此要想实现Encoder-Transformer模块则还需要实现mlp模块,也就是FFN网络。

FFN层是一种前馈神经网络,其结构相对简单,对输入进行非线性变换,生成最终的输出,该网络的实现非常简单,其本质上就是一个两层的全连接网络,中间由激活函数(如ReLU)进行非线性转换,这里用的是gelu函数做线性变换。

代码实现中定义了一个PoswiseFeedForwardNet类来定义FFN网络,可以看到该网络主要是由两层的全连接网络组成,前向函数中使用了gelu非线性函数。

#Feed Forward 网络
d_ff = 3072 # 4*d_model
def gelu(x):
    return x * 0.5 * (1.0 + torch.erf(x / math.sqrt(2.0)))

#对输入词做线性变化,增强模型表达能力。
class PoswiseFeedForwardNet(nn.Module):
    def __init__(self):
        super(PoswiseFeedForwardNet, self).__init__()
        self.fc1 = nn.Linear(d_model, d_ff)
        self.fc2 = nn.Linear(d_ff, d_model)

    def forward(self, x):
        # (batch_size, len_seq, d_model) -> (batch_size, len_seq, d_ff) -> (batch_size, len_seq, d_model)
        return self.fc2(gelu(self.fc1(x)))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

在上述FFN网络实现了的基础上,我们可以构建一个基础的Encoder,根据Transform中encoder模块定义,我们定义了一个EncoderLayer来表示一个基础的Encoder模块,其由MultiHeadAttention与PoswiseFeedForwardNet相互串联而成。

class EncoderLayer(nn.Module):
    def __init__(self):
        super(EncoderLayer, self).__init__()
        self.enc_self_attn = MultiHeadAttention()
        self.pos_ffn = PoswiseFeedForwardNet()

    def forward(self, enc_inputs, enc_self_attn_pad):
        # enc_inputs to same Q,K,V enc_self_attn_mask是pad符号矩阵
        enc_outputs, attn = self.enc_self_attn(enc_inputs, enc_inputs, enc_inputs, enc_self_attn_pad) 
        # enc_outputs: [batch_size x len_q x d_model]
        enc_outputs = self.pos_ffn(enc_outputs)
        return enc_outputs, attn
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
Bert网络模型及架构分析

在上述所有基础模块均实现的基础上,我们现在可以来搭建一个Bert网络模型。

由于上述Attention操作过程中用到了padding操作,因此为了得到句子中的padding信息,方便给到模型输入,去掉pad数值对模型的影响,这里定义了一个函数用来获取句子中pad的位置信息。

def get_attn_pad_mask(seq_q, seq_k):
    batch_size, len_q = seq_q.size()
    batch_size, len_k = seq_k.size()
    # eq(zero) is PAD token
    # eq(0)表示和0相等的返回True,不相等返回False。
     # batch_size x 1 x len_k(=len_q), one is masking
    pad_attn_mask = seq_k.data.eq(0).unsqueeze(1) 
     # 重复了len_q次  batch_size x len_q x len_k 不懂可以看一下例子
    return pad_attn_mask.expand(batch_size, len_q, len_k)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

基于上述input embeddings、Self-Attention、MutiHeadAttention、Encoder-Transformer模块实现与定义,我们可以开始定义一个Bert类来实现Bert网络。

该类也是包含两个函数'init '与'forward'函数,其中'init'函数用于初始化,forward定义了整个BERT网络的前向过程,也就是整个BERT网络结构的定义。

在'init'函数中,'embedding'根据词向量构建词表矩阵;'layers'利用torch.nn的ModuleList方法将n_layers个encoder层堆叠起来;'fc'层定义为线性层;'activ1'与'activ2'分别是两种类型的激活函数Tanh与gelu;'norm'实现正则化操作;'classifier'是线性分类层,维度是从d_model到4,因为本实验介绍的四分类任务,所以这里为4,其他的分类任务可以根据需要自行进行更改;此外,由于BERT是基于Encoder结构,因此相比transform中的Decoder模块,这里定义层一个线性层用于微调任务,对应'init'中'decoder'。

在'forward'函数中,输入层'input'为输入给Embedding后加权求和以后的结果,该结果将输入单词转化为矩阵信息输入给整个Eccoder模块;'get_attn_pad_mask'调用'get_attn_pad_mask'得到句子中pad的位置信息;后续一个是Encoder模块与输出模块,具体信息在代码中有注释。

# Bert结构中Encoder模块堆叠的个数
n_layers = 6

class BERT(nn.Module):
    def __init__(self):
        super(BERT, self).__init__()
        self.embedding = Embedding()
        self.layers = nn.ModuleList([EncoderLayer() for _ in range(n_layers)])
        self.fc = nn.Linear(d_model, d_model)
        self.activ1 = nn.Tanh()
        self.linear = nn.Linear(d_model, d_model)
        self.activ2 = gelu 
        self.norm = nn.LayerNorm(d_model)
        self.classifier = nn.Linear(d_model, 4)

        
        embed_weight = self.embedding.tok_embed.weight
        n_vocab, n_dim = embed_weight.size()
        self.decoder = nn.Linear(n_dim, n_vocab, bias=False)
        self.decoder.weight = embed_weight
        self.decoder_bias = nn.Parameter(torch.zeros(n_vocab))

    def forward(self, input_ids, segment_ids, masked_pos):
        input = self.embedding(input_ids, segment_ids) 
        # Get postion info of padding for sentence
        enc_self_attn_pad = get_attn_pad_mask(input_ids, input_ids)

        '''
         Func: construct Encoder model 
         attn : [batch_size, n_heads, d_mode, d_model]
         output : [batch_size, len, d_model]
        '''
        for layer in self.layers:
            output, enc_self_attn = layer(input, enc_self_attn_pad)
        h_pooled = self.activ1(self.fc(output[:, 0]))
        logits_clsf = self.classifier(h_pooled)
        masked_pos = masked_pos[:, :, None].expand(-1, -1, output.size(-1))
        # get masked position from final output of transformer.
        h_masked = torch.gather(output, 1, masked_pos)
        h_masked = self.norm(self.activ2(self.linear(h_masked)))
        
        '''
         Func: construct Outputmodel
         logits_lm: [batch_size, max_pred, n_vocab]
        '''
        logits_lm = self.decoder(h_masked) + self.decoder_bias
        return logits_lm, logits_clsf
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.

Bert网络进行文本分类


本章节主要介绍如何在一个简单的文本实例上如何训练上述搭建好的bert网络。

text = (
        'Hello, how are you? I am Romeo.\n'
        'Hello, Romeo My name is Juliet. Nice to meet you.\n'
        'Nice meet you too. How are you today?\n'
        'Great. My baseball team won the competition.\n'
        'Oh Congratulations, Juliet\n'
        'Thanks you Romeo'
    )
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

相关库模块导入

由于使用的是昇腾硬件,因此还需要导入昇腾npu相关模块,其中transfer_to_npu可以使得模型快速的迁移至昇腾上运行。

import torch_npu
from torch_npu.contrib import transfer_to_npu

# 指定npu作为训练后端
device = 'npu'
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
分词器

将文本进行分词(Tokenization),并将这些词转换为模型可以理解的数字编码形式,由于本文实验所使用的实验文本较为简单,如果用户想要训练复杂的数据集,可以使用BERT tokenizer提供的BertTokenizer标记器,将文本转化为模型可以直接处理的数据。

这里主要是针对上述text样例进行分词操作。

首先,对输入的文本进行预处理,包括去除多余的空格、标点符号('.', ',', '?', '!')等,以便后续的分词操作,对应代码段're.sub()'。

接下来,将分词好的文本结合'\[PAD\]'、'\[CLS\]'、'\[SEP\]'与'\[MASK\]'拼接在一起并将分词后的文本编码,内容保存在'word_dict'中,注意这里赋值是从i + 4开始,是因为前面'\[PAD\]'、'\[CLS\]'、'\[SEP\]'与'\[MASK\]'占据了四个位置。

最后,读取输入文本,将输入的文本句子转化为编码后的数字,这里的数字可以直接供给模型使用输入,也就是'token_list'。

import re
sentences = re.sub("[.,!?\\-]", '', text.lower()).split('\n')  
word_list = list(set(" ".join(sentences).split()))
word_dict = {'[PAD]': 0, '[CLS]': 1, '[SEP]': 2, '[MASK]': 3}
for i, w in enumerate(word_list):
    word_dict[w] = i + 4
    
number_dict = {i: w for i, w in enumerate(word_dict)}
vocab_size = len(word_dict)
token_list = list()
for sentence in sentences:
    arr = [word_dict[s] for s in sentence.split()]
    token_list.append(arr)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
训练任务数据构建

bert网络在预训练过程中使用了掩码语言建模 (MLM)与下一句预测 (NSP)的思想模型用来学习语言的内部关联,然后可以使用该关联提取对下游任务有用的特征:例如,如果您有一个带标签的句子数据集,则可以使用BERT模型生成的特征作为输入来训练标准分类器。

掩码语言建模 (MLM):取一个句子,模型随机掩码输入中的 15% 的单词,然后通过模型运行整个掩码句子,并预测被掩码的单词。这与通常一个接一个地看到单词的传统循环神经网络 (RNN) 或内部掩码未来标记的 GPT 等自回归模型不同。它允许模型学习句子的双向表示。 下一句预测 (NSP):模型在预训练期间将两个掩码句子连接起来作为输入。有时它们对应于原文中彼此相邻的句子,有时则不是。然后,模型必须预测这两个句子是否彼此相连。

from random import *
# 定义一次性送入模型的句子个数,上述文本共有6句话,所以这里定义为6
batch_size = 6 
# 预测的最大token数
max_pred = 5

def make_batch():
    batch = []
    positive = negative = 0
    while positive != batch_size/2 or negative != batch_size/2:
        tokens_a_index, tokens_b_index= randrange(len(sentences)), randrange(len(sentences))
        tokens_a, tokens_b= token_list[tokens_a_index], token_list[tokens_b_index]
        input_ids = [word_dict['[CLS]']] + tokens_a + [word_dict['[SEP]']] + tokens_b + [word_dict['[SEP]']]
        segment_ids = [0] * (1 + len(tokens_a) + 1) + [1] * (len(tokens_b) + 1)
        
        # MASK LM
        n_pred = min(max_pred, max(1, int(round(len(input_ids) * 0.15))))
        cand_maked_pos = [i for i, token in enumerate(input_ids)
                          if token != word_dict['[CLS]'] and token != word_dict['[SEP]']]
        shuffle(cand_maked_pos)
        masked_tokens, masked_pos = [], []
        for pos in cand_maked_pos[:n_pred]: 、
            masked_pos.append(pos)
            masked_tokens.append(input_ids[pos])
            if random() < 0.8:
                input_ids[pos] = word_dict['[MASK]']
            elif random() < 0.5:
                index = randint(0, vocab_size - 1)
                input_ids[pos] = word_dict[number_dict[index]]

        # Zero Paddings
        n_pad = maxlen - len(input_ids)
        input_ids.extend([0] * n_pad)
        segment_ids.extend([0] * n_pad)

        if max_pred > n_pred:
            n_pad = max_pred - n_pred
            masked_tokens.extend([0] * n_pad)
            masked_pos.extend([0] * n_pad)

        if tokens_a_index + 1 == tokens_b_index and positive < batch_size/2:
            batch.append([input_ids, segment_ids, masked_tokens, masked_pos, True])
            positive += 1
        elif tokens_a_index + 1 != tokens_b_index and negative < batch_size/2:
            batch.append([input_ids, segment_ids, masked_tokens, masked_pos, False])
            negative += 1
    return batch
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
基于昇腾Npu训练Bert模型

'make_batch'作用是构建预训练任务的数据,包括MLM与NSP功能。

'map'使用zip函数将batch中的元素转化成LongTensor的格式。

'model'为我们上述搭建好的BERT模型。

'criterion'使用了交叉熵损失函数(这里ignore_index=0表示只计算mask位置的损失,其他位置的损失不参与计算)

'optimizer'使用Adam优化器,并设置学习率为0.001。

for循环中使用上述的单个文本对Bert模型开启迭代了100个epoch,每个10次打印一次损失,可以观察到损失值逐渐下降。

'zero_grad()'初始化梯度为0。

logits_lm与logits_clsf分别对应Bert模型输出中的词表输出与类输出部分,其中,logits_lm:\[batch_size, max_pred, n_vocab\]。

batch = make_batch()
input_ids, segment_ids, masked_tokens, masked_pos, isNext = map(torch.LongTensor, zip(*batch))

model = BERT()
model.to(device)
criterion = nn.CrossEntropyLoss(ignore_index=0)
optimizer = optim.Adam(model.parameters(), lr=0.001)

for epoch in range(100):
    optimizer.zero_grad()
    
    logits_lm, logits_clsf = model(input_ids, segment_ids, masked_pos)## logits_lm 【6,5,29】 bs*max_pred*voca  logits_clsf:[6*2]
    loss_lm = criterion(logits_lm.transpose(1, 2), masked_tokens) # for masked LM ;masked_tokens [6,5]
    loss_lm = (loss_lm.float()).mean()
    loss_clsf = criterion(logits_clsf, isNext) # for sentence classification
    loss = loss_lm + loss_clsf
    if (epoch + 1) % 10 == 0:
        print('Epoch:', '%04d' % (epoch + 1), 'cost =', '{:.6f}'.format(loss))
    loss.backward()
    optimizer.step()
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

导入torch及npu相关包

Reference


\[1\] Devlin J. Bert: Pre-training of deep bidirectional transformers for language understanding\[J\]. arXiv preprint arXiv:1810.04805, 2018.

\[2\] Vaswani A. Attention is all you need\[J\]. Advances in Neural Information Processing Systems, 2017.

参考的博客与资料:

 https://www.cnblogs.com/nickchen121/p/15114385.html#四word2vec-模型  https://www.luxiangdong.com/2023/09/10/trans/  https://blog.youkuaiyun.com/LiRongLu_/article/details/126384067