Word2Vec之Skip-Gram模型实现代码详解


转载自: Skip-Gram代码

引入

对于一个句话,我们认为这句话中的每两个词之间都可能存在一定联系,但这种联系不是必然的,比如“明天要下雨”这句话,我们不能推测明天和下雨一定有关系。但我们基于统计学来看,给我们大量的逻辑上正确的话,当我们总是能在这些话里面找到某两个词同时出现,那么就可以说明这两个词确实存在关系,比如分析100万句话,然后发现人工智能和机器学习出现在同一句话的频次较高,那么我们就可以推断这两个词之间是存在联系。
如何去实现我们的想法呢?大量数据总是和神经网络相关的

网络模型

原理

给我们一篇非常长的文章,可以认为文章中相邻的词(或者是位置相近的词)之间存在关系。
具体是操作方法各类各样,为了方便弄清原理,我们简化了操作。
例如下面的文章
在这里插入图片描述这篇文章已经去掉了标点符号,直接把全文当做一个整体。那么我们怎么去找词与词之间的联系呢?
对于前几个单词anarchism originated as a term of abuse
首先设置参数:
skip_window=1,表示去左侧1个词和右侧1个词进入窗口。
num_skips=2,表示我们从当前整个窗口中选择多少个不同的词作为我们的output word。
那么当我们选中单词originated作为input word时,我们会得到两组(input word, output word) 形式的训练数据,即 (‘originated’, ‘as’),(‘originated’, ‘anarchism’)。我们便得到了两个训练数据:
输入值:originated 输出值:as
输入值:originated 输出值:anarchism
但是计算机无法直接对单词进行计算和处理,所以需要先把单词变成数字形式。

单词格式转化数字格式

因为文中单词种类特别多,所以我们只取50000个出现最多的单词分析。
将所有单词按出现频次从大到小的顺序,并以二维数组形式保存前49999个,且将剩余的单词记为UNK,数量记为-1,放在数组的第一位。
这个二维数组的前十个元素为:[[‘UNK’, -1], (‘the’, 1061396), (‘of’, 593677), (‘and’, 416629), (‘one’, 411764), (‘in’, 372201), (‘a’, 325873), (‘to’, 316376), (‘zero’, 264975), (‘nine’, 250430)]
再将这些单词按在二维数组中的序号来代替出现频次,并存放在字典中。
这个字典前十个元素为:{“UNK”:0,“the”:1,…,“nine”:9}
至此所有单词都有了唯一的编号与之对应
最后将原文中所有的单词都转化为数字编号,并保存在一个新的数组data中。

print(data[:10]# 查看data里面的前十个元素,即前十个单词对应的数字编号
'''[5234, 3081, 12, 6, 195, 2, 3134, 46, 59, 156]'''

代码详解

导入库

# 1.导入所依赖的库
import time
import collections
import math
import os
import random
import zipfile
import numpy as np
import urllib
import pprint
import tensorflow as tf
import matplotlib.pyplot as plt
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"

准备数据集

# 2.准备数据集

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


def maybe_download(filename, expected_bytes):
    """
    判断文件是否已经下载,如果没有,则下载数据集
    """
    if not os.path.exists(filename):
        # 数据集不存在,开始下载
        # 按网址:http://mattmahoney.net/dc/text8.zip下载文件,同时以名字filename(在这里是text8.zip)保存在当前目录
        filename, _ = urllib.request.urlretrieve(url + filename, filename)
        # 有关函数urllib.request.urlretrieve的详解博客https://blog.csdn.net/pursuit_zhangyu/article/details/80556275
    # 核对文件尺寸
    stateinfo = os.stat(filename)
    # 有关系统stat调用的详细解释链接https://www.cnblogs.com/fmgao-technology/p/9056425.html
    if stateinfo.st_size == expected_bytes:
        print("数据集已存在,且文件尺寸合格!", filename)
    else :
        print(stateinfo.st_size)
        raise Exception(
            "文件尺寸不对 !请重新下载,下载地址为:"+url
        )
    return filename


"""
测试文件是否存在
"""
filename = maybe_download("text8.zip", 31344016)

解压文件

def read_data(filename):
    # 有关zipfile模块的详细使用介绍博客https://www.cnblogs.com/ManyQian/p/9193199.html
    with zipfile.ZipFile(filename) as f:    # 提取压缩文件并用f表示该文件
        # 有关python中split()函数的详细解释博客https://www.runoob.com/python/att-string-split.html
        data = tf.compat.as_str(f.read(f.namelist()[0])).split()    # 取压缩文件里面的第一个文件(索引为0)
        '''
        使用 zipfile.ZipFile()来提取压缩文件,然后我们可以使用
        zipfile 模块中的读取器功能。首先,namelist()函数检索该
        档案中的所有成员——在本例中只有一个成员,所以我们可以使
        用 0 索引对其进行访问。然后,我们使用 read()函数读取文
        件中的所有文本,并传递给 TensorFlow 的 as_str 函数,以确
        保文本保存为字符串数据类型。最后,我们使用 split()函数
        创建一个列表,该列表包含文本文件中所有的单词,并用空格字
        符分隔'''
    return data

filename = "text8.zip"
words = read_data(filename)
print("总的单词个数:", len(words))

构造单词与编号对应表

# 4.构建词汇表,并统计每个单词出现的频数,同时用字典的形式进行存储,取频数排名前50000的单词
vocabulary_size = 50000


def build_dataset(words):   # words是输入的原文
    count = [["UNK", -1]]   # 这里不可以用unknown,因为原文中有unknown,所以会出错
    # collections.Counter()返回的是形如[["unkown",-1],("the",4),("physics",2)]
    count.extend(collections.Counter(words).most_common(vocabulary_size - 1))
    # 有关collection模块中counter类的详细解释链接http://www.pythoner.com/205.html
    # most_common()函数用来实现Top n 功能 即截取counter结果的前多少个子项
    # 对于列表的一些常见基本操作详细链接https://blog.csdn.net/ywx1832990/article/details/78928238
    # print(count[:10]) # 这时候count中存放的是频次前50000的单词以及它们出现的频次,其余的认为是unknown,频次为-1
    dictionary = {}
    # 将全部单词转为编号(以频数排序的编号),我们只关注top50000的单词,并记UNK编号为0,同时统计一下这类词汇的数量
    for word, _ in count:
        dictionary[word] = len(dictionary)
        # len(dictionary)是输出字典中元素个数,即键的个数
        # 开始时字典为空,len(dictionary)为0,第一个键是unknown,则有"unknown":0
        # 第二个键是the,此时字典中只有一个元素,则有"the":1
        # 形如:{"the":1,"UNK":0,"a":12}
    # print(dictionary["UNK"])
    data = []
    unk_count = 0  # 准备统计top50000以外的单词的个数
    for word in words:
        # 对于其中每一个单词,首先判断是否出现在字典当中
        if word in dictionary:
            # 如果已经出现在字典中,则转为其编号
            index = dictionary[word]
        else:
            # 如果不在字典,则转为编号0
            index = 0
            unk_count += 1
        data.append(index)  # data存放的是words即原文中所有单词对应的编号
    """
    print(data[:10])
    [5234, 3081, 12, 6, 195, 2, 3134, 46, 59, 156]
    """
    count[0][1] = unk_count  # 将统计好的unknown的单词数,填入count中
    # 将字典进行翻转,形如:{3:"the",4:"an"},字典翻转有很多方法,可以百度选择一种即可
    reverse_dictionary = dict(zip(dictionary.values(), dictionary.keys()))
    return data, count, dictionary, reverse_dictionary
    # 返回值意义分别是data:words即原文中所有单词对应的编号
    # count:二维数组,频次前50000的单词以及它们出现的频次,其余的认为是unknown,频次为-1,并放在第一个
    # dictionary:对count中内容按序编号
    # reverse_dictionary:翻转dictionary

# 为了节省内存,将原始单词列表进行删除
data,count,dictionary,reverse_dictionary = build_dataset(words)
del words
# 将部分结果展示出来
# print("出现频率最高的单词(包括未知类别的):",count[:10])
# 将已经转换为编号的数据进行输出,从data中输出频数,从翻转字典中输出编号对应的单词
# print("样本数据(排名):",data[:10],"\n对应的单词",[reverse_dictionary[i] for i in data[:10]])

生成训练样本

# 5.生成Word2Vec的训练样本,使用skip-gram模式
data_index = 0


def generate_batch(batch_size, num_skips, skip_window):
    """

    :param batch_size: 每个训练批次的数据量,8
    :param num_skips: 每个单词生成的样本数量,不能超过skip_window的两倍,并且必须是batch_size的整数倍,2
    :param skip_window: 单词最远可以联系的距离,设置为1则表示当前单词只考虑前后两个单词之间的关系,也称为滑窗的大小,1
    :return:返回每个批次的样本以及对应的标签
    """
    global data_index  # 声明为全局变量,方便后期多次使用
    # 使用Python中的断言函数,提前对输入的参数进行判别,防止后期出bug而难以寻找原因
    assert batch_size % num_skips == 0
    assert num_skips <= skip_window * 2

    batch = np.ndarray(shape=(batch_size), dtype=np.int32)  # 创建一个batch_size大小的数组,数据类型为int32类型,数值随机
    labels = np.ndarray(shape=(batch_size, 1), dtype=np.int32)  # 数据维度为[batch_size,1]
    span = 2 * skip_window + 1  # 入队的长度,左右滑窗大小+本身
    buffer = collections.deque(maxlen=span)  # 创建双向队列。最大长度为span
    """
    print(batch,"\n",labels)
    batch :[0 ,-805306368  ,405222565 ,1610614781 ,-2106392574 ,2721-,2106373584 ,163793]
    labels: [[         0]
            [-805306368]
            [ 407791039]
            [ 536872957]
            [         2]
            [         0]
            [         0]
            [    131072]]
    """
    # 对双向队列填入初始值
    for _ in range(span):
        buffer.append(data[data_index])
        data_index = (data_index+1) % len(data)
    """
    print(buffer,"\n",data_index)  输出:
                                    deque([5234, 3081, 12], maxlen=3)
                                    3
    """
    # 进入第一层循环,i表示第几次入双向队列
    for i in range(batch_size // num_skips):    # i:0->3
        target = skip_window  # 定义buffer中第skip_window个单词是目标
        targets_avoid = [skip_window]  # 定义生成样本时需要避免的单词,因为我们要预测的是语境单词,不包括目标单词本身,因此列表开始包括第skip_window个单词
        for j in range(num_skips):  # j:0->1
            """第二层循环,每次循环对一个语境单词生成样本,先产生随机数,直到不在需要避免的单词中,也即需要找到可以使用的语境词语"""
            while target in targets_avoid:
                target = random.randint(0, span-1)
                # print(target)   # 找到2时退出一次,2作为可使用的语境词语。第二次for j时,只能找0,0作为可使用的语境词语。(0和2顺序可变)
            targets_avoid.append(target)  # 因为该语境单词已经被使用过了,因此将其添加到需要避免的单词库中
            batch[i * num_skips + j] = buffer[skip_window]  # 目标词汇 i=0时:0,1   i=1时:2,3  i=2时:4,5
            labels[i * num_skips + j, 0] = buffer[target]  # 语境词汇
        # 此时buffer已经填满,后续的数据会覆盖掉前面的数据
        buffer.append(data[data_index])
        data_index = (data_index + 1) % len(data)
    # print(batch,labels)
    return batch, labels
# 举例子看看是否正确
batch, labels = generate_batch(8, 2, 1)


# for i in range(8):
#       print("目标单词:"+reverse_dictionary[batch[i]]+"对应编号为:".center(20)+str(batch[i])+"   对应的语境单词为: ".ljust(20)+reverse_dictionary[labels[i,0]]+"    编号为",labels[i,0])
"""
for i in range(8):
    print("目标单词:"+reverse_dictionary[batch[i]]+"对应编号为:".center(20)+str(batch[i])+"   对应的语境单词为: ".ljust(20)+reverse_dictionary[labels[i,0]]+"    编号为",labels[i,0])
测试结果:
目标单词:originated         对应编号为:   3080    对应的语境单词为:  as           编号为 12
目标单词:originated         对应编号为:   3080    对应的语境单词为:  anarchism    编号为 5233
目标单词:as                 对应编号为:   12      对应的语境单词为:  originated   编号为 3080
目标单词:as                 对应编号为:   12      对应的语境单词为:  a            编号为 6
目标单词:a              对应编号为:   6       对应的语境单词为:  as           编号为 12
目标单词:a              对应编号为:   6       对应的语境单词为:  term         编号为 195
目标单词:term           对应编号为:   195     对应的语境单词为:  of           编号为 2
目标单词:term           对应编号为:   95      对应的语境单词为:  a            编号为 6
"""

训练模型

# 6.定义训练数据的一些参数
batch_size = 128  # 训练样本的批次大小
embedding_size = 128  # 单词转化为稠密词向量的维度
skip_window = 1  # 单词可以联系到的最远距离
num_skips = 1  # 每个目标单词提取的样本数

# 7.定义验证数据的一些参数
valid_size = 16  # 验证的单词数
valid_window = 100  # 指验证单词只从频数最高的前100个单词中进行抽取
valid_examples = np.random.choice(valid_window, valid_size, replace=False)  # 进行随机抽取
num_sampled = 64  # 训练时用来做负样本的噪声单词的数量

# 8.开始定义Skip-Gram Word2Vec模型的网络结构
# 8.1创建一个graph作为默认的计算图,同时为输入数据和标签申请占位符,并将验证样例的随机数保存成TensorFlow的常数
graph = tf.Graph()
with graph.as_default():
    # 生成了一个占位符,这样申请位置对后面运算可以加速,此时还没有存东西
    # tf.placeholder()函数作为一种占位符用于定义过程,可以理解为形参,在执行的时候再赋具体的值。
    train_inputs = tf.compat.v1.placeholder(tf.int32, [batch_size])  # 128行,多少列未知,每一行都是独热编码
    train_labels = tf.compat.v1.placeholder(tf.int32, [batch_size, 1])  # 128行,1列
    valid_dataset = tf.constant(valid_examples, tf.int32)   # 16行,多少列未知,valid_dataset是包含16个编号的数组
    # 选择运行的device为CPU
    with tf.device("/cpu:0"):
        # 单词大小为50000,向量维度为128,随机采样在(-1,1)之间的浮点数
        embeddings = tf.Variable(tf.compat.v1.random_uniform([vocabulary_size, embedding_size], -1.0, 1.0))  #(50000,128)
        # 使用tf.nn.embedding_lookup()函数查找train_inputs对应的向量embed,根据索引选择对应的元素,避开了对输入值的独热,本篇仅对输出值独热
        embed = tf.nn.embedding_lookup(embeddings, train_inputs)    # 根据train_inputs中的id号,寻找(独热编码与之点积)embeddings中的对应元素,(128,128),即输入的128个词的嵌入词向量。
        # 使用截断正太函数初始化权重,偏重初始化为0
        weights = tf.Variable(tf.compat.v1.truncated_normal([vocabulary_size, embedding_size], stddev=1.0 /math.sqrt(embedding_size)))  # (50000,128)
        biases = tf.Variable(tf.zeros([vocabulary_size]))
        # 隐藏层实现
        hidden_out = tf.matmul(embed, tf.transpose(weights)) + biases   # embed与转置后的weights点积,得到(128,50000)
        # 将标签使用one-hot方式表示,便于在softmax的时候进行判断生成是否准确
        train_one_hot = tf.one_hot(train_labels, vocabulary_size)   # (128,50000),500000表示向量维度。根据train_labels的不同,在vocabulary_size中分配独热编码,每轮训练分配的都不一样
        # print(train_one_hot)
        cross_entropy = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(logits=hidden_out, labels=train_one_hot))  # 两个(128,50000)计算损失
        # 优化选择随机梯度下降
        optimizer = tf.compat.v1.train.GradientDescentOptimizer(1.0).minimize(cross_entropy)
        # 为了方便进行验证,采用余弦定理验证相似性  链接https://blog.csdn.net/u012160689/article/details/15341303
        # 归一化
        norm = tf.compat.v1.sqrt(tf.compat.v1.reduce_sum(tf.square(weights), 1, keepdims=True))
        normalized_embeddings = weights / norm  # 这两行的目的是将embeddings的每行的向量归一化   (50000,128)
        valid_embeddings = tf.nn.embedding_lookup(normalized_embeddings, valid_dataset)  # 根据这16个编号去查询验证归一化的单词嵌入向量,(16,128)
        # 计算验证单词的嵌入向量与词汇表中所有单词的相似性
        similarity = tf.matmul(
            valid_embeddings, normalized_embeddings, transpose_b=True   # (16,50000),这16个验证单词与所有50000个单词的相似性
        )
        init = tf.compat.v1.global_variables_initializer()  # 因为tf中建立的变量是没有初始化的,执行init后便可开始定义参数的初始化

开始训练

# 9.启动训练
num_steps = 150001  # 进行15W次的迭代计算
t0 = time.time()
# 创建一个回话并设置为默认
with tf.compat.v1.Session(graph=graph) as session:
    init.run()  # 启动参数的初始化
    print("初始化完成!")
    average_loss = 0  # 计算误差

    # 开始迭代训练
    for step in range(num_steps):
        batch_inputs, batch_labels = generate_batch(batch_size, num_skips, skip_window)  # 调用生成训练数据函数生成一组batch和label
        feed_dict = {train_inputs: batch_inputs, train_labels: batch_labels}  # 待填充的数据
        # 启动回话,运行优化器optimizer和损失计算函数,并填充数据
        # sess.run(函数名,feed_dict = {字典形式的给形参赋值}),这个时候才给,这是才开始往batch_inputs和batch_labels里面填充值,没run之前里面还没放值
        # 只是一个对应形状的占位符,所以若要直接获取值,则需要 matrix.eval()
        optimizer_trained, loss_val = session.run([optimizer, cross_entropy], feed_dict=feed_dict)  # optimizer_trained没用,可以直接改为:_,loss_val=
        average_loss += loss_val  # 统计NCE损失

        # 为了方便,每2000次计算一下损失并显示出来
        if step % 2000 == 0:
            if step > 0:
                average_loss /= 2000
            print('第%d轮迭代用时:%s' % (step, time.time() - t0))
            t0 = time.time()
            print("第{}轮迭代后的损失为:{}".format(step, average_loss))
            average_loss = 0

        # 每10000次迭代,计算一次验证单词与全部单词的相似度,并将于验证单词最相似的前8个单词呈现出来
        if step % 1000 == 0:
            sim = similarity.eval()  # 把张量的值给算出来,否则里面是占位符
            # 从频次最高的100个单词里抽16个验证单词,并分别找出与这16个验证单词相似度最接近的8个单词
            for i in range(valid_size):
                valid_word = reverse_dictionary[valid_examples[i]]  # 得到对应的验证单词
                top_k = 8
                # argsort()函数是将矩阵中的元素从小到大排列,提取其对应的index(索引),然后输出到y。
                # 50000个值进行排序,本质上是根据相似度排序
                # -sim即把矩阵中的所有值变成负数,再从小到大排序,本质上是从大到小排序,取下标1到top_k+1,因为0是这个词本身,其余top_k个词是与这个词最接近的8个词的编号
                # 那么输入的8个词就是按相似度从大到小输出的了,如果用sim画,后面会面临顺序反了的问题
                nearest = (-sim[i, :]).argsort()[1:top_k+1]  # 计算每一个验证单词相似度最接近的前8个单词
                # nearest = (sim[i, :]).argsort()[49991:49999]  # 这时的nearest的顺序是相反的
                log_str = "与单词 {} 最相似的: ".format(str(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()  # 最终的嵌入词向量

可视化

# 这里暂不了解
# 10.可视化Word2Vec效果


def plot_with_labels(low_dim_embs, labels, filename = "tsne.png"):
    # assert是如果满足条件,则继续运行,否则抛出AssertionError错误
    assert low_dim_embs.shape[0] >= len(labels), "标签数超过了嵌入向量的个数!!"
    plt.figure(figsize=(20, 20))
    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)
from sklearn.manifold import TSNE
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)
plt.show()
  • 1
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值