ELMo 一词多义

作者: 莫烦 编辑: 莫烦 2020-09-02

学习资料:

怎么了

不管是图片识别还是自然语言处理,模型都朝着越来越臃肿,越来越大的方向发展。 每训练一个大的模型,都会消耗掉数小时甚至数天的时间。我们并不希望浪费太多的时间在训练上,所以拿到一个预训练模型就十分重要了。 基于预训练模型,我们能够用较少的模型,较快的速度得到一个适合于我们自己数据的新模型,而且这个模型效果也不会很差。

所以预训练的核心价值就是:

  1. 手头只有小数据集,也能得到一个好模型;
  2. 训练速度大大提升,不用从零开始训练。

你也可以观看我制作的这个短片简介来了解更多关于预训练模型的好处。 今天以及后面内容将要提到的都是目前比较厉害的、可以用作预训练模型的架构,包括GPT, bert.

词向量有问题

传统使用skip gram 或者 CBOW 训练出来的词向量, 在ELMo看起来,是有问题的。ELMo的全称是 Embeddings from Language Models,他的主要目标是:找出词语放在句子中的意思

具体展开,ELMo还是想用一个向量来表达词语,不过这个词语的向量会包含上下文的信息。

ELMo word embedding

想要让模型给出的词向量拥有上下文信息。我们就在词向量中加上从前后文来的信息就好了,这就是ELMo最核心的思想。 那么ELMo是怎样训练,为什么这样训练又可以拿到前后文信息呢?

如果学习过RNN的同学,其实你很容易理解下面的内容,ELMo对你来说,只是另一种双向RNN架构。ELMo里有两个RNN(LSTM), 一个从前往后看句子,一个从后往前看句子,每一个词的向量表达,就是下面这几个信息的累积:

  1. 从前往后的前文信息;
  2. 从后往前的后文信息;
  3. 当前词语的词向量信息。

ELMo how combine context info

没错!就这么简单。在预训练模型中,EMLo应该是最简单的种类之一了。有了这些加入了前后文信息的词向量,模型就能提供这个词在句子中的意思了。

如何训练

一般来说,我们希望预训练模型都是在无监督的条件下被训练的。所谓NLP的无监督学习,实际上就是拿着网上一大堆论坛,wiki等文本, 用它们的前文预测后文,或者后文预测前文,或者两个一起混合预测。不管是 BERT还是GPT, 都是这样的方式,因为我们网上的无标签文本还是特别多的。 如果模型能理解人类在网上说话的方式,那么这个模型就学习到了人类语言的内涵。

ELMo train

ELMo的训练,就是上面这种模式。它的前向LSTM预测后文的信息,后向LSTM预测前文的信息。训练一个顺序阅读者+一个逆序阅读者,在下游任务的时候, 分别让顺序阅读者和逆序阅读者,提供他们从不同角度看到的信息。这就是EMo的训练和使用方法。

学习案例

这次的案例我们就上些真实点的数据,这样才好判断这种体量大点的预训练模型的优势在哪。 所以我挑选到了学术界上常用的 Microsoft Research Paraphrase Corpus (MRPC) 数据集来测试训练过程。 这个数据集的内容大概是用这种形式组织的。

Quality #1 ID #2 ID #1 String #2 String
1 1089874 1089925 PCCW 's chief operating officer , Mike Butcher , and Alex Arena , the chief financial officer , will report directly to Mr So . Current Chief Operating Officer Mike Butcher and Group Chief Financial Officer Alex Arena will report to So .
0 1430402 1430329 A tropical storm rapidly developed in the Gulf of Mexico Sunday and was expected to hit somewhere along the Texas or Louisiana coasts by Monday night . A tropical storm rapidly developed in the Gulf of Mexico on Sunday and could have hurricane-force winds when it hits land somewhere along the Louisiana coast Monday night .

每行有两句话 #1 String#2 String, 如果他们是连贯的上下文的话,Quality 为1,反之为0。这份数据集可以做两件事:

  1. 两句合起来训练文本匹配;
  2. 两句拆开单独对待,理解人类语言,学一个语言模型。

这个教学中,我们在训练ELMo理解人类语言的时候,用的是无监督的方法训练第2种模式。让ELMo通读人类语言,然后在大数据中寻找人类说话的规律。 学完之后,模型就有对词语和句子有一定的理解能力了。

秀代码

如果你习惯直接看全部代码,请点击这里。 这次我们主要需要构建一个正向的多层LSTM模型,一个反向的LSTM模型,在构建正向LSTM的时候,我相信大家应该都没什么问题,但是在做反向的时候,有一些小技巧可以说明一下。 另外,让模型做预测的时候也要稍微注意下,每次用上文预测的是紧接着的下一个词。

代码的训练模式和我们之前写的那些都非常接近,为了让你将重点放在学习模型上,我在utils.py 将数据处理相关的代码封装进了utils.MRPCSingle()这里,现在我们只需要直接调用就能自动下载数据并处理数据了。

def train(model, data, step):
    for t in range(step):
        seqs = data.sample(BATCH_SIZE)      # 拿数据
        loss, (fo, bo) = model.step(seqs)   # 练数据,这里我让它返回了loss和前后向预测的logits

开启训练后,你就能看到类似这样的结果。

step:  0 | time: 1.52 | loss: 9.463
| tgt:  <GO> hovan , a resident of trumbull , conn . , had worked as a bus driver since <NUM> and had no prior criminal record . <SEP>
| f_prd:  atsb knew knew competition competition competition competition markup floors festivals merit merit merit korkuc korkuc korkuc fingerprinting grade grade car car nicky roush thoughts roush gain gain
| b_prd:  stockwell stockwell stockwell mta mta mta mta mta mta mta tornadoes tornadoes tornadoes router router halliburton halliburton talked engaged ona db2 life rashid rashid ursel ursel


step:  80 | time: 8.37 | loss: 7.975
| tgt:  <GO> the winner of the williams-mauresmo match will play the winner of justine henin-hardenne vs. chanda rubin . <SEP>
| f_prd:  the , , , , , , , , , , , , , , , , , ,
| b_prd:  , , , , , , , , , , , , , , , , , . <SEP> <SEP> <SEP> <SEP> <SEP> <SEP> <SEP> <SEP> <SEP> <SEP> <SEP> <SEP> <SEP> <SEP> <SEP> <SEP> <SEP> <SEP> <SEP>

...

step:  9840 | time: 8.35 | loss: 0.570
| tgt:  <GO> montgomery was one of the first places to enact such a law , but many places , including new york city , now ban smoking in bars . <SEP>
| f_prd:  the was being of the american places , enact such a law , but he places , including the york city , where ban smoking in bars . <SEP>
| b_prd:  while montgomery <GO> one at the <NUM> places to enact such a <NUM> , but <NUM> places , including new york <NUM> , air bans smoking in <NUM> .


step:  9920 | time: 8.36 | loss: 0.543
| tgt:  <GO> of personal vehicles , <NUM> percent are cars or station wagons , <NUM> percent vans or suvs , <NUM> percent light trucks . <SEP>
| f_prd:  the the vehicles , <NUM> percent are cars or station wagons , <NUM> percent vans or suvs , <NUM> percent light trucks . <SEP>
| b_prd:  <GO> of personal vehicles , <NUM> percent are cars or station wagons , <NUM> percent vans or suvs , <NUM> percent light <NUM> .

大概过了一万步训练后,loss 从9降到0.5,正向LSTM在句末的预测都会相对准确,反向LSTM在句首的预测也会相对准确了。这就说明模型真的在认真学习,并且学习得还好。

class ELMo(keras.Model):
    def __init__(self, ...):
        # encoder
        self.word_embed = keras.layers.Embedding(...) # [n_vocab, emb_dim]

        # forward lstm
        self.fs = [keras.layers.LSTM(units, return_sequences=True) for _ in range(n_layers)]
        self.f_logits = keras.layers.Dense(v_dim)

        # backward lstm
        self.bs = [keras.layers.LSTM(units, return_sequences=True, go_backwards=True) for _ in range(n_layers)]
        self.b_logits = keras.layers.Dense(v_dim)

d = utils.MRPCSingle("./MRPC", rows=2000)
m = ELMo(d.num_word, emb_dim=UNITS, units=UNITS, n_layers=N_LAYERS, lr=LEARNING_RATE)

模型的架构相比之前的Transformer真的是简单太多了。 构建一个最初的word embedding, 获取到词语的信息,然后再分别构建前向LSTM和后向LSTM,在构建后向LSTM的时候,要注意写上go_backwards=True表明是逆向读取的。 最后在将从LSTM出来的信息转成logits就能预测了。

在非监督学习阶段,前向LSTM用前文预测后文,后向LSTM用后文预测前文。这个call()的过程就和我这张图展示的一模一样。一张图胜过千百句~

ELMo train

class ELMo(keras.Model):
    def call(self, seqs):
        embedded = self.word_embed(seqs)        # [n, step, dim]
        mask = self.word_embed.compute_mask(seqs)
        fxs, bxs = [embedded[:, :-1]], [embedded[:, 1:]]    # recode all layers output
        for fl, bl in zip(self.fs, self.bs):
            fx = fl(
                fxs[-1], mask=mask[:, :-1], initial_state=fl.get_initial_state(fxs[-1])
            )           # [n, step-1, dim]
            bx = bl(
                bxs[-1], mask=mask[:, 1:], initial_state=bl.get_initial_state(bxs[-1])
            )  # [n, step-1, dim]
            fxs.append(fx)      # predict 1234
            bxs.append(tf.reverse(bx, axis=[1]))    # predict 0123
        return fxs, bxs

在计算loss时,将要考虑前向和后向的误差,将两者加起来一起计算。所以step()函数可以这样写。

class ELMo(keras.Model):
    def step(self, seqs):
        with tf.GradientTape() as tape:
            fxs, bxs = self.call(seqs)
            fo, bo = self.f_logits(fxs[-1]), self.b_logits(bxs[-1])     # last layer prediction
            loss = (self.cross_entropy1(seqs[:, 1:], fo) + self.cross_entropy2(seqs[:, :-1], bo))/2
        grads = tape.gradient(loss, self.trainable_variables)
        self.opt.apply_gradients(zip(grads, self.trainable_variables))
        return loss, (fo, bo)

这就是整个训练过程,在把ELMo用在下游任务时,不管是下有任务的训练还是下游任务的预测,并不会像训练的call()那样。 时刻记得,我们要的是ELMo对于句子或者是词的理解。所以,我们只管拿着它训练出来的embedding使用就好了。这就是之前这张图所表达的意思。

ELMo how combine context info

举个例子,在预测句中某个词的属性时,我们就可以拿着这个词在每一层的向量表达,把它们整合起来,这样就有了词本身的信息和词在句中的信息。 对于这个词在句中到底表达什么意思有了更全面的信息。这也就是为什么我在call()这个函数中,返回的是每一层的output,而不是最后一层的output。

def get_emb(self, seqs):
    fxs, bxs = self.call(seqs)
    xs = [tf.concat((f[:, :-1, :], b[:, 1:, :]), axis=2).numpy() for f, b in zip(fxs, bxs)]
    for x in xs:
        print("layers shape=", x.shape)
    return np.mean(xs)

"""
layers shape= (4, 36, 512)
layers shape= (4, 36, 512)
layers shape= (4, 36, 512)
"""

在concat LSTM中间产物的时候需要注意一下对齐原句的信息就好。这个例子中,layer shape=(4, 36, 512) 的意思是4句话,句长36,512的向量表达。 你还可以对每层的向量进行加权求和,或者样另一个下游任务的网络学习一种注意力机制对这些层向量进行加工。这都取决于你的下有任务是怎样考虑的了。

举个例子,现在ELMo给了我3层(4, 36, 512), 如果我的下游任务是句子分类,最简单的一种方式就是,将这三层向量在第3个维度取平均,从 3(4,36,512) 变成 1(4,36,512), 然后再过有一个LSTM得到分类结果。

总结

我们知道了现在的模型都是越来越大,也知道预训练是解决这一问题的有效途径,ELMo作为预训练模型的先驱,的确为我们提供了有效的经验。 而且它也认真对待了一词多义的情况。但是预训练模型的并不仅限于ELMo这种RNN模式的框架,Transformer同样可以做预训练,而且训练效果和结果还比ELMo更好。 接下来我们就看看这些预训练语言模型的价值吧。 GPTBert

降低知识传递的门槛

莫烦很常从互联网上学习知识,开源分享的人是我学习的榜样。 他们的行为也改变了我对教育的态度: 降低知识传递的门槛免费 奉献我的所学正是受这种态度的影响。 通过 【赞助莫烦】 能让我感到认同,我也更有理由坚持下去。

想当算法工程师拿高薪?转行AI无门道?莫烦也想祝你一臂之力,市面上机构繁杂, 经过莫烦的筛选,七月在线脱颖而出, 莫烦和他们合作,独家提供大额 【培训优惠券】, 让你更有机会接触丰富的教学资源、培训辅导体验, 祝你找/换工作/学习顺利~