深入浅出了解word2vec模型 (理论与源码剖析)


关于算法工程师来说, Word2Vec 能够说是咱们耳熟能详的一种词向量核算算法,Goole在2013年一开源该算法就引起了工业界与学术界的广泛重视。

一般来说,word2vec 是依据序列中隔得越近的word越类似的根底假定来练习的模型, 模型的丢失函数也是依据该原理进行设计的。 开端word2vec是用来练习得到词向量,后边把它引申用于进行序列类(seq/list)数据中各个item ( 后边把word,node,item 等序列元素统称为 item ) 的embeding生成。

Google的论文以及广大算法工程师们现已从大量的实践中证明:两个item间的向量距离能够反映它们之间的联络(一般经过L2距离或余弦类似度得到), 而且两个item之间的语义联络核算能够用 他们的embeding 核算来替代。论文中提出的一个有意思的比方是:

Embedding(woman)=Embedding(man)+[Embedding(queen)-Embedding(king)]

embeding 的这个特性为可量化和可核算的item联络打开了新世界的大门,具有十分重要的参阅含义。


(1) Word2Vec根底

咱们都知道,Word2Vec开端是作为言语模型提出的,实践是一种从大量文本语料中以无监督办法学习语义常识的浅层神经网络模型,包括Skip-Gram 和 Continuous Bag Of Words(简称CBOW)两种网络结构。


(1.1)Skip-Gram和CBOW 结构

Skip-Gram和CBOW都能够表明成由输入层(Input) 、映射层(Projection) 和 输出层(Output)组成的神经网络。其间 Skip-Gram 的方针是依据当时词来猜测上下文中各词的生成概率,而 CBOW 的方针是依据上下文呈现的词来猜测当时词的生成概率。 网络结构图如下所示:

深入浅出理解word2vec模型 (理论与源码分析)

上图中,w(t) 为当时所重视的词,而w(t-2),w(t-1),w(t+1),w(t+2)为上下文呈现的单词。这儿前后滑动窗口的巨细均设置为2。

输入与输出均是语料库数据本身,所以底层本质上Word2Vec是无监督模型。

输入层 每个词由独热编码办法表明,即一切词均表明成一个N维向量,其间N为词汇表中单词的总数。在向量中,每个词都将与之对应的维度置为1,其他维度均为0。

映射层(也称隐含层) 中,K个隐含单元的取值能够由N维输入向量以及隐含单元之间的N*K维权重矩阵核算得到。在CBOW中,还需求将各个输入词所核算出的隐含单元求和,这儿是得到各个 输入词embeding 之后进行了sum pooling 操作(当然也能够挑选其他pooling办法,如 Average), 得到一个K维躲藏向量。Skip-Gram 这儿则是仅仅得到当时重视词的embeding, 无需pooling操作 。

同理,输出层 向量的值能够经过隐含层向量(K维)以及衔接隐含层和输出层之间的KN维权重矩阵核算得到。输出层也是一个N维向量,每维与词汇表中的每个单词对应。这儿,CBOW和 Skip-Gram 均是用一个 1 * K维的隐含层数据(embeding)和 K * N 维度的数据核算得到 1 * N 维度的logit值, 终究对输出层的 N维度向量(N维度logit值)应用SoftMax激活函数,能够核算出每个单词的生成概率。然后再和每个单词是0或1的标签核算丢失。


(1.2) Skip-Gram和CBOW 好坏点剖析

关于 Skip-Gram和 CBOW 模型的对比剖析,知乎上的卡门同学剖析的十分具有特色,具有很强的参阅含义。

从上面图中,咱们也能够看出:

Skip-Gram 是用中心词来猜测周围的词,在skip-gram中,会运用周围的词的猜测成果状况,运用GradientDecent来不断的调整中心词的词向量,终究一切的文本遍历结束之后,也就得到了文本一切词的词向量。可是在skip-gram当中,每个词都要收到周围的词的影响,每个词在作为中心词的时分,都要进行K次的猜测、调整。因而, 当数据量较少,或者词为冷僻词呈现次数较少时, 这种屡次的调整会使得词向量相对的愈加准确。

CBOW 是用上下文词猜测中心词,从而得到中心词的猜测成果状况,运用GradientDesent办法不断去优化调整周围词的向量。当练习完结之后,每个词都会作为中心词,把周围词的embeding进行了调整,这样也就获得了整个文本里一切词的词向量。 要注意的是, cbow的对周围词的调整是统一的:求出的gradient的值会相同的作用到每个周围词的词向量当中去。虽然cbow从别的一个角度来说,某个词也是会遭到屡次周围词的影响(屡次将其包含在内的窗口移动),进行词向量的跳帧,可是他的调整是跟周围的词一同调整的,grad的值会均匀分到该词上, 相当于该冷僻词没有收到专门的练习,它仅仅沾了周围词的光而已。

因而,从更通俗的角度来说:

在Skip-Gram里面,每个词在作为中心词的时分,实践上是 1个学生 VS K个教师,K个教师(周围词)都会对学生(中心词)进行“专业”的练习,这样学生(中心词)的“才能”(向量成果)相对就会扎实(准确)一些,可是这样肯定会运用更长的时刻。CBOW是 1个教师 VS K个学生,K个学生(周围词)都会从教师(中心词)那里学习常识,可是教师(中心词)是一视同仁的,教给咱们的相同的常识。

所以,一般来说 CBOW比Skip-Gram练习速度快,练习进程愈加安稳,原因是CBOW运用上下文的办法进行练习,每个练习step会见到更多样本。而在冷僻字(呈现频率低的字)处理上,skip-gram比CBOW作用更好,学习的词向量更详尽,原因就如上面剖析: CBOW 是公共课,Skip-gram 是私教


(2) Skip-Gram模型详解

书接上文, 在咱们实践中的数据集中绝大部份的数据都是高维稀疏的数据集,大量的实践证明Skip-Gram的确作用更好,所以这儿以 Skip-Gram为框架解说Word2Vec模型的细节。


(2.1) 丢失函数阐明

如上文图中右边所示,Skip-Gram的学习使命是用中间的词与猜测周围的词,练习得到词的embedding便能够用于下游使命。Skip-Gram 的办法化界说: 给定单词序列 W1, W2, W3 … Wt, 选取一个长度为2m+1(方针词前后各选取m个词)的滑动窗口,将滑动窗口从左到右华东区,每移动一次,窗口中的词组就形成了一个练习样本。

咱们知道, 概率是用于已知模型参数,猜测接下来观测到样本的成果; 而似然性用语已知某些观测所得到的成果,对有关事务的性质参数进行估量。

而在 Skip-Gram中每个词 Wt 都决议了相邻的词 Wt+j , 在观测序列已定的状况下,咱们能够依据极大似然估量的办法,希望一切样本的条件概率 P( Wt+j / Wt ) 之积最大,这儿运用对数概率。因而Skip-Gram的方针函数是最大化均匀的对数概率,即:

深入浅出理解word2vec模型 (理论与源码分析)

其间 m 是窗口巨细,m越大则样本数越多,猜测准确性越高,但一起练习时刻也越长。当然咱们还会在上述公司前面乘以 负 1/ T 以便利现有最优化办法的梯度优化。

作为一个多分类问题,Skip-Gram 界说概率 P( Wt+j / Wt ) 的最直接的办法是运用SoftMax函数。 假定咱们用一个向量 Vw表明词w, 用词之间的内积距离VitVj表明两词语义的挨近程度。则条件概率 P( Wt+j / Wt ) 能够用下式给出:

深入浅出理解word2vec模型 (理论与源码分析)

其间,Wo代表Wt+j , 被称为输出词; Wi 代表 Wt, 被称为输入词。 注意在上面的公式中,Wo和Wi 并不在一个向量空间内,Vwo 和Vwi 分别是词W的输出向量表达和输入向量表达。

在上文里咱们曾经说过,从输入层到映射层的维度为 N * K,而从映射层到输出层的维度为 K * N。 这也便是说每个单词均有两套向量表达办法。实践证明:每个单词用两套向量表达的办法一般作用比单套向量表达办法的作用好,由于两套向量表达的办法应用到下游使命时能够去取两个embeding的均匀值


(2.2) 练习进程优化

需求注意,咱们上面说的输入层和输出层的维度 N 是词表中单词的总数,在实践中一般都十分大,千万甚至上亿的量级都是十分常见。但事实上,完全遵循原始的Word2Vec多分类结构的练习办法并不可行。

假定语料库中词的数量为1KW,则意味着输出层神经元有1KW个,在每次迭代更新到躲藏层神经元的权重时,都需求核算一切字典中1KW个词的猜测误差,这在实践核算 的时分是不切实践的。

Word2vec 提出了2种办法处理该问题,一种是层次化的 Hierarchical Softmax, 另一种是 负采样(Negative Sampling) 。

层次softmax 根本思想是将复杂的归一化概率分解为一系列条件概率乘积的办法,每一层条件概率对应一个二分类问题,经过逻辑回归函数能够去拟合。对v个词的概率归一化问题就转化成了对logv个词的概率拟合问题。Hierarchical softmax经过结构一棵二叉树将方针概率的核算复杂度从开端的V下降到了logV的量级。可是却增加了词与词之间的耦合性。比方一个word呈现的条件概率的改变会影响到其途径上一切非叶子节点的概率改变。间接地对其他word呈现的条件概率带来影响, 一起层次softmax 也由于完结比较困难,功率较低且并没有比负采样更好的作用,所以在实践中运用的并不多。

这儿咱们主要阐明 负采样 (Negative Sampling ) 的办法。比较于本来需求核算一切字典中一切词的猜测误差,负采样的办法只需求对采样出的几个负样本核算猜测误差。再次状况下,word2vec 模型的优化方针从一个多分类问题退化成了一个近似二分类问题。

其优化方针界说为:

深入浅出理解word2vec模型 (理论与源码分析)

其间,Pn(w) 是噪声散布,采样生成k个负样本,使命变成从噪声中区分出方针单词 Wo, 整体的核算量与K成线性联络,K在经历上去2~20即可,远小于 Hierarchical Softmax 所需求的 log(W) 词核算。

Pn(w) 的经历取值是一元散布的四分之三次方,作用远超简略的一元散布或均匀散布。


(3) 实践经历

类似于 Word2Vec依据单词序列数据练习Embedding,咱们也能够把用户行为点击过的item序列数据 喂给Word2Vec 算法,练习得到item 的Embeding , 这种办法也称为 item2Vec


(3.1) 灵敏构建序列

在运用Word2Vec学习的进程中,喂入模型的item序列的构建是十分重要的,咱们能够在item ID序列中参加item的类目等特点信息来构建序列。咱们构建的序列并不一定紧紧只要一个类其他数据,例如这个序列也能够是: userid, ip,sn, email , 可是在某些状况下,为了更好的建模用户和各个特点的联络,咱们能够构建这样的序列: userid, ip,userid,sn,userid,email。

构建序列的办法是十分灵敏的,咱们依据自己的了解和业务需求动态调整即可。

相同,在依据随机游走的 Graph Embeding 算法中,咱们能够在同构图上运用深度游走( deepwalk ) 的办法,或则在异构图上运用元途径 (meta path) 的办法得到一些 item 的游走序列,然后把这些序列喂入 skip-gram 模型中, 也能够得到不错的作用。


(3.2) Airbnb的word2vec建模实践

这儿要要点介绍下 Airbnb在2018年 发布在KDD 的最佳论文 Real-time Personalization using Embeddings for Search Ranking at Airbnb

该论文中介绍了embedding在 Airbnb 爱彼迎房源查找排序和实时个性化推荐中的实践。他们运用了 listing embeddings(房源嵌入)和 用户点击行为来学习用户的短期爱好,用user-type & listing type embeddings和用户预定行为来学习用户的长时刻爱好,而且这两种办法都成功上线,用于实时个性化生成推荐。

其论文中有很多值得参阅的点,这儿简略罗列下:

(1) Airbnb 运用session内点击数据构建了序列建模用户的短期爱好。

(2) 将预定房源引入方针函数,即不论这个被预定房源在不在word2vec的滑动窗口中都架设这个被预定房源与滑动窗口的中心房源相关,即相当于引入了一个大局上下文到方针函数中。

(3) 依据某些特点规则做类似用户和类似房源的聚合,用户和房源被界说在同一个向量空间中,用word2vec负采样的办法练习,能够一起得到用户和房源的Embedding,二者之间的Cos类似度代表了用户对某些房源的长时刻爱好偏好。

(4) 把查找词和房源置于同一向量空间进行Embeding。

(5) 与中心房源同一市场的房源调集中进行随机采样作为负样本,能够更好发现同一市场内部房源的差异性。

才开端作者在看论文的时分想自己去复现一下论文,成果发现网络上也搜不到论文中介绍的怎么自界说丢失函数的办法,知乎上联络Airbnb的官方账号也没有收到回复,悲催。


(3.3) 一种可行的实践

终究,作者经过苦苦摸索,总算找到了自己的处理办法,下面进行简略的介绍:

首要要想清楚的一个点是:咱们喂入模型中的序列其实便是咱们练习Word2Vec模型的样本。观察上面的丢失函数公式咱们发觉,丢失函数其实便是依据咱们开篇所介绍的假定:序列中隔得越近的word越类似 。 然后,咱们用设计好的相应的数据去练习模型,是不是意味着咱们就修正了模型的练习方针呢。

例如:上面第5点,论文中介绍说,在方针函数中引入了同一地区的负样本调集参加到丢失函数,那咱们在构建样本pair的时分,是不是能够让负样本的挑选契合论文中说的办法,然后构建成 (pos sample, same place neg sample, -1 ) 这样的办法的样本增加到本来的样本集中呢。

这儿我想阐明的一点便是:咱们挑选什么样的样本输入模型,就等价于在模型练习的丢失函数中参加了什么类型的丢失。明着看起来是增加负样本,没有修正模型,可是本质上便是修正了丢失函数。

依据此,关于一切的序列数据,咱们都能够挑选Word2Vec(推荐Skip-Gram)进行序列item的embeding学习,仅仅咱们要要点重视练习样本序列的构建,由于这涉及到咱们模型终究的练习方针。

我不知道这一点我说清楚了没有,可是希望你能了解我大概要表明的意思,如有任何问题,欢迎联络我进行评论哈 ~


(4)代码时刻

talk is cheap , show me the code !!!

下面的代码是运用tensorflow2.0,选用了tf.keras 中阶API来构建模型结构,其间包括了怎么构建word2vec需求的pair数据,怎么在一元表上完结负采样,怎么导出word对应的embeding 以及怎么进步模型的练习速度。本代码具有具体的注释以及完结的思路阐明,具有极高的参阅价值与可重用性,有问题欢迎评论~

该工程完整代码,能够去 算法全栈之路 大众号回复 word2vec源码 下载。

@ 欢迎重视作者大众号 算法全栈之路
#!/usr/bin/env python
# -*- coding: utf-8 -*-
from tensorflow.python.ops import array_ops
from tensorflow.python.util import nest
import tensorflow as tf
from tensorflow.keras.layers import *
from tensorflow.keras.models import Model
from tensorflow.keras.optimizers import Adam
import tensorflow.keras.backend as backend
from tensorflow.python.ops import control_flow_ops
from collections import defaultdict
import numpy as np
import tqdm
import math
import random
class SaveEmbPerEpoch(tf.keras.callbacks.Callback):
    def set_emb(self, emb, vocab, inverse_vocab, word_embedding_file):
        self.word_embedding_file = word_embedding_file
        self.words_embedding_in = emb
        self.vocabulary = vocab
        self.inverse_vocab = inverse_vocab
    def on_epoch_end(self, epoch, logs=None):
        with open(self.word_embedding_file + '.{}'.format(epoch), 'w') as f:
            weights = self.words_embedding_in.get_weights()[0]
            for i in range(len(self.vocabulary)):
                emb = weights[i, :]
                line = '{} {}\n'.format(
                    self.inverse_vocab[i],
                    ' '.join([str(x) for x in emb])
                )
                f.write(line)
# wget http://mattmahoney.net/dc/text8.zip -O text8.gz
# gzip -d text8.gz -f
train_file = './train.txt'
class Word2Vec(object):
    def __init__(self, train_file, sample=1e-4, embedding_dim=200):
        # 练习文件
        self.train_file = train_file
        # 最大语句长度
        self.MAX_SENTENCE_LENGTH = 1024
        # 过滤掉词语频率低于count 的词语在count中
        self.min_count = 5
        # 子采样权重
        self.subsampling_power = 0.75
        # 采样率
        self.sample = sample
        # 依据练习数据发生的词典 word freq 字典保存起来
        self.save_vocab = './vocab.txt'
        # 窗口大熊啊
        self.window = 5
        # 每个中心词,挑选一个上下文词构建一个正样本,就随机采样挑选几个负样本.
        self.negative = 5
        # 从源码中仍是从论文中的构建样本办法 源码[context_word, word] or 论文[word, context_word]
        self.skip_gram_by_src = True
        # 维度
        self.embedding_dim = embedding_dim
        # 保存embeding 的文件
        self.word_embedding_file = 'word_embedding.txt'
        self.tfrecord_file = 'word_pairs.tfrecord'
        self.train_file = "./train.txt"
        self.vocabulary = None
        self.next_random = 1
        # 词表最大尺寸,一元模型表巨细
        self.table_size = 10 ** 8
        self.batch_size = 256
        self.epochs = 1
        # 是否生成tf_redocd数据参加练习
        self.gen_tfrecord = True
        # build vocabulary
        print('build vocabulary ...')
        self.build_vocabulary()
        # build dataset
        print('transfer data to tfrecord ...')
        # 是否生成 tf_record格局的数据
        if self.gen_tfrecord:
            self.data_to_tfrecord()
        # 运用from_generator,速度十分慢,遍历100个语句需求50s
        # self.dataset = tf.data.Dataset.from_generator(
        #     self.train_data_generator,
        #     output_types=(tf.int32, tf.int32),
        #     output_shapes=((2,), (),)
        # ).batch(self.batch_size).prefetch(1)
        print('parse tfrecord data to dataset ...')
        # 运用tfrecord后,100个语句需求6s
        self.dataset = self.make_dataset()
        # build model
        print('build model ...')
        self.build_model()
    # 构建练习数据调集,也便是解析tfrecord数据
    def make_dataset(self):
        # 解析单个样本
        def parse_tfrecord(record):
            features = tf.io.parse_single_example(
                record,
                features={
                    'pair': tf.io.FixedLenFeature([2], tf.int64),
                    'label': tf.io.FixedLenFeature([1], tf.float32)
                })
            label = features['label']
            pair = features['pair']
            return pair, label
        # 读入tfrecord file
        dataset = tf.data.TFRecordDataset(self.tfrecord_file) \
            .map(parse_tfrecord, num_parallel_calls=8) \
            .batch(self.batch_size).prefetch(self.batch_size)
        return dataset
    # 输入 word ,采样率
    # 构建一元模型表,进行高效的负采样
    # 概率巨细和数组宽度保持了共同性,在数组上随机采样,便是依照概率分层抽样
    def build_unigram_table(self, word_prob):
        # 构建unigram 表,一元表
        self.table = [0] * self.table_size
        word_index = 1
        # 用当时词语index的采样概率当时词语的长度
        # 初始化当时长度
        cur_length = word_prob[word_index]
        for a in range(len(self.table)):
            # 对表中每一个元素,找到该下表对应词语的index,也便是该词语
            # 每个词语对应一个下标,不满足下面那个判别条件的时分,当时下标放的元素依然是word_index
            self.table[a] = word_index
            # 当满足这个条件的时分,就需求进一步更新下标对应的值了。
            # 保持下标占比a 和概率占比cur_length处于共同的空间,不共同的时分就修正放的元素。
            # 占比比较
            if a / len(self.table) > cur_length:
                # 下一位放word_index+1
                word_index += 1
                # cur_len 构建了累计散布函数
                cur_length += word_prob[word_index]
            # Misra-Gries算法
            # 运用Misra-Gries算法,当词汇字典的巨细抵达极限值时,拜访词典的每一个key,将其value值的巨细-1,
            # 当某个key的value为0时将其移除字典,直到能够参加新的key.
            if word_index >= len(self.vocabulary):
                word_index -= 1
    def build_vocabulary(self):
        # 构建词频字典
        word_freqs = defaultdict(int)
        # 循环读取练习数据,得到某一行的各个单词
        for tokens in self.data_gen():
            # tokens 里得到某一行的各个单词
            for token in tokens:
                word_freqs[token] += 1
        # 低频过滤
        word_freqs = {word: freq for word, freq in word_freqs.items() \
                      if freq >= self.min_count}
        # 依照词语频率降序构建字典 {word :index },index 从1开端
        self.vocabulary = {word: index + 1 for (index, (word, freq)) in enumerate(
            sorted(word_freqs.items(), key=lambda x: x[1], reverse=True))}
        # index 0 特别处理
        self.vocabulary['</s>'] = 0
        # 倒排表 index,word
        self.inverse_vocab = {index: token for token, index in self.vocabulary.items()}
        # save vocab
        with open(self.save_vocab, 'w') as f:
            for i in range(len(self.vocabulary)):
                word = self.inverse_vocab[i]
                if i > 0:
                    freq = word_freqs[word]
                else:
                    freq = 0
                f.write(f"{word} {freq}\n")
        # 负采样的采样概率,f(w)^(3/4)/Z
        # 采样率核算的分母, 归一化求和,频率散布的 3/4
        train_words_ns = sum([freq ** (self.subsampling_power) for freq in word_freqs.values()])
        # 得到每一个单词index对应的采样频率
        self.ns_word_prob = {self.vocabulary[word]: (freq ** self.subsampling_power) / train_words_ns for word, freq in
                             word_freqs.items()}
        # 构建一元模型,在上面随机采样就能够做到word散布上的分层抽样
        self.build_unigram_table(self.ns_word_prob)
        #         self.unigrams_prob = [0]
        #         for i in range(1, len(self.vocabulary)):
        #             # print(inverse_vocab[i])
        #             self.unigrams_prob.append(self.ns_word_prob[i])
        # (sqrt(vocab[word].cn / (sample * train_words)) + 1) * (sample * train_words) / vocab[word].cn;
        # subsampling
        # 假如采样率大于0
        if self.sample > 0:
            # 一切频率的和
            train_words = sum([freq for freq in word_freqs.values()])
            # 依据每个词语的频率得到drop ratio
            self.subsampling_drop_ratio = {
                word: (math.sqrt(freq / (self.sample * train_words)) + 1) * (self.sample * train_words) / freq \
                for word, freq in word_freqs.items()
            }
    # 构建 word2vec 模型
    def build_model(self):
        vocab_size = len(self.vocabulary)
        # embedding_dim = 100
        inputs = Input(shape=(2,))
        target = inputs[:, 0:1]
        context = inputs[:, 1:2]
        self.words_embedding_in = tf.keras.layers.Embedding(
            vocab_size,
            self.embedding_dim,
            input_length=1,
            name="word_embedding_in"
        )
        self.words_embedding_out = tf.keras.layers.Embedding(
            vocab_size,
            self.embedding_dim,
            input_length=1,
            name="word_embedding_out"
        )
        word_emb = self.words_embedding_in(target)  # batch_size,1,embeing_size
        context_emb = self.words_embedding_out(context)
        dots = tf.keras.layers.Dot(axes=(2, 2))([word_emb, context_emb])
        outputs = tf.keras.layers.Flatten()(dots)
        self.model = Model(inputs, outputs)
        self.model.compile(
            optimizer='adam',
            # loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True),
            # loss=tf.keras.losses.binary_crossentropy(from_logits=True),
            loss=tf.keras.losses.BinaryCrossentropy(from_logits=True),
            metrics=['accuracy'])
    # 模型练习
    def train(self):
        # tensorboard_callback = tf.keras.callbacks.TensorBoard(log_dir="logs")
        my_callback = SaveEmbPerEpoch()
        my_callback.set_emb(self.words_embedding_in,
                            self.vocabulary,
                            self.inverse_vocab,
                            self.word_embedding_file)
        self.model.fit(word2vec.dataset, epochs=self.epochs, callbacks=[my_callback])
    def save_word_embeddings(self):
        with open(self.word_embedding_file, 'w') as f:
            f.write('{} {}\n'.format(len(self.vocabulary), self.embedding_dim))
            weights = self.words_embedding_in.get_weights()[0]
            for i in range(len(self.vocabulary)):
                emb = weights[i, :]
                line = '{} {}\n'.format(
                    self.inverse_vocab[i],
                    ','.join([str(x) for x in emb])
                )
                f.write(line)
    def data_to_tfrecord(self):
        # 写入到 tf_record file
        with tf.io.TFRecordWriter(self.tfrecord_file) as writer:
            # 得到进度条
            for item in tqdm.tqdm(self.train_data_gen()):
                # list  [context_word,word], 1.0/0.0
                # [word, context_word]
                pair, label = item
                feature = {
                    'pair': tf.train.Feature(int64_list=tf.train.Int64List(value=pair)),
                    'label': tf.train.Feature(float_list=tf.train.FloatList(value=[label]))
                }
                example = tf.train.Example(features=tf.train.Features(feature=feature))
                writer.write(example.SerializeToString())
    # 生成练习数据,生成完结之后传入下一个办法生成tf_record 数据
    def train_data_gen(self):
        cnt = 0
        sample_list = []
        # 得到上面采样过的一切的多行练习的单词 token_index
        for tokens_index in self.tokens_gen():
            # print(len(tokens_index), cnt)
            # 当时行的token 列表
            for i in range(len(tokens_index)):
                # print('cnt={}, i={}'.format(cnt, i))
                # 一共现已处理多少个单词了
                cnt += 1
                # 当时序列现已处理了当时第 i 个了。
                # 当时单词的 index, 中心词
                word = tokens_index[i]
                # 生成一个窗口巨细之内的随机数
                b = random.randint(0, self.window - 1)
                # 中心词前取几个,后取几个的分界线
                window_t = self.window - b
                # c为上下文坐标
                #                 context_ = [tokens_index[c] for c in range(i - window_t, i + window_t + 1) \
                #                                if c >=0 and c <=len(tokens_index) and c != i]
                #                 print('window_t = {}, contexts words={}'.format(window_t, context_))
                # 中心词为i ,i前取 i - window_t个,i 后取 i + window_t + 1个。
                for c in range(i - window_t, i + window_t + 1):
                    # 越界的和中心词跳过。
                    if c < 0 or c >= len(tokens_index) or c == i:
                        continue
                    # 当时列表中的当时中心词的上下文
                    #
                    context_word = tokens_index[c]
                    # print('c={}, context_word={}'.format(c, context_word))
                    # 结构副样本
                    # 选用np.random.choice的办法,10句话要5分钟。
                    # 选用tf.random.fixed_unigram_candidate_sampler,10句话要7分钟。
                    # 所以终究还得用hash的办法搞。10句话根本不需求时刻
                    # 可是改成dataset后,仍然需求5s
                    #                     neg_indexs = [np.random.choice(
                    #                         list(self.ns_word_prob.keys()),
                    #                         p=list(self.ns_word_prob.values())) for _ in range(self.negative)]
                    #
                    # 做self.negative 次负采样
                    # 每个中心词,挑选一个上下文词构建一个正样本,就随机采样挑选几个负样本.
                    neg_indexs = [self.table[random.randint(0, len(self.table) - 1)] \
                                  for _ in range(self.negative)]
                    # 调用当时函数就回来一个迭代值,下次迭代时,代码从 yield 当时的下一条语句继续履行,
                    # 而函数的本地变量看起来和前次中止履行前是完全相同的,所以函数继续履行,直到再次遇到 yield。
                    if self.skip_gram_by_src:
                        # 从源码中仍是从论文中的构建样本办法 源码[context_word, word] or 论文[word, context_word]
                        # 回来正样本
                        sample_list.append(([context_word, word], 1.0))
                        # 遍历负采样样本
                        for negative_word in neg_indexs:
                            # 假如负采样的词不等于当时词
                            if negative_word != word:
                                # 回来一组负样本,替换掉中心词语
                                sample_list.append(([context_word, negative_word], 0.0))
                    else:
                        # 和上面的唯一性区别便是
                        sample_list.append(([context_word, word], 1.0))
                        for negative_word in neg_indexs:
                            if negative_word != word:
                                sample_list.append(([context_word, negative_word], 0.0))
        return sample_list
    # 回来 token_index list ,练习的单词
    def tokens_gen(self):
        cnt = 0
        lines_tokens_list = []
        all_tokens_count = 0
        # 读入原始练习数据,得到一切行的数据
        for tokens in self.data_gen():
            # 当时行
            tokens_index = []
            for token in tokens:
                if token not in self.vocabulary:
                    continue
                if self.sample > 0:
                    # 假如需求进行采样
                    # 得到word,drop_ratio概率,大于这个概率就丢弃
                    if np.random.uniform(0, 1) > self.subsampling_drop_ratio[token]:
                        # if self.subsampling_drop_ratio[token] < self.w2v_random():
                        continue
                # 增加该练习词语的索引
                tokens_index.append(self.vocabulary[token])
                all_tokens_count += 1
            # if cnt == 10:
            #     return None
            cnt += 1
            lines_tokens_list.append(tokens_index)
        print("lines_tokens_list line len :" + str(cnt))
        print("lines_tokens_list all tokens  :" + str(all_tokens_count))
        return lines_tokens_list
    def data_generator_from_memery(self):
        data = open(train_file).readlines()[0].split(' ')
        cur_tokens = []
        index = 0
        while index + 100 < len(data):
            yield data[index: index + 100]
            index += 100
        yield data[index:]
        # for i in range(len(data)):
        #     cur_tokens.append(data[i])
        #     if i % 100 == 0:
        #         yield cur_tokens
        #         cur_tokens = []
    # 数据生成办法
    def data_gen(self):
        raw_data_list = []
        prev = ''
        # 读取练习数据文件
        with open(train_file) as f:
            # 死循环去读
            while True:
                # 单词读取最大语句长度
                buffer = f.read(self.MAX_SENTENCE_LENGTH)
                if not buffer:
                    break
                # print('|{}|'.format(buffer))
                # 把语句分割成各行
                lines = (prev + buffer).split('\n')
                # print(len(lines))
                for idx, line in enumerate(lines):
                    # 处理当时行
                    # 分红一个个词
                    tokens = line.split(' ')
                    if idx == len(lines) - 1:
                        # 终究一行
                        cur_tokens = [x for x in tokens[:-1] if x]
                        # 把当时 MAX_SENTENCE_LENGTH 终究一个词保存起来, 和下一次读取的时分进行拼接
                        prev = tokens[-1]
                    else:
                        # 回来当时行的各个词语
                        cur_tokens = [x for x in tokens if x]
                    raw_data_list.append(cur_tokens)
        print("raw_data_list length:" + str(len(raw_data_list)))
        return raw_data_list
if __name__ == "__main__":
    print(tf.__version__)
    word2vec = Word2Vec(train_file, sample=1e-4)
    word2vec.train()

到这儿,深入浅出了解word2vec模型理论与源码剖析就完结了,欢迎留言交流 ~


码字不易,觉得有收成就点赞、分享、再看三连吧~

欢迎扫码重视作者的大众号: 算法全栈之路