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]
    '''
    d