Tensorflow实现Word2Vec

Posted 河南骏

tags:

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

首先是载入各种依赖库,因为要从网络中下载数据,粗腰的依赖库比较多。

import collections
import math
import os
import random
import zipfile
import numpy as np
import urllib
import tensorflow as tf

这里使用urllib.request.urlretrieve下载数据的亚索文件并核对文件尺寸,如果已经下载了文件则跳过。

url='http://mattmahoney.net/dc/'

def maybe_download(filename,expected_bytes):
        if not os.path.exists(filename):
                filename,_=urllib.request.urlretrieve(url+filename,filename)
        statinfo=os.stat(filename)
        if statinfo.st_size==expected_bytes;
                print 'Found and verified',filename
        else:
                print statinfo.st_size
                raise exception('Failed to verify'+filename+',can you get to it with a browser?')
        return filename

filename=maybe_download(text8.zip,31344016)


接下来解压下载的亚索文件,并使用tf.conpat.as_str将数据转成单词的列表。通过程序输出,可以知道数据最后被转为一个包含17005207个单词的列表。

def read_data(filename):
        with zipfile.ZipFile(filename) as f:
                data=tf.compat.as_str(f.read(f.namelist()[0])).split()
        return data

words=read_data(filename)
print 'Data size',len(words)

接下来是创建vocabulary词汇表,我们使用collections.Counter统计单词列表中单词聘书,然后使用Most_common方法取top 50000频数的单词作为vocabulary,再创建一个dict,将top 50000词汇的vocabulary放入dictionary中,一遍快速查询。python中dict查询复杂度为o(1),性能非常好。接下来将全部单词转为编号(以频数排序的编号),top 50000词汇之外的单词,我们认定其为unknown,将其编号为0,并统计这类词汇的数量。下面遍历单词列表,对其中每一个单词,先判断是否出现在dictionary中,如果是则转为其编号,如果不是则转为编号0(unknown)。最后返回转换后的编码(data)、每个单词的频数统计(count)、词汇表(dictionary)及其反转的形式(reverse_dictionary)。

vocabulary_size=50000
def build_dataset(words):
        count=[['UNK',-1]]
        count.extend(collections.Counter(words).most_common(vocabulary_size -1))
        dictionary=dict()
        for word,_ in count:
                dictionary[word]=len(dictionary)
        data=list()
        unk_count=0
        for word in words;
                if word in dictionary:
                        index=dictionary[word]
                else:
                        index=0
                        unk_count+=1
                data.append(index)
        count[0][1]=unk_count
        reverse_dictionary=dict(zip(dictionary.values(),dictionary.keys()))
        return data,count,dictionary,reverse_dictionary

data,count,dictionary,reverse_dictionary = build_dataset(words)

然后我们删除原始单词列表,可以解决内存。再打印vocabulary中最高频出现的词汇及其数量(包括unknown词汇),可以看到“UNK”这类一共有418391个,我们的data中前10个单词为[“anarchism”,“originated”,“as”,“a”,“term”,“of”,“abuse”,“first”,“used”,“against”],对应的编号为[5235,3084,12,6,,195,2,3137,46,59,156]

del words
print 'Most common words (+UNK)',count[:5]
print 'Sample data',data[:10],[reverse_dictionary[i] for i in data[:10]]

下面生成word2vec的训练样本。我们使用的Skip-Gram模式(从目标单词反推语境),将原始数据“the quick brown fox jumped over the lazy dog”转为(quick,the),(quick,brown),(brown,quick),(brown,fox)等样本。我们定义函数generate_batch用来生成训练用的Batch数据,参数中batch_size为batch大小,skip_window指单词最远可以联系的距离,设为1代表只能跟紧邻的两个单词生成样本,比如quick只能和前后的单词生成连个样本(quick,the)和(quick,brown)。num_skips为对每个单词生成多少个样本,它不能大于skip_window值的两倍,并且batch_size必须是它的整数倍(确保每个batch包含了一个词汇对应的所有样本)。我们定义单词需要data_index为global变量,因为我们会反复调用generate_batch,所以要确保data_index可以在函数generate_batch中被修改。我们也是用assert确保num_skips和batch_size满足前面提到的条件。然后用np.ndarray将batch和labels初始化为数组。这里定义span为对某个单词创建相关样本时会使用到的单词数量,包括目标单词本身和前后的单词,因此span=2*skip_window+1.并创建一个最大容量为soan的deque,即双向队列,在对deque使用append方法添加变量时,只会保留最会插入的span个变量。

data_index=0

def generate_batch(batch_size,num_skips,skip_window):
        global data_index
        assert batch_size % num_skips==0
        assert num_skips <=2*skip_window
        batch=np.ndaaray(shape=(batch_size),dtype=np.int32)
        labels=np.ndaaray(shape=(batch_size,1),dtype=np.int32)
        span=2*skip_window+1
        buffer=collection.deque(maxlen=span)
接下来从需要data_index开始,把span个单词顺序读入buffer作为初始值。因为buffer是容量为span的deque,所以此时buffer已填充满,后续数据将替换掉前面的数据。然后我们进入第一层循环(次数为batch_size/num_skips),每次循环内对一个目标单词生成样本。现在buffer中是目标单词和所有相关单词,我们定义target=skip_window,即buffer中第skip_window个变量为目标单词。然后我们定义生成样本时需要避免的单词李彪target_to_avoid,这个列表一开始包括skip_window个单词(即目标单词)因为我们要预测的是语境单词,不包括目标单词本身。接下来进入第二层循环(次数为num_skips),每次循环中对一个语境单词生成样本,先产生随机数,知道随机数不在target_to_avoid中,代表可以使用的语境单词,然后产生一个样本,feature即目标词汇buffer[skip_window],label则是buffer[target]。同时,因为这个语境单词被使用了,所再把它添加到target_to_avoid中过滤。在对一个目标单词生成完所有样本后(num_skips个样本),我们再读入下一个单词(同时会跑掉buffer中的第一个单词),即把滑窗向后移动一位,这样我们的目标单词也向后移动一个,语境打次也整体后移了,便可以开始生成下一个目标单词的训练样本。两侧循环完成后,我们已经获得了batch_size个训练样本,将batch和labels作为函数结果返回。
        for _ in range(span):
                buffer.append(data[data_index])
                data_index=(data_index+1)%len(data)
        for i in range(batch_size//num_skips):
                target=skip_window
                target_to_avoid=[skip_window]
                for j in range(num_skips):
                        while target in target_to_avoid:
                                target=random.randint(o,span-1)
                        targets_to_avoid.append(target)
                        batch[i*num_skips+j]=buffer[skip_window]
                        labels[i*num_skips+j,0]=buffer[target]
                buffer.append(data[data_index])
                data_index=(data_index+1)%len(data)
        return batch,labels

这里调用generate_batch函数简单测试一下其功能。参数中将batch_size设为8,num_skips设为2,skip_window设为1,然后执行generate_batch并获得batch和labels。再打印batch和labels的数据,可以看到我们生成的样本是“3084 originated->5235 anarchism”,“3084 originated-> 12 as”,“12 as-> 3084 originated”等。以第一个样本为例,3084是目标单词originated的编号,这个单词对应的语境单词是anarchism,其编号为5235。

batch,labels=generate_batch(batch_size=8,num_skips=2,skip_window=1)
for i in range(8):
        print batch[i],reverse_dictionary[batch[i]],'->',labels[i,0],reverse_dictionary[labels[i,0]]

我们定义训练时的batch_size为128,embedding_size为128,embedding_size即将单词转稠密向量的维度,一般是50-1000这个范围内的值,这里使用128作为词向量的维度,skip_window即前面提到的单词间最远可以联系的距离,设为1;num_skips即对每个目标单词提取的样本数,设为2.然后我们再生成验证数据valid_examples,这里随机抽取一些频数最高的单词,看向量空间上跟它们最近的单词是否相关性比较高。valid_size=16指用来抽取的验证单词数,valid_window=100是指验证单词只从频数最高的100个单词中抽取,我们使用np.random.choice函数进行随机抽取。而num_sampled是训练时用来做负样本的噪声单词的数量。

batch_size=128
embedding_size=128
skip_window=1
num_skips=2

valid_size=16
valid_window=100
valid_examples=np.random.choice(valid_window,valid_size,replace=False)
num_sampled=64
下面就开始定义skip-Gram Word2Vec模型的网络结构。我们先创建一个tf.Graph并设置为默认的graph。然后创建训练数据中inputs和lebels的placeholder,同时将前面随机产生的valid_examples转为Tensorflow中的constant。接下来,先使用with tf.device('/cpu:0')限定所有计算在CPU上执行,因为接下去的一些计算操作在GPU上可能还没有实现。然后实现tf.random_uniform随机生成所有单词的词向量embeddings,单词表大小为50000,向量维度为128,再使用tf.nn.embedding_lookup查找输入train_inputs对应的向量embed。下面使用之前提到的NCE Loss中的权重参数nce_weights,并将其nce_biases初始化为0.最后使用tf.nn.nce_loss计算出词向量embedding在训练数据上的Loss,并是引用tf.reduce_mean进行汇总。

graph=tf.Graph()
with graph.as_default():
        train_inputs=tf.placeholder(tf.int32,shape=[batch_size]) 
        train_labels=tf.placeholder(tf.int32,shape=[batch_size,1])
        valid_dataset=tf.constant(valid_examples,dtype=tf.int32)

        with tf.device('/cpu:0'):
                embeddings=tf.Variable(tf.random_uniform([vocabulary_size,embedding_size],-1.0,1.0))
                embed=tf.nn.embedding_lookup(embeddings,train_inputs)              
                nce_weights=tf.Variable(tf.zeros([vocabulary_size]))
        loss=tf.reduce_mean(tf.nn.nce_loss(weights=nce_weights,biases=nce_biases,labels=train_labels,inputs=embed,num_sampled=num_sampled,num_classes=vocabulary_size))
我们定义优化器为SGD,且学习速率为1.0.然后计算嵌入向量embeddings的L2范数norm,再将embeddings除以其L2范数得到标准化后的normalized_embeddings。再使用tf.nn.embedding_lookup查询验证单词的嵌入向量,并计算验证单词的嵌入向量与词汇表中所有单词的相似性。最后,我们使用tf.global_variables_initializer初始化所有模型参数。
    optimizer=tf.train.GradientDescentOptimizer(1.0).minimize(loss)

        norm=tf.sqrt(tf.reduce_sum(tf.square(embeddings),1,keep_dims=True))
        normalized_embeddings=embeddings/norm
        valid_embeddings=tf.nn.embedding_lookup(normalized_embeddings,valid_dataset)
        similarity=tf.matmul(valid_embeddings,normalized_embeddings,transpose_b=True)

        init=tf.global_variables_initializer()

我们定义最大的迭代次数为10万次,然后我们创建并设置默认的session,并执行参数初始化。在每一步训练迭代中,先使用generate_batch生成一个batch的inputs和labels数据,并用它们创建feed_dict。然后使用session.run()执行一次优化器运算(即一次参数更新)和算是计算,并将这一步训练的loss累积到average_loss。

num_steps=100001 

with tf.Session(graph=graph) as session:
        init.run()
        print "Initialized"

        average_loss=0
        for step in range(num_steps):
                batch_inputs,batch_labels=generate_batch(batch_size,num_skips,skip_window)
                feed_dict=train_inputs:batch_inputs,train_labels:batch_labels
                _,loss_val=session.run([optimizer,loss],feed_dict=feed_dict)
                average_loss+=loss_val

之后每2000次循环,计算一下平均loss并显示出来。

           if step % 2000==0:
                        if step>0:
                                average_loss/=2000
                        print 'Average loss at step',step,':',average_loss
                        average_loss=0
每10000次循环,计算一次验证单词与全部单词的相似度,并将其与每个验证单词最相似的8个单词展示出来。

            if step % 10000==0:
                        sim=similarity.eval()
                        for i in range(valid_size):     
                                valid_word=reverse_dictionary[valid_examples[i]]
                                top_k=8
                                nearest=(-sim[i,:]).argsort()[1:top_k+1]
                                log_str='Nearest to %s:' % valid_word
                                for k in range(top_k):
                                        close_word=reverse_dictionary[nearest[k]]
                                        log_str='%s %s,' % (log_str,close_word)
                                print log_str
        final_embeddings=normalized_embeddings.eval()

=======================================================================================下面定义一个用来可视化Word2Vec效果的函数。这里low_dim_embs是降维到2维的单词的空间向量,我们将在图标中展示每个单词的位置。我们使用plt.scatter(一般讲matplotlib.pyplot命名为plt)显示散点图(单词的位置),并用plt.annotate展示单词本身。同时,使用plt.savefig保存图片到本地文件

def plot_with_labels(low_dim_embs,labels,filename='tsne.png'):
        assert low_dim_embs.shape[0]>=len(labels),'More labels than embeddings'
        plt.figure(figsize=(18,18))
        for i,label in enumerate(labels);
                x,y=low_dim_embs[i,:]
                plt.scatter(x,y)
                plt.annotate(label,xy=(x,y).xytext=(5,2),textcoords='offset points',ha='right',va='bottom')
        plt.savefig(filename)
~                                                                                                                                            
~                              
我们使用sklearn.manifold.TSNE实现降维,这里直接将原始的128维的嵌入向量降到2维,再用前面的plot_with_labels函数进行展示。这里只显示词频最高的100个单词的可视化结果。

from sklearn.manifold import TSNE
import matplotlib.pyplot as plt
tsne=TSNE(PERPLEXITY=30,N_COMPONENTS=2,INIT='PCA',N_ITER=5000)
plot_only=100
low_dim_embs=tsne.fit_transform(final_embeddings[:plot_only,:])
labels=[reverse_dictionary[i] for i in range(plot_only)]
plot_with_labels(low_dim_embs,labels)







以上是关于Tensorflow实现Word2Vec的主要内容,如果未能解决你的问题,请参考以下文章

tensorflow笔记:使用tf来实现word2vec

tensorflow笔记:使用tf来实现word2vec

利用 TensorFlow 入门 Word2Vec

tensorflow实现RNN及Word2Vec

在 TensorFlow 中使用预训练的词嵌入(word2vec 或 Glove)

教程 | 在Python和TensorFlow上构建Word2Vec词嵌入模型