建模角度理解word embedding及tensorflow实现

1、 前言

本篇主要介绍关键词的向量表示,也就是大家熟悉的word embedding。自Google 2013 年开源word2vec算法程序以后,它的简单、高效、实用,很快引起业界众人的关注和应用,为搜索引擎、[广告系统-谷歌的wide & deep learning][2]、[推荐系统][1]等互联网服务提供新的基础技术和思路。

何为Embedding?

开篇之前首先需要明白一个概念何为Embedding?Embedding可以看作是数学上的一个空间映射(Mapping):map( lambda y: f(x) ),该映射的特点是:单射(在数学里,单射函数为一函数,其将不同的引数连接至不同的值上。更精确地说,函数f被称为是单射时,对每一值域内的y,存在至多一个定义域内的x使得f(x) = y。)、映射前后结构不变,对应到word embedding概念中可以理解为寻找一个函数或映射,生成新的空间上的表达,把单词one-hot所表达的X空间信息映射到Y的多维空间向量。
接下来,将以模型的角度分解embedding映射函数及新空间内表达的建模过程:

非监督的“监督学习”

从应用角度,新空间内映射函数的学习方法不需要大量的人工标记样本就可以得到质量还不错的embedding向量,没有具体的应用任务导向,从这个角度可以看作非监督的学习过程,而从建模角度,向量提取的建模过程是 个分类模型,又可以看做是监督学习,只是这个监督没有实际的监督意义,当然后来有的应该将word2vec的前段表达方式喂给标注的过文本,形成真正意义上的监督学习,如Facebook的FastText。

2、一层隐层神经网络

带有一个隐层的神经网络有以下普遍特性:理论上给定足够该隐层节点数,这个隐层可以近似拟合任意函数,本质上,隐层是上一层的嵌入(Embedding)函数的近似表示,而且可以被用做lookup表(后面会介绍),word2vec也是基于该层去找到输入word的嵌入向量表示,然后再建立下一层和当前层的连接(connections),来控制目标函数的误差。【进一步抽象,如果从统计的角度,其实不同层之间的统计关系是一种递归的广义线性关系(递归广义线性模型),每一层通过线性组合对前一层进行变换,然后以一些非线性连接函数(不同函数对应output label不同的统计分布,比如softmax对应多项目分布,sigmoid对应二项分布等)得到非线性结果喂给下一层,参见图rglm】


建模角度理解word embedding及tensorflow实现_第1张图片
model_net.png
建模角度理解word embedding及tensorflow实现_第2张图片
rglm.png

3、Embedding函数

从前面的定义,我们期望在隐层中找到一个/组嵌入函数W(这里采用lookup table的方式),使得![][3]具体的,假设指定固定的向量维度,W("篮球")=(0.2, -0.4, 0.7, ...),W("苹果")=(0.0, 0.6, -0.1, ...),W初始化时可以赋值给每个维度一个随机数,并通过与output层连接建立学习模型/任务后得到有意义的向量。

4、建模

接下来来看看如何建立和训练模型。

  • 数据准备
    为给模型准备数据,我们首先需要定义或获取n个样本:![][4]
    假如我们有一个句子“姚明 的 篮球 打得 很不错”。常规方式是首先由统计语言模型,由中间词预测周围词(SKIP-GRAM),或由周围词预测中间词(CBOW)等方式,然后以指定的窗口向前推进,以SKIP-GRAM方式为例,假设推进窗口为2,我们可以得到样本对:("篮球","的"),("篮球","姚明"),("篮球","打得"),("篮球","很不错"),X skip至"打得"时,得到样本对 :("打得","篮球"),("打得","的"),("打得","很不错"),以此类推...我们可以得到用于模型的训练样本。

  • 样本表示
    样本拆解出来了,接下来如何用数值来表达这些样本对呢?常用的办法是将所有的训练数据,即“word”对抽取出唯一不重复的单词来构建词典表(vocabulary),然后将样本数据中的“word”表达成one-hot编码,编码时只对有值的位置上为1其他位置均为0,以上面例子为例,“姚明 的 篮球 打得 很不错”。基于这个句子可以构建维度为5的词典表:{"姚明":0,"":1,"的":2,"篮球":3,"打得":4,"很不错":5},那么训练样本("篮球","姚明")即可表达为([0,0,1,0,0],0),看起来比较像常规的多分类数据了,这里为了好理解Y表示成了位置编号,后续在模型中仍以one-hot向量表达。

  • 各层条件分布
    神经网络基于这些训练样本将会输出一个概率分布,这个概率代表着我们的词典中的每个词是output word的可能性。更一般的,假设隐层有K个节点(即生成word对应vector向量的维度),对每个样本,我们需要做两件事情:

    • 给定隐层后预测output word的概率,即需要建个模型来估计![][5]
    • 将观测到的input word喂给隐层嵌入函数,得到隐层的概率分布,![][6]用连接函数表达即上面提到的(常见的一般会是K个关于x线性组合的方程组,后面会讲到为何不用该方式)![][3]

    接下来我们需要构建整体的似然函数进行优化:

  • 目标函数
    分别建立input层-隐层及隐层-output层的连接函数(RGLM),input层和隐层的函数上面已给出,如果假设p(y|w)为正态分布,则 log-likelihood loss便是(negative) L2 loss:![][7],如果假设p(y|w)为多项分布,则likelihood loss便是softmax loss:![][8]从训练样本可以看出,output层为多分类,即隐层-output可采用softmax loss.
    为了准确预测output word,该网络需要根据上述损失函数学习参数矩阵W和R(output层),实际上,对于我们来说,整个学习任务是为了学习隐层的W函数,即隐层节点参数。当然对于其他任务,比如神经网络推荐或Fasttext,网络构造过程类似,只是学习的任务是学习输出层的参数和结构。

  • 模型训练
    常规优化方法会采用梯度下降和反向传播,由上面的样本定义,我们的训练样本中input和output均以one-hot表示,向量极其稀疏(通常完整字典表会是几十万维,假设200000),仅有一个位置的数值为1,其余均为0,如果input到隐层的嵌入函数采用常见方式的话,假设节点数即嵌入向量维度为200,则隐层参数矩阵每个样本的迭代将会是1x200000的向量和200000x200矩阵的相乘,显然会带来巨大计算资源的消耗,其实每个样本的隐层参数仅需要根据one-hot向量中数值为1的索引对应的隐层参数参数矩阵的该索引行对应的向量取出即可:


    建模角度理解word embedding及tensorflow实现_第3张图片
    embedding.png

    经过抽象后我们可以得到上面定义的Embedding函数/参数矩阵:


    建模角度理解word embedding及tensorflow实现_第4张图片
    embedding-abstract.png

    这种方式其实联系上面提到的lookup table就容易理解了,即模型中的隐层权重矩阵便成了一个”查找表“(lookup table),进行矩阵计算时,只需要直接去查输入的one-hot向量中提取非零位置的索引,在隐层的对应行输出就是每个输入单词的“嵌入词向量”,该过程即完成了嵌入的动作。
    对于输出层:
    经过隐层的嵌入计算,input word会被映射为1x200的dense向量,再喂给输出层经过softmax的分类器的计算,对随机给定任意output word的嵌入向量计算其预测概率:![][8],这样基于同一input word,替换不同的beta(output word的嵌入向量)得到不同output word的预测概率。

    至此,数据的表示及目标损失函数的定义以及模型训练过程已拆解完毕。接下来,再看看训练性能提升和优化的方法。

5、抽样

基于上面的拆解,我们会发现其实训练过程涉及的参数数量会非常庞大,以上面的200000个单词的字典表为例,隐层嵌入200维的词向量,那么每次迭代的输入-隐层权重矩阵和隐层-输出层的权重矩阵都会有 200000 x 200 = 4000万个权重,在如此庞大的神经网络中进行梯度下降是相当慢的,而且需要大量的训练数据来调整这些权重并且避免过拟合。所以对性能的要求仍然很高,虽然上面已经采用lookup table的方式简化了一些计算,针对这个问题,Word2Vec的作者在论文提出了有效的方法,叫“negative sampling”,每个训练样本的训练只会更新一小部分的模型权重,从而降低计算负担,甚至是词向量的质量。基于对假设是,我们的数据中存在大量冗余和噪音,举例:对于“的”这种常用高频单词,我们会发现一些问题:当我们得到成对的单词训练样本时,**("的", "篮球") *这样的训练样本并不会给我们提供关于“篮球”更多的语义信息,因为“的”这样的噪音词在大部分单词的上下文中几乎都会出现。由于在语料中“的”这样的常用词出现概率很大,因此我们将会有大量的(”的“,...)这样的训练样本,而这些样本数量远远超过了我们学习“的”这个词向量所需的训练样本数。所以在设计抽样方法的时候可以对这样的样本直接排除在训练样本之外,对于其他样本对随机抽取少量的负样本进行参数的更新,而不是对one-hot向量中所有200000个位置对样本都进行计算,从而大大提高训练效率。
上面叙述的有点繁杂,总结起来就是在对给定input word计算softmax时,不去更新所有词表中word的输出概率,而是从该样本的output word之外随机抽样有限个(比如只抽样5个word)作为负样本计算其概率,进一步进行梯度和参数的更新。也就是说通过负样本抽样对于每次训练只更新(5+1)个beta向量对应的参数,也就是200
6=1200个参数,这样与4000万个相比,需要更新的参数占比仅为0.003%,效率提升可想而知。

6、基于tensorflow的实现

  • 数据加载
import os
def load_w2c_textcn_dataset(path='./data/'):
    """
    Returns
    --------
    word_list_all : a list
        a list of string (word).\n
     要求:中文语料需要先分词  
    """

    print("Load or Download chinese text corpus Dataset> {}".format(path))

    filename = 'wiki_cn.cut'
    word_list_all=[]
    with open(os.path.join(path, filename)) as f:
        for line in f:
            word_list=line.strip().split()
            for idx, word in enumerate(word_list):
                word_list[idx] = word_list[idx].decode('utf-8') 
                #print word_list[idx]
                word_list_all.append(word_list[idx])
    return word_list_all
words=load_w2c_textcn_dataset(path='./data/')
print len(words)
  • 字典构建
import collections
vocabulary_size = 200000
count = [['UNK', -1]]
count.extend(collections.Counter(words).most_common(vocabulary_size - 1))
dictionary = dict()

for word, _ in count:
    dictionary[word] = len(dictionary)
data = list()
unk_count = 0
for word in words:
    if word in dictionary:
        index = dictionary[word]
    else:
        index = 0  # dictionary['UNK']
        unk_count = unk_count + 1
    data.append(index)

count[0][1] = unk_count
reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys())) 
del words
  • batch数据生成器
data_index = 0

def generate_batch(batch_size, num_skips, skip_window):
    global data_index
    batch = np.ndarray(shape=(batch_size), dtype=np.int32)
    labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)
    span = 2 * skip_window + 1  # [ skip_window target skip_window ]
    buf = collections.deque(maxlen=span)
    for _ in xrange(span):
        buf.append(data[data_index])
        data_index = (data_index + 1) % len(data)
    for i in xrange(batch_size // num_skips):
        target = skip_window  # target label at the center of the buffer
        targets_to_avoid = [ skip_window ]
        for j in xrange(num_skips):
            while target in targets_to_avoid:
                target = random.randint(0, span - 1)
            targets_to_avoid.append(target)
            batch[i * num_skips + j] = buf[skip_window]
            labels[i * num_skips + j, 0] = buf[target]
        buf.append(data[data_index])
        data_index = (data_index + 1) % len(data)
    return batch, labels
  • 模型构建
import tensorflow as tf
import collections
import numpy as np
batch_size = 128
embedding_size = 128  # 生成向量维度.
skip_window = 2       # 左右窗口.
num_skips = 2        # 同一个keyword产生label的次数.
num_sampled = 64      # 负样本抽样数.

graph = tf.Graph()

with graph.as_default(), tf.device('/cpu:0'):
    train_dataset = tf.placeholder(tf.int32, shape=[batch_size])
    train_labels  = tf.placeholder(tf.int32, shape=[batch_size, 1])
  
    embeddings = tf.Variable(tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))
    softmax_weights = tf.Variable(
        tf.truncated_normal([vocabulary_size, embedding_size], stddev=1.0/np.sqrt(embedding_size)))
    softmax_biases = tf.Variable(tf.zeros([vocabulary_size]))
  
    embed = tf.nn.embedding_lookup(embeddings, train_dataset)
    loss = tf.reduce_mean(
        tf.nn.sampled_softmax_loss(weights=softmax_weights, biases=softmax_biases, inputs=embed,
                                   labels=train_labels, num_sampled=num_sampled, num_classes=vocabulary_size))

    optimizer = tf.train.AdagradOptimizer(1.0).minimize(loss)

    norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True))
    normalized_embeddings = embeddings / norm
  • 模型训练
num_steps = 500001
import random
with tf.Session(graph=graph) as session:
    tf.global_variables_initializer().run()
    average_loss = 0
    for step in range(num_steps):
        batch_data, batch_labels = generate_batch(batch_size, num_skips, skip_window)
        feed_dict = {train_dataset : batch_data, train_labels : batch_labels}
        _, l = session.run([optimizer, loss], feed_dict=feed_dict)
        average_loss += l
        if step % 100000 == 0 and step > 0:
            print('Average loss at step %d: %f' % (step, average_loss / 100000))
            average_loss = 0
    word2vec = normalized_embeddings.eval()
  • 最近邻
distances = -word2vec[dictionary[u'数据']].reshape((1, -1)).dot(word2vec.T)
inds = np.argsort(distances.ravel())[1:6]
print(' '.join([reverse_dictionary[i] for i in inds]))
----------------------------------------------
资料 统计 显示 信息 证据

[1] Peter McCullagh, John A Nelder, Generalized linear models., , 1989
[2] The seminal paper, A Neural Probabilistic Language Model (Bengio, et al. 2003) has a great deal of insight about why word embeddings are powerful.
[3]:https://erikbern.com/2014/06/28/recurrent-neural-networks-for-collaborative-filtering.html
[4]:https://research.googleblog.com/2016/06/wide-deep-learning-better-together-with.html?utm_source=tuicool&utm_medium=referral

你可能感兴趣的:(建模角度理解word embedding及tensorflow实现)