目录
1:为什么要用词向量
自然语言处理系统通常将词汇作为离散的单一符号,例如 “cat” 一词或可表示为 索引537 ,而 “dog” 一词或可表示为 索引143。这些符号编码毫无规律,无法提供不同词汇之间可能存在的关联信息。换句话说,在处理关于 “dogs” 一词的信息时,模型将无法利用已知的关于 “cats” 的信息(例如,它们都是动物,有四条腿,可作为宠物等等)。可见,将词汇表达为上述的独立离散符号将进一步导致数据稀疏,使我们在训练统计模型时不得不寻求更多的数据。而词汇的向量表示将克服上述的难题。上面的表示也就是NLP 中最直观,也是目前最常用的词表示方法是 One-hot Representation,它是将词符号化的一种方式,也就是每个词的向量大小就是词典大小,一个向量只有一个位置为1,其他位置为0,当然,这样的向量不包含任何语义信息。
onehot编码缺点:
- 对于one hot编码,有多少个字,就得有多少维向量,假如有1万字,那么每个字向量就是1万维(常用的字可能不多,几千个左右,但是按照词的概念来看,常用的词可能就有十几万了),这样可能会造成维数爆炸
- 词向量,能够包涵语义信息,向量的夹角余弦能够在某种程度上表示字、词的相似度等等
2:Word2Vec简介
Word2vec是一种可以进行高效率词嵌入学习的预测模型。NLP中的词语,是人类的抽象总结,是符号形式的(比如中文、英文、拉丁文等等),所以需要把他们转换成数值形式,或者说——嵌入到一个数学空间里,这种嵌入方式,就叫词嵌入(word embedding),而 Word2vec,就是词嵌入( word embedding) 的一种。其有两种变体是现在比较常用的,分别为:连续词袋模型(CBOW)及Skip-Gram模型。从算法角度看,这两种方法非常相似,其区别为CBOW根据源词上下文词汇(’the cat sits on the’)来预测目标词汇(例如,‘mat’),而Skip-Gram模型做法相反,它通过目标词汇来预测源词汇。
Skip-Gram模型采取CBOW的逆过程的动机在于:CBOW算法对于很多分布式信息进行了平滑处理(例如将一整段上下文信息视为一个单一观察量)。很多情况下,对于小型的数据集,这一处理是有帮助的。相比之下,Skip-Gram模型将每个“上下文-目标词汇”的组合视为一个新观察量,这种做法在大型数据集中会更为有效。
2.1:CBOW模型
2.1.1:模型结构
1 输入层:上下文单词的onehot
2 所有onehot分别乘以共享的输入权重矩阵W.
3 所得的向量 {因为是onehot所以为向量} 相加求平均作为隐层向量, size为1N.
4 乘以输出权重矩阵W'
5 得到向量 激活函数处理得到V-dim概率分布 {PS: 因为是onehot,其中的每一维都代表着一个单词概率}
6 概率最大的index所指示的单词为预测出的中间词与true label的onehot做比较,误差越小越好(根据误差更新权重矩阵)
2.1.2:推导过程
2.2:Skip-Gram模型
2.2.1:模型结构
2.2.2:推导过程
3:分层Hierarchical Softmax
1.输入层:包含(Context(w)中2c个词的词向量v(Context(w)1),v(Context(w)2),...,v(Context(w)2c)∈Rm。这里,m表示词向量的长度。
2.投影层:将输入层的2c个向量求和。
3.输出层:输出层对应一棵二叉树,它以语料中出现过的词当叶节点,以各词在语料中出现的次数当权值构造出来的哈夫曼树。在这颗哈夫曼树中,叶节点有N(=|D|)个,分别对应词典D中的词。优点:在神经概率语言模型中,模型的大部分计算集中在隐藏层和输出层之间的矩阵向量运算,以及输出层上的softmax 归一化运算.而从上面的对比中可见,CBOW 模型对这些计算复杂度高的地方有针对性地进行了改变,首先,去掉了隐藏层,其次,输出层改用了Huffiman 树,所有叶子节点加起来的概率和为1,从而为利用Hierarchical softmax 技术奠定了基础。
举例:
首先建立哈夫曼树,非叶节点上都有一个未知向量,因为哈夫曼树是一个二叉树,每一次向下遍历只有向左和向右两个方向,即二分类。那么自然联想到用逻辑回归来做这个事情,每一次向左记为1;每一次向右记为0;比如在给定了“足球”的上下文的条件下,我们当然希望模型预测出“足球”的概率最大,那我们就建立多个逻辑回归模型进行多次分类(向着“足球”行进),然后,就找到优化目标了:最大化最大对数似然函数(将所有的p相乘然后取对数)。然后使用梯度下降法更新参数(哈夫曼树中非叶结点上都有一个未知向量的参数)和词向量(之前都是随机生成的)
对于从根节点出发到达“足球”这个叶子节点所经历的4次二分类,将每次分类结果的概率写出来就是:
最优化目标:
其中是投影层计算出来的“足球”上下文向量的和,就是哈夫曼非叶节点 j 上的向量。接下来就要更新Xw和,其中更新上下文向量,是将梯度共享到上下文向量上面。
4:Negative Sampling
Negative Sampling是用来提高训练速度并改善所得词向量的质量。其不再使用复杂的哈夫曼树,而是利用简单的随机负采样,能大幅提高性能,因而Negative Sampling可作为Hierarchical Softmax的一种替代。在Hierarchical Softmax框架里我们用了哈夫曼树作为构造优化目标的桥梁,但是效率较低。Negative Sampling(负采样)框架是如何作为构造优化目标的桥梁的呢?负采样的思想通俗讲就是:还是用“足球”的例子,比如已经给定了“足球”的上下文,那我们需要让模型预测为“足球”的概率最大,预测为其他词(负样本)的概率较小。负采样技术就是随机的从词典里抽一些不是“足球”的词。因此优化目标就是让他是“足球”的概率最大,是采样出来的其他负样本的概率较小。
优化目标:
增大正样本的概率同时降低负样本的概率。于是,对于一个给定的语料库C,函数就可以作为整体优化的目标。为了计算方便,对G取对数,最终的目标函数就是:
为了推倒方便,简记
采样的规则:
word2vec中的采样的方法并不复杂,如果词汇表的大小为VV,那么我们就将一段长度为1的线段分成VV份,每份对应词汇表中的一个词。当然每个词对应的线段长度是不一样的,高频词对应的线段长,低频词对应的线段短。每个词ww的线段长度由下式决定:
5:代码实战
没有进行优化的实现,后面有时间会用负采样实现一下。
import os
import collections
import numpy as np
import tensorflow.compat.v1 as tf
tf.disable_v2_behavior()
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 does not exist --> %s" % self.file)
f = open(self.file, mode='rt', encoding='utf8')
self.data = tf.compat.as_str(f.read()).split()
print("the length of data %d" % len(self.data))
print("data value:" , self.data)
if f:
f.close()
c = collections.Counter(self.data).most_common()
print(c)
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)#建立word到id的映射
self.id2word[len(self.id2word)] = word#建立id到word的映射
def gen_batch_inputs(self, batch_size, skip_window):
raise NotImplementedError()
class SkipGramDataSet(DataSet):
def gen_batch_inputs(self, batch_size, window_size):
features = np.ndarray(shape=(batch_size,), dtype=np.int32)#shape(4,)和shape(4,1)是维度不一样
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]]
labels[i] = self.word2id[self.data[k]]
i += 1
if i == batch_size:
return features, labels
self.data_index += 1
dataset = SkipGramDataSet('/Users/zhongqiqiang/nease_projrct/recommend_recall/word2vec/test.txt')
VOCAB_SIZE = dataset.vocab_size
EMBEDDING_SIZE = 128
SKIP_WINDOW = 2
NUM_SAMPLED = 64
BATCH_SIZE = 32
WINDOW_SIZE = 2
LOG_DIR = "./tmp/word2vec"
TRAIN_STEPS = 10000
LEARNING_RATE = 0.1
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)
with tf.name_scope("layer1"):
self.W1 = tf.Variable(tf.random_uniform([VOCAB_SIZE, EMBEDDING_SIZE], -1, 1),dtype=tf.float32)
self.b1 = tf.Variable(tf.random_normal([EMBEDDING_SIZE]),dtype=tf.float32)
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)
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], keepdims=True))
self.opt = tf.train.GradientDescentOptimizer(LEARNING_RATE).minimize(self.loss)
def _one_hot_input(self, dataset):
features, labels = dataset.gen_batch_inputs(BATCH_SIZE, WINDOW_SIZE)
f, l = [], []
for w in features:
tmp = np.zeros([VOCAB_SIZE])
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 = sess.run([self.prediction, self.loss],
feed_dict={
self.x: features,
self.y: labels
})
print("loss:%s" % loss)
word2vec = Word2Vec()
word2vec.train(dataset, TRAIN_STEPS)
参考文章:
https://www.cnblogs.com/itmorn/p/8196605.html