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”等。这样做可以加速训练,并且减少训练过程中的噪音。使用到的被剔除频率公式如下:
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训练完成后的信息打印结果如下图所示。
4.2 测试结果
最近邻测试结果如下图所示。可以看到有些词语的最近邻词效果还是不错的,比如[亚拉巴马州:alabama]最近邻词[加利福尼亚州:california]。不过总体效果还是一般,这是由于选择的text8语料库规模不是很大。而且这样随机选出来的词,生僻词居多,效果也不太明显。这里仅仅展示一个形式。
4.3 可视化结果
可视化结果如下图所示,前500个词被绘制在了二维图中,词语在图中距离越近说明在我们训练的词向量中,它们的近似程度越高。优秀的词向量就可以在图中把语义相似的词聚在一起。
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。
例如:(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
% word2vec在tf实现的代码笔记
[5] http://www.jeyzhang.com/tensorflow-learning-notes-3.html
% 另一篇word2vec不错的笔记
[6] http://blog.csdn.net/u011500062/article/details/51728830
% 介绍了tf的saver类用法
[7] https://www.zhihu.com/question/37489735/answer/73314819
% 关于语言模型评价,知乎网友的部分解释
以上是关于NLPTensorflow下word2vec代码详解的主要内容,如果未能解决你的问题,请参考以下文章