NLPTensorflow下word2vec代码详解

Posted JRunning

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NLPTensorflow下word2vec代码详解相关的知识,希望对你有一定的参考价值。

注意


文中所有代码块均可向右滑动!
苹果手机中的代码显示可能会折行
若出现此情况,请用电脑浏览。


0 前言


word2vec是将自然语言中的字词转为计算机可以理解的稠密向量的一种方法,转换后可以进一步进行各种自然语言处理的任务。具体模型的细节可以参考以下几篇笔记:





本文将对tensorflow下word2vec(基于Negative Sampling的skip-gram方法)代码实现进行了详细解读,并对语言模型的部分评估方法进行介绍。


1 项目出处


https://github.com/NELSONZHAO/zhihu/tree/master/skip_gram

这个实现相较于官方的实现,其效率与效果都不如后者,但是它的代码简介,逻辑清晰,适合初学时理解与复写。


2 代码总览


代码的主要逻辑可以通过下面这张图来表示。


理解的重点在于预处理部分(运用一些常规自然语言处理方法)以及构造模型部分(tensorflow的相关用法、词嵌入及负采样的实现)。


最终的实现是输入一个语料库进行训练,输出每个词转化后对应的向量,即emeddings,我们通过“最近邻词实验”对训练效果进行测试,并将部分词语可视化于二维坐标系的图中。



3 代码详解


3.1 导入包


import time
import numpy as np
import tensorflow as tf
import random
from collections import Counter


我们使用到的有:time,用于计算每个batch训练使用的时间;numpy,用于矩阵运算,几乎是必备了;tensorflow,这个不用多说;random,用于抽取随机数;counter,隶属于collections包,用于计数。


3.2 加载数据


with open('data/text8') as f:
text = f.read()


这里使用到的语料库是text8,出处见[2]。它来源于enwiki8,它是从wikipedia上爬下来的前100,000,000个字符,再经过预处理,把数字转化成英语单词,大写字符转小写字符,并删去所有非英文字符(除空格)得到的。对该语料库进行加载,保存在text中。


3.3 预处理


def preprocess(text, freq=5):
   '''
   【功能】对文本进行预处理
   【参数】text: 文本数据/freq: 词频阈值
   '''

   text = text.lower()#大写字符转小写字符
   text = text.replace('.', ' <PERIOD> ')#去除各种符号,下同
   text = text.replace(',', ' <COMMA> ')
   text = text.replace('"', ' <QUOTATION_MARK> ')
   text = text.replace(';', ' <SEMICOLON> ')
   text = text.replace('!', ' <EXCLAMATION_MARK> ')
   text = text.replace('?', ' <QUESTION_MARK> ')
   text = text.replace('(', ' <LEFT_PAREN> ')
   text = text.replace(')', ' <RIGHT_PAREN> ')
   text = text.replace('--', ' <HYPHENS> ')
   text = text.replace('?', ' <QUESTION_MARK> ')
   text = text.replace('\n', ' <NEW_LINE> ')
   text = text.replace(':', ' <COLON> ')
   words = text.split()#进行分词

   # 删除低频词,减少噪音影响
   word_counts = Counter(words)#对词语进行计数,得到字典,key是词,value是次数
   trimmed_words = [word for word in words if word_counts[word] > freq]#留下所有出现次数超过5次的词

   return trimmed_words

words = preprocess(text)#使用预处理函数处理语料


这部分代码构造了一个预处理函数,实现的功能包括了文本中大写字符转小写字符,去除文本中符号,文本分词以及去除文本中低频词语。后续只要对语料调用该函数就可以实现这几个预处理步骤。3.2中说到我们使用的text8语料是没有符号的,这里仍然这样去做是为了后续也可以让它处理其它语料。


#构建映射表
vocab = set(words)#取出所有不重复词语,形成词汇表
vocab_to_int = {w: c for c, w in enumerate(vocab)}#构建文本转数值的映射表
int_to_vocab = {c: w for c, w in enumerate(vocab)}#构建数值转文本的映射表
int_words = [vocab_to_int[w] for w in words]#将输入语料中文本全部转为数字编码


这部分代码实现了构建映射表,将每个单词赋予其一个编码,根据映射表,将文本中的所有单词全部转换成数字编码。


#采样
t = 1e-5 #t值
threshold = 0.8 #剔除概率阈值
int_word_counts = Counter(int_words)#统计每个单词频数
total_count = len(int_words)#统计单词总数
word_freqs = {w: c/total_count for w, c in int_word_counts.items()}#计算每个单词频率
prob_drop = {w: 1 - np.sqrt(t / word_freqs[w]) for w in int_word_counts}#计算被剔除的概率
train_words = [w for w in int_words if prob_drop[w] < threshold]#对单词进行采样,概率低于阈值则剔除


这部分代码实现了[1]中2.3节提到的采样,这个操作可以剔除一些高频停用词,如“in”, “a”, “the”等。这样做可以加速训练,并且减少训练过程中的噪音。使用到的被剔除频率公式如下:

【NLP】Tensorflow下word2vec代码详解

P(wi)表示被剔除频率,f(wi)表示单词频率,t表示阈值参数,一般取1e-5左右。


3.4 构造模型


def get_targets(words, idx, window_size=5):
    '''
   【功能】获得input word的上下文单词列表
   【参数】words: 单词列表/idx: input word的索引号/window_size: 窗口大小
   '''

   target_window = np.random.randint(1, window_size+1)#根据窗口大小生成输出的随机窗口
   start_point = idx - target_window if (idx - target_window) > 0 else 0#根据中心词的索引得到起始词的索引,讨论input word前面单词不够的情况
   end_point = idx + target_window#根据中心词的索引得到结束词的索引
   targets = set(words[start_point: idx] + words[idx+1: end_point+1])#将上下文取出,并且去重复
   return list(targets)


这部分代码构造了一个获取上下文单词列表的函数,首先根据输入的窗口大小随机生成一个目标窗口,大小在[1,window_size]之间,这样做是在语料较小的情况下,更好地让模型去关注离中心更近的单词。然后得到上下文两端的索引编号,将这部分单词提取出来并去除重复,得到一个列表。


def get_batches(words, batch_size, window_size=5):
   '''
   【功能】获取batch的生成器
   【参数】words: 单词列表/batch_size:每个batch的大小/window_size: 窗口大小
   '''

   n_batches = len(words) // batch_size#获取batch的数量,双斜杠表示取整
   words = words[:n_batches*batch_size]#仅取full batches,余下的单词剔除
   for idx in range(0, len(words), batch_size):
       x, y = [], []#先创建空列表
       batch = words[idx: idx+batch_size]#将这个batch中的词切分出来
       for i in range(len(batch)):
           batch_x = batch[i]#中心词
           batch_y = get_targets(batch, i, window_size)#获取上下文单词列表
           x.extend([batch_x]*len(batch_y))#由于一个中心词会对应多个上下文单词,因此需要长度统一,扩充列表x
           y.extend(batch_y)#扩充列表y
       yield x, y#yield将函数转换为generator,迭代更新返回值x,y


这部分代码构造了一个batch的生成器,根据batch的大小,将语料切分成不同部分,在后续的训练中每次以一个batch为单位输入。在batch中遍历每个单词作为中心词,使用刚才构造的上下文单词列表获取函数,得到每个中心词对应的上下文单词,作为对比计算损失的标签。


train_graph = tf.Graph()#定义计算图
with train_graph.as_default():
   inputs = tf.placeholder(tf.int32, shape=[None], name='inputs')#定义inputs占位符,后续训练时feed
   labels = tf.placeholder(tf.int32, shape=[None, None], name='labels')#定义labels占位符,后续训练时feed


准备好了刚才两个函数,从这里开始正式的模型构建,这部分代码主要完成了定义输入,先定义计算图,通过占位符placeholder对inputs和labels进行定义,这里要注意的是inputs是一维数组,而labels是二维数组。


vocab_size = len(int_to_vocab)#不重复的总词汇数
embedding_size = 200#嵌入维度定义为200
with train_graph.as_default():
   embedding = tf.Variable(tf.random_uniform([vocab_size, embedding_size], -1, 1))#嵌入层权重矩阵,[-1,1]均匀随机分布,后续通过训练修正
   embed = tf.nn.embedding_lookup(embedding, inputs)#对inputs实现lookup,将中心词都转为词向量


这部分代码完成的是词嵌入,定义好嵌入维度的大小后,通过Variable定义embedding,即词向量转换矩阵,然后用lookup方法查表,将inputs转换成词向量形式。


#使用负采样Negative sampling加速训练,即多次二分类训练
n_sampled = 100#负采样数量定义为100
with train_graph.as_default():
   softmax_w = tf.Variable(tf.truncated_normal([vocab_size, embedding_size], stddev=0.1))#负采样的权重矩阵,初始值正态分布,标准差0.1
   softmax_b = tf.Variable(tf.zeros(vocab_size))#负采样的偏置矩阵,初始值全0
   loss = tf.nn.sampled_softmax_loss(softmax_w, softmax_b, labels, embed, n_sampled, vocab_size)#计算negative sampling下的损失
   cost = tf.reduce_mean(loss)#计算全局平均值作为训练要优化的损失
   optimizer = tf.train.AdamOptimizer().minimize(cost)#使用Adam优化器去优化cost损失


这部分代码使用负采样Negative sampling的方法,对传统skip-gram模型作了优化,降低了其计算复杂度。首先定义负采样数量为100,然后通过Variable定义softmax需要使用到的权重与偏置。这里loss的计算其实就是几个正负类的概率连乘问题,其计算直接封装在tf.nn.sampled_softmax_loss函数中,直接调即可。然后计算全局的平均值,使用Adam优化器通过梯度下降法去优化。


3.5 训练与测试


训练与测试的代码是在一起写的,使用“最近邻测试”,随机选出一些词语,在所有词中找出与它们相似度最高的词,并打印出来。


#随机挑选一些单词,用于后续作最近邻测试
with train_graph.as_default():
   valid_size = 16#共选16个词用于验证
   valid_window = 100#选词区间大小
   valid_examples = np.array(random.sample(range(valid_window), valid_size//2))#从索引1到100中选8个索引号
   valid_examples = np.append(valid_examples,
                              random.sample(range(1000,1000+valid_window), valid_size//2))#从索引1000到1100中选8个索引号
   valid_dataset = tf.constant(valid_examples, dtype=tf.int32)#将选出的单词索引号列表定义为tf常量
   norm = tf.sqrt(tf.reduce_sum(tf.square(embedding), 1, keep_dims=True))#计算每个词向量的模,reduce_sum第二个参数1表示纵向求和
   normalized_embedding = embedding / norm#对词向量进行单位化
   valid_embedding = tf.nn.embedding_lookup(normalized_embedding, valid_dataset)#查找验证单词的词向量
   similarity = tf.matmul(valid_embedding, tf.transpose(normalized_embedding))#使用点积计算选出词与其余所有词的余弦相似度


这部分代码主要完成了随机词语的选取以及词向量单位化、相似度计算等测试时需要的前期准备任务。词向量相似度是通过点积去计算的,tf.transpose表示矩阵转置,tf.matmul表示矩阵乘法。


#定义训练参数
epochs = 10 #迭代轮数
batch_size = 1000 #batch大小
window_size = 10 #窗口大小
isTrain=True #训练时选True;测试时选False
checkpoint_dir='checkpoints/text8.ckpt' #checkpoints的目录


这部分代码定义了训练时的一些参数。迭代轮数是一共对所有文本遍历训练的次数;batch大小表示一个批次中送入的输入词的数量(=中心词数*窗口数);窗口大小表示一个中心词对应的上下文的单词数量;isTrain是后续判断是跳入训练代码还是跳入测试代码的布尔值。


with train_graph.as_default():
   saver = tf.train.Saver() #文件存储


文件存储,在训练过程中保存结果Saver类提供了向checkpoints文件保存和从checkpoints文件中恢复变量的相关方法。checkpoints文件是一个二进制文件,它把变量名映射到对应的tensor值 。


with tf.Session(graph=train_graph) as sess:
   iteration = 1#初始化循环计数
   loss = 0#初始化损失
   sess.run(tf.global_variables_initializer())#所有variable变量初始化
   if isTrain:#训练
       for e in range(1, epochs+1):
           batches = get_batches(train_words, batch_size, window_size)#生成batches
           start = time.time()#记录一个batch训练开始时间
           for x, y in batches:
               feed = {inputs: x,
                       labels: np.array(y)[:, None]}
               train_loss, _ = sess.run([cost, optimizer], feed_dict=feed)#训练记录每次的train_loss
               loss += train_loss#损失求和
               if iteration % 100 == 0: #100次循环,每次窗口10,对应batch大小为1000
                  end = time.time()#记录一个batch训练结束时间
                  print("Epoch {}/{}".format(e, epochs),#打印:当前次数/总迭代次数
                        "Iteration: {}".format(iteration),#打印:当前batch内循环次数
                        "Avg. Training loss: {:.4f}".format(loss/100),#打印:平均损失
                        "{:.4f} sec/batch".format((end-start)/100))#打印:训练每个batch需要的时间
                  loss = 0#重置损失
                  start = time.time()#重置时间
               iteration += 1
           save_path = saver.save(sess, checkpoint_dir)#每次迭代进行checkpoint的保存
   else:#测试
       saver.restore(sess, checkpoint_dir)#从从训练中保存的checkpoints文件加载模型
       embed_mat = sess.run(normalized_embedding)#得到训练好后单位化的词向量
       sim = similarity.eval()#得到测试词与其它所有词相似度的数值
       for i in range(valid_size):
           valid_word = int_to_vocab[valid_examples[i]]#将测试词的索引号转回单词文本
           top_k = 8
           nearest = (-sim[i, :]).argsort()[1:top_k+1]#取最相似单词的前8个
           log = 'Nearest to [%s]:' % valid_word
           for k in range(top_k):
               close_word = int_to_vocab[nearest[k]]#将最相似词的索引号转回单词文本
               log = '%s %s,' % (log, close_word)
           print(log)#打印输出最近邻测试结果



这部分代码是训练与测试的主体部分。


训练部分,首先对所有变量进行初始化,进入每一轮迭代后通过get_batches函数获得batch,对每一个batch依次训练。batch中的x,y通过feed送入模型,填补inputs与labels的占位符。每100次循环即1个batch(100次对应100个中心词,即1000个上下文词),每个batch训练完成后,打印该batch的平均损失以及训练该batch的耗时。每轮迭代完成后,保存模型。


测试部分,首先从checkpoint中载入模型,然后将训练好的词向量单位化。由于测试词和它们与其他词的相似度前面已经计算好,这里直接使用,将相似度最高的前八位的词打印出来。这里要注意的是,之前选出的词都是用其索引号表示,词向量也是通过对应的索引号来查询的,在打印时要将索引号转回文本。


3.6 结果可视化


结果可视化主要是通过TSNE降维,并将每个词的二维坐标绘制在图上来实现的。TSNE提供了一种有效的降维方式,让我们对高于二维数据的聚类结果以二维的方式展示出来。


import matplotlib.pyplot as plt#导入绘图包
from sklearn.manifold import TSNE#导入TSNE降维方法
viz_words = 500#准备将500个词绘在二维图上
tsne = TSNE()
embed_tsne = tsne.fit_transform(embed_mat[:viz_words, :])#前500个词进行数据降维,降成两维
fig, ax = plt.subplots(figsize=(14, 14))#创建一个绘图,定义图的大小
for idx in range(viz_words):
   plt.scatter(*embed_tsne[idx, :], color='steelblue')#根据降维后的二维坐标绘制散点
   plt.annotate(int_to_vocab[idx], (embed_tsne[idx, 0], embed_tsne[idx, 1]), alpha=0.7)#将对应的词语标注在每个点后,alpha表示透明度


首先导入绘图所需要的matplotlib包以及降维所需要的TSNE。viz_words表示需要绘出的词的个数500个,然后通过tsne.fit_transform方法对词向量矩阵中的前500个进行降维。plt.scatter绘制各词在坐标位置上的散点,plt.annotate绘制每个点代表的文本。


4 运行效果


4.1 训练过程

 

每个batch训练完成后的信息打印结果如下图所示。


【NLP】Tensorflow下word2vec代码详解


4.2 测试结果


最近邻测试结果如下图所示。可以看到有些词语的最近邻词效果还是不错的,比如[亚拉巴马州:alabama]最近邻词[加利福尼亚州:california]。不过总体效果还是一般,这是由于选择的text8语料库规模不是很大。而且这样随机选出来的词,生僻词居多,效果也不太明显。这里仅仅展示一个形式。


【NLP】Tensorflow下word2vec代码详解


4.3 可视化结果


可视化结果如下图所示,前500个词被绘制在了二维图中,词语在图中距离越近说明在我们训练的词向量中,它们的近似程度越高。优秀的词向量就可以在图中把语义相似的词聚在一起。


【NLP】Tensorflow下word2vec代码详解


5 模型评估


词向量评估主要有单词相似性任务、单词类比任务、单词分类任务、选择约束任务等,这些都属于是利用词向量的语言学特征,平时前两种用的比较多一些;此外,还有通过对实际NLP任务的贡献来评估的,包括文本分类、命名实体识别等任务。它们都是在特定的数据集上去训练,然后去分别进行这些不同的任务,与标准进行对比得到得分,再横向与其它模型进行比较。这里介绍一下利用词向量的语言学特征的语言模型评估方法。


5.1 单词相似性


单词相似性任务就是通过计算词向量之间的相似度(cos距离),找出词向量空间中与之最近的单词。这类数据集,例如Simword353,通常包括了每对词语的得分,越接近其得分越高,最后统计总得分进行比较。


5.2 单词类比


著名的A-B=C-D。


假设给了一对单词 (a , b) 和一个单独的单词c,通过类比task会找到一个单词d,使得c与d之间的关系相似于a与b之间的关系。在给定词向量的前提下,task一般是通过在词向量空间寻找离(b-a+c)最近的词向量来找到d。


【NLP】Tensorflow下word2vec代码详解


例如:(a=中国,b=北京)和(c=日本),如果词向量优秀,那么找到的单词d应该是东京。


这类数据集(Mikolove,2013),包括了数万个这样的问题,然后根据模型找到的与标准答案进行对比,得到得分。



5.3 分类任务


任务目标是将不同的词语,根据其词向量,分类到不同的类别中。完成分类后,再与已经标记好的分类正确结果进行比较,得到得分。


5.4 选择约束


这个任务不太常用,数据集也比较陈旧(Ulrike Pado,2007),包括211个词对。目标是确定一个动词最常搭配的主体名词或客体名词。首先给定动词,根据词向量,计算出20个与之最可能搭配的名词,将它们向量求平均,再与数据集中给定的标签对应的词向量进行比较,得到得分。


6 总结


本文对tensorflow下word2vec(基于Negative Sampling的skip-gram方法)代码实现进行了详细解读,从宏观实现的整体构架到微观每行代码的具体功能均有说明,并对代码的运行结果进行了展示。在最后,对语言模型的部分评价方法进行了相关介绍。


文章较长,看到最后不容易,谢谢你的支持,也欢迎指出各种错误。本文仅作为自己备忘,日后打捞学习过的知识之用。向前辈们致敬。


Reference


  
    
    
  
    
      
      
    

[1] Mikolov T, Sutskever I, Chen K, et al. Distributed Representations of Words and Phrases and their Compositionality[J]. 2013, 26:3111-3119.
% skip-gram, negative sampling模型出处
[2] http://mattmahoney.net/dc/textdata
% Text8语料库出处,还有更多语料数据集下载
[3] Schnabel T, Labutov I, Mimno D, et al. Evaluation methods for unsupervised word embeddings[C]/ Conference on Empirical Methods in Natural Language Processing. 2015:298-307.
% 总结了各种语言模型评价方法
[4] https://www.leiphone.com/news/201706/QprrvzsrZCl4S2lw.html
% word2vectf实现的代码笔记
[5] http://www.jeyzhang.com/tensorflow-learning-notes-3.html
% 另一篇word2vec不错的笔记
[6] http://blog.csdn.net/u011500062/article/details/51728830
% 介绍了tfsaver类用法
[7] https://www.zhihu.com/question/37489735/answer/73314819
% 关于语言模型评价,知乎网友的部分解释


以上是关于NLPTensorflow下word2vec代码详解的主要内容,如果未能解决你的问题,请参考以下文章

如何在windows下使用word2vec

word2vec代码解释

word2vec原理推导与代码分析

Windows下使用 npm 命令安装 Appium(详)

Word2Vec原理及代码

NLP实战 | 使用《人民的名义》的小说原文训练一个word2vec模型