word2vec

背景

  最近在用fasttext进行文本分类,说到文本分类就不得不提起word2vec。Word2Vec是由Google的Mikolov等人提出的一个词向量计算模型,其重要意义在于将自然语言转换成了计算机能够理解的向量,能够更好地计算词与词之间的相似性。

One-Hot representation

  一般比较常见的表示词向量的方式就是独热码表示(One-Hot representation),它首先将文章中不同的单词提取出来作为一个词集,然后把词集的大小作为词向量的维度,对于每个具体的词将对应的位置置1。例如,我们又下面5个词汇组成的词集[King,Queue,MAN,WOMAN,CHILD],则词Queue的One-Hot representation表达形式为[0,1,0,0,0],同理WOMAN的表达形式为[0,0,0,1,0]。

  One-Hot representation用来表示词向量简单粗暴,但是有很多问题:

  1. 向量维度和词集大小一致,能够达到百万级,不利于内存计算
  2. 两个不一样的词做内积永远为0,无法表达词之间的相似关系

Distributed representation

  Distributed representation被翻译为稠密表示(对于One-Hot representation只有在词汇对应位置上才置1,所以它是稀疏的,而Distributed representation不需要那么多维度来表示所有词汇,它可能在每个维度上都有值,所以在表现形式上更稠密),它通过训练的方式把每个词映射到一个较短的词向量上来,所有的词向量构成了向量空间,进而可以通过统计学方法来研究词与词之间的关系。

原理

  word2vec接收分词后的文本作为输入,通过神经网络学习输入的每个词汇前后N个单词可能出现的词(这里应该是预测前面所有和后面所有的单词,但是简化为N-gram模型,相关原理详见文章最后的参考链接),最后产生一个稠密向量表示每个词。因为网络能够学习到一个词前后可能会出现什么词,所以通过word2vec产生的词向量可以计算词之间的相似性。

网络结构

  word2vec是一个简单的三层神经网络,即一个输入层、一个隐藏层、一个输出层。输入层就是词的One-Hot representation,隐层的神经网络单元数量就是embedding size,即最终词向量的维度,隐藏层之后不需要使用激活函数,直接接到输出层。输出层是softmax(这里实际是Hierarchical Softmax,为了简化直接使用softmax),得到每个预测结果的概率。其网络结构示意图如下所示。



  假设词集大小为10000,词向量的维度为300,即embedding_size=300

输入层

  一个词的one-hot表达形式,长度为10000,例如[0,0,1,0…,0,0]

隐藏层

  隐藏层的神经元数量就是词向量的长度,隐层参数是一个[10000,300]的矩阵。实际上,这个矩阵就是最后的词向量。其实输入和隐藏层的作用就是把词的one-hot表达形式转化为词向量的表达形式,下面的图更清楚一些



输出层

  输出层就是一个10000维的向量,每个值代表一个词的概率。

skip-gram

  skip-gram核心思想是通过中心词来预测周围词。假设中心词是cat,窗口长度为2,则根据cat预测左边两个词和右边两个词。例如,对于文本“the quick brown fox jumps over the lazy dog”,窗口长度为2时,有

  1. 当中心词为the时,有(the,quick),(the,brown)
  2. 当中心词为quick时,有(quick,the),(quick,brown),(quick,fox)
  3. 当中心词为brown时,有(brown,the),(brown,quick),(brown,fox),(brown,jumps)
  4. 当中心词为fox时,有(fox,quick),(fox,brown),(fox,jumps),(fox,over)

上面的窗口移动了4次,产生了13个样本。

CBOW

  CBOW(continuous bag of words)的核心思想与skip-gram相反,它通过中心词周围的词来预测中心词。例如,对于文本“the quick brown fox jumps over the lazy dog”,窗口长度为2时,有

  1. 当中心词为the时,有([quick,brown],the)
  2. 当中心词为quick时,有([the,brown,fox],quick)
  3. 当中心词为brown时,有([the,quick,fox,jumps],brown)
  4. 当中心词为fox时,有([quick,brown,jumps,over],fox)

上面的窗口移动了4次,但是只产生了4个样本。这时候input是4个词,label是一个词,经过隐藏层之后输入的4个词被映射成了4个EmbedingSize维度的向量,对4个向量求平均后才能作为下一层的输入。

   两个模型相比,skip-gram模型能产生更多的训练样本,抓住更多词与词之间语义上的细节,在语料多足够好的理想条件下,skip-gram模型优于CBOW模型。在语料较少的情况下,难以抓住足够词与词之间之间的细节,CBOW模型求平均的特性反而效果可能更好。

代码实现

  tensorflow官网的示例中给出了word2vec的代码实现,但是该实现太繁琐,现给出简单版实现,详见word2vec

网络结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# coding=utf-8

import os
import tensorflow as tf
from data import SkipGramDataSet
import numpy as np

dataset = SkipGramDataSet(os.path.join(os.path.curdir, 'test.txt'))

VOCAB_SIZE = dataset.vocab_size
print 'vocab_size:{}'.format(VOCAB_SIZE)
EMBEDDING_SIZE = 128
LEARNING_RATE = 0.01

TRAIN_STEPS = 10000

BATCH_SIZE = 32
WINDOW_SIZE = 2


class Word2Vec(object):

def __init__(self):
self.graph = tf.Graph()
with self.graph.as_default():
# 输入层,维度为词集大小
with tf.name_scope('inputs'):
self.x = tf.placeholder(shape=(None, VOCAB_SIZE), dtype=tf.float32)
self.y = tf.placeholder(shape=(None, VOCAB_SIZE), dtype=tf.float32)
# 隐藏层,w1就是词向量
with tf.name_scope('layer1'):
self.W1 = tf.Variable(tf.random_uniform([VOCAB_SIZE, EMBEDDING_SIZE], -1, 1), dtype=tf.float32,
name='w1')
self.b1 = tf.Variable(tf.random_normal([EMBEDDING_SIZE], dtype=tf.float32))
# hidden是把输入的one-hot转化为词向量的结果
hidden = tf.add(self.b1, tf.matmul(self.x, self.W1))
with tf.name_scope('layer2'):
self.W2 = tf.Variable(tf.random_uniform([EMBEDDING_SIZE, VOCAB_SIZE], -1, 1), dtype=tf.float32)
self.b2 = tf.Variable(tf.random_normal([VOCAB_SIZE]), dtype=tf.float32)
# 输出层是softmax求概率之后的结果
self.prediction = tf.nn.softmax(tf.add(tf.matmul(hidden, self.W2), self.b2))
# 损失函数是交叉熵
log = self.y * tf.log(self.prediction)
self.loss = tf.reduce_mean(-tf.reduce_sum(log, reduction_indices=[1], keep_dims=True))
# 梯度下降
self.opt = tf.train.GradientDescentOptimizer(LEARNING_RATE).minimize(self.loss)

# 把词的下标值转化为one-hot表示
def _one_hot_input(self, dataset):
# features和labels记录了词在词集中的位置
features, labels = dataset.generate_batch_inputs(BATCH_SIZE, WINDOW_SIZE)
f, l = [], []
for w in features:
# 产生全0向量
tmp = np.zeros([VOCAB_SIZE])
# 下标位置置1
tmp[w] = 1
f.append(tmp)
for w in labels:
tmp = np.zeros([VOCAB_SIZE])
tmp[w] = 1
l.append(tmp)
return f, l

def train(self, dataset, n_iters, ):
with tf.Session(graph=self.graph) as sess:
sess.run(tf.global_variables_initializer())
for i in range(n_iters):
features, labels = self._one_hot_input(dataset)
predi, loss, w1 = sess.run([self.prediction, self.loss],
feed_dict={self.x: features, self.y: labels})
print 'loss:{}'.format(loss)

skip-gram数据准备

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
class DataSet(object):
def __init__(self, file):
self.file = file
self.data_index = 0
self._build_dataset()

def _build_dataset(self):
if not os.path.exists(self.file):
raise ValueError("file doesn't exists --> {}".format(self.file))
f = open(self.file, 'r')
# 保存词集
self.data = tf.compat.as_str(f.read()).split()
if f:
f.close()
c = collections.Counter(self.data).most_common()
# 计算词集大小
self.vocab_size = len(c)
self.counter = c.insert(0, ('UNK', -1))
self.vocab_size += 1
# 词-下标字典
self.word2id = dict()
# 下标-词字典
self.id2word = dict()
for word, _ in c:
self.word2id[word] = len(self.word2id)
self.id2word[len(self.id2word)] = word

def generate_batch_inputs(self, batch_size, window_size):
raise NotImplementedError()


class SkipGramDataSet(DataSet):
def generate_batch_inputs(self, batch_size, window_size):
features = np.ndarray(shape=(batch_size,), dtype=np.int32)
labels = np.ndarray(shape=(batch_size,), dtype=np.int32)
i = 0
while True:
if self.data_index == len(self.data):
self.data_index = 0
# 窗口的左侧位置
left = max(0, self.data_index - window_size)
# 窗口的右侧位置
right = min(len(self.data), self.data_index + window_size + 1)
# 遍历窗口里的每个单词
for k in range(left, right):
if k != self.data_index:
# 输入是中心词
features[i] = self.word2id[self.data[self.data_index]]
# label值是中心词周围在窗口内的值
labels[i] = self.word2id[self.data[k]]
i += 1
if i == batch_size:
return features, labels
self.data_index += 1

参考

  1. Word2vec原理浅析及tensorflow实现
  2. 自己动手实现word2vec(Skip-gram模型)
-------------本文结束感谢您的阅读-------------

本文标题:word2vec

文章作者:小建儿

发布时间:2019年01月27日 - 11:01

最后更新:2019年01月28日 - 10:01

原始链接:http://yajian.github.io/word2vec/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。