词嵌入技术解析(一)

1. 词向量介绍

在讨论词嵌入以前,先要理解词向量的表达形式,注意,这里的词向量不是指Word2Vec。关于词向量的表达,现阶段采用的主要有One hot representation和Distributed representation两种表现形式。html

1.1 One hot representation

顾名思义,采用独热编码的方式对每一个词进行表示。网络

例如,一段描述“杭州和上海今天有雨”,经过分词工具能够把这段描述分为[‘杭州’,‘和’,‘上海’,今天’,‘有’,‘雨’],所以词表的长度为6,那么‘杭州’、‘上海’、'今天'的One hot representation分别为[1 0 0 0 0 0],[0 0 1 0 0 0],[0 0 0 1 0 0]。app

能够看到,One hot representation编码的每一个词都是一个维度,元素非0即1,且词与词之间彼此相互独立。dom

1.2 Distributed representation

Distributed representation在One hot representation的基础上考虑到词与词之间的联系,例如词义、词性等信息。每个维度元素再也不是0或1,而是连续的实数,表示不一样的程度。Distributed representation 又包含了如下三种处理方式:分布式

  • 基于矩阵的分布表示。,矩阵中的一行,就成为了对应词的表示,这种表示描述了该词的上下文的分布。因为分布假说认为上下文类似的词,其语义也类似,所以在这种表示下,两个词的语义类似度能够直接转化为两个向量的空间距离。
  • 基于聚类的分布表示。
  • 基于神经网络的分布表示。

而咱们如今常说的Distributed representation主要是基于神经网络的分布式表示的。例如‘杭州’、‘上海’的Distributed representation分别为[0.3 1.2 0.8 0.7] 和 [0.5 1.2 0.6 0.8 ] 。ide

因此对于词嵌入,咱们能够理解为是对词的一种分布式表达方式,而且是从高维稀疏向量映射到了相对低维的实数向量上。函数

2. 为何使用词嵌入

词嵌入,每每和Distributed representation联系在一块儿。这里主要从计算效率、词关系和数量这三点说明。工具

  1. 计算效率。采用One hot representation的每一个词的向量长度是由词汇表的数量决定,若是词汇表数量很大,那么每一个词的长度会很长,同时,因为向量元素只有一个元素为1,其他元素为0,因此,每一个词的向量表达也会很是稀疏。而对于海量的词语来说,计算效率是须要考虑的。
  2. 词关系。和One hot representation相比,Distributed representation可以表达词与词之间的关系。
  3. 数量。对于把词语做为模型输入的任务,对于类似的词语,能够经过较少样本完成目标任务的训练,而这是One hot representation所没法企及的优点。

3. Language Models

因为词嵌入目的是为了能更好地对NLP的输入作预处理。因此在对词嵌入技术做进一步讨论以前,有必要对语言模型的发展作一些介绍。学习

3.1 Bag of words model

Bag of words model又称为词袋模型,顾名思义,一段文本能够用一个装着这些词的袋子来表示。词袋模型一般将单词和句子表示为数字向量的形式,其中向量元素为句子中此单词在词袋表出现的次数。而后将数字向量输入分类器(例如Naive Bayes),进而对输出进行预测。这种表示方式不考虑文法以及词的顺序。测试

例如如下两个句子:

  1. John likes to watch movies. Mary likes movies too.
  2. John also likes to watch football games.

基于以上两个句子,能够建构词袋表:"John""likes""to""watch""movies""also""football""games""Mary""too" ]

因为词袋表的长度为10,因此每一个句子的数字向量表示长度也为10。下面是每一个句子的向量表示形式:

  1. [1, 2, 1, 1, 2, 0, 0, 0, 1, 1]
  2. [1, 1, 1, 1, 0, 1, 1, 1, 0, 0]

Bag of words model的优缺点很明显:优势是基于频率统计方法,易于理解。缺点是它的假设(单词之间彻底独立)过于强大,没法创建准确的模型。

3.2 N-Gram model

N-gram model的提出旨在减小传统Bag of words model的一些强假设。

语言模型试图预测在给定前t个单词的前提下观察t第 + 1个单词w t + 1的几率:

利用几率的链式法则,咱们能够计算出观察整个句子的几率:

能够发现,估计这些几率多是困难的。所以能够用最大似然估计对每一个几率进行计算:

然而,即便使用最大似然估计方法进行计算,仍然很是困难:咱们一般没法从语料库中观察到足够多的数据,而且计算长度仍然很长。所以采用了马尔可夫链的思想。

马尔可夫链规定:系统下一时刻的状态仅由当前状态决定,不依赖于以往的任何状态。即第t + 1个单词的发生几率表示为:

所以,一个句子的几率能够表示为:

一样地,马尔可夫假设能够推广到:系统下一时刻的状态仅由当前0个、1个、2个...n个状态决定这就是N-gram model的N的意思:对下一时刻的状态设置当前状态的个数。下面分别给出了unigram(一元模型)和bigram(二元模型)的第t + 1个单词的发生几率:

能够发现,N-Gram model 在Bag of words model的基础上,经过采用马尔科夫链的思想,减小了几率计算的复杂度,同时考虑了单词间的相关性。

3.3 Word2Vec Model

Word2Vec模型实际上分为了两个部分,第一部分为训练数据集的构造,第二部分是经过模型获取词嵌入向量,即word embedding。

Word2Vec的整个建模过程实际上与自编码器(auto-encoder)的思想很类似,即先基于训练数据构建一个神经网络,当这个模型训练好之后,并不会用这个训练好的模型处理新任务,而真正须要的是这个模型经过训练数据所更新到的参数。

关于word embedding的发展,因为考虑上下文关系,因此模型的输入和输出分别是词汇表中的词组成,进而产生出了两种词模型方法:Skip-Gram和CBOW。同时,在隐藏层-输出层,也从softmax()方法演化到了分层softmax和negative sample方法。

因此,要拿到每一个词的词嵌入向量,首先须要理解Skip-Gram和CBOW。下图展现了CBOW和Skip-Gram的网络结构:

本文以Skip-Gram为例,来理解词嵌入的相关知识。Skip-Gram是给定input word来预测上下文。咱们能够用小学英语课上的造句来帮助理解,例如:“The __________”。

关于Skip-Gram的模型结构,主要分为几下几步:

  1. 从句子中定义一个中心词,即Skip-Gram的模型input word
  2. 定义skip_window参数,用于表示从当前input word的一侧(左边及右边)选取词的数量。
  3. 根据中心词和skip_window,构建窗口列表。
  4. 定义num_skips参数,用于表示从当前窗口列表中选择多少个不一样的词做为output word。

假设有一句子"The quick brown fox jumps over the lazy dog" ,设定的窗口大小为2(window\_size=2),也就是说仅选中心词(input word)先后各两个词和中心词(input word)进行组合。以下图所示,以步长为1对中心词进行滑动,其中蓝色表明input word,方框表明位于窗口列表的词。

因此,咱们可使用Skip-Gram构建出神经网络的训练数据。

咱们须要明白,不能把一个词做为文本字符串输入到神经网络中,因此咱们须要一种方法把词进行编码进而输入到网络。为了作到这一点,首先从须要训练的文档中构建出一个词汇表,假设有10,000个各不相同的词组成的词汇表。那么须要作的就是把每个词作One hot representation。此外神经网络的输出是一个单一的向量(也有10000个份量),它包含了词汇表中每个词随机选择附近的一个词的几率。

3.4 Skip-Gram网络结构

下图是须要训练的神经网络结构。左侧的神经元Input Vector是词汇表中进行One hot representation后的一个词,右侧的每个神经元则表明着词汇表的每个词。实际上,在对该神经网络feed训练数据进行训练时,不只输入词input word(中心词)是用One hot representation表示,输出词output word也是用One hot representation进行表示。但当对此网络进行评估预测时,输出向量其实是经过softmax()函数计算获得的一个词汇表全部词的几率分布(即一堆浮点值,而不是一个One hot representation)。

3.5 Word2Vec Model隐藏层

假设咱们正在学习具备300个特征的词向量。所以,隐藏层将由一个包含10,000行(每一个单词对应一行)和300列(每一个隐藏神经元对应一列)的权重矩阵来表示。(注:谷歌在其发布的模型中的隐藏层使用了300个输出(特征),这些特征是在谷歌新闻数据集中训练出来的(您能够从这里下载)。特征的数量300则是模型进行调优选择后的“超参数”)。

下面左右两张图分别从不一样角度表明了输入层-隐层的权重矩阵。

从左图看,每一列表明一个One hot representation的词和隐层单个神经元链接的权重向量。从右图看,每一行实际上表明了每一个词的词向量,或者词嵌入。

因此咱们的目标就是学习输入层-隐藏层的权矩阵,而隐藏层-输出层的部分,则是在模型训练完毕后不须要保存的参数。这一点,与自编码器的设计思想是相似的。

你可能会问本身,难道真的分别要把每个One hot representation的词(1 x 10000)与一个10000 x 300的权矩阵相乘吗?实际上,并非这样。因为One hot representation的词具备只有一个元素这为1,其他元素值为0的特性,因此能够经过查找One hot representation中元素为1的位置索引,进而得到对应要乘以的10000 x 300的权矩阵的向量值,从而解决计算速度缓慢的问题。下图的例子,可帮助咱们进一步理解。

能够看到,One hot representation中元素为1的位置索引为3,因此只须要乘以10000 x 300的权矩阵中位置索引一样为3的向量值便可获得相应的输出。

3.6 Word2Vec Model输出层

下面是计算“car”这个单词的输出神经元的输出的例子:

4. 基于Tensorflow的Skip-Gram极简实现

网上找了一些Tensorflow版本的skip-gram实现,但都有一个问题,输入单词并无按照论文的要求作One hot representation,不知道是否是出于计算速度方面的考虑。所以,本小节的代码仍是遵循原论文的描述,对输入单词及输出单词首先作了One hot representation。

首先,是训练数据的构造,包括skip_window上下文参数、词的One hot representation以及中心词、输出词对的构造。

import numpy as np

corpus_raw = 'He is the king . The king is royal . She is the royal  queen '

# 大小写转换
corpus_raw = corpus_raw.lower()
words = []
for word in corpus_raw.split():
    if word != '.':
        words.append(word)


# 建立一个字典,将单词转换为整数,并将整数转换为单词。
words = set(words)
word2int = {}
int2word = {}
vocab_size = len(words)
for i, word in enumerate(words):
    word2int[word] = i
    int2word[i] = word

raw_sentences = corpus_raw.split('.')
sentences = []
for sentence in raw_sentences:
    sentences.append(sentence.split())

# 构造训练数据

WINDOW_SIZE = 2
data = []
for sentence in sentences:
    for word_index, word in enumerate(sentence):
        for nb_word in sentence[max(word_index - WINDOW_SIZE, 0): min(word_index + WINDOW_SIZE, len(sentence)) + 1]:
            if nb_word != word:
                data.append([word, nb_word])


# one-hot编码
def to_one_hot(data_point_index, vocab_size):
    """
    对单词进行one-hot representation
    :param data_point_index: 单词在词汇表的位置索引
    :param vocab_size: 词汇表大小
    :return: 1 x vocab_size 的one-hot representatio
    """
    temp = np.zeros(vocab_size)
    temp[data_point_index] = 1
    return temp


# 输入单词和输出单词
x_train = [] 
y_train = []  

for data_word in data:
    x_train.append(to_one_hot(word2int[data_word[0]], vocab_size))
    y_train.append(to_one_hot(word2int[data_word[1]], vocab_size))

其次,是Tensorflow计算图的构造,包括输入输出的定义、输入层-隐藏层,隐藏层-输出层的构造以及损失函数、优化器的构造。最后输出每一个词的word embedding。具体代码以下所示:

import tensorflow as tf

# 定义输入、输出占位符
x = tf.placeholder(tf.float32, shape=(None, vocab_size))
y_label = tf.placeholder(tf.float32, shape=(None, vocab_size))

# 定义word embedding向量长度
EMBEDDING_DIM = 5

# 隐藏层构造
W1 = tf.Variable(tf.random_normal([vocab_size, EMBEDDING_DIM]))
b1 = tf.Variable(tf.random_normal([EMBEDDING_DIM]))  # bias
hidden_representation = tf.add(tf.matmul(x, W1), b1)

# 输出层构造
W2 = tf.Variable(tf.random_normal([EMBEDDING_DIM, vocab_size]))
b2 = tf.Variable(tf.random_normal([vocab_size]))
prediction = tf.nn.softmax(tf.add(tf.matmul(hidden_representation, W2), b2))

# 构建会话并初始化全部参数
sess = tf.Session()
init = tf.global_variables_initializer()
sess.run(init)

# 定义损失,这里只是采用常规DNN+softmax,未使用分层softmax和negative sample
cross_entropy_loss = tf.reduce_mean(-tf.reduce_sum(y_label * tf.log(prediction), reduction_indices=[1]))

# 优化器
train_step = tf.train.GradientDescentOptimizer(0.1).minimize(cross_entropy_loss)

n_iters = 10000
# train for n_iter iterations

for _ in range(n_iters):
    sess.run(train_step, feed_dict={x: x_train, y_label: y_train})
    # print('loss is : ', sess.run(cross_entropy_loss, feed_dict={x: x_train, y_label: y_train}))

# 词嵌入 word embedding
vectors = sess.run(W1 + b1)
print('word embedding:')
print(vectors)

上述代码的计算图能够简单表示为如下形式:

最后,打印出每一个单词的词嵌入向量以下所示:

 当词嵌入向量训练完成后,咱们能够进行一个简单的测试,这里经过计算词嵌入向量间的欧氏距离寻找相近的词:

# 测试

def euclidean_dist(vec1, vec2):
    """欧氏距离"""
    return np.sqrt(np.sum((vec1 - vec2) ** 2))


def find_closest(word_index, vectors):
    min_dist = 10000  # to act like positive infinity
    min_index = -1
    query_vector = vectors[word_index]
    for index, vector in enumerate(vectors):
        if euclidean_dist(vector, query_vector) < min_dist and not np.array_equal(vector, query_vector):
            min_dist = euclidean_dist(vector, query_vector)
            min_index = index
    return min_index


print('与 king 最接近的词是:', int2word[find_closest(word2int['king'], vectors)])
print('与 queen 最接近的词是:', int2word[find_closest(word2int['queen'], vectors)])
print('与 royal 最接近的词是:', int2word[find_closest(word2int['royal'], vectors)])

下面是输出的测试结果:

5. 总结

  1. 词嵌入是一种把词从高维稀疏向量映射到了相对低维的实数向量上的表达方式。
  2. Skip-Gram和CBOW的做用是构造神经网络的训练数据。
  3. 目前设计的网络结构其实是由DNN+softmax()组成。
  4. 因为每一个输入向量有且仅有一个元素为1,其他元素为0,因此计算词嵌入向量实际上就是在计算隐藏层的权矩阵。
  5. 对于单位矩阵的每一维(行)与实矩阵相乘,能够简化为查找元素1的位置索引从而快速完成计算。

6. 结束了吗?

仔细阅读代码,咱们发现prediction时,使用的是softmax()。即输入词在输出层分别对词汇表的每个词进行几率计算,若是在海量词汇表的前提下,计算效率是否须要考虑在内?有没有更快的计算方式呢?

此外,本文第3节提到的分层softmax是什么?negative samples又是什么?Huffman code又是怎样使用的?关于这些问题的思考,请关注:词嵌入的那些事儿(二)

7. 参考资料

[1] Word2Vec Tutorial - The Skip-Gram Model

相关文章
相关标签/搜索