word2vec CBOW模型学习笔记

寒假前做的一个软著,用到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模型构建

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


模型训练过程

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


这里,θ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 = wordself.count = 0self.path = None  self.code = None  

语料库的处理代码:

class Vocab:def __init__(self, fi, min_count):vocab_items = [] #用来存语料库中所有词语的列表vocab_hash = {} #词典记录语料库中词语和对应该词在列表中的索引word_count = 0 #总的词语数fi = open(fi, 'r') #读入语料库文件fifor 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 += 1word_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 - 1pos2 = vocab_sizefor i in range(vocab_size - 1):# 查找词频中的最小值min1if pos1 >= 0:if count[pos1] < count[pos2]:min1 = pos1pos1 -= 1else:min1 = pos2pos2 += 1else:min1 = pos2pos2 += 1# 查找除去第一个最小值min1后的另一个最小值min2if pos1 >= 0:if count[pos1] < count[pos2]:min2 = pos1pos1 -= 1else:min2 = pos2pos2 += 1else:min2 = pos2pos2 += 1count[vocab_size + i] = count[min1] + count[min2]parent[min1] = vocab_size + i #min1的父节点parent[min2] = vocab_size + i #min2的父节点binary[min2] = 1 #min2的哈夫曼编码,相对的min1的编码为1root_idx = 2 * vocab_size - 2for 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?#求和得到Xwneu1 = 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 syn1classifiers = 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  # 更新权值矩阵syn1for context_word in context:self.syn0[context_word] += neu1e  # 更新词向量syn0word_count += 1# 保存模型self.save(self.syn0, self.syn1, fo, binary)  #save为自定义的保存模型函数                                                                                           fi.close()




本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部