ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

无所不能的Embedding1 - 词向量三巨头之Word2vec模型详解代码实现

无所不能的Embedding1 - 词向量三巨头之Word2vec模型详解代码实现 前言word2vec是google 2013年提出的从大规模语料中训练词向量的模型在许多场景中都有应用信息提取相似度计算等等。也是从word2vec开始embedding在各个领域的应用开始流行所以拿word2vec来作为开篇再合适不过了。本文希望可以较全面的给出Word2vec从模型结构概述推导训练和基于tf.estimator实现的具体细节。模型概述word2vec模型结构比较简单是为了能够在大规模数据上训练降低了模型复杂度移除了非线性隐藏层。根据不同的输入输出形式又分成CBOW和SG两种方法。让我们先把问题简化成1v1的bigram问题单词i作为context,单词j是target。V是单词总数N是词向量长度D是训练词对输入\(x_i \in R ^{1*V}\)是one-hot向量。模型训练两个权重矩阵,\(W \in R{V*N}\)是输入矩阵每一行对应输入单词的词向量,\(W{} \in R ^{V*N}\)是输出矩阵每一行对应输出单词的词向量。词i和词j的共现信息用词向量的内积来表达通过softmax得到每个单词的概率如下\[\begin{align} h v_{wI} W^T x_i \\ v_{w^{}j} W^{‘T} x_j \\ u_j v_{w{}j}T h \\ y_j p(w_j|w_I) \frac{exp(u_j)}{\sum_{j{}1}Vexp(u_{j^{’}})}\\ \end{align} \]对每个训练样本模型的目标是最大化条件概率\(p(w_j|w_I)\), 因此我们的对数损失函数如下\[\begin{align} E - logP(w_j|w_I) \\ -u_j^* log\sum_{j{}1}Vexp(u_{j^{}}) \end{align} \]CBOW : Continuous bag of wordsCBOW是把bigram的输入context扩展成了目标单词周围2*window_size内的单词用中心词前后的语境来预测中心词。对比bigram, CBOW只多做了一步操作对输入的2 * Window_size个单词在映射得到词向量后需要做average_pooling得到1*N的输入向量, 所以差异只在h的计算。假定KaTeX parse error: Undefined control sequence: \* at position 7: C 2 \̲*̲ \\text{window\…KaTeX parse error: Expected EOF, got at position 19: …begin{align} h ̲ \\frac{1}{C}…SG : Skip GramSG是把bigram的输出target扩展成了输入单词周围2*window_size内的单词用中心词来预测周围单词的出现概率。对比bigramSG的差异只在于输出概率多项分布不再是一个而是C个\[\begin{align} E -log \, p(w_{O,1},w_{O,2},…w_{O,C}|w_I) \\ \sum_{c1}Cu_{j,c}* C\cdot log\sum_{j{}1}Vexp(u_{j^{}}) \end{align} \]模型推导word embedding是如何得到的下面我们从back propogation推导下以上模型结构是如何学到词向量的为简化我们还是先从bigram来看\(\eta\)是learning rate。首先是hidden-output \(W^{}\)的词向量的更新\[\begin{align} \frac{\partial E}{\partial v_{w^{‘}j}} \frac{\partial E}{\partial u_j}\frac{\partial u_j}{\partial v_{w^{’}j}}\\ (p(w_j|w_i) - I(jj^*))\cdot h \\ e_j\cdot h \\ v_{w{}j}{(new)} v_{w{}j}{(old)} - \eta \cdot e_j \cdot h \\ \end{align} \]\(e_j\)是单词j的预测概率误差所以\(W{}\)的更新可以理解为如果单词j被高估就从\(v_{w{‘}j}\)中减去\(\eta \cdot e_j \cdot h\)降低h和\(v_{w{}j}\)的向量内积(similarity)反之被低估则在\(v_{w{’}j}\)上叠加\(\eta \cdot e_j \cdot h\)增加内积相似度误差越大更新的幅度越大。然后是input-hidden W的词向量的更新\[\begin{align} \frac{\partial E}{\partial h} \sum_{j1}^V\frac{\partial E}{\partial u_j}\frac{\partial u_j}{\partial h}\\ \sum_{j1}^V e_j \cdot v_{w^{‘}j}\\ v_{w_I}^{(new)} v_{w_I}^{(old)} - \eta \cdot \sum_{j1}^V e_j \cdot v_{w^{’}j} \\ \end{align} \]每个输入单词对应的词向量\(v_{wI}\)都用所有单词的输出词向量按预测误差加权平均得到的向量进行更新。和上述的逻辑相同高估做subtraction低估的做addition然后按误差大小进行加权来更新输入词向量。所以模型学习过程会是输入词向量更新输出词向量输出词向量再更新输入词向量然后back-and-forth到达稳态。把bigram拓展到CBOW唯一的变化在于更新input-hidden的词向量时不是每次更新一个单词对应的向量而是用相同的幅度同时更新C个单词的词向量.\[v_{w_{I,c}}^{(new)} v_{w_{I,c}}^{(old)} - \frac{1}{C}\eta \cdot \sum_{j1}^V e_j \cdot v_{w^{}j} \]把bigram拓展到SG唯一的变化在于更新hidden-output的词向量时不再是用单词j的预测误差而是用C个单词的预测误差之和\[v_{w{}j}{(new)} v_{w{}j}{(old)} - \eta \cdot \sum_{c1}^C e_{c,j} \cdot h \]模型训练虽然模型结构已经做了优化移除了非线性的隐藏层但是模型训练起来并不高效,瓶颈在于Word2vec本质是多分类任务类别有整个vocabulary这么多所以\(p(w_j|w_I) \frac{exp(u_j)}{\sum_{j{}1}Vexp(u_{j^{}})}\)每次需要计算整个vocabulary的概率\(O(VN)\)。即便batch只有1个训练样本也需要更新所有单词hidden-output的embedding矩阵。针对这个问题有两种解决方案Hierarchical Softmax如果把softmax看作一个1-layer tree,每个单词都是一个叶节点, 因为需要归一化所以计算每个单词的概率的复杂度是\(O(V)\)。Hierarchical Softmax只是把1-layer变成了multi-layer在不增加embedding大小的情况下V个叶节点树有V-1个inner node), 把计算每个单词概率的复杂度降低到\(O(logV)\)直接用从root到叶节点的路径来计算每个单词的概率。树的构造作者选用了huffman tree,优点在于高频词从root到leaf的路径会比低频词更短这样可以进一步加速训练。例如下图图片来源KaTeX parse error: Expected EOF, got at position 26: …lign} P(Horse) ̲ P(0,left)\\cd…那具体上面的p(0,left)要如何计算呢每一个node都有自己的embedding \(v_n{(w,j)}\), 既单词w路径上第j个node的embedding,输入输出的单词内积变为输入单词和node的内积 每个单词的概率计算如下\[p(ww_o) \prod_{j1}^{L(w)-1}\sigma([n(w,j1) ch(n(w,j))] \cdot {v_{n(w,j)}}^{T} h) \]不得不说这个式子写的真是生怕别人能看懂_\([n(w,j1) ch(n(w,j))]\) 是个啥ch是left child\([\cdot]\)只是用来判断path是往左还是往右\[\ [\cdot] \begin{cases} 1 \quad \text{if 往左} \\ -1 \quad \text{if 往右} \end{cases} \ \]所以\[\begin{align} p(n,left) \sigma(v_n^T\cdot h )\\ p(n, right) \sigma(-v_n^T\cdot h ) 1- \sigma(v_n^T\cdot h ) \end{align} \]对应上面的模型推导hidden-ouput的部分发生变化, 损失函数变为以下\[E -log P(ww_j|w_I) - \sum_{j1}{L(w)-1}log([\cdot]v_jT h) \]每次output单词对应的路径上的embedding会被更新预测任务变为该路径上每个inner_node应该往左还是往右。简单的huffman Hierarchy softmax的实现如下classTreeNode(object):total_node0def__init__(self,frequency,charNone,word_indexNone,is_leafFalse):self.frequencyfrequency self.charchar# word characterself.word_indexword_index# word look up indexself.leftNoneself.rightNoneself.is_leafis_leaf self.counter(is_leaf)defcounter(self,is_leaf):# node_index will be used for embeeding_lookupself.node_indexTreeNode.total_nodeifnotis_leaf:TreeNode.total_node1def__lt__(self,other):returnself.frequencyother.frequencydef__repr__(self):ifself.is_leaf:returnLeaf Node char [{}] index {} freq {}.format(self.char,self.word_index,self.frequency)else:returnInner Node [{}] freq {}.format(self.node_index,self.frequency)classHuffmanTree(object):def__init__(self,freq_dic):self.nodes[]self.rootNoneself.max_depthNoneself.freq_dicfreq_dic self.all_paths{}self.all_codes{}self.node_index0staticmethoddefmerge_node(left,right):parentTreeNode(left.frequencyright.frequency)parent.leftleft parent.rightrightreturnparentdefbuild_tree(self): Build huffman tree with word being leaves TreeNode.total_node0# avoid train_and_evaluate has different node_indexheap_nodes[]forword_index,(char,freq)inenumerate(self.freq_dic.items()):tmpTreeNode(freq,char,word_index,is_leafTrue)heapq.heappush(heap_nodes,tmp)whilelen(heap_nodes)1:node1heapq.heappop(heap_nodes)node2heapq.heappop(heap_nodes)heapq.heappush(heap_nodes,HuffmanTree.merge_node(node1,node2))self.rootheapq.heappop(heap_nodes)propertydefnum_node(self):returnself.root.node_index1deftraverse(self): Compute all node to leaf path and direction: list of node_id, list of 0/1 defdfs_helper(root,path,code):ifroot.is_leaf:self.all_paths[root.word_index]path self.all_codes[root.word_index]codereturnifroot.left:dfs_helper(root.left,path[root.node_index],code[0])ifroot.right:dfs_helper(root.right,path[root.node_index],code[1])dfs_helper(self.root,[],[])self.max_depthmax([len(i)foriinself.all_codes.values()])classHierarchySoftmax(HuffmanTree):def__init__(self,freq_dic):super(HierarchySoftmax,self).__init__(freq_dic)defconvert2tensor(self):# padded to max_depth and convert to tensorwithtf.name_scope(hstree_code):self.code_tabletf.convert_to_tensor([code[INVALID_INDEX]*(self.max_depth-len(code))forword,codeinsorted(self.all_codes.items(),keylambdax:x[0])],dtypetf.float32)withtf.name_scope(hstree_path):self.path_tabletf.convert_to_tensor([path[INVALID_INDEX]*(self.max_depth-len(path))forword,pathinsorted(self.all_paths.items(),keylambdax:x[0])],dtypetf.int32)defget_loss(self,input_embedding_vector,labels,output_embedding,output_bias,params): :param input_embedding_vector: [batch * emb_size] :param labels: word index [batch * 1] :param output_embedding: entire embedding matrix [] :return: loss loss[]labelstf.unstack(labels,numparams[batch_size])# list of [1]inputstf.unstack(input_embedding_vector,numparams[batch_size])# list of [emb_size]forlabel,inputinzip(labels,inputs):pathself.path_table[tf.squeeze(label)]# (max_depth,)codeself.code_table[tf.squeeze(label)]# (max_depth,)pathtf.boolean_mask(path,tf.not_equal(path,INVALID_INDEX))# (real_path_length,)codetf.boolean_mask(code,tf.not_equal(code,INVALID_INDEX))# (real_path_length,)output_embedding_vectortf.nn.embedding_lookup(output_embedding,path)# real_path_length * emb_sizebiastf.nn.embedding_lookup(output_bias,path)# (real_path_length,)logitstf.matmul(tf.expand_dims(input,axis0),tf.transpose(output_embedding_vector))bias# (1,emb_size) *(emb_size, real_path_length)loss.append(tf.nn.sigmoid_cross_entropy_with_logits(labelscode,logitstf.squeeze(logits)))losstf.reduce_mean(tf.concat(loss,axis0),axis0,namehierarchy_softmax_loss)# batch - scalerreturnlossNegative SamplingNegative Sampling理解起来更加直观因为模型的目标是训练出高质量的word embedding也就是input word embedding那是否每个batch都更新全部的output word embedding并不重要我们可以每次只sample K个embedding来做更新。原始的正样本保留我们再采样 K组负样本来进行训练模型只需要学习正样本vs负样本也就绕过了用V个单词来做归一化的问题把多分类问题成功简化为二分类问题。作者表示小样本K520大样本k25。对应上述的模型推导hidden-output的部分发生变化, 损失函数变为\[E -log\sigma(v_j^Th) - \sum_{w_j \in neg} log\sigma(-v_{w_j}^Th) \]每个iteration只有K个embedding被更新\[v_{w{}j}{(new)} v_{w{}j}{(old)} - \eta \cdot e_j \cdot h \,\,\,\, \text{where } j \in k \]而input-hidden的部分,只有k个embedding的加权向量会用于输入embedding的更新\[v_{w_I}^{(new)} v_{w_I}^{(old)} - \eta \cdot \sum_{j1}^K e_j \cdot v_{w^{}j} \]tensorflow有几种candidate sample的实现两种比较常用的是nn.sampled_softmax_loss和nn.nce_loss, 它们调用了相同的采样函数。差异在于sampled_softmax_loss用的是softmax排他单分类)而nce_loss是求logistic (不排他多分类。这两种实现都和negative sampling有些许差异细节可以看下Notes on Noise Contrastive Estimation and Negative Sampling。而这二者之间比较是有观点说nce更适合skip-gram, sample更适合CBOW具体差异我也还得再多用用试试看。Subsampling论文还有一个重点是subsampling针对出现频率高的词对于它们过多的训练样本不能进一步提高表现因此可以对这些样本进行downsample。t是词频阈值 \(f(w_i)\)是单词在corpus里的出现频率所有出现频率高于t的单词都会按照以下概率被降采样\[p(w_i) 1 - \sqrt{\frac{t}{f(w_i)}} \]模型实现手残党现实体验是word2vec比较复杂的部分不是模型。。。而是input_pipe和loss function所以在实现的时候也希望尽可能把dataset, model_fn, 和train的部分分割开来。以下只给出model_fn的核心部分defavg_pooling_embedding(embedding,features,params): :param features: (batch, 2*window_size) :param embedding: (vocab_size, emb_size) :return: input_embedding : average pooling of context embedding input_embedding[]samplestf.unstack(features,params[batch_size])forsampleinsamples:sampletf.boolean_mask(sample,tf.not_equal(sample,INVALID_INDEX),axis0)# (real_size,)tmptf.nn.embedding_lookup(embedding,sample)# (real_size, emb_size)input_embedding.append(tf.reduce_mean(tmp,axis0))# (emb_size, )input_embeddingtf.stack(input_embedding,nameinput_embedding_vector)# batch * emb_sizereturninput_embeddingdefmodel_fn(features,labels,mode,params):ifparams[train_algo]HS:# If Hierarchy Softmax is used, initialize a huffman tree firsthstreeHierarchySoftmax(params[freq_dict])hstree.build_tree()hstree.traverse()hstree.convert2tensor()ifparams[model]CBOW:featurestf.reshape(features,shape[-1,2*params[window_size]])labelstf.reshape(labels,shape[-1,1])else:featurestf.reshape(features,shape[-1,])labelstf.reshape(labels,shape[-1,1])withtf.variable_scope(initialization):w0tf.get_variable(shape[params[vocab_size],params[emb_size]],initializertf.truncated_normal_initializer(),nameinput_word_embedding)ifparams[train_algo]HS:w1tf.get_variable(shape[hstree.num_node,params[emb_size]],initializertf.truncated_normal_initializer(),namehierarchy_node_embedding)b1tf.get_variable(shape[hstree.num_node],initializertf.random_uniform_initializer(),namebias)else:w1tf.get_variable(shape[params[vocab_size],params[emb_size]],initializertf.truncated_normal_initializer(),nameoutput_word_embedding)b1tf.get_variable(shape[params[vocab_size]],initializertf.random_uniform_initializer(),namebias)add_layer_summary(w0.name,w0)add_layer_summary(w1.name,w1)add_layer_summary(b1.name,b1)withtf.variable_scope(input_hidden):# batch_size * emb_sizeifparams[model]CBOW:input_embedding_vectoravg_pooling_embedding(w0,features,params)else:input_embedding_vectortf.nn.embedding_lookup(w0,features,nameinput_embedding_vector)add_layer_summary(input_embedding_vector.name,input_embedding_vector)withtf.variable_scope(hidden_output):ifparams[train_algo]HS:losshstree.get_loss(input_embedding_vector,labels,w1,b1,params)else:lossnegative_sampling(modemode,output_embeddingw1,biasb1,labelslabels,input_embedding_vectorinput_embedding_vector,paramsparams)optimizertf.train.AdagradOptimizer(learning_rateparams[learning_rate])update_opstf.get_collection(tf.GraphKeys.UPDATE_OPS)withtf.control_dependencies(update_ops):train_opoptimizer.minimize(loss,global_steptf.train.get_global_step())returntf.estimator.EstimatorSpec(mode,lossloss,train_optrain_op)最后感谢你们的阅读和喜欢我收藏了很多技术干货可以共享给喜欢我文章的朋友们如果你肯花时间沉下心去学习它们一定能帮到你。因为这个行业不同于其他行业知识体系实在是过于庞大知识更新也非常快。作为一个普通人无法全部学完所以我们在提升技术的时候首先需要明确一个目标然后制定好完整的计划同时找到好的学习方法这样才能更快的提升自己。这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】一、全套AGI大模型学习路线AI大模型时代的学习之旅从基础到前沿掌握人工智能的核心技能二、640套AI大模型报告合集这套包含640份报告的合集涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师还是对AI大模型感兴趣的爱好者这套报告合集都将为您提供宝贵的信息和启示。三、AI大模型经典PDF籍随着人工智能技术的飞速发展AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型如GPT-3、BERT、XLNet等以其强大的语言理解和生成能力正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。四、AI大模型商业化落地方案五、面试资料我们学习AI大模型必然是想找到高薪的工作下面这些面试题都是总结当前最新、最热、最高频的面试题并且每道题都有详细的答案面试前刷完这套面试题资料小小offer不在话下。这份完整版的大模型 AI 学习资料已经上传CSDN朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费】
返回列表