【论文解读】NLP重铸篇之Word2vec

2023-11-19


论文标题:Efficient Estimation of Word Representations in Vector Space
论文链接:https://arxiv.org/pdf/1301.3781.pdf
复现代码地址:https://github.com/wellinxu/nlp_store/blob/master/papers/word2vec.py


论文标题:word2vec Parameter Learning Explained
论文链接:https://arxiv.org/pdf/1411.2738.pdf

word2vec原论文讲得比较简单,几乎没有细节,本文会根据另一篇论文【word2vec Parameter Learning Explained】,来详细介绍两种加速方法。本文使用python+tensorflow2.0来复现word2vec模型,所以模型中的反向梯度计算与参数优化更新,都是使用的tf中的自动求导与优化器实现,也因此本文中只涉及到word2vec的两种结构(CBOW与Skip-gram)及两种加速方式(Huffman树-层次softmax和负采样)从输入到loss的前向计算,完整代码已开源,具体请查看https://github.com/wellinxu/nlp_store。

重铸系列会分享论文的解析与复现,主要是一些经典论文以及前沿论文,但知识还是原汁原味的好,支持大家多看原论文。分享内容主要来自于原论文,会有些整理与删减,以及个人理解与应用等等,其中涉及到的算法复现都会开源在:https://github.com/wellinxu/nlp_store 。

  • 引言

  • 模型结构

    • CBOW结构

    • skip-gram结构

    • softmax的loss计算

  • Huffman树——层次softmax

    • Huffman树的构建

    • Huffman树压缩为数组

    • Huffman树loss计算

  • 负采样

    • 采样权重调整

    • 负采样loss计算

  • 模型训练

  • 论文结果

  • 论文之外

  • 参考

引言

word2vec的目标是从十亿量级的文章跟百万量级的词汇中学习出高质量的词向量表示,实验结果表明,其向量可以达到类似“江苏-南京+杭州≈浙江”的效果。

模型结构

在word2vec之前,已经有不少关于词的连续表示模型提出来,比如LSA(Latent Semantic Analysis)跟LDA( Latent Dirichlet Allocation)。跟LAS相比,word2vec效果更好,跟LDA相比,word2vec的计算速度更快。
论文中提出了两种新的模型结构:CBOW、Skip-gram,跟NNLM(Feedforward Neural Net Language Model)相比,word2vec为了追求更简单的模型结构,删除了中间的非线性隐藏层,尽管这样可能会让词向量没有NNLM的更加精确,但这可以让模型在更大的数据集上高效训练。
两种结构都是利用中心词跟上下文互为输入与输出,上下文就是中心词前后的n个词,实验表明,提高上下文的范围(即n的大小),可以提高词向量的质量,但这也加大了计算复杂度。

CBOW结构


第一种模型结构是CBOW(Continuous Bag-of-Words Model),其思想是利用中心词前面n个词跟后面n个词来预测中心词(n可自定义),如上图所示,用的是前面2个词跟后面2个词预测当前词,输入层直接求和变成投影层,没有其他任何操作,然后直接预测中心词。整体公式可直接表示为:

其中f表示将projection映射为output的变换函数, 是其中的参数,f的具体方式后续会讲解。

Skip-gram结构


第二种模型结构是Skip-gram(Continuous Skip-gram Model),其思想与CBOW相反:利用中心词预测中心词上下文的n个词(n可自定义),如上图所示,利用中心词预测上下文各2个词,输入层直接等价变化到投影层,然后预测上下文的词。整体公式可以直接表示为:

本文复现过程中,将Skip-gram结构视为多标签预测问题,word2vec的源码中应该是将每个输出拆开(参考【2】得出的结论,并未真正看过源码),组成多个单分类问题。从过程上来说,这两种方式更新参数的顺序会有差别,有些类似于批梯度下降与随机梯度下降的区别,从工程上来说,word2vec源码中是简化了问题,处理更简单。

softmax的loss计算

如果用softmax来取代上面式子中的f函数,那么ouput的计算方式可具体为:

其中 表示预测为第i个词的概率, 表示第i个词的输出权重向量,N表示词表大小。
这是一个比较标准的分类问题,所以使用负对数似然概率或者交叉熵来计算其loss,具体的CBOW结构的loss可表示为:

其中j表示中心词的索引, 表示预测是真实中心词的概率。
Skip-gram结构的loss则可表示为:

其中M是上下文的数量, 表示预测是第j个上下文词的概率。
根据上面两种模型结构以及loss计算,可以完成相应代码,因为模型不关心输出,只关心词权重的更新,所以会在模型内部直接计算loss,具体代码如下:

import tensorflow as tf
import numpy as np
import random
import time

class BaseWord2vecModel(tf.keras.models.Model):
    # 当前的实现没有batch维度,是一个样本一个样本进行训练
    def __init__(self, voc_size, emb_dim, is_huffman=True, is_negative=False):
        super(BaseWord2vecModel, self).__init__()
        self.voc_size = voc_size
        self.is_huffman = is_huffman
        self.is_negative = is_negative
        self.embedding = tf.keras.layers.Embedding(voc_size, emb_dim)
        self.layer_norm = tf.keras.layers.LayerNormalization(
            name="layer_norm", axis=-1, epsilon=1e-12, dtype=tf.float32)
        if not self.is_huffman and not is_negative:
            # 不使用huffman树也不使用负采样,所有词的输出参数
            self.output_weight = self.add_weight(shape=(voc_size, emb_dim), 
                                                 initializer=tf.zeros_initializer, trainable=True)
            self.softmax = tf.keras.layers.Softmax()
        if self.is_huffman:
            # 所有节点的参数,huffman树压缩为数组的时候,保留了所有叶子节点,所以数组长度为2*voc_size
            # 也可以选择只保留非叶子节点,这样长度可减半
            self.huffman_params = tf.keras.layers.Embedding(2*voc_size, emb_dim, 
                                                            embeddings_initializer=tf.keras.initializers.zeros)
            self.huffman_choice = tf.keras.layers.Embedding(2, 1, weights=(np.array([[-1], [1]]),))
        if self.is_negative:
            # 负采样时,每个词的输出参数
            self.negative_params = tf.keras.layers.Embedding(voc_size, emb_dim, 
                                                             embeddings_initializer=tf.keras.initializers.zeros)

    def call(self, inputs, training=None, mask=None):
        # x:[context_len]
        # huffman_label: [label_size, code_len]
        # huffman_index: [label_size, code_len]
        # y : [label_size]
        # negative_index: [negatuve_num]
        x, huffman_label, huffman_index, y, negative_index = inputs
        # skip-gram的context_len就是1
        x = self.embedding(x)    # [context_len, emb_dim]
        x = self.layer_norm(x)    # 层标准化,自己添加非模型原始结构
        # skip-gram单个求和就是本身
        x = tf.reduce_sum(x, axis=-2)    # [emb_dim]

        loss = 0
        if not self.is_huffman and not self.is_negative:
            # 不使用huffman树也不使用负采样,则使用原始softmax
            output = tf.einsum("ve,e->v", self.output_weight, x)    # [voc_size]
            output = self.softmax(output)
            y_index = tf.one_hot(y, self.voc_size)    # [label_size, voc_size]
            y_index = tf.reduce_sum(y_index, axis=0)    # [voc_size]
            l = tf.einsum("a,a->a", output, y_index)    # [voc_size]
            loss -= tf.reduce_sum(l)

从上面计算公式可以看出,当词表量级特别大的时候(百万级别), 的分母计算以及后续反向传播更新所有 的计算将会非常耗时,这种昂贵的计算代价使得在大语料或者大词表情况下训练变得不可能。要解决这个问题,一个直觉的方法是限制每个训练样本必须更新的输出向量的数量,后续会介绍两种方式来实现这一点,分别是层次softmax与负采样。

Huffman树——层次softmax

层次softmax是一种高效计算softmax的方法,其使用二叉树来表示词表中的所有词,每一个词都必须是树的叶子结点,对于每一个结点,都存在唯一的路径从根结点到当前叶子结点,该路径就被用来估计此叶子结点表示的词出现的概率。理论上说,可以使用任何形式的树来计算层次softmax,word2vec里面使用的是二叉Huffman树来进行训练。因为huffman树中权重越高的结点越靠近根结点,这样频率高的词的路径就越短,计算的次数也就越少,从而可以进一步提高训练速度。

Huffman树的构建

给定n个结点,每个结点都有一个权重,构造一棵二叉树,如果它的带权路径长度最小,则称为最优二叉树,也称为Huffman树。给定n个值 作为n个结点的权重,可以通过下面方法构造huffman树:

  1. 看成是有n棵树的森林(每棵树只有一个结点)。

  2. 在森林中选出两个根节点权重最小的树合并,分别作为新树的左右子树,新树根结点的权重为左右子树根结点权重之和。

  3. 删除森林中选取的两棵树,并将新树添加到森林。

  4. 重复上面两步操作,直到森林里就剩一棵树,该树就是huffman树。


如上图所示,显示了一个简单huffman树的构造过程,在本文的实现中,将叶子结点优先放在右子树上,但这并不是必要的,也可以将权重大(或小)的结点放在右子树上。
为了实现其构造过程,我们需要先定义结点类,如下所示:

class Node(object):
    def __init__(self, key, value):
        self.key = key    # 本文代码里huffman中,非叶子节点都为None
        self.value = value    # 权重
        # 编码,即重跟节点走到本节点的方向,0表示左子树,1表示右子树
        # 010表示跟节点->左子树->右子树->左子树(本节点)
        self.code = []    # 记录当前节点在整个huffman树中的编码
        self.index = 0    # 第几个节点,压缩为数组用,即为该节点在数组型的huffman树种的索引位置
        self.left = None
        self.right = None

    def combine(self, node):
        # 两棵树(两个节点)合并成一个新树,叶子节点放在新树的右子树上
        new_node = Node(None, self.value + node.value)
        if self.key is not None:
            new_node.right = self
            new_node.left = node
        else:
            new_node.right = node
            new_node.left = self
        return new_node

Node类中定义了权重(value)、词(key)、编码(code)、索引位置(index)以及左右结点,此外还提供了combine方法,用来合并两棵子树。定义好结点,就可以通过以下方式来构建huffman树:

class HuffmanTree(object):
    # 用一个数组表示huffman树的所有非叶子节点
    # 用word_code_map表记录根到每个叶子节点的编码
    def __init__(self, words):
        start = time.time()
        words.sort()  # 根据频率排序
        self.words = words    # [(value:频次, key:词)],由小到大排序
        self.word_code_map = {}    # 词在huffman树中的编码映射
        self.nodes_list = []     # 压缩为数组的huffman树
        self.build_huffman_tree()
        print("build huffman tree end,time:", time.time()-start)

    def build_huffman_tree(self):
        # 构建huffman树
        # 每个元素都构成单节点的树,并按照权重重大到小排列
        # 合并权重最小的两个子树,并以权重和作为新树的权重
        # 将新树按照权重大小插入到序列中
        # 重复上述两步,直到只剩一棵树
        nodes = [Node(key, value) for value, key in self.words]
        while len(nodes) > 1:
            a_node = nodes.pop(0)
            b_node = nodes.pop(0)
            new_node = a_node.combine(b_node)
            left, right = 0, len(nodes)
            l = right - 1
            i = right >> 1
            while True:
                if nodes and nodes[i].value >= new_node.value:
                    if i == 0 or nodes[i-1].value < new_node.value:
                        nodes.insert(i, new_node)
                        break
                    else:
                        right = i
                        i = (left+right) >> 1
                else:
                    if i == 0 or i == l:
                        nodes.insert(i, new_node)
                        break
                    left = i
                    i = (left+right) >> 1

其中输入words是以(词频,词)为元素的list,并以词频的大小由小到大排好序。

Huffman树压缩为数组

构建完huffman树,可以根据其设计二进制前缀编码,也称Huffman编码,下图展示了一个huffman树的编码示例,左子树为0右子树为1(这个可自由设定),如“巴西”就可以编码为“011”。在后续计算过程中,不仅需要知道词的huffman编码,还需要知道该词经过了哪些结点,为了方便根据编码得知结点,我们将树形结构压缩为数组型结构(并不是必要的,也可以通过原始huffman树,获取该编码经过的结点),如上图所示,huffman树按照层次遍历的顺序跟数组的索引一一对应,数组中的值表示当前结点左结点的索引值,如果数组的值跟索引相同,则表示该结点是叶子节点(其实也可以只将非叶子结点压缩到数组里面,因为后续计算中不需要叶子结点)。
根据上述需求,我们对huffman树进行一次层次遍历就可以实现,具体代码如下:

        # 将树压缩为数组
        # 数组中每一个元素都是树种的一个节点,数组中的值表示该节点的左节点的位置,如果值跟索引一样大,表示此节点是叶子节点没有子节点
        # 跟节点 root.index = 0
        # 叶子节点 nodes_list[node.index] = node.index
        # 非叶子节点 nodes_list[node.index] = node.left.index
        # 非叶子节点 nodes_list[node.index] + 1 = node.right.index
        stack = [nodes[0]]
        while stack:
            new_stack = []
            for node in stack:
                node.index = len(self.nodes_list)
                if node.key is not None:   # 叶子结点
                    self.word_code_map[node.key] = node.code  # 保存编码
                    self.nodes_list.append(node)  # 在数组中添加叶子结点本身
                if node.right:
                    node.right.code = node.code + [1]
                    new_stack.append(node.right)
                if node.left:
                    # 在数组相应位置添加该结点的左结点
                    self.nodes_list.append(node.left)
                    node.left.code = node.code + [0]
                    new_stack.append(node.left)
            stack = new_stack
        self.nodes_list = [node.index for node in self.nodes_list]

将huffman树压缩为数组后,就可以根据以下公式计算每个编码所经历的结点:

所有路径的第一个结点都是根结点

其中 表示编码的长度, 表示第i个编码值, 表示经过的第i个结点的位置。比如“巴西”的编码值是“011”,则可以计算第一个结点位置是0,第二个是 ,第三个是 ,所以“巴西”这个词经过的内部结点位置依次为0-1-4(不算巴西本身这个结点)。
根据上面公式,我们可以构建通过huffman树处理的训练数据:

        # 构建huffman数据
        if self.is_huffman:
            huffman_tree = HuffmanTree(self.words)    # 根据词与词频,构建huffman树
            for labels in self.ys:
                tlabels = []
                for label in labels:
                    tlabels.append(np.array(huffman_tree.word_code_map[label]))
                index = []
                for tlabel in tlabels:
                    tem_index = [0]
                    for l in tlabel[:-1]:
                        ind = huffman_tree.nodes_list[tem_index[-1]] + l
                        tem_index.append(ind)
                    index.append(np.array(tem_index))
                self.huffman_label.append(tlabels)  # 获取标签词在huffman树中的编码
                self.huffman_index.append(index)    # 获取标签词在huffman树中的编码上对应的所有非叶子节点

Huffman树loss计算

Huffman树中每个词出现的概率,是将该词到根结点路径上的每个结点出现的概率相乘,具体公式如下:

其中 是编码长度, 是第i个编码值,为0时表示是往左结点走,此时用 计算概率,为1时则用 来计算概率; 是上面两种结构的投影层结果, 表示进过的第i个结点的位置, 表示第j个结点的参数。
跟一般softmax相比,原来需要计算与更新N(词表大小)次参数,现在只需要计算与更新 次, 的大小跟 接近,从而大大提高了计算效率。
上面已经得到单个词似然函数,如果是CBOW模型结构,那Huffman的loss则可以取其负对数:

当模型结构是Skip-gram结构时,会有多个输出,此时的Huffman的loss则应该为:

其中 表示输出的个数, 表示第j个输出词的似然概率。
根据公式,则huffman相关代码为:

        # huffman树loss计算
        if self.is_huffman:
            for tem_label, tem_index in zip(huffman_label, huffman_index):
                # 获取huffman树编码上的各个节点参数
                huffman_param = self.huffman_params(tem_index)    # [code_len, emb_dim]
                # 各节点参数与x点积
                huffman_x = tf.einsum("ab,b->a", huffman_param, x)    # [code_len]
                # 获取每个节点是左节点还是右节点
                tem_label = tf.squeeze(self.huffman_choice(tem_label), axis=-1)    # [code_len]
                # 左节点:sigmoid(-WX),右节点sigmoid(WX)
                l = tf.sigmoid(tf.einsum("a,a->a", huffman_x, tem_label))    # [code_len]
                l = tf.math.log(l)
                loss -= tf.reduce_sum(l)

负采样

负采样的思想比层次softmax更加直接:为了解决softmax要计算和更新的参数太多的问题,负采样每次只计算和更新几个参数。也就是说,原来每个样本进行训练的时候,需要从所有词汇中选出某个或某几个词,现在只需要从某个小的词集里面选出某个或者某几个词,因为小词集的数量远小于原本词表数量,所以计算量与需要更新的参数都小很多,从而大大提高训练速度。

采样权重调整

根据负采样的思想,预测的词肯定需要在小词集中出现,然后只需要从其他词中抽取一些词作为负样本(负采样的由来)。显而易见的,可以通过词频计算词的分布概率从而进行抽样,但为了降低词频过高的词被抽的概率,以及提高词频过低的词被抽的概率,word2vec中将频率进行了0.75次幂运算,然后计算词的分布概率。相关代码,可参考:

    def build_word_dict(self):
        # 构建词典,获取词频
        self.words = []    # (频率,id)
        self.word_map = {}    # {词:id}
        word_num_map = {}    # 频率
        for doc in self.docs:
            if len(doc) < self.windows: continue
            for word in doc:
                if word not in self.word_map.keys():
                    self.word_map[word] = len(self.word_map)
                    self.words.append(word)
                word_num_map[word] = word_num_map.get(word, 0) + 1
        # 词频设为原本的0.75次方,根据词频进行负采样的时候,可以降低高词频的概率,提高低词频的概率(高词频的概率仍然大于低词频的概率)
        word_num_map = {k:np.power(v, 0.75) for k, v in word_num_map.items()}
        num = sum(word_num_map.values())
        word_num_map = {k: v/num for k, v in word_num_map.items()}

        self.words = [(word_num_map[w], self.word_map[w]) for w in self.words]
        self.words.sort()    # 根据频率排序
        self.voc_size = len(self.words)    # 词表大小
        self.id_word_map = {v: k for k, v in self.word_map.items()}  # {id:词}

有了每个词的概率,可以将每个词映射到0-1之间的一段范围之内,然后生成随机数,根据其落在的空间判断抽取的词,遇到正例的词则跳过,不断重复,直到抽取到一定的负样本数量,具体代码如下:

        # 构建负采样数据
        if self.is_negative:
            # 如果"我 爱 你 啊"出现的概率分别是0.4,0.2,0.3,,0.1,
            # 那么word_end_p就为[0.4,0.6,0.9, 1.0],即[0.4,0.4+0.2,0.4+0.2+0.3,0.4+0.2+0.3+0.1]
            word_end_p = [self.words[0][0]]    # 每个词出现的概率段
            for i in range(1, self.voc_size):
                word_end_p.append(word_end_p[-1]+self.words[i][0])
            # 为每一条训练数据抽取负样本
            for y in self.ys:
                indexs = []
                while len(indexs) < self.negative_num * len(y):
                    index = self._binary_search(random.random(), word_end_p, 0, self.voc_size-1)
                    # 随机抽取一个词,不能再标签中也不能已经被抽到
                    if index not in indexs and index not in y:
                        indexs.append(index)
                self.negative_index.append(np.array(indexs))
                
    def _binary_search(self, n, nums, start, end):
        # 二分查找,查找n在nums[start:end]数组的那个位置
        if start == end: return end
        mid = (start+end) >> 1
        if nums[mid] >= n:
            return self._binary_search(n, nums, start, mid)
        return self._binary_search(n, nums, mid+1, end)

负采样loss计算

负采样的基本思想是减少更新的参数数量,具体操作又是怎样的呢?简单的说就是,每一个样本,对于所有抽样出来的词(包括正例词跟负例词),都做一个二分类,正例词计算正例的概率,负例词计算负例的概率,目标是使得所有概率的乘积最大。更具体的解释及相关公式可以参考【3】。
这样就可以得到输出概率:

其中 表示输出词(正例词)的索引集合, 表示第i个输出词的负例词索引集合。当模型结构是CBOW时,输出词只有一个,当模型结构是Skip-gram时,才是多个。
这时候可以计算loss:

根据公式,相关代码如下:

        # 负采样loss计算
        if self.is_negative:
            y_param = self.negative_params(y)    # [label_size, emb_dim]
            negative_param = self.negative_params(negative_index)    # [negative_num, emb_dim]
            y_dot = tf.einsum("ab,b->a", y_param, x)    # [label_size]
            y_p = tf.math.log(tf.sigmoid(y_dot))    # [label_size]
            negative_dot = tf.einsum("ab,b->a", negative_param, x)    # [negative_num]
            negative_p = tf.math.log(tf.sigmoid(-negative_dot))    # [negative_num]
            l = tf.reduce_sum(y_p) + tf.reduce_sum(negative_p)
            loss -= l

以下引用为错误思想
根据负采样思想,可以得到每个正例样本输出的概率:

其中 表示索引为i的输出词的预测概率, 表示所有正例词跟采样得到的负例词索引集合, 是上面两种结构的投影层结果。
此时,根据负对数似然或者交叉熵可以计算loss函数,当模型是CBOW结构时:

其中 表示输出词的概率。
当模型是Skip-gram结构时:

其中 表示第j个输出词的概率, 表示输出的个数。
根据公式,相关代码如下:

        # 负采样loss计算
        if self.is_negative:
            y_param = self.negative_params(y)    # [label_size, emb_dim]
            negative_param = self.negative_params(negative_index)    # [negative_num, emb_dim]
            y_dot = tf.einsum("ab,b->a", y_param, x)    # [label_size]
            y_exp = tf.exp(y_dot)    # [label_size]
            negative_dot = tf.einsum("ab,b->a", negative_param, x)    # [negative_num]
            negative_exp = tf.exp(negative_dot)    # [negative_num]
            y_sum = tf.reduce_sum(y_exp)    # 分子
            negative_sum = tf.reduce_sum(negative_exp)    # 负样本的分母
            loss -= tf.math.log(y_sum/(y_sum+negative_sum))

模型训练

上面已经讲述了整个word2vec模型的前向传播环节,本次模型复现使用的是tensorflow2的框架,所以反向传播以及参数更新过程都使用的是框架的自动求导等功能,本文中也不会讲述反向传播相关原理及公式。模型整体训练代码如下:

class Word2vec(object):
    def __init__(self, docs=None, emb_dim=100, windows=5, negative_num=10, is_cbow=True, is_huffman=True, is_negative=False, epochs=3, save_path=None):
        self.docs = docs    # [[我 是 一段 文本],[这是 第二段 文本]]
        self.windows = windows    # 窗口长度
        self.emb_dim = emb_dim    # 词向量维度
        self.is_cbow = is_cbow   # 是否使用CBOW模式,False则使用SG模式
        self.is_huffman = is_huffman    # 是否使用huffman树
        self.is_negative = is_negative    # 是否使用负采样
        self.huffman_label = []    # huffman数据的标签,判断每次选择左子树还是右子树
        self.huffman_index = []   # huffman数据的编码,用来获取编码上节点的权重
        self.negative_index = []    # 负采样的词索引
        self.negative_num = negative_num    # 负采样数量
        self.epochs = epochs    # 训练轮次
        self.save_path = save_path    # 模型保存路径
        if docs:  # 训练模型
            self.build_word_dict()    # 构建词典,获取词频
            self.create_train_data()    # 创建训练数据
            self.train()    # 进行训练
            if self.save_path:
                self.save_txt(self.save_path)    # 保存词向量
        elif self.save_path:  # 直接加载词向量
            self.load_txt(self.save_path)

    def train(self):
        sample_num = len(self.xs)    # 样本数量
        optimizer = tf.optimizers.SGD(0.025)    # 优化器
        # 基础word2vec模型
        self.model = BaseWord2vecModel(self.voc_size, self.emb_dim, self.is_huffman, self.is_negative)
        # 模型训练
        for epoch in range(self.epochs):
            print("start epoch %d" % epoch)
            i = 0
            for inputs in zip(self.xs, self.huffman_label, self.huffman_index,self.ys, self.negative_index):
                with tf.GradientTape() as tape:
                    loss = self.model(inputs)
                grads = tape.gradient(loss, self.model.trainable_variables)
                optimizer.apply_gradients(zip(grads, self.model.trainable_variables))
                if i % 1000 == 0:
                    print("-%s->%d/%d" % ("-" * (i * 100 // sample_num), i, sample_num))
                i += 1
        # 获取词向量
        self.word_embeddings = self.model.embedding.embeddings.numpy()
        norm = np.expand_dims(np.linalg.norm(self.word_embeddings, axis=1), axis=1)
        self.word_embeddings /= norm    # 归一化

论文结果

如下表所示,论文中设置了5种语义问题与9种句法问题来测试向量效果,通过类似vec(big)-vec(bigger)≈vec(small)-vec(smaller)的方式来判断。

测试结果如下图所示,在相同数据上训练的640维词向量,CBOW在句法层面上表现得较好,Skip-gram在语义层面上表现得更好,总体来说还是Skip-gram更好些。

论文也对比了训练语料、向量维度以及训练轮次对结果的影响,实验结果表明,训练语料越多效果更好,600维的结果比300维的结果更优,3轮的训练结果比1轮的训练结果更好。

论文之外

word2vec是2013年提出的模型,限于当时的算力,在大规模的语料上进行训练,确实需要更简单的模型以及一些加速训练方式;之后提出的fasttext在模型结构上可以说跟word2vec一样,主要区别在输入上,fasttext增加了词的形态特征;基于算力的提升,最近的预训练模型参数越来越大,比如BERT、XLNET,甚至有GPT3这种庞然大物。
再反过来看word2vec,是不是用现在的算力训练,就不需要层次softmax或者负采样的加速方式了呢?这还是要分情况讨论,虽然说像BERT这样的模型,都是直接用的softmax进行计算,这是因为训练BERT的人(机构、公司)具有远超普通情况下的算力,而且BERT这种输出维度也只有几万大小,词向量的输出维度则会有百万大小(可以想象下词跟字在数量上的差别),所以自己进行预训练的时候,需要根据拥有的算力和输出维度来判断是否要使用加速手段。

参考

【1】基于tf2的word2vec模型复现:https://github.com/wellinxu/nlp_store/blob/master/papers/word2vec.py
【2】word2vec 中的数学原理详解:https://www.cnblogs.com/peghoty/p/3857839.html
【3】word2vec Explained_Deriving Mikolov et al.’s Negative-Sampling Word-Embedding Method:https://arxiv.org/pdf/1402.3722v1.pdf

往期精彩回顾



适合初学者入门人工智能的路线及资料下载机器学习及深度学习笔记等资料打印机器学习在线手册深度学习笔记专辑《统计学习方法》的代码复现专辑
AI基础下载机器学习的数学基础专辑
获取本站知识星球优惠券,复制链接直接打开:
https://t.zsxq.com/qFiUFMV
本站qq群704220115。

加入微信群请扫码:

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【论文解读】NLP重铸篇之Word2vec 的相关文章

  • 从 1D 列表创建 2D 列表

    我对 Python 有点陌生 我想将一维列表转换为二维列表 给定width and length这个的matrix 说我有一个list 0 1 2 3 我想做一个2 by 2该列表的矩阵 我怎样才能得到matrix 0 1 2 3 widt
  • 更改seaborn.clustermap中ytick标签的颜色

    是否可以更改seaborn clustermap中ytick标签的颜色 所以对于Seaborn 鸢尾花示例 http seaborn pydata org generated seaborn clustermap html 可以根据物种设置
  • 如何从网站中抓取动态内容?

    所以我使用 scrapy 从亚马逊图书部分抓取数据 但不知何故我知道它有一些动态数据 我想知道如何从网站中提取动态数据 到目前为止我已经尝试过以下方法 import scrapy from items import AmazonsItem
  • 如何获取右侧数据框中不在左侧数据框中的数据

    我有两个数据帧 我正在尝试输出其中一个数据帧中的数据 而不是另一个数据帧中的数据 我可以使用第一个数据帧中的数据 但不能使用第二个数据帧中的数据 only new old merge new outer on Employee ID Ben
  • 将字段重新格式化为列,其他字段(与先前结构中成为列的字段配对)成为新列中的字段

    我的任务是清理慈善机构设计的移动应用程序中的数据 在一个部分中 用户问答应用程序使用会话由一行表示 该部分由重复的问题答案字段对组成 其中一个字段代表所提出的问题 然后它旁边的字段代表相应的答案 每个问题 字段和答案列对一起代表一个独特的问
  • 使用记事本打开文本文件作为python中的帮助文件?

    我想为我的简单程序的用户提供打开帮助文件的机会 以指导他们如何充分利用我的程序 理想情况下 我希望在 GUI 上有一个蓝色的小帮助链接 可以随时单击该链接 从而在本机文本编辑器 例如记事本 中打开 txt 文件 有没有一种简单的方法可以做到
  • 通过 rpy 将 SPSS 文件(.sav)导入 pandas 时如何保留标签?

    我正在寻找使用 SPSS 文件 sav pandas 在没有 SPSS 程序的情况下 典型文件转换为 csv 后的样子如下 在调查前两行的含义时 我不知道 SPSS 似乎第一行包含Labels 而第二行包含VarNames 当我将文件带入
  • 从内存中发送图像

    我正在尝试为 Discord 机器人实现一个系统 该系统可以动态修改图像并将其发送给机器人用户 为此 我决定使用 Pillow PIL 库 因为它对于我的目的来说似乎简单明了 这是我的工作代码的示例 它加载一个示例图像 作为测试修改 在其上
  • 参数太少错误,同时未使用参数占位符

    我尝试使用 PYODBC 在 Access 数据库中执行 SQL 查询 但出现以下错误 pyodbc Error 07002 07002 Microsoft ODBC Microsoft Access 驱动程序 参数太少 预期为 1 301
  • 如何在 Django 1.4 中自定义管理过滤器

    我是 Python 和 Django 开发的新手 我从社区提供的易于阅读的示例中学到了很多东西 但最近我想为 Django 附带的管理控制台实现一个自定义的管理过滤器 我进行了很多搜索 只发现了一些过时的方法来完成它 例如 Django 1
  • Buildozer Numpy RuntimeError:工具链损坏:无法链接简单的 C 程序

    用 Python 编写我的第一个 Android 应用程序并使用 Buildozer 对其进行打包 因为稍后在项目中需要使用numpy 所以我尝试打包以下测试代码 import numpy import kivy kivy require
  • 带有redirect_uri的social-auth-app-django Facebook后端状态

    我知道我的问题听起来像是重复的 但我到处寻找但没有找到任何解决方案 我正在努力为我的 django web 应用程序实现社交登录 到目前为止 谷歌 推特和雅虎登录均按预期工作 但facebook总是给出以下错误 URL 被阻止 此重定向失败
  • 如何在 iPython 中获取最后分配的变量的值?

    我是一个完全的 iPython 新手 但我想知道是否有办法获取最后分配的变量的值 In 1 long variable name 333 In 2
  • Tkinter 按钮鼠标右键和左键单击有不同的命令

    我正在用 Python 制作扫雷游戏 并使用 tkinter 库来创建 gui 有没有 绑定到 tkinter 按钮两个命令的方法 一个是右键单击按钮时的命令 另一个是单击左键时的命令 通常 按钮仅设计用于单击 但 tkinter 允许您为
  • 更新或插入 MySQL Python

    如果记录已存在 我需要更新一行 如果不存在 我需要创建一个新记录 我理解 ON DUPLICATE KEY 将使用 MYSQLdb 完成此操作 但是我无法使其正常工作 我的代码如下 cursor database cursor cursor
  • 在Python中将用户昵称转换为正式名字

    我正在尝试根据 Python 中的用户名字和姓氏映射来自不同系统的用户 一个问题是 名字在很多情况下都是 昵称 例如 对于用户来说 他的名字在一个系统中是 Dave 而在另一个系统中是 David python 中有没有简单的方法可以将这些
  • 如何避免在matplotlib中调用latex(输出到pgf)

    我使用 matplotlib 及其 pgf 后端来生成包含在 LaTeX 投影仪文档中的绘图 当我使用未定义的乳胶命令时 我遇到了麻烦 但对于我的应用程序 我不需要 matplotlib 来使用 Latex 生成标签或注释 我只想要正确的
  • 了解字典的深度

    假设我们有这个字典 d a 1 b c 了解嵌套的最直接方法是什么depth of it 您需要创建一个递归函数 gt gt gt def depth d if isinstance d dict return 1 max map dept
  • 有没有比 Python 内置 == 运算符更快的方法来测试两个列表是否具有完全相同的元素?

    如果我有两个列表 每个列表有 800 个元素长并填充整数 有没有比使用内置元件更快的方法来比较它们具有完全相同的元件 如果没有 则短路 操作员 a 6 2 3 88 54 486 b 6 2 3 88 54 486 a b gt gt gt
  • *Python 内的 Kaggle API 文档?

    我想写一个python从 Kaggle com 下载公共数据集的脚本 Kaggle API 是用 python 编写的 但是我能找到的几乎所有文档和资源都是关于如何在命令行中使用该 API 的 而关于如何使用kaggle图书馆内python

随机推荐

  • 查看apk签名,查看key签名,adb常用命令

    在使用第三方sdk时经常要求绑定签名 这里提供两种查看签名的方式 如果只是想查看一下手机上应用的签名 那么可以安装一个app直接输入包名即可查看该应用的签名 提供一个微信的签名查看apk 下载连接http download csdn net
  • 【认证证书】计算机应急响应 - 中国信息安全测评中心CISP-IRE

    计算机应急响应 中国信息安全测评中心CISP IRE 知识体系 简略图片版 详细 官方 中国信息安全测评中心 http www itsec gov cn ryzc rsqsxz 版 考核方式 公安部第三研究所CCSS R 考核方式 资料 题
  • 使用wget从Google Drive下载数据集

    许多用于训练的数据集很大 一般通过Google Drive进行分享 而出于磁盘容量和方便训练的考虑 这些数据我们一般放在服务器上 服务器没有图形界面 因此我们需要使用wget来进行下载 Google Drive的链接格式是这个样子的 htt
  • go语言教程哪里有?go 语言优秀开源项目汇总

    目录 监控系统 容器技术 PaaS工具 大数据 微服务 CI CD 数据库技术 存储技术 分布式系统 消息系统 服务器管理 安全工具 网络工具 Web工具 Web框架 区块链技术 其它 监控系统 项目 简介 OpenFalcon OpenF
  • python:转义字符和input()的用法

    1 1转义字符 1序列 表反斜杆 表单引号 表双引号 a表ASCLL响铃符 BEL b表ASCLL退格符 BS f表ASCLL进纸符 FF n表ASCLL换行符 LF N name 表Udicode数据库中的字符名 其中name时它的名字
  • python 使用 openpyxl 打开及读取 excel 表格

    python 使用 openpyxl 打开及读取 excel 表格 openpyxl简介 安装openpyxl 打开及读取表格内容 1 打开 Excel 表格并获取表格名称 2 通过 sheet 名称获取表格 3 获取表格的尺寸大小 几行几
  • Spring创建Bean实例的方式

    目录 简介 源码分析 resolveBeforeInstantiation创建Bean实例 doCreateBean创建Bean实例 Supplier创建实例 FactoryMethod创建实例 自动装配构造函数创建实例 无参构造函数创建实
  • nslookup命令详解

    nslookup命令用于查询DNS的记录 查看域名解析是否正常 在网络故障的时候用来诊断网络问题 nslookup的用法相对来说还是蛮简单的 主要是下面的几个用法 1 直接查询 这个可能大家用到最多 查询一个域名的A记录 nslookup
  • 解决Proteus仿真时候提示Could not load simulator DLL错误

    之前在进行Proteus仿真时候 弹出来三个错误 分别是 1 Could not load simulator DLL E Program Files x86 BIN PROSPICE DLL 2 Could not find or run
  • 二阶电路的零状态响应

    二阶电路零状态响应公式推导 下图所示电路在 时电容和电感上储能都为零 即 t 0 时开关闭合 电压源 Us 开始对电路供电 现讨论 时响应的变化规律 电路的 KVL 方程为换路后电路的初始状态为 0 即 这是二阶线性非齐次微分方程 它的解由
  • vue中点击第一次没有触发按钮怎么操作_vue如何触发某个元素的单击事件?

    我来回答一波吧 因为没复习 导致知识点结合不紧密 原生的中 我们的写法是这样的 王蒿大爷 function myFunction document getElementById demo innerHTML Hello World wind
  • 程序员简历应该怎么写?

    说到程序员简历 这两个月 我看过不下10 000份简历 答主不是HR 也不是技术负责人 但是在网站的运营工作中 每天最开心的事情就是研究候选人的简历了 这些人中 有BAT的资深大牛程序员 也有90后程序员小鲜肉 有人到中年的程序员渴望去创业
  • MyBatis参数传入集合之foreach动态sql

    foreach的主要用在构建in条件中 它可以在SQL语句中进行迭代一个集合 foreach元素的属性主要有item index collection open separator close item表示集合中每一个元素进行迭代时的别名
  • 期货反向跟单--交易员的培养问题

    根据我们统计的数据显示 今年做国内期货反向跟单的团队 无论是从赢利金额 稳定性 还是成功概率 都比做国际期货的团队要高 尤其是最近纯碱 焦煤焦炭 PTA 红枣等几个品种的行情 更是频繁拉爆了很多盘手的账户 本文转发自公众号 反跟单交易 转载
  • 【Mysql】Communications link failure,The last packet sent successfully to the server was 0 millisecond

    项目背景是数据库和项目不在同一台服务器下 在启动时 突然遇到以下错误 Exception in thread main com mysql jdbc exceptions jdbc4 CommunicationsException Comm
  • Java图书馆

    io流用的不是很熟练 还有Book类的应用出了点问题 越改越错 从2个错误改到102个QAQ 孩子想哭 问了好多人也没改成 最后勉强成型 而且上个星期内分泌系统出了点小问题 天天往医院跑 开始敲的太晚了 现在要备战期末考 等期末考结束再改改
  • Linux系统编程:多线程交替打印ABC

    引言 分享关于线程的一道测试题 因为网上基本都是Java的解决方法 决定自己写一篇来记录一下线程的学习 问题描述 编写一个至少具有三个线程的程序 称之为线程 A B 和 C 其中线程 A 输出字符 A 线程 B 输出字符 B 线程 C 输出
  • Spring源码深度解析:文章目录

    文章目录 序号 内容 链接地址 1 一 Spring整体架构和源码环境搭建 https blog csdn net wts563540 article details 126686645 2 二 手写模拟Spring https blog
  • Windows server 2016 部署 AD域

    AD域的简单介绍 为什么要使用域 假设你是协会的系统管理员 管理高职部所有的机房 如果你要为每台电脑设置登录帐户 设置权限 比如是否允许登录帐户安装软件 那你要分别坐在所有电脑前一一设置 如果你要做一些改变 你也要分别在这所有电脑上修改 相
  • 【论文解读】NLP重铸篇之Word2vec

    论文标题 Efficient Estimation of Word Representations in Vector Space论文链接 https arxiv org pdf 1301 3781 pdf复现代码地址 https gith