Task10 BERT

目录

 

1. Transformer的原理和实现

1.1 概述

1.2 Encoder-Decoder框架

1.3 整体架构

1.4 Encoder

EncoderLayer和残差网络

多头注意力机制

         层归一化  

1.5 词向量

前馈网络

位置编码

1.6 Deocder

1.7 线性层和Softmax

2. BERT的原理

2.1 BERT模型总体结构

2.2 BERT模型输入

2.3 BERT模型预训练任务

2.3.1 Masked LM

2.3.2 Next Sentence Prediction

2.4 模型比较

3. 利用预训练的BERT模型将句子转换为句向量,进行文本分类

参考文献


1. Transformer的原理和实现

1.1 概述

       所谓 ”工预善其事,必先利其器“, Bert之所以取得这么惊才绝艳的效果,很大一部分原因源自于Transformer。为了后面更好、更快地理解BERT模型,这一节从Transformer的开山鼻祖说起,先来跟着”Attention is All You Need[1]“ 这篇文章,走近transformer的世界,在这里你再也看不到熟悉的CNN、RNN的影子,取而代之的是,你将看到Attention机制是如何被发挥的淋漓尽致、妙至毫颠,以及它何以从一个为CNN、RNN跑龙套的配角实现华丽逆袭。对于Bert来说,transformer真可谓天纵神兵,出匣自鸣!

看完本文,你大概能够:

  • 掌握Encoder-Decoder框架
  • 掌握残差网络
  • 掌握BatchNormalization(批归一化)和LayerNormalization(层归一化)
  • 掌握Position Embedding(位置编码)

当然,最重要的,你能了解Transformer的原理和代码实现。

1.2 Encoder-Decoder框架

     Encoder-Decoder是为seq2seq(序列到序列)量身打造的一个深度学习框架,在机器翻译、机器问答等领域有着广泛的应用。这是一个抽象的框架,由两个组件:Encoder(编码器)和Decoder(解码器)组成。对于给定的输入source(x1, x2, x3, …,xn), 首先编码器将其编码成一个中间表示向量z=(z1, z2, …, zn)。接着,解码器根据z和解码器自身前面的输出,来生成下一个单词(如Figure 1所示)。

class EncoderDecoder(nn.Module):
    """
    A standard Encoder-Decoder architecture. Base for this and many 
    other models.
    """
    def __init__(self, encoder, decoder, src_embed, tgt_embed, generator):
        super(EncoderDecoder, self).__init__()
        self.encoder = encoder
        self.decoder = decoder
        self.src_embed = src_embed
        self.tgt_embed = tgt_embed
        self.generator = generator
        
    def forward(self, src, tgt, src_mask, tgt_mask):
        "Take in and process masked src and target sequences."
        return self.decode(self.encode(src, src_mask), src_mask,
                            tgt, tgt_mask)
    
    def encode(self, src, src_mask):
        return self.encoder(self.src_embed(src), src_mask)
    
    def decode(self, memory, src_mask, tgt, tgt_mask):
        return self.decoder(self.tgt_embed(tgt), memory, src_mask, tgt_mask)

 

        上述代码呈现了一个标准的Encoder-Decoder框架。在实际应用中,编码器和解码器可以有多种组合,比如{RNN, RNN}、{CNN,RNN}等等,这就是传统的seq2seq框架。后来引入了attention机制,上述框架也被称为”分心模型“。为什么说他”分心“呢?因为对于解码器来说,他在生成每一个单词的时候,中间向量的每一个元素对当前生成词的贡献都是一样的。Attention的思想则是对于当前生成的单词,中间向量z的每个元素对其贡献的重要程度不同,跟其强相关的赋予更大的权重,无关的则给一个很小的权重。

    例如:假如我们要将 “knowledge is power” 翻译成 中文,在翻译”knowledge“这个单词时, 显然”knowledge“这个单词对翻译出来的”知识“贡献最大,其他两个单词贡献就很小了。这实际上让模型有个区分度,不会被无关的东西干扰到,翻译出来的准确度当然也就更高了。在这里Attention其实还是一个小弟,主角仍然是RNN、CNN这些大佬。我们不妨先顺着这个思路往下想,attention在这里充当了Encoder和Decoder的一个桥梁,事实证明有很好的效果。既然效果这么好,那在Encoder中是不是也可以用呢?文本自身对自身的编码进行有区分度的表示,事实上,这在以往的很多文本分类的工作中已被采用[2]。这看上去已经是个值得尝试的good idea了。继续开脑洞,Encoder都用了,Decoder能落后吗,好歹人家是一对CP,当然要妇唱夫随了。于是,Encoder和Decoder都用了自注意力(self-attention)。回想一下,到这里我们已经在三个地方用到了注意力机制了。这时候RNN大佬不愿意了,原本我的名声地盘都被你们分走了,散伙!Attention反正是初生牛犊不怕虎,说好,分分账分道扬镳吧,反正你的序列计算并行不起来一直让人诟病,没你我可能更潇洒。于是两兄弟就分开了。相见时难别亦难,RNN老大哥深谋远虑,临走时不忘嘱咐一句”苟富贵,勿相忘!“。于是一个故事的结束就成了另一个故事的开始,注意力就此开启创业之路,寒来暑往,春去秋来,在黑暗中不断寻找光亮,学习PPT技巧…终于有一天,它的PPT做完了,找到了融资,破茧成蝶,横空出道,并给自己取了个亮闪闪的名字:Transformer, 自此,一个新的时代开始了…

1.3 整体架构

     这部分我们来看看Transformer的架构。如Figure 2 所示, Transformer遵循了Encoder-Decoder的架构。在Encoder方面,6个编码器组件协同工作,组成一个大的编码器,解码器同样由6个解码器组件组成。我们先看Encoder。6个编码器组件依次排列,每个组件内部都是由一个多头attention加上一个前馈网络,attenion和前馈的输出都经过层归一化(LayerNormalization),并且都有各自的残差网络 。Decoder呢,组件的配置基本相同, 不同的是Decoder有两个多头attention机制,一个是其自身的mask自注意力机制,另一个则是从Encoder到Decoder的注意力机制,而且是Decoder内部先做一次attention后再接收Encoder的输出。说完了Encoder和Decoder,再说说输入,模型的输入部分由词向量(embedding)经位置编码(positional Encoding)后输入到Encoder和Decoder。编码器的输出由一个线性层和softmax组成,将浮点数映射成具体的符号输出。

 

下面是代码实现。

1.4 Encoder

class Encoder(nn.Module):
    "Core encoder is a stack of N layers"
    def __init__(self, layer, N):
        super(Encoder, self).__init__()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)
        
    def forward(self, x, mask):
        "Pass the input (and mask) through each layer in turn."
        for layer in self.layers:
            x = layer(x, mask)
        return self.norm(x)

   

     以上便是Encoder的核心实现。它由N个encoderLayer组成。输入一次通过每个encoderLayer,然后经过一个归一化层。下面来看下encoderLayer和LayerNorm是什么样子。

EncoderLayer和残差网络

EncoderLayer如Figure 3所示。

class EncoderLayer(nn.Module):
    "Encoder is made up of self-attn and feed forward (defined below)"
    def __init__(self, size, self_attn, feed_forward, dropout):
        super(EncoderLayer, self).__init__()
        self.self_attn = self_attn
        self.feed_forward = feed_forward
        self.sublayer = clones(SublayerConnection(size, dropout), 2)
        self.size = size

    def forward(self, x, mask):
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, mask))
        return self.sublayer[1](x, self.feed_forward)
class SublayerConnection(nn.Module):
    """
    A residual connection followed by a layer norm.
    Note for code simplicity the norm is first as opposed to last.
    """
    def __init__(self, size, dropout):
        super(SublayerConnection, self).__init__()
        self.norm = LayerNorm(size)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x, sublayer):
        "Apply residual connection to any sublayer with the same size."
        return x + self.dropout(sublayer(self.norm(x)))

 

         我们先看什么是残差网络(即代码中的SublayerConnection),见Figure 4 。其实非常简单,就是在正常的前向传播基础上开一个绿色通道,这个通道里x可以无损通过。这样做的好处不言而喻,避免了梯度消失(求导时多了一个常数项)。最终的输出结果就等于绿色通道里的x加上sublayer层的前向传播结果。注意,这里输入进来的时候做了个norm归一化,关于norm我们后面再说。

让我们从输入x开始,再从头理一遍这个过程:

  • 输入x
  • x做一个层归一化: x1 = norm(x)
  • 进入多头self-attention: x2 = self_attn(x1)
  • 残差加成:x3 = x + x2
  • 再做个层归一化:x4 = norm(x3)
  • 经过前馈网络: x5 = feed_forward(x4)
  • 残差加成: x6 = x3 + x5
  • 输出x6

以上就是一个Encoder组件所做的全部工作了。里面有两点暂未说明,一个是多头attention, 另一个是层归一化。

多头注意力机制

      多头注意力机制这里使用的是点乘attention,而不是加性(additive)attention。但是再提一点,在encoder和decoder的自注意力中,attention层的输入分为self_attn(x, x, x, mask)和self_attn(t, t, t, mask), 这里的x和t分别为source和target输入。后面会看到,从encoder到decoder层的注意力输入时attn(t, m, m), 这里的m是Encoder的输出。

def attention(query, key, value, mask=None, dropout=None):
    """因子化的点乘Attention-矩阵形式
    Query: 查询 (batch_size, heads, max_seq_len, d_k)
    Key: 键 (batch_size, heads, max_seq_len_d_k)
    Value: 值 (batch_size, heads, max_seq_len, d_v)
    d_v = d_k
    Q=K=V
    """
    d_k = query.size(-1)
    # (batch_size, heads, max_seq_len, d_k) * (batch_size, heads, d_k, max_seq_len)
    #  = (batch_size, heads, max_seq_len, max_seq_len)
    # 为了方便说明,只看矩阵的后两维 (max_seq_len, max_seq_len), 即
    #       How  are  you
    # How [[0.8, 0.2, 0.3]
    # are  [0.2, 0.9, 0.6]
    # you  [0.3, 0.6, 0.8]]
    # 矩阵中每个元素的含义是,他对其他单词的贡献(分数)
    # 例如,如果我们想得到所有单词对单词“How”的打分,取矩阵第一列[0.8, 0.2, 0.3], 然后做softmax
    scores = torch.matmul(query, key.transpose(-2, -1)) \
             / math.sqrt(d_k)
    # 对于padding部分,赋予一个极大的负数,softmax后该项的分数就接近0了,表示贡献很小
    if mask is not None:
        scores = scores.masked_fill(mask == 0, -1e9)
    p_attn = F.softmax(scores, dim = -1)
    if dropout is not None:
        p_attn = dropout(p_attn)
    # 接着与Value做矩阵乘法:
    # (batch_size, heads, max_seq_len, max_seq_len) * (batch_size, heads, max_seq_len, d_k)
    # = (batch_size, heads, max_seq_len, d_k)
    return torch.matmul(p_attn, value), p_attn


class MultiHeadedAttention(nn.Module):
    def __init__(self, h, d_model, dropout=0.1):
        "Take in model size and number of heads."
        super(MultiHeadedAttention, self).__init__()
        assert d_model % h == 0, "heads is not a multiple of the number of the in_features"
        # We assume d_v always equals d_k
        self.d_k = d_model // h
        self.h = h
        self.linears = clones(nn.Linear(d_model, d_model), 4)
        self.attn = None
        self.dropout = nn.Dropout(p=dropout)

    def forward(self, query, key, value, mask=None):
        '''
        这里的query, key, value与attention函数中的含义有所不同,这里指的是原始的输入.
        对于Encoder的自注意力来说,输入query=key=value=x
        对于Decoder的自注意力来说,输入query=key=value=t
        对于Encoder和Decoder之间的注意力来说, 输入query=t, key=value=m
        其中m为Encoder的输出,即给定target,通过key计算出m中每个输出对当前target的分数,在乘上m
        '''
        if mask is not None:
            # Same mask applied to all h heads.
            mask = mask.unsqueeze(1)
        nbatches = query.size(0)

        # 1) Do all the linear projections in batch from d_model => h x d_k
        query, key, value = \
            [l(x).view(nbatches, -1, self.h, self.d_k).transpose(1, 2)
             for l, x in zip(self.linears, (query, key, value))]

        # 2) Apply attention on all the projected vectors in batch.
        ##   x: (batch_size, heads, max_seq_len, d_k)
        x, self.attn = attention(query, key, value, mask=mask,
                                 dropout=self.dropout)

        # 3) "Concat" using a view and apply a final linear.
        ##   x: (batch_size, max_seq_len, d_k*h)
        x = x.transpose(1, 2).contiguous() \
            .view(nbatches, -1, self.h * self.d_k)
        ## output: (batch_size, max_seq_len, d_model)
        return self.linears[-1](x)


def clones(module, N):
    "Produce N identical layers."
    return nn.ModuleList([copy.deepcopy(module) for _ in range(N)])

 

        注意下attention当中的mask。我们之前提到,在三个地方用到了attention。在Encoder的自注意力机制中,mask是用来过滤padding部分的作用,对于source中的每一个词来讲,其他的词对他都是可见的,都可以做出贡献的。但是在Decoder中,mask的作用就有所不同了。这可能又要从Encoder-Decoder框架说起。在这个框架下,解码器实际上可看成一个神经网络语言模型,预测的时候,target中的每一个单词是逐个生成的,当前词的生成依赖两方面:一是Encoder的输出,二是target的前面的单词。例如,在生成第一个单词是,不仅依赖于Encoder的输出,还依赖于起始标志[CLS];生成第二个单词是,不仅依赖Encoder的输出,还依赖起始标志和第一个单词…依此类推。这其实是说,在翻译当前词的时候,是看不到后面的要翻译的词。由上可以看出,这里的mask是动态的。

def subsequent_mask(size):
    "Mask out subsequent positions."
    # size: 序列长度
    attn_shape = (1, size, size)
    # 生成一个上三角矩阵
    subsequent_mask = np.triu(np.ones(attn_shape), k=1).astype('uint8')
    return torch.from_numpy(subsequent_mask) == 0

下面详细介绍下subsequent_mask是如何起作用的。函数的参数size指的是target句子的长度。以”[CLS] That is it“这个长度为4的target输入为例,这个函数的输出是什么呢?

print(subsequent_mask(size=4))

tensor([[[1, 0, 0, 0],
         [1, 1, 0, 0],
         [1, 1, 1, 0],
         [1, 1, 1, 1]]], dtype=torch.uint8)

可以看到,输出为一个下三角矩阵,维度为(1,4,4)。现在我们再来看下attention函数,mask起作用的地方是在Query和Key点乘后,结果矩阵的维度为(batch_size, heads, max_seq_len, max_seq_len)。为方便起见,我们只看一条数据,即batch_size=1。进入多头attention时,注意到对mask做了一步操作:

mask = mask.unsqueeze(1)
mask:
tensor([[[[1, 0, 0, 0],
          [1, 1, 0, 0],
          [1, 1, 1, 0],
          [1, 1, 1, 1]]]], dtype=torch.uint8)   

写成了上面的样子,mask的作用就很显然了。例如,对于”CLS“来说,预测它下一个词时,只有”CLS“参与了attention,其他的词(相对于CLS为未来的词)都被mask_fill掉了,不起作用。后面的情况依此类推。可能发现了,这里的解释并没有考虑padding部分。事实上,就算加了padding部分(为0),也不影响上述过程,有兴趣的话可以在上面it后面加上个0,下面的矩阵加一列[0 0 0 0 ], 就可以一目了然。

层归一化

class LayerNorm(nn.Module):
    "Construct a layernorm module (See citation for details)."
    def __init__(self, features, eps=1e-6):
        super(LayerNorm, self).__init__()
        self.a_2 = nn.Parameter(torch.ones(features))
        self.b_2 = nn.Parameter(torch.zeros(features))
        self.eps = eps

    def forward(self, x):
        mean = x.mean(-1, keepdim=True)
        std = x.std(-1, keepdim=True)
        return self.a_2 * (x - mean) / (std + self.eps) + self.b_2

在前面多次用到了层归一化(LayerNormalization),那么它是何方神圣呢?或许你对BatchNormalization比较熟悉,但千万不要在这里错以为是它。可以说层归一化是BatchNormalization的2.0版本,它是由Hinton神和他的学生提出的[3]。

BatchNormalization

      BatchNormalization的出现无疑是广大AI调参侠的福音,将大家从繁琐的权重初始化、学习率调节中释放出来。它不仅能够大大加快收敛速度,还自带正则化功能,是Google 2015年提出的[4]。

     机器学习的一个重要的假设是:数据是独立同分布的。训练集合测试集的数据是同分布的,这样模型才有好的泛化效果。神经网络其实也是在学习这个分布。在这个假设前提下,一旦我们知道了(x,y)的联合分布,很多问题就能通过条件概率P(x|y)计算出来了。但是在实际训练过程中,数据经过前一个隐藏层向后一个隐藏层传播(线性+非线性运算),分布通常会发生变化(作者称之为Internal Covariate Shift),这会导致网络学习变慢。我们从两个方面来稍微理解一下这个问题。

一方面:我们现在只看两个隐藏层(隐藏层A和隐藏层B)之间的传播。第一轮,来自A的数据经过线性操作和激活函数后到达B,反向传播时,B层为了学习到这个分布(满足A的需求),调整了权重W1。接着又进行第二轮传播了,A数据一到B,A说,现在需求变了,要这样这样。B一脸懵,盘算了一下,发现前面的白学了,没办法,换个方向重来…就这样,A一直在变,B就得跟着变,来来回回磨合…这听起来就是个非常耗时的工作。就好比A说今天要吃汤圆,B和好了面粉,准备了调料,A又说我要吃饭…虽然在B的不懈努力下A最后能吃上饭,但如果一开始A就告诉B我要吃饭不是更快一点?…网络越深,这个问题就越严重。

另一方面则是与激活函数有关,我们用sigmoid为例来说明一下。假设两层传播之间可表示为

z=g(Wu+b)
其中g是sigmoid函数,我们令

x=Wu+b

那么:z= sigmoid(x)=g(x)=\frac{1}{1+exp(-x)}


计算下梯度:
我们关注一下中间那一项,是sigmoid函数的导数,它的分布如Figure 5 所示,可见随着|x|不断增大,该项趋近于0,这也就意味着整个梯度趋近于0,进入饱和区了,导致的结果就是收敛变慢!要想加快收敛怎么办,把|x|拉到靠近0的位置就行了,这里导数值最大。

1.5 词向量

前馈网络

每个encoderLayer中,多头attention后会接一个前馈网络。这个前馈网络其实是两个全连接层,进行了如下操作:

FFN(x)=max(0,xW1+b1)W2+b2

class PositionwiseFeedForward(nn.Module):
    '''Implements FFN equation.
    d_model=512
    d_ff=2048
    '''
    def __init__(self, d_model, d_ff, dropout=0.1):
        super(PositionwiseFeedForward, self).__init__()
        self.w_1 = nn.Linear(d_model, d_ff)
        # self.w_1 = nn.Conv1d(in_features=d_model, out_features=d_ff, kenerl_size=1)
        self.w_2 = nn.Linear(d_ff, d_model)
        # self.w_2 = nn.Conv1d(in_features=d_ff, out_features=d_model, kenerl_size=1)
        self.dropout = nn.Dropout(dropout)

    def forward(self, x):
        return self.w_2(self.dropout(F.relu(self.w_1(x))))

这两层的作用等价于两个 kenerl_size=1的一维卷积操作。

词向量

这里就是普通的不能再普通的词向量,将词语变成d_model维的向量。

class Embeddings(nn.Module):
    def __init__(self, d_model, vocab):
        super(Embeddings, self).__init__()
        self.lut = nn.Embedding(vocab, d_model)
        self.d_model = d_model

    def forward(self, x):
        return self.lut(x) * math.sqrt(self.d_model)

位置编码

由于Transformer没有用到CNN和RNN,因此,句子单词之间的位置信息就没有利用到。显然,这些信息对于翻译来说是非常有用的,同样一句话,每个单词的意思能够准确的翻译出来,但如果顺序不对,表达出来的意思就截然不同了。举个栗子感受一下,原句:”A man went through the Big Buddhist Temple“, 翻译成:”人过大佛寺“和”寺佛大过人“,意思就完全不同了。

那么如何表达一个序列的位置信息呢?对于某一个单词来说,他的位置信息主要有两个方面:一是绝对位置,二是相对位置。绝对位置决定了单词在一个序列中的第几个位置,相对位置决定了序列的流向。作者利用了正弦函数和余弦函数来进行位置编码:

其中pos是单词处于句子的第几个位置。我们来考察一下第一个公式,看是否每个位置都能得到一个唯一的值作为编码。为简单起见,不妨令i=0,那么:

PE(pos,0)=sin(pos)

我们反过来想,假如存在位置j和k的编码值相同,那么就有:

sin(i)=sin(j)

i,j为非负整数且i不等于j,


这显然是不可能的,因为左边是个无理数(无限不循环小数),而右边是个有理数。通过反证法就证明了在这种表示下,每个位置确实有唯一的编码。

    上面的讨论并未考虑i的作用。i决定了频率的大小,不同的i可以看成是不同的频率空间中的编码,是相互正交的,通过改变i的值,就能得到多维度的编码,类似于词向量的维度。这里2i<=512(d_model), 一共512维。想象一下,当2i大于d_model时会出现什么情况,这时sin函数的周期会变得非常大,函数值会非常接近于0,这显然不是我们希望看到的,因为这样和词向量就不在一个量级了,位置编码的作用被削弱了。另外,值得注意的是,位置编码是不参与训练的,而词向量是参与训练的。作者通过实验发现,位置编码参与训练与否对最终的结果并无影响。

class PositionalEncoding(nn.Module):
    "Implement the PE function."
    def __init__(self, d_model, dropout, max_len=5000):
        super(PositionalEncoding, self).__init__()
        self.dropout = nn.Dropout(p=dropout)
        
        # Compute the positional encodings once in log space.
        pe = torch.zeros(max_len, d_model)
        position = torch.arange(0, max_len).unsqueeze(1)
        div_term = torch.exp(torch.arange(0, d_model, 2) *
                             -(math.log(10000.0) / d_model))
        pe[:, 0::2] = torch.sin(position * div_term)
        pe[:, 1::2] = torch.cos(position * div_term)
        pe = pe.unsqueeze(0)
        self.register_buffer('pe', pe)
        
    def forward(self, x):
        x = x + Variable(self.pe[:, :x.size(1)], 
                         requires_grad=False)
        return self.dropout(x)

之所以对奇偶位置分别编码,是因为编码前一个位置是可以由另一个位置线性表示的(公差为1的等差数列),在编码之后也希望能保留这种线性。我们以第1个位置和第k+1个位置为例,还是令i=0:

PE(1,0)=cos(1)

PE(k,0)=sin(1+k)=sin(1)cos(k)+cos(1)sin(k)=A+B⋅PE(1,0)

1.6 Deocder

我们先在看一眼刚开始的那张框架图。左半部分是Encoder,右半部分是Decoder。不难看出,Decoder和Encoder极其相似。

首先,Decoder也是由6个相同的decoder组件构成。

class Decoder(nn.Module):
    "Generic N layer decoder with masking."
    def __init__(self, layer, N):
        super(Decoder, self).__init__()
        self.layers = clones(layer, N)
        self.norm = LayerNorm(layer.size)
        
    def forward(self, x, memory, src_mask, tgt_mask):
        for layer in self.layers:
            x = layer(x, memory, src_mask, tgt_mask)
        return self.norm(x)

每个组件长什么样子呢?首先输入经过词向量和位置编码,进入target的自注意力层,这里和Encoder一样,也是用了残差和层归一化。然后呢,这个输出再和Encoder的输出做一次context attention,相当于把上面的那层重复了一次,唯一不同的是,这次的attention有点不一样的,不再是自注意力,所有的技术细节都可以参照Encoder部分,这里不再复述。

class DecoderLayer(nn.Module):
    "Decoder is made of self-attn, src-attn, and feed forward (defined below)"
    def __init__(self, size, self_attn, src_attn, feed_forward, dropout):
        super(DecoderLayer, self).__init__()
        self.size = size
        self.self_attn = self_attn
        self.src_attn = src_attn
        self.feed_forward = feed_forward
        self.sublayer = clones(SublayerConnection(size, dropout), 3)
 
    def forward(self, x, memory, src_mask, tgt_mask):
        m = memory
        x = self.sublayer[0](x, lambda x: self.self_attn(x, x, x, tgt_mask))
        x = self.sublayer[1](x, lambda x: self.src_attn(x, m, m, src_mask))
        return self.sublayer[2](x, self.feed_forward)

1.7 线性层和Softmax

这是整个模型的最后一步了。从Decoder拿到的输出是维度为(batch_size, max_seq_len, d_model)的浮点型张量,我们希望得到最终每个单词预测的结果,首先用一个线性层将d_model映射到vocab的维度,得到每个单词的可能性,然后送入softmax,找到最可能的单词。

线性层的参数个数为d_model vocab_size, 一般来说,vocab_size会比较大,拿20000为例,那么只这层的参数就有51220000个,约为10的8次方,非常惊人。而在词向量那一层,同样也是这个数值,所以,一种比较好的做法是将这两个全连接层的参数共享,会节省不少内存,而且效果也不会差。

class Generator(nn.Module):
    "Define standard linear + softmax generation step."
    def __init__(self, d_model, vocab):
        super(Generator, self).__init__()
        self.proj = nn.Linear(d_model, vocab)

    def forward(self, x):
        return F.log_softmax(self.proj(x), dim=-1)

2. BERT的原理

     BERT模型的全称是Bidirectional Encoder Representations from Transformers,它是一种新型的语言模型。之所以说是一种新型的语言模型,是因为它通过联合调节所有层中的双向Transformer来训练预训练深度双向表示。

   想深入了解BERT模型,首先应该理解语言模型。预训练的语言模型对于众多自然语言处理问题起到了重要作用,比如SQuAD问答任务、命名实体识别以及情感识别。目前将预训练的语言模型应用到NLP任务主要有两种策略,一种是基于特征的语言模型,如ELMo模型;另一种是基于微调的语言模型,如OpenAI GPT。这两类语言模型各有其优缺点,而BERT的出现,似乎融合了它们所有的优点,因此才可以在诸多后续特定任务上取得最优的效果。

2.1 BERT模型总体结构

      BERT是一种基于微调的多层双向Transformer编码器,其中的Transformer与原始的Transformer是相同的,并且实现了两个版本的BERT模型,在两个版本中前馈大小都设置为4层:

lBERTBASE:L=12,H=768,A=12,Total Parameters=110M

lBERTLARGE:L=24,H=1024,A=16,Total Parameters=340M

其中层数(即Transformer blocks块)表示为L,隐藏大小表示为H,自注意力的数量为A。

2.2 BERT模型输入

    输入表示可以在一个词序列中表示单个文本句或一对文本(例如,[问题,答案])。对于给定的词,其输入表示是可以通过三部分Embedding求和组成。Embedding的可视化表示如下图所示:

2.3 BERT模型预训练任务

BERT模型使用两个新的无监督预测任务对BERT进行预训练,分别是Masked LM和Next Sentence Prediction

2.3.1 Masked LM

    为了训练深度双向Transformer表示,采用了一种简单的方法:随机掩盖部分输入词,然后对那些被掩盖的词进行预测,此方法被称为“Masked LM”(MLM)。预训练的目标是构建语言模型,BERT模型采用的是bidirectional Transformer。那么为什么采用“bidirectional”的方式呢?因为在预训练语言模型来处理下游任务时,我们需要的不仅仅是某个词左侧的语言信息,还需要右侧的语言信息。

    在训练的过程中,随机地掩盖每个序列中15%的token,并不是像word2vec中的cbow那样去对每一个词都进行预测。MLM从输入中随机地掩盖一些词,其目标是基于其上下文来预测被掩盖单词的原始词汇。与从左到右的语言模型预训练不同,MLM目标允许表示融合左右两侧的上下文,这使得可以预训练深度双向Transformer。Transformer编码器不知道它将被要求预测哪些单词,或者哪些已经被随机单词替换,因此它必须对每个输入词保持分布式的上下文表示。此外,由于随机替换在所有词中只发生1.5%,所以并不会影响模型对于语言的理解。

2.3.2 Next Sentence Prediction

   很多句子级别的任务如自动问答(QA)和自然语言推理(NLI)都需要理解两个句子之间的关系,譬如上述Masked LM任务中,经过第一步的处理,15%的词汇被遮盖。那么在这一任务中我们需要随机将数据划分为等大小的两部分,一部分数据中的两个语句对是上下文连续的,另一部分数据中的两个语句对是上下文不连续的。然后让Transformer模型来识别这些语句对中,哪些语句对是连续的,哪些对子不连续。

2.4 模型比较

ELMo、GPT、BERT都是近几年提出的模型,在各自提出的时候都取得了不错的成绩。并且相互之间也是相辅相成的关系。

3个模型比较如下:

     语言模型的每一次进步都推动着NLP的发展,从Word2vec到ELMo,从OpenAI GPT到BERT。通过这些发展我们也可以洞悉到,未来表征学习(Deep learning is representation learning)将会越来越多的应用到NLP相关任务中,它们可以充分的利用目前海量的数据,然后结合各种任务场景,去训练出更为先进的模型,从而促进AI项目的落地。

3. 利用预训练的BERT模型将句子转换为句向量,进行文本分类

https://blog.csdn.net/u012526436/article/details/84637834

参考文献

猜你喜欢

转载自blog.csdn.net/yanyiting666/article/details/88708724