NLP教程笔记:ELMo 一词多义

NLP教程

TF_IDF
词向量
句向量
Seq2Seq 语言生成模型
CNN的语言模型
语言模型的注意力
Transformer 将注意力发挥到极致
ELMo 一词多义
GPT 单向语言模型
BERT 双向语言模型
NLP模型的多种应用


怎么了

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

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

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

词向量有问题

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

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

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

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

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

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

如何训练

一般来说,我们希望预训练模型都是在无监督的条件下被训练的。所谓NLP的无监督学习,实际上就是拿着网上一大堆论坛,wiki等文本, 用它们的前文预测后文,或者后文预测前文,或者两个一起混合预测。不管是 BERT还是GPT, 都是这样的方式,因为我们网上的无标签文本还是特别多的。 如果模型能理解人类在网上说话的方式,那么这个模型就学习到了人类语言的内涵。
在这里插入图片描述
ELMo的训练,就是上面这种模式。它的前向LSTM预测后文的信息,后向LSTM预测前文的信息。训练一个顺序阅读者+一个逆序阅读者,在下游任务的时候, 分别让顺序阅读者和逆序阅读者,提供他们从不同角度看到的信息。这就是ELMo的训练和使用方法。

学习案例

这次的案例我们就上些真实点的数据,这样才好判断这种体量大点的预训练模型的优势在哪。 所以我挑选到了学术界上常用的 Microsoft Research Paraphrase Corpus (MRPC) 数据集来测试训练过程。 这个数据集的内容大概是用这种形式组织的。
在这里插入图片描述
每行有两句话 #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使用就好了。

举个例子,在预测句中某个词的属性时,我们就可以拿着这个词在每一层的向量表达,把它们整合起来,这样就有了词本身的信息和词在句中的信息。 对于这个词在句中到底表达什么意思有了更全面的信息。这也就是为什么我在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 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作为预训练模型的先驱,的确为我们提供了有效的经验。 而且它也认真对待了一词多义的情况。

全部代码

MRPCSingle

class MRPCSingle:
    pad_id = PAD_ID

    def __init__(self, data_dir="./MRPC/", rows=None, proxy=None):
        maybe_download_mrpc(save_dir=data_dir, proxy=proxy)
        data, self.v2i, self.i2v = _process_mrpc(data_dir, rows)

        self.max_len = max([len(s) + 2 for s in data["train"]["s1id"] + data["train"]["s2id"]])
        x = [
            [self.v2i["<GO>"]] + data["train"]["s1id"][i] + [self.v2i["<SEP>"]]
            for i in range(len(data["train"]["s1id"]))
        ]
        x += [
            [self.v2i["<GO>"]] + data["train"]["s2id"][i] + [self.v2i["<SEP>"]]
            for i in range(len(data["train"]["s2id"]))
        ]
        self.x = pad_zero(x, max_len=self.max_len)
        self.word_ids = np.array(list(set(self.i2v.keys()).difference([self.v2i["<PAD>"]])))

    def sample(self, n):
        bi = np.random.randint(0, self.x.shape[0], size=n)
        bx = self.x[bi]
        return bx

    @property
    def num_word(self):
        return len(self.v2i)

set_soft_gpu

def set_soft_gpu(soft_gpu):
    import tensorflow as tf
    if soft_gpu:
        gpus = tf.config.experimental.list_physical_devices('GPU')
        if gpus:
            # Currently, memory growth needs to be the same across GPUs
            for gpu in gpus:
                tf.config.experimental.set_memory_growth(gpu, True)
            logical_gpus = tf.config.experimental.list_logical_devices('GPU')
            print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs")
from tensorflow import keras
import tensorflow as tf
import utils    
import time
import os


class ELMo(keras.Model):
    def __init__(self, v_dim, emb_dim, units, n_layers, lr):
        super().__init__()
        self.n_layers = n_layers
        self.units = units

        # encoder
        self.word_embed = keras.layers.Embedding(
            input_dim=v_dim, output_dim=emb_dim,  # [n_vocab, emb_dim]
            embeddings_initializer=keras.initializers.RandomNormal(0., 0.001),
            mask_zero=True,
        )
        # 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)

        self.cross_entropy1 = keras.losses.SparseCategoricalCrossentropy(from_logits=True)
        self.cross_entropy2 = keras.losses.SparseCategoricalCrossentropy(from_logits=True)
        self.opt = keras.optimizers.Adam(lr)

    def call(self, seqs):
        embedded = self.word_embed(seqs)        # [n, step, dim]
        """
        0123    forward
        1234    forward predict
         1234   backward 
         0123   backward predict
        """
        mask = self.word_embed.compute_mask(seqs)
        fxs, bxs = [embedded[:, :-1]], [embedded[:, 1:]]
        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

    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])
            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)

    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 xs


def train(model, data, step):
    t0 = time.time()
    for t in range(step):
        seqs = data.sample(BATCH_SIZE)
        loss, (fo, bo) = model.step(seqs)
        if t % 80 == 0:
            fp = fo[0].numpy().argmax(axis=1)
            bp = bo[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] if i != data.pad_id]),
                "\n| f_prd: ", " ".join([data.i2v[i] for i in fp if i != data.pad_id]),
                "\n| b_prd: ", " ".join([data.i2v[i] for i in bp if i != data.pad_id]),
                )
            t0 = t1
    os.makedirs("./visual/models/elmo", exist_ok=True)
    model.save_weights("./visual/models/elmo/model.ckpt")


def export_w2v(model, data):
    model.load_weights("./visual/models/elmo/model.ckpt")
    emb = model.get_emb(data.sample(4))
    print(emb)


if __name__ == "__main__":
    utils.set_soft_gpu(True)
    UNITS = 256
    N_LAYERS = 2
    BATCH_SIZE = 16
    LEARNING_RATE = 2e-3
    d = utils.MRPCSingle("./MRPC", rows=2000)
    print("num word: ", d.num_word)
    m = ELMo(d.num_word, emb_dim=UNITS, units=UNITS, n_layers=N_LAYERS, lr=LEARNING_RATE)
    train(m, d, 10000)
    export_w2v(m, d)
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值