Tensorflow实战:Word2Vec_Skip_Gram原理及实现(多注释)

        Word2Vec也称Word Embeddings,中文的叫法为“词向量”或“词嵌入”,是一种非常高效的,可以从原始语料中学习字词空间向量的预测模型。

        在Word2Vec出现之前,通常将字词转为One-Hot Encoder ,一个词对应一个向量(一个向量中只有一个1,其余皆为0),通常要将一篇文章中每一个词都转成一个向量,而整篇文章则变为一个稀疏矩阵。这样的方法没有考虑字词之间的关系,没有提供任何关联信息,例如我们对我们对动物 和 狗之间的从属关系,关联程度一无所知。且稀疏数据的训练的效率较低。

       而向量空间模型(Vector Space Models)则可以有效的解决这个问题,它将字词转为连续值(相对于One-Hot编码的离散值)的向量表达,并且其中意思相近的词将映射到向量空间中相似位置。

       Word2Vec就是一种计算非常高效的,可以从原始语料中学习字词空间向量的预测模型。它主要分为CBOW和Skip-Gram两种模式,其中CBOW是从原始语句(比如:中国的首都是_____)推测目标字词(比如:北京);而Skip-Gram则正好相反,它是从目标字词推测出原始语句。其中CBOW对小型数据比较合适,而Skip-Gram在大型语料中表现的更好,使用Word2Vec训练语料能得到一些非常有趣的结果,比如意思相近的词在向量空间中的位置会非常接近。

       本文将使用Skip-Gram模式的Word2-Vec,以“the quick brown fox jumped over the lazy dog”为例,我们要构造一个语境与目标词汇的映射关系,其中语境包含一个单词的左边和右边的词汇,假设我们的滑窗尺寸为1,则以quick为例,其单词及语境包含(the, quick, brown),则对目标词汇quick,其对应的训练样本为(quick, the)和(quick, brown)。我们训练时,希望模型能够从目标词汇quick预测出语境the,同时也需要制造随机的词汇作为负样本(噪声),我们希望预测的概率分布在正样本the上尽可能大,而在随机的负样本上尽可能的小。这里的做法就是通过优化算法比如SGD来更新模型中Word Embedding的参数,让概率分布的损失函数(NCE Loss)尽可能的小。这样每个单词的词向量就会随着训练过程不断调整,直到处于一个最适合语料的空间位置。这样我们的损失函数最小,最符合语料,同时预测出正确单词的概率也最高。

       以下为训练Word2Vec之Skip-Gram的步骤及重要参数的意义:

一、读入文本,将每个单词分隔开,存为一个单词列表。

二、统计单词列表中词频,

二维数组count:记录单词频数,形如[['UNK', -1], [top1, 25631], [top2, 3541],…]

词典dictionary:将top50000词频的单词按词频从大到小的顺序存入,其中key为单词了,value为编号(1-50000)

单词编号列表data:遍历单词列表,如当前单词在词典中,则值为value,否则为0

reverse_dictionary:dictionary的key与value值反转

三、生成word2vec的训练样本,返回目标单词batch数组和其对应语境labels数组

batch:一维数组,长度为batch_size,存放目标单词编号

labels:二维数组(batch_size, 1) ,在与batch数组下标相同的位置存放目标单词的语境单词编号

训练样本的构成:

以“the quick brown fox jumped over the lazy dog”这句话为例。我们要构造一个语境与目标词汇的映射关系,其中语境包括一个单词左边和右边的词汇,假设我们的滑窗尺寸为1,因为Skip-Gram模型是从目标词汇预测语境,所以我们的数据集就成了(quick, the)、(quick, brown)、(brown, quick)、(brown, fox)。我们训练时,希望模型能从目标词汇quick预测出语境the。

四、训练

随机生成所有单词的词向量为一个词向量矩阵,

在词向量矩阵中根据单词编号查找到训练数据(即batch)对应的向量们,

tf.nn.nce_loss计算出词向量embedding在训练数据上的loss,并使用tf.reduce_mean进行汇总

训练完后embeddings除以其L2范数得到标准化后的normalized_embeddings,即最终的词向量矩阵

 

以下为本文实现Word2Vec的实现代码,进行了前向计算的测评,代码及详细注释如下:

import collections
import math
import os
import random
import zipfile
import numpy as np
import urllib
import tensorflow as tf
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE

'''############################################05《TensorFlow实战》实现Word2Vec Skip-Gram##################################################'''


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 verfied', filename)
    else:
        print(statinfo.st_size)
        raise Exception(
            'Failed to verify ' + filename + '. Can you get to it with browser?'
        )
    return filename

filename = maybe_download('text8.zip', 31344016)    #根据文件名和byte下载数据


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))
# print(words[:5])


vocabulary_size = 50000


'''创建vocabulary词汇表'''
def build_dataset(words):

    count = [['UNK', -1]]   #词频统计,['UNK', -1]表示未知单词的频次设为-1

    #使用collections.Counter()方法统计单词列表中单词的频数, 使用.most_common()方法取top50000频数的单词(频次从大到小)作为vocabulary
    #     # >> > Counter('abcdeabcdabcaba').most_common(3)
    #     # [('a', 5), ('b', 4), ('c', 3)]
    count.extend(collections.Counter(words).most_common(vocabulary_size - 1))

    #创建一个词典dict,将top50000的单词存入,以便快速查询
    dictionary = dict()
    #将top50000单词按频次给定其编号存入dictionary,key为词,value为编号(从1到50000)
    for word, _ in count:   #_用作一个名字,来表示某个变量是临时的或无关紧要的
        dictionary[word] = len(dictionary)
        #print(len(dictionary))

    data = list()   #单词列表转换后的编码
    unk_count = 0

    for word in words:  #遍历单词列表
        if word in dictionary:  #如果该单词存在于dictionary中
            index = dictionary[word]    #取该单词的频次为编号
        else:   #如果dictionary中没有该单词
            index = 0   #编号设为0
            unk_count += 1
        data.append(index)

    count[0][1] = unk_count #未知单词数(除了top50000以外的单词)

    reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
    #返回单词列表中单词转换为编码(编码为该单词的频次)的data列表、每个单词的频数统计count、词汇表dictionary及其反转形式reverse_dictionary
    return data, count, dictionary, reverse_dictionary

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

del words   #删除原始单词表,节约内存
print("Most common words (+UNK)", count[:5])    #打印未知单词和最高频单词及它们的频次
print("Sample data", data[:10], [reverse_dictionary[i] for i in data[:10]])  #打印单词列表中前10个单词的编号及单词本身


data_index = 0


'''生成word2vec的训练样本,返回目标单词编号batch数组和其对应语境编号labels数组
skip-gram模式将原始数据"the quick brown fox jumped"转为(quick,the),(qucik,brown),(brown,quick),(brown,fox)等样本'''
def generate_batch(batch_size,  #一个批次的大小
                   num_skips,   #num_skips为对每个单词生成多少个样本
                   skip_window): #指单词最远可以联系的距离,设为1代表只能跟紧邻的两个单词生成样本,比如quick只能和前后的单词生成(quick,the),(qucik,brown)
    global data_index   #单词序号设为global,确保在调用该函数时,该变量可以被修改
    #python 中assert断言是声明其布尔值必须为真的判定,其返回值为假,就会触发异常
    assert batch_size % num_skips == 0  #skip-gram中参数的要求
    assert num_skips <= 2 * skip_window
    batch = np.ndarray(shape=(batch_size), dtype=np.int32)   #初始化batch,存放目标单词
    labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)  #初始化labels,存放目标单词的语境单词们
    span = 2 * skip_window + 1  #对某个单词创建相关样本时会用到的单词数量,包含目标单词和它前后的单词
    buffer = collections.deque(maxlen=span) #创建一个最大容量为span的deque,即双向队列

    for _ in range(span):
        buffer.append(data[data_index]) #从序号data_index开始, 把span个单词的编码顺序读入buffer作为初始值,循环完后,buffer填满,里面为目标单词和需要的单词
        data_index = (data_index + 1) % len(data)

    for i in range(batch_size // num_skips):    #批次的大小➗每个单词生成的样本数=该批次中单词的数量
        target = skip_window    #即buffer中下标为skip_window的变量为目标单词
        targets_to_avoid = [skip_window]    #避免使用的单词列表,要预测的时语境,不包含单词本身

        for j in range(num_skips):

            while target in targets_to_avoid:   #生成随机数直到不在targets_to_avoid中,代表可以使用的语境单词
                target = random.randint(0, span-1)

            targets_to_avoid.append(target)     #该语境单词已使用,加入避免使用的单词列表
            batch[i * num_skips + j] = buffer[skip_window]  #feature即目标词汇
            labels[i * num_skips + j, 0] = buffer[target]   #label即当前语境单词

        buffer.append(data[data_index]) #对一个目标单词生成完所有样本后,再读入下一个单词(同时会抛掉buffer中第一个单词)
        data_index = (data_index + 1) % len(data)   #单词序号+1

    #获得了batch_size个训练样本,返回目标单词编号batch数组和其对应语境单词编号labels数组
    return batch, labels


'''测试word2vec训练样本生成'''
# 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    #单词转为词向量的维度,一般为50-1000这个范围内的值
skip_window = 1
num_skips = 2

#生成验证数据valid_samples
valid_size = 16     #用来抽取的验证单词数
valid_window = 100  #验证单词从频数最高的100个单词中抽取
valid_examples = np.random.choice(valid_window, valid_size, replace=False)  #从valid_window中随机抽取valid_size个数字,返回一维数组
num_sampled = 64    #训练时用来做负样本的噪声单词的数量


'''定义Skip-Gram Word2Vec模型的网络结构'''
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)     #验证单词的索引   shape(1, 16)

    with tf.device('/cpu:0'):   #限定所有计算在CPU上执行,因为接下去的一些计算操作在GPU上可能还没有实现
        embeddings = tf.Variable(
            tf.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))   #随机生成所有单词的词向量embeddings,范围[-1, 1]

        embed = tf.nn.embedding_lookup(embeddings, train_inputs)    #在embeddings tensor中查找输入train_inputs编号对应的向量embed

        nce_weights = tf.Variable(  #使用tf.truncated_normal截断的随机正态分布初始化NCE Loss中的权重参数nce_weights
            tf.truncated_normal([vocabulary_size, embedding_size], stddev=1.0 / math.sqrt(embedding_size)))

        nce_biases = tf.Variable(tf.zeros([vocabulary_size]))   #偏置初始化为0

    #使用tf.nn.nce_loss计算出词向量embedding在训练数据上的loss,并使用tf.reduce_mean进行汇总
    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))  #可能的分类的数量(单词数)

    optimizer = tf.train.GradientDescentOptimizer(1.0).minimize(loss)

    '''余弦相似度计算'''
    #L2范数又叫“岭回归”,作用是改善过拟合,L2范数计算方法:向量中各元素的平方和然后开根号
    norm = tf.sqrt(tf.reduce_sum(tf.square(embeddings), 1, keep_dims=True)) #计算嵌入向量embeddings的L2范数
    normalized_embeddings = embeddings / norm   #embeddings除以其L2范数得到标准化后的normalized_embeddings

    valid_embeddings = tf.nn.embedding_lookup(  #根据验证单词的索引valid_dataset,查询验证单词的嵌入向量
        normalized_embeddings, valid_dataset)
    # 计算验证单词的嵌入向量与词汇表中所有单词的相似性, valid_embeddings * (normalized_embeddings的转置)
    similarity = tf.matmul(valid_embeddings, normalized_embeddings, transpose_b=True)

    init = tf.global_variables_initializer()


'''可视化Word2Vec,low_dim_embds为降维到2维的单词的空间向量'''
def plot_with_labels(low_dim_embds, labels, filename='tsne.png'):
    assert low_dim_embds.shape[0] >= len(labels), "More labels than embeddings"
    plt.figure(figsize=(18, 18))    #图片大小

    for i, label in enumerate(labels):
        x, y = low_dim_embds[i,:]
        plt.scatter(x, y)   #显示散点图
        plt.annotate(label,     #展示单词本身
                     xy=(x, y),
                     xytext=(5, 2),
                     textcoords='offset points',
                     ha='right',
                     va='bottom')
    plt.savefig(filename)



'''测试'''
num_steps = 100001  #训练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    #损失值累加

        if step % 2000 == 0:    #每两千轮计算几次平均损失值
            if step > 0:
                average_loss /= 2000
            print("Average loss at step", step, ": ", average_loss)
            average_loss = 0

        #每一万轮,计算一次验证单词与全部单词的相似度,并将与每个验证单词最相近的8各单词展示出来
        if step % 10000 == 0:
            # tensor.eval():在`Session`中评估这个张量。调用此方法将执行所有先前的操作,这些操作将生成生成此张量的操作所需的输入。
            sim = similarity.eval()     #shape,(16, 50000)
            #print(tf.shape(sim).eval())

            for i in range(valid_size): #对每一个验证单词
                valid_word = reverse_dictionary[valid_examples[i]]  #根据前面随机抽取的验证单词编号(即频次),在反转字典中取出该验证单词
                top_k = 8
                #.argsort()从小到大排列,返回其对应的索引,由于-sim(),所以返回的索引是相似度从大到小的
                nearest = (-sim[i, :]).argsort()[1:top_k+1]     #计算得到第i个验证单词相似度最接近的前8个单词的索引
                log_str = "Nearest to %s:" % valid_word

                for k in range(top_k):
                    close_word = reverse_dictionary[nearest[k]]     #相似度最接近的第i个单词
                    log_str = "%s %s," % (log_str, close_word)
                print(log_str)

    final_embeddings = normalized_embeddings.eval() #最终训练完的词向量矩阵


'''展示降维后的可视化效果'''
#使用sklearn.manifold.TSNE实现降维,这里直接将原始的128维嵌入向量降到2维
tsne = TSNE(perplexity=30,      #困惑度,默认30
            n_components=2,     #降到多少维
            init='pca',         #初始化的嵌入
            n_iter=5000)        #优化的最大迭代次数。至少应该是250。
plot_only = 150
low_dim_embs = tsne.fit_transform(final_embeddings[:plot_only, :])  #进行降维,输入shape为 (n_samples, n_features) or (n_samples, n_samples)
labels = [reverse_dictionary[i] for i in range(plot_only)]
plot_with_labels(low_dim_embs, labels)  #用该可视化函数进行展示


结果:

将训练好的向量降维并可视化,如图所示,数字等相近的单词在空间中的位置相近

Tensorflow实战:Word2Vec_Skip_Gram原理及实现(多注释)_第1张图片

你可能感兴趣的:(深度学习)