莫烦nlp-BERT双向语言模型

莫烦视频:https://mofanpy.com/tutorials/machine-learning/nlp/bert/

  • 跳过了GPT模型;但代码里面bert模型继承了GPT模型。。。
  • 本节不涉及莫烦对于bert的训练改进(trick),模型的任务改变,或者说标签不一样。改进版存在的问题莫烦在他的主页文字部分有详细解释。

所以这里只是介绍最基本的bert模型,它的任务和标签是什么,即怎么训练此模型。

bert是什么

  BERT 和 GPT 还有 ELMo 是一个性质的东西。 它存在的意义是要变成一种预训练模型,提供 NLP 中对句子的理解。
  ELMo 用了双向 LSTM 作为句子信息的提取器,同时还能表达词语在句子中的不同含义;
  GPT它是一种单向的语言模型,同样也可以用 attention 的方式提取到更加丰富的语言意思信息。而BERT,它就和GPT是同一个家族,都是从Transformer 演变而来的。那么 BERT 和 GPT 有有什么不同之处呢?

  其实最大的不同之处是,BERT 认为如果看一个句子只从单向观看,是不是还会缺少另一个方向的信息?所以 BERT 像 ELMo 一样,算是一种双向的语言模型。 而这种双向性,其实正是原封不动的 Transformer 的 Encoder 部分。
莫烦nlp-BERT双向语言模型
对比:

为了让BERT理解语义内容,它的训练会比GPT tricky得多。 GPT之所以训练方案上比较简单,是因为我们把它当成一个RNN一样训练,比如用前文预测后文(用mask挡住了后文的信息)。前后没有信息的穿越,这也是单向语言模型好训练的一个原因。 但是如果又要利用前后文的信息(不mask掉后文信息),又要好训练,这就比较头疼了。
因为我在预测词X的时候,实际上是看着X来预测X,这样并没有什么意义。'我’要预测‘也’,所以要mask掉传过来‘也’的信息

bert如何训练

李宏毅的讲解

Mask

BERT的研发人员想到了一个还可以的办法,就是我在句子里面遮住X,不让模型看到X,然后来用前后文的信息预测X。这就是BERT训练时最核心的概念了。

莫烦nlp-BERT双向语言模型

但是这样做又会导致一个问题。我们人类理解完形填空的意思,知道那个“空”(mask)是“无”或者“没有”的意思。 但是模型不知道呀,它的“空”(mask)会被当成一个词去理解。因为我们给的是一个叫“mask”的词向量输入到模型里的。 模型还以为你要用“mask”这个词向量来预测个啥。为了避免这种情况发生,研究人员有做了一个取巧的方案: 除了用“mask”来表示要预测的词,我还有些时候,把“mask”随机替换成其他词,或者原封不动。具体下来就是下面三种方式:

随机选取15%的词做如下改变

  1. 80% 的时间,将它替换成 [MASK]
  2. 10% 的时间,将它替换成其他任意词
  3. 10% 的时间,不变

举个例子:

Input: The man went to [MASK] store with [MASK] dog
Target:                  the               his

Next Sentence Prediction

预测 [MASK] 是BERT的一项最主要的任务。在非监督学习中,我们还能怎么玩?让模型有更多的可以被训练的任务? 其实呀,我们还能借助上下文信息做件事,就是让模型判断,相邻这这两句话是不是上下文关系。
莫烦nlp-BERT双向语言模型

举个例子,我在一个两句话的段落中将这两句话拆开,然后将两句话同时输入模型,让模型输出True/False判断是否是上下文。 同时我还可以随机拼凑不是上下文的句子,让它学习这两句不是上下文。

Input : the man went to the store [SEP] he bought a gallon of milk [SEP]
Is next : True

Input = the man heading to the store [SEP] penguin [MASK] are flight ##less birds [SEP]
Is next : False 

有了这两项任务,一个[MASK],一个上下文预测,我们应该就能创造出非常多的训练数据来给模型训练进行监督训练啦。 其实也就是把非监督的数据做成了两个监督学习的任务,模型还是被监督学习的。

莫烦的特别

请注意:我写的BERT代码和原文有一处不同,我认为不用传递给模型一个[CLS]信息让模型知道当前在做的是什么任务,因为我想要得到的是一个语言理解器, 至于对于不同的任务,可以 Finetune 出不同的头来适应,因为谁知道你下游是不是一个你训练过的任务(Task)呢?所以我觉得没必要专门为了Task去搞一个Task input。 我更关注的是训练出一个语言模型,而不是一个语言任务模型。

‘头’是指你下游任务,如:使用全连接进行分类任务。【CLS】李宏毅中有介绍bert可以完成的任务。

代码

 这里选择的数据还是和做ELMo,GPT 时相同的数据(MRPC),可以进行横向对比。

上次训练的GPT只用了5000步就收敛到一个比较好的地方, 但是这次的BERT训练了10000步,还是没能收敛到特别好。这也是BERT在训练上的一个硬伤。

random_mask_or_replace() 这个功能怎么设计呢?我大概讲一下思路。 简单来说也就是要将原始句子替换一下他们的[MASK]位置,或者是replace成其他词,又或者啥都不做。 我还有个tricky的做法,为了只计算被masked或者replaced这些位置的loss,在模型前向完了,他会对每一个词位都计算一下误差, 但是我们可以在计算真正loss的时候,只保留这些被masked/replaced位置的loss,其他词语的位子都忽略掉。 所以我这里还会生成一个loss_mask,用来在计算loss时,只关注需要计算的部分。

因为BERT的主架构是Transformer的Encoder,而我们之前写的GPT也是用的它的encoder。 所以这里我们只需要在GPT的结构上修改一下计算loss的方案和双向mask的方案即可。(我的GPT代码是继承的Transformer的架构,所以他们都是通用的)

最主要的原因是BERT每次的训练太没有效率了。每次输入全部训练数据,但是只能预测15%的词,而GPT能够预测100%的词,这不就让BERT单次训练少了很多有效的label信息。

position embedding不同

Transformer时:self.pe是常量

def __init__(self, max_len, model_dim, n_vocab):
	pos = np.arange(max_len)[:, None]
	pe = pos / np.power(10000, 2. * np.arange(model_dim)[None, :] / model_dim)  # [max_len, dim]
	pe[:, 0::2] = np.sin(pe[:, 0::2])
	pe[:, 1::2] = np.cos(pe[:, 1::2])
	pe = pe[None, :, :]  # [1, max_len, model_dim]    for batch adding
	self.pe = tf.constant(pe, dtype=tf.float32)
def call(self, x):
        x_embed = self.embeddings(x) + self.pe  # [n, step, dim]

bert时,self.add_weight()是keras内置函数,创建能够学习的权值参数

def  __init__():
	self.position_emb = self.add_weight(
	     name="pos", shape=[1, max_len, model_dim], dtype=tf.float32,   # [1, step, dim] 相加时broadcast
	     initializer=keras.initializers.RandomNormal(0., 0.01))
def input_emb(self, seqs, segs):
    return self.word_emb(seqs) + self.segment_emb(segs) + self.position_emb  # [n, step, dim]

训练任务

任务一:猜测被mask掉的词
任务二:判断两句话是否(语义相同或上下句关系)

class GPT(keras.Model):
    def __init__(self, model_dim, max_len, n_layer, n_head, n_vocab, lr, max_seg=3, drop_rate=0.1, padding_idx=0):
        super().__init__()
        self.padding_idx = padding_idx
        self.n_vocab = n_vocab
        self.max_len = max_len
        ...
        self.task_mlm = keras.layers.Dense(n_vocab) #Masked LM
        self.task_nsp = keras.layers.Dense(2) #Next Sentence Prediction

    def call(self, seqs, segs, training=False):
        embed = self.input_emb(seqs, segs)  # [n, step, dim]
        z = self.encoder(embed, training=training, mask=self.mask(seqs))     # [n, step, dim]
        mlm_logits = self.task_mlm(z)  # [n, step, n_vocab]
        nsp_logits = self.task_nsp(tf.reshape(z, [z.shape[0], -1]))  # [n, n_cls]
        return mlm_logits, nsp_logits

z 形状 [n, step, dim],二分类任务的时候,把step和dim合并。
而RNN的话只用最后一个step(循环)来预测。

网络模型

在这里,bert继承gpt的类,而gpt的网络架构就是Transformer的Encoder。从上面代码中可以看到,数据输入到encoder,再将输出传给全连接做预测任务。不同的是

  1. mask函数,求attention时需要mask的部分
  2. step函数,loss函数的计算

self.encoder = Encoder(n_head, model_dim, drop_rate, n_layer)
Encoder的部分,完全使用Transformer的。包括class EncodeLayer、PositionWiseFFN、MultiHead。

还没看gpt那一节,但与bert的区别,就是bert重写了def step和def mask函数。

class bert(GPT):
    def step(self, seqs, segs, seqs_, loss_mask, nsp_labels):
        with tf.GradientTape() as tape:
            mlm_logits, nsp_logits = self.call(seqs, segs, training=True)
            mlm_loss_batch = tf.boolean_mask(self.cross_entropy(seqs_, mlm_logits), loss_mask)
            mlm_loss = tf.reduce_mean(mlm_loss_batch) ##参数axis如果不指定,则计算所有元素的均值;
            nsp_loss = tf.reduce_mean(self.cross_entropy(nsp_labels, nsp_logits))
            loss = mlm_loss + 0.2 * nsp_loss
            grads = tape.gradient(loss, self.trainable_variables)
            self.opt.apply_gradients(zip(grads, self.trainable_variables))
        return loss, mlm_logits

    def mask(self, seqs):
        mask = tf.cast(tf.math.equal(seqs, self.padding_idx), tf.float32)
        return mask[:, tf.newaxis, tf.newaxis, :]  # [n, 1, 1, step]

mask

mask用在def scaled_dot_product_attention里,padding位置为1,乘无穷小,在softmax后,padding位置的值为0。

def scaled_dot_product_attention(self, q, k, v, mask=None):
        dk = tf.cast(k.shape[-1], dtype=tf.float32)
        score = tf.matmul(q, k, transpose_b=True) / (tf.math.sqrt(dk) + 1e-8)  # [batch, heads, q_step, step]
        if mask is not None:
            score += mask * -1e9
        self.attention = tf.nn.softmax(score, axis=-1)                               # [batch, heads, q_step, step]
        context = tf.matmul(self.attention, v)         # [batch, heads, q_step, step] @ [batch, heads, step, dv] = [batch, heads, q_step, dv]

step

固定步骤:计算loss、tape.gradient、apply_gradients。

self.cross_entropy = keras.losses.SparseCategoricalCrossentropy(from_logits=True, reduction=“none”) 没经过one-hot,在softmax之前的数据,计算交叉熵后不会求平均

经过网络模型,我们得到了预测值。关键是制作标签label

  • seqs, 输入数据,每个词用id表示,self.v2i。经过处理后,某些词被mask掉(改变)
self.word_emb = keras.layers.Embedding(
            input_dim=n_vocab, output_dim=model_dim,  # [n_vocab, dim]
            embeddings_initializer=tf.initializers.RandomNormal(0., 0.01),)
#调用
self.word_emb(seqs)
  • segs,输入数据,每个词属于那句话。值域{0,1,2} 有必要加这个embedding吗?
self.segment_emb = keras.layers.Embedding(
            input_dim=max_seg, output_dim=model_dim,  # [max_seg, dim]
            embeddings_initializer=tf.initializers.RandomNormal(0., 0.01),)
#调用            
self.segment_emb(segs)
  • seqs_, 原始数据。要预测被mask掉的词,则标签label为原始没被mask的词。
  • loss_mask, 被mask掉的词的位置,类似cv的目标检测,因为不是每一个检测框都含有目标。
  • nsp_labels, 任务next sentence prediction的二分类标签

下面就看如何制作训练所需要的标签

Train训练

np.random.choice
numpy.full_like

随机选取15%的词做上述3种改变,这里举其中一个:70% 的时间,将它替换成 [MASK]

  • 下面注释中,选取句子的起始和终止有点奇怪

arange = np.arange(0, data.max_len)
def random_mask_or_replace(data, arange, batch_size):
    seqs, segs, xlen, nsp_labels = data.sample(batch_size)
    # (batch,v2i)  (batch,num_seg) [[s1len,s2len]] (batch,1)
    seqs_ = seqs.copy() #深拷贝
    p = np.random.random()
    if p < 0.7:
        # mask
        loss_mask = np.concatenate(
            [do_mask(
                seqs[i], #<go>...<last-1>  <seg>...<last-1>
                np.concatenate((arange[:xlen[i, 0]], arange[xlen[i, 0] + 1:xlen[i].sum() + 1])),
                data.pad_id,
                data.v2i["<MASK>"]) for i in range(len(seqs))], axis=0)
    ...
    return seqs, segs, seqs_, loss_mask, xlen, nsp_labels
          
def do_mask(seq, len_arange, pad_id, mask_id):
    loss_mask, rand_id = _get_loss_mask(len_arange, seq, pad_id)
    seq[rand_id] = mask_id #改变原数据
    return loss_mask
    
def _get_loss_mask(len_arange, seq, pad_id):
    ##replace:True表示可以取相同数字,False表示不可以取相同数字 返回一维数组
    rand_id = np.random.choice(len_arange, size=max(2, int(MASK_RATE * len(len_arange))), replace=False)
    loss_mask = np.full_like(seq, pad_id, dtype=np.bool) #pad_id=0
    loss_mask[rand_id] = True
    return loss_mask[None, :], rand_id

self.cross_entropy(seqs_, mlm_logits)这里已经把[n, step]个词做了交叉熵。
loss_mask[rand_id] = True,需要预测的位置为True。结合下面语句,得到想要的loss:
mlm_loss_batch = tf.boolean_mask(self.cross_entropy(seqs_, mlm_logits), loss_mask)

通过布尔值 过滤元素,当 tensor 与 mask 维度一致时,return 一维

def train():函数中循环调用model.step()函数得到loss和pred

    def step(self, seqs, segs, seqs_, loss_mask, nsp_labels):
        with tf.GradientTape() as tape:
            # [n, step, n_vocab]  # [n, n_cls]
            mlm_logits, nsp_logits = self.call(seqs, segs, training=True)
            mlm_loss_batch = tf.boolean_mask(self.cross_entropy(seqs_, mlm_logits), loss_mask)
            mlm_loss = tf.reduce_mean(mlm_loss_batch) #参数axis如果不指定,则计算所有元素的均值
            nsp_loss = tf.reduce_mean(self.cross_entropy(nsp_labels, nsp_logits))
            loss = mlm_loss + 0.2 * nsp_loss
            grads = tape.gradient(loss, self.trainable_variables)
            self.opt.apply_gradients(zip(grads, self.trainable_variables))
        return loss, mlm_logits
       
def train(model, data, step=10000, name="bert"):
    t0 = time.time()
    st = t0
    arange = np.arange(0, data.max_len)
    for t in range(step):
        seqs, segs, seqs_, loss_mask, xlen, nsp_labels = random_mask_or_replace(data, arange, 16)
        loss, pred = model.step(seqs, segs, seqs_, loss_mask, nsp_labels)
        if t % 100 == 0:
            pred = pred[0].numpy().argmax(axis=1)
            t1 = time.time()
            print(
                "\n\nstep: ", t,
                "| time: %.2f" % (t1 - t0),
                "| loss: %.3f" % loss.numpy(),
                "\n| tgt: ", " ".join([data.i2v[i] for i in seqs[0][:xlen[0].sum()+3]]),
                "\n| prd: ", " ".join([data.i2v[i] for i in pred[:xlen[0].sum()+3]]),
                "\n| tgt word: ", [data.i2v[i] for i in seqs_[0]*loss_mask[0] if i != data.v2i["<PAD>"]],
                "\n| prd word: ", [data.i2v[i] for i in pred*loss_mask[0] if i != data.v2i["<PAD>"]],
                )
            t0 = t1

结果

1 Physical GPUs, 1 Logical GPUs
num word:  12880

step:  9900 | time: 11.98 | loss: 2.934 
| tgt:  <GO> the mta had argued it <MASK> to raise fares to <MASK> <MASK> two-year deficit it estimated at different times ranged from less than $ <NUM> billion to $ <NUM> billion . <MASK> the mta argued it needed to raise fares <MASK> close a two-year <MASK> it estimated , <MASK> different times , to be <MASK> <NUM> million or $ <NUM> <MASK> . <SEP> 
| prd:  <GO> the it had that it to to to fares to to a deficit deficit it estimated at times times times from more than $ <NUM> billion to $ <NUM> billion . <SEP> the deficit , it needed to to fares to close a fares it it billion , , times times , to be $ <NUM> million or $ <NUM> billion . <SEP> 
| tgt word:  ['needed', 'close', 'a', '<SEP>', 'to', 'deficit', 'at', '$', 'billion'] 
| prd word:  ['to', 'to', 'a', '<SEP>', 'to', 'it', ',', '$', 'billion']

 total time: 20 min 26 second

存储可视化数据

def export_attention(model, data, name="bert"):
    model.load_weights("./visual/models/%s/model.ckpt" % name)

    # save attention matrix for visualization
    seqs, segs, xlen, nsp_labels = data.sample(32)
    model.call(seqs, segs, False)
    data = {"src": [[data.i2v[i] for i in seqs[j]] for j in range(len(seqs))], "attentions": model.attentions}
    path = "./visual/tmp/%s_attention_matrix.pkl" % name
    os.makedirs(os.path.dirname(path), exist_ok=True)
    with open(path, "wb") as f:
        pickle.dump(data, f)

接下来看看GPT有时如何效率高的。

前面我们还提到这个BERT训练了10000步还收敛不到一个好结果,而GPT只需要5000步就能收敛得比较好了。这是为什么呢? 最主要的原因是BERT每次的训练太没有效率了。每次输入全部训练数据,但是只能预测15%的词,而GPT能够预测100%的词,这不就让BERT单次训练少了很多有效的label信息。

莫烦建议

BERT 完美实现了双向语言模型的概念,在我的认知中,双向肯定会比单向语言模型(GPT)获取到更多的信息,所以按理来说应该会更优秀。但是在训练双向语言模型时, 会有很多tricks,我们要多多研究一下trick才能使得训练更加有效率更快。

上一篇:【关系抽取-R-BERT】定义训练和验证循环


下一篇:重磅!京东云自研第四代云主机发布;曝国外物理学家开发出用于量子计算机的汇编语言