切换视频源:

Continuous Bag of Words (CBOW)

作者: 莫烦 编辑: 莫烦 2020-07-20

学习资料:

怎么了

之前我们在介绍搜索引擎的时候,提到过TF-IDF算法, 这种算法是通过一种统计学的方式来用文章中的词的重要程度,转化成向量来表示一篇文章的。 向量化思维在机器学习中也非常常见,我们可以认为,一张图片是一个向量,一篇文章是一个向量,一句话也可以是一个向量。 这样的向量化表示优点也很明显,就是能被计算机计算,是计算机能够理解的模式。

那么我们还能更加细化能被向量化的东西吗?比如说有没有词的向量呢?词的向量有怎么得到呢? 这里的短片简介将会清楚的阐述这样的过程。

w2v

什么是CBOW

CBOW 是 Continuous Bag-of-Word 的简称,同篇论文中, 还有另外一个一起提出的,十分相似的模型,Skip-Gram, 我们会在下一节内容中继续阐述Skip-Gram. 那么这个CBOW是什么个东西呢?用一句话概述:挑一个要预测的词,来学习这个词前后文中词语和预测词的关系

举个例子吧,有这样一句话。

我爱莫烦Python,莫烦Python通俗易懂。

模型在做的事情如图中所示,将这句话拆成输入和输出,用前后文的词向量来预测句中的某个词。

cbow illustration

那么这个模型的输入输出可以是:

# 1
# 输入:[我,爱] + [烦,Python]
# 输出:莫

# 2
# 输入:[爱,莫] + [Python, ,]
# 输出:烦

# 3
# 输入:[莫,烦] + [,,莫]
# 输出:Python

# 4
# 输入:[烦,Python] + [莫,烦]
# 输出:,

通过在大数据量的短语或文章中学习这样的词语关系,这个模型就能理解要预测的词前后文的关系。而图中彩色的词向量就是这种训练过程的一个副产品。

词向量的应用

词向量的几种典型应用:

  • 把这些对词语理解的向量通过特定方法组合起来,就可以有对某句话的理解了;
  • 可以在向量空间中找寻同义词,因为同义词表达的意思相近,往往在空间中距离也非常近;
  • 词语的距离换算。

词语距离计算这个比较有意思,比如可以拿词语做加减法。公猫 - 母猫 就约等于 男人 - 女人。 如果我哪天想知道 莫烦Python 的友商有哪些,我可以做一下这样的计算

友商 = 莫烦Python - (腾讯 - 阿里)

秀代码

为了做一个有区分力的词向量,我做了一些假数据,想让计算机学会这些假词向量的正确向量空间。 如果你习惯直接看所有源码,那么源码在这里

我将训练的句子人工分为两派(数字派,字母派),虽然都是文本,但是期望模型能自动区分出在空间上,数字和字母是有差别的。因为数字总是和数字一同出现, 而字母总是和字母一同出现。并且我还在字母中安排了一个数字卧底,这个卧底的任务就是把字母那边的情报向数字通风报信。 所以期望的样子就是数字 9 不但靠近数字,而且也靠近字母。

corpus = [
    # numbers
    "5 2 4 8 6 2 3 6 4",
    "4 8 5 6 9 5 5 6",
    "1 1 5 2 3 3 8",
    "3 6 9 6 8 7 4 6 3",
    "8 9 9 6 1 4 3 4",
    "1 0 2 0 2 1 3 3 3 3 3",
    "9 3 3 0 1 4 7 8",
    "9 9 8 5 6 7 1 2 3 0 1 0",

    # alphabets, expecting that 9 is close to letters
    "a t g q e h 9 u f",
    "e q y u o i p s",
    "q o 9 p l k j o k k o p",
    "h g y i u t t a e q",
    "i k d q r e 9 e a d",
    "o p d g 9 s a f g a",
    "i u y g h k l a s w",
    "o l u y a o g f s",
    "o p i u y g d a s j d l",
    "u k i l o 9 l j s",
    "y g i s h k j l f r f",
    "i o h n 9 9 d 9 f a 9",
]

这就是最终的实验结果。可以看到内奸9已经被暴露啦~

CBOW w2v result

我们就开始写模型吧,这个模型还相对比较简单。tensorflow 2.0 版本之后,我比较推荐使用keras来编写模型。如果对这一套还不是很熟悉的朋友, 可以参考一下我写的一些简易 TF2.+ 的代码, 如果你会 TF1.+,或者 Pytorch,那么你可能只需要10分钟的入门时间。

下面就是CBOW中的词向量组件了,最为核心的就是 self.embeddings,词向量就存在于这里里面。之后我们会将它可视化

from tensorflow import keras
import tensorflow as tf

class CBOW(keras.Model):
    def __init__(self, v_dim, emb_dim):
        super().__init__()
        self.embeddings = keras.layers.Embedding(
            input_dim=v_dim, output_dim=emb_dim,  # [n_vocab, emb_dim]
            embeddings_initializer=keras.initializers.RandomNormal(0., 0.1),
        )
        ...

接下来就是模型的预测是如何进行的了,我们用classcall功能定义模型的前向预测部分。说白了,其实也就是把预测时的embedding词向量给拿出来, 然后求一个词向量平均,这样输出就够了。在用这个平均的向量预测一下目标值。

class CBOW(keras.Model):
    ...
    def call(self, x, training=None, mask=None):
        # x.shape = [n, skip_window*2]
        o = self.embeddings(x)          # [n, skip_window*2, emb_dim]
        o = tf.reduce_mean(o, axis=1)   # [n, emb_dim]
        return o

在求loss的时候我们稍微做一些手脚,这样可以在训练拥有庞大词汇的模型上有好处。使用nce_loss能够大大加速softmax求loss的方式,它不关心所有词汇loss, 而是抽样选取几个词汇用来传递loss,因为如果考虑所有词汇,那么当词汇量大的时候,会很慢。

class CBOW(keras.Model):
    def __init__(self, v_dim, emb_dim):
        ...
        # noise-contrastive estimation
        self.nce_w = self.add_weight(
            name="nce_w", shape=[v_dim, emb_dim],
            initializer=keras.initializers.TruncatedNormal(0., 0.1))  # [n_vocab, emb_dim]
        self.nce_b = self.add_weight(
            name="nce_b", shape=(v_dim,),
            initializer=keras.initializers.Constant(0.1))  # [n_vocab, ]

        self.opt = keras.optimizers.Adam(0.01)

    # negative sampling: take one positive label and num_sampled negative labels to compute the loss
    # in order to reduce the computation of full softmax
    def loss(self, x, y, training=None):
        embedded = self.call(x, training)
        return tf.reduce_mean(
            tf.nn.nce_loss(
                weights=self.nce_w, biases=self.nce_b, labels=tf.expand_dims(y, axis=1),
                inputs=embedded, num_sampled=5, num_classes=self.v_dim))

    def step(self, x, y):
        with tf.GradientTape() as tape:
            loss = self.loss(x, y, True)
            grads = tape.gradient(loss, self.trainable_variables)
        self.opt.apply_gradients(zip(grads, self.trainable_variables))
        return loss.numpy()

这就完成了整个模型的搭建啦。我加上我写好的处理数据组件,就能直接开始训练啦。

from utils import process_w2v_data

def train(model, data):
    for t in range(2500):
        bx, by = data.sample(8)
        loss = model.step(bx, by)
        if t % 200 == 0:
            print("step: {} | loss: {}".format(t, loss))


if __name__ == "__main__":
    d = process_w2v_data(corpus, skip_window=2, method="cbow")
    m = CBOW(d.num_word, 2)
    train(m, d)

值得注意的是,在数据处理方面,我没在代码中特别强调,但是可以看出,process_w2v_data() 这个功能需要确定我们的skip_window这个参数, 这个参数的作用就是,在我们要预测的词周围,我们要选取多少个词作为他的输入。如果skip_window=1则意味着我们选取这个词前后各1个词作为网络的输入, 如果skip_window=2则意味着我们选取这个词前后各2个词,以此类推。具体代码可以看到我写在 utils.py 当中的代码。

最后如果将学到的embedding向量结果进行可视化,就有了我们之前在文章前面看到的那个样子,观看一下字母和数字的距离,我们也能知道CBOW学会了这些词之间的内在联系。

思考括展

我们已经能成功的训练出这些词向量了,除了可视化展示他们,我们还能怎么使用这些向量呢?在这里我举一个例子。

句子是由词语组成的,那么有一种理解句子的方式,就是将这个句子中所有词语的词向量都加起来,然后就变成了句子的理解。 不过这种空间上的向量相加,从直观上理解,就不是特别成立,因为它加出来以后,还是在这个词汇空间中的某个点, 你说它是句向量吧,好像也不行,说它是一个词的理解吧,好像也不对。

所以更常用的方式是将这些训练好的词向量当做预训练模型,然后放入另一个神经网络(比如RNN)当成输入,使用另一个神经网络加工后,训练句向量。

总结

词向量是词语的向量表示,我们的计算机最擅长使用这种数字化向量表示来计算和理解词语。所以词向量对于理解词语甚至是句子都有很强的适用性。 下一节我们来见识一下另一种词向量训练方法Skip-Gram,我个人更喜欢后者, 因为后者没有我觉得说不太通的词向量相加过程。

降低知识传递的门槛

莫烦的对教育的态度是: 降低知识传递的门槛,不希望给"学习"设置金钱障碍。 这是我花大量业余时间贡献 免费 AI分享的原因。 通过 【赞助】 能及时让我看到你对 【莫烦态度】 的认同,我也更有理由坚持下去。

如果你当前目标是找工作或者转行AI,想接受更加丰富的教学资源、培训辅导体验,我想推荐我的朋友 七月在线 给你, 通过这个 【莫烦Python为你提供的注册链接】, 你将可以获得莫烦专门为你协商的课程优惠券。祝你找/换工作顺利~