在讨论词嵌入以前,先要理解词向量的表达形式,注意,这里的词向量不是指Word2Vec。关于词向量的表达,现阶段采用的主要有One hot representation和Distributed representation两种表现形式。html
顾名思义,采用独热编码的方式对每一个词进行表示。网络
例如,一段描述“杭州和上海今天有雨”,经过分词工具能够把这段描述分为[‘杭州’,‘和’,‘上海’,今天’,‘有’,‘雨’],所以词表的长度为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
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
因此对于词嵌入,咱们能够理解为是对词的一种分布式表达方式,而且是从高维稀疏向量映射到了相对低维的实数向量上。函数
词嵌入,每每和Distributed representation联系在一块儿。这里主要从计算效率、词关系和数量这三点说明。工具
因为词嵌入目的是为了能更好地对NLP的输入作预处理。因此在对词嵌入技术做进一步讨论以前,有必要对语言模型的发展作一些介绍。学习
Bag of words model又称为词袋模型,顾名思义,一段文本能够用一个装着这些词的袋子来表示。词袋模型一般将单词和句子表示为数字向量的形式,其中向量元素为句子中此单词在词袋表出现的次数。而后将数字向量输入分类器(例如Naive Bayes),进而对输出进行预测。这种表示方式不考虑文法以及词的顺序。测试
例如如下两个句子:
基于以上两个句子,能够建构词袋表:[ "John", "likes", "to", "watch", "movies", "also", "football", "games", "Mary", "too" ]
因为词袋表的长度为10,因此每一个句子的数字向量表示长度也为10。下面是每一个句子的向量表示形式:
Bag of words 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的基础上,经过采用马尔科夫链的思想,减小了几率计算的复杂度,同时考虑了单词间的相关性。
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的模型结构,主要分为几下几步:
假设有一句子"The quick brown fox jumps over the lazy dog" ,设定的窗口大小为2(),也就是说仅选中心词(input word)先后各两个词和中心词(input word)进行组合。以下图所示,以步长为1对中心词进行滑动,其中蓝色表明input word,方框表明位于窗口列表的词。
因此,咱们可使用Skip-Gram构建出神经网络的训练数据。
咱们须要明白,不能把一个词做为文本字符串输入到神经网络中,因此咱们须要一种方法把词进行编码进而输入到网络。为了作到这一点,首先从须要训练的文档中构建出一个词汇表,假设有10,000个各不相同的词组成的词汇表。那么须要作的就是把每个词作One hot representation。此外神经网络的输出是一个单一的向量(也有10000个份量),它包含了词汇表中每个词随机选择附近的一个词的几率。
下图是须要训练的神经网络结构。左侧的神经元Input Vector是词汇表中进行One hot representation后的一个词,右侧的每个神经元则表明着词汇表的每个词。实际上,在对该神经网络feed训练数据进行训练时,不只输入词input word(中心词)是用One hot representation表示,输出词output word也是用One hot representation进行表示。但当对此网络进行评估预测时,输出向量其实是经过softmax()函数计算获得的一个词汇表全部词的几率分布(即一堆浮点值,而不是一个One hot representation)。
假设咱们正在学习具备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的向量值便可获得相应的输出。
下面是计算“car”这个单词的输出神经元的输出的例子:
网上找了一些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)])
下面是输出的测试结果:
仔细阅读代码,咱们发现prediction时,使用的是softmax()。即输入词在输出层分别对词汇表的每个词进行几率计算,若是在海量词汇表的前提下,计算效率是否须要考虑在内?有没有更快的计算方式呢?
此外,本文第3节提到的分层softmax是什么?negative samples又是什么?Huffman code又是怎样使用的?关于这些问题的思考,请关注:词嵌入的那些事儿(二)