寒假前做的一个软著,用到word2vec里的cbow模型,整理一下关于词向量重新学到的知识,方便以后查阅

1.参考资料

word2vec数学原理

Mikolov的两篇论文:


distributed-representations-of-words-and-phrases-and-their-compositionality


Efficient estimation of word representations in vector 

2.word2vec模型简述

        所谓词向量就是用向量来表示语料库中的词,方便其他数学模型对词语进行直接计算。而词嵌入(word embeding)可理解为将词向量由原来的稀疏向量形式(如one-hot)嵌入到低维稠密的连续实数向量中表示(常用的有word2vec,glove),这种表示方法即解决了one-hot向量表示的维度灾难问题,而且还能用词向量来挖掘词与词之间的关联属性。

        这里记录的是词向量里用的较多的word2vec方法,word2vec是用神经网络的方法得到词语向量空间的一种语言模型训练方法,而词向量其实是word2vec模型训练所得的副产品。包括CBOW和Skip-gram两种模型。由下面模型图可以看出,CBOW模型是根据语料库中词语的上下文来推断中心词的概率,而skip-gram模型是根据中心词来推断上下文的概率。其中Mikolov在13年发表的paper(distributed-representations-of-words-and-phrases-and-their-compositionality)介绍了两种用于减少word2vec模型计算量的算法:分层softmax和负采样的方法,这里主要记录分层softmax的方法。    




word2vec模型中文名称 word2vec cbow模型_中心词



word2vec模型构建


        以cbow模型为例,分层softmax算法输入层为某个中心词上下文对应的词向量,隐藏层将输入层的各向量求和得到Xw,在输出层构建一颗以语料库中的词为叶子节点的哈夫曼树,非叶子结点组成权值矩阵。从下图例子中可看出,哈夫曼树根据词语的频值来构建,比如足球对应的结点上的数字3,就是足球在语料库中的频值,即出现次数。因此,分层softmax算法需要在模型训练前完成哈夫曼树的构建(包括语料库各词语词频的统计),图例中,足球的哈夫曼编码值为1001,Xw“到达”足球这个节点需经过的权值节点为θ1,θ2,θ3,θ4。

word2vec模型中文名称 word2vec cbow模型_词向量_02

模型训练过程

        cbow模型训练时,按序逐行读取语料库句子,根据设定的窗口长度,每次读入固定长度的上下文和对应的一个中心词,获取上下文对应的词向量,求和,之后再分别与中心词对应的哈夫曼路径中的θ1,θ2,θ3...θn相乘之后求sigmoid函数。哈夫曼树训练的目标就是使Xw和θn相乘在求sigmoid函数的值偏向中心词对应的路径。因此,cbow模型需要训练的两个量就是Xw和非叶子结点对应的权值矩阵θi,word2vec采用梯度上升法训练,简化后的更新公式为:(具体推导过程参考wordvec的数学原理)

word2vec模型中文名称 word2vec cbow模型_word2vec模型中文名称_03

这里,θi-1为结点的权值向量,η为梯度上升法的学习率,di为中心词的哈夫曼编码d1,d2...di。Xv即上面的Xw,σ()表示sigmoid函数。(上面提到窗口长度的概念涉及到语言模型的一个假设:一个句子的某个词出现的概率取决于它的上下文,且可以由该词前面的若干个词来决定,即P(Wn|context(Wn))=P(Wn|Wn-m,Wn=m+1...Wn-1),这里的m表示Wn与其前面的m个词有关,当取m=1时,表示Wn只与自己有关,对应语言模型中的unigram模型,当m取2时,对应bigram模型,当m取3时,对应trigram模型...而窗口长度就是2m,因为在实际训练中,都是读取中心词左右两边的m个词作为上下文的。)

3.代码实现(python)

构建哈夫曼树

这里的代码中,先建了一个VocabItem类来记录语料库中的每个词,该类只包含4个属性:vocabitem.word(该词对应的字符串),vocabitem.count(该词在语料库中的出现ci),vocabitem.path(词的哈夫曼树路径)vocabitem.code(哈夫曼编码)

class VocabItem:                                                                                                                                                  
    def __init__(self, word):
        self.word = word
        self.count = 0
        self.path = None  
        self.code = None

语料库的处理代码:

class Vocab:
    def __init__(self, fi, min_count):
        vocab_items = [] #用来存语料库中所有词语的列表
        vocab_hash = {} #词典记录语料库中词语和对应该词在列表中的索引
        word_count = 0 #总的词语数
        fi = open(fi, 'r') #读入语料库文件fi
        for line in fi:
            tokens = line.split()
            for token in tokens:
                if token not in vocab_hash:
                    vocab_hash[token] = len(vocab_items)
                    vocab_items.append(VocabItem(token))
                assert vocab_items[vocab_hash[token]].word == token, 'Wrong vocab_hash index'
                vocab_items[vocab_hash[token]].count += 1
                word_count += 1

最后将统计完语料库的列表和词典等保存为Vocab的属性中:

self.vocab_items = vocab_items  
        self.vocab_hash = vocab_hash                                                                                                                
        self.word_count = word_count

哈夫曼树构建:

def encode_huffman(self):
        vocab_size = len(self)
        count = [t.count for t in self] + [1e15] * (vocab_size - 1)
        parent = [0] * (2 * vocab_size - 2)
        binary = [0] * (2 * vocab_size - 2)

        pos1 = vocab_size - 1
        pos2 = vocab_size

        for i in range(vocab_size - 1):
            # 查找词频中的最小值min1
            if pos1 >= 0:
                if count[pos1] < count[pos2]:
                    min1 = pos1
                    pos1 -= 1
                else:
                    min1 = pos2
                    pos2 += 1
            else:
                min1 = pos2
                pos2 += 1

            # 查找除去第一个最小值min1后的另一个最小值min2
            if pos1 >= 0:
                if count[pos1] < count[pos2]:
                    min2 = pos1
                    pos1 -= 1
                else:
                    min2 = pos2
                    pos2 += 1
            else:
                min2 = pos2
                pos2 += 1

            count[vocab_size + i] = count[min1] + count[min2]
            parent[min1] = vocab_size + i #min1的父节点
            parent[min2] = vocab_size + i #min2的父节点
            binary[min2] = 1 #min2的哈夫曼编码,相对的min1的编码为1

        root_idx = 2 * vocab_size - 2
        for i, token in enumerate(self):
            path = []  # 记录词语哈夫曼路径的列表
            code = []  # 记录哈夫曼编码

            node_idx = i                                                                                                                                   
            while node_idx < root_idx:
                if node_idx >= vocab_size: path.append(node_idx)
                code.append(binary[node_idx])
                node_idx = parent[node_idx]
            path.append(root_idx)

            token.path = [j - vocab_size for j in path[::-1]]
            token.code = code[::-1]



训练过程

def train(self, fi, fo, dim, alpha, win, binary):
        fi = open(fi, 'r')
        line = fi.readline().strip()
        sent = self.indices(line.split())
        word_count = 0 #记录训练的句子数
        for sent_pos, token in enumerate(sent):
            if word_count % 10000 == 0:  #自适应学习率,没训练10000个词更新一次学习率
                alpha = alpha * (1 - float(word_count) / vocab.word_count)
                if alpha < alpha * 0.0001:alpha = alpha * 0.0001
            #确定当前的窗口
            current_win = np.random.randint(low=1, high=win + 1)
            context_start = max(sent_pos - current_win, 0)
            context_end = min(sent_pos + current_win + 1, len(sent))
            context = sent[context_start:sent_pos] + sent[sent_pos + 1:context_end]  # Turn into an iterator?

            #求和得到Xw
            neu1 = np.mean(np.array([self.syn0[c] for c in context]), axis=0)
            assert len(neu1) == dim, 'neu1 and dim do not agree'
            neu1e = np.zeros(dim)

            # Compute neu1e and update syn1
            classifiers = zip(vocab[token].path, vocab[token].code)
            for target, label in classifiers:
                z = np.dot(neu1, self.syn1[target])
                p = self.sigmoid(z)
                g = alpha * (label - p)
                neu1e += g * self.syn1[target] 
                self.syn1[target] += g * neu1  # 更新权值矩阵syn1
            for context_word in context:
                self.syn0[context_word] += neu1e  # 更新词向量syn0
            word_count += 1
        # 保存模型
        self.save(self.syn0, self.syn1, fo, binary)  #save为自定义的保存模型函数                                                                                           
        fi.close()