word2vec学习小记

作者: ginobefun | 来源:发表于2017-03-19 16:03 被阅读935次

    word2vec是Google于2013年开源推出的一个用于获取词向量的工具包,它简单、高效,因此引起了很多人的关注。最近项目组使用word2vec来实现个性化搜索,在阅读资料的过程中做了一些笔记,用于后面进一步学习。

    前注:word2vec涉及的相关理论和推导是非常严(ku)格(zao)的,本文作为一个初学者的学习笔记,希望能从自己的理解中尽量用简单的描述,如有错误或者歧义的地方,欢迎指正。

    word2vec是什么?

    This tool provides an efficient implementation of the continuous bag-of-words and skip-gram architectures for computing vector representations of words. These representations can be subsequently used in many natural language processing applications and for further research.

    从官方的介绍可以看出word2vec是一个将词表示为一个向量的工具,通过该向量表示,可以用来进行更深入的自然语言处理,比如机器翻译等。

    为了理解word2vec的设计思想,我们有必要先学习一下自然语言处理的相关发展历程和基础知识。

    基础知识

    语言模型

    • 语言模型其实就是看一句话是不是正常人说出来的。这玩意很有用,比如机器翻译、语音识别得到若干候选之后,可以利用语言模型挑一个尽量靠谱的结果。在 NLP 的其它任务里也都能用到。
    • 用数学表达的话,就是给定T个词w1,w2,...wT,看它是自然语言的概率P,公式如下所示:
    语言模型形式化表达
    • 举例来说,比如一段语音识别为“我喜欢吃梨”和“我喜欢吃力”,根据分词和上述公式,可以得到两种表述的概率计算分别为下面的公式,而其中每一个子项的概率我们可以事先通过大量的语料统计得到,这样我们就可以得到更好的识别效果。
    P('我喜欢吃梨') = P('我') * P('喜欢'|'我') * P('吃'|'我','喜欢') * P('梨'|'我','喜欢','吃')
    P('我喜欢吃力') = P('我') * P('喜欢'|'我') * P('吃力'|'我','喜欢')
    

    N-gram模型

    • 通过上面的语言模型计算的例子,大家可以发现,如果一个句子比较长,那么它的计算量会很大;
    • 牛逼的科学家们想出了一个N-gram模型来简化计算,在计算某一项的概率时Context不是考虑前面所有的词,而是前N-1个词;
    • 当然牛逼的科学家们还在此模型上继续优化,比如N-pos模型从语法的角度出发,先对词进行词性标注分类,在此基础上来计算模型的概率;后面还有一些针对性的语言模型改进,这里就不一一介绍。
    • 通过上面简短的语言模型介绍,我们可以看出核心的计算在于P(wi|Contenti),对于其的计算主要有两种思路:一种是基于统计的思路,另外一种是通过函数拟合的思路;前者比较容易理解但是实际运用的时候有一些问题(比如如果组合在语料里没出现导致对应的条件概率变为0等),而函数拟合的思路就是通过语料的输入训练出一个函数P(wi|Contexti) = f(wi,Contexti;θ),这样对于测试数据就直接套用函数计算概率即可,这也是机器学习中惯用的思路之一。

    词向量表示

    • 自然语言理解的问题要转化为机器学习的问题,第一步肯定是要找一种方法把这些符号数学化。
    • 最直观的就是把每个词表示为一个很长的向量。这个向量的维度是词表的大小,其中绝大多数元素为0,只有一个维度的值为1,这个维度就代表了当前的词。这种表示方式被称为One-hot Representation。这种方式的优点在于简洁,但是却无法描述词与词之间的关系。
    • 另外一种表示方法是通过一个低维的向量(通常为50维、100维或200维),其基于“具有相似上下文的词,应该具有相似的语义”的假说,这种表示方式被称为Distributed Representation。它是一个稠密、低维的实数向量,它的每一维表示词语的一个潜在特征,该特征捕获了有用的句法和语义特征。其特点是将词语的不同句法和语义特征分布到它的每一个维度上去表示。这种方式的好处是可以通过空间距离或者余弦夹角来描述词与词之间的相似性。
    • 以下我们来举个例子看看两者的区别:
    // One-hot Representation 向量的维度是词表的大小,比如有10w个词,该向量的维度就是10w
    v('足球') = [0 1 0 0 0 0 0 ......]
    v('篮球') = [0 0 0 0 0 1 0 ......]
    
    // Distributed Representation 向量的维度是某个具体的值如50
    v('足球') = [0.26 0.49 -0.54 -0.08 0.16 0.76 0.33 ......]
    v('篮球') = [0.31 0.54 -0.48 -0.01 0.28 0.94 0.38 ......]
    
    • 最后需要说明的是一个词的向量表示对于不同的语料和场景结果是不同的。下面就介绍一种最常用的计算词向量的语言模型。

    神经网络概率语言模型

    • 神经网络概率语言模型(NNLM)把词向量作为输入(初始的词向量是随机值),训练语言模型的同时也在训练词向量,最终可以同时得到语言模型和词向量。
    • Bengio等牛逼的科学家们用了一个三层的神经网络来构建语言模型,同样也是N-gram 模型。 网络的第一层是输入层,是是上下文的N-1个向量组成的(n-1)m维向量;第二层是隐藏层,使用tanh作为激活函数;第三层是输出层,每个节点表示一个词的未归一化概率,最后使用softmax激活函数将输出值归一化。
    神经网络概率语言模型
    • 得到这个模型,然后就可以利用梯度下降法把模型优化出来,最终得到语言模型和词向量表示。

    word2vec的核心模型

    • word2vec在NNLM和其他语言模型的基础进行了优化,有CBOW模型和Skip-Gram模型,还有Hierarchical Softmax和Negative Sampling两个降低复杂度的近似方法,两两组合出四种实现。
    • 无论是哪种模型,其基本网络结构都是在下图的基础上,省略掉了隐藏层;
    word2vec的核心模型

    CBOW和Skip-gram模型


    word2vec的核心模型
    • CBOW(Continuous Bag-of-Words Model)是一种根据上下文的词语预测当前词语的出现概率的模型,其图示如上图左。CBOW是已知上下文,估算当前词语的语言模型;
    • 而Skip-gram只是逆转了CBOW的因果关系而已,即已知当前词语,预测上下文,其图示如上图右;
    • Hierarchical Softmax使用的办法其实是借助了分类的概念。假设我们是把所有的词都作为输出,那么“足球”、“篮球”都是混在一起的。而Hierarchical Softmax则是把这些词按照类别进行区分的,二叉树上的每一个节点可以看作是一个使用哈夫曼编码构造的二分类器。在算法的实现中,模型会赋予这些抽象的中间节点一个合适的向量,真正的词会共用这些向量。这种近似的处理会显著带来性能上的提升同时又不会丢失很大的准确性。
    • Negative Sampling也是用二分类近似多分类,区别在于它会采样一些负例,调整模型参数使得可以区分正例和负例。换一个角度来看,就是Negative Sampling有点懒,它不想把分母中的所有词都算一次,就稍微选几个算算。
    • 这一部分的模型实现比较复杂,网上也有很多资料可以参考,感兴趣的可以读读这两篇:word2vec原理推导与代码分析深度学习word2vec笔记之算法篇

    通过实践了解word2vec

    学习了上面的基础知识之后,我们就通过一个例子来感受一下word2vec的效果。

    下载语料

    从搜狗实验室下载全网新闻数据(SogouCA),该语料来自若干新闻站点2012年6月—7月期间国内、国际、体育、社会、娱乐等18个频道的新闻数据。

    下载该文件解压后大约为1.5G,包含120w条以上的新闻,文件的内容格式如下图所示:

    搜狗CA语料内容格式

    获取新闻内容

    这里我们先对语料进行初步处理,只获取新闻内容部分。可以执行以下命令获取content部分:

    cat news_tensite_xml.dat | iconv -f gbk -t utf-8 -c | grep "<content>"  > corpus.txt
    

    分词处理

    由于word2vec处理的数据是单词分隔的语句,对于中文来说,需要先进行分词处理。这里采用的是中国自然语言处理开源组织开源的ansj_seg分词器,核心代码如下所示:

    public class WordAnalyzer {
        private static final String TAG_START_CONTENT = "<content>";
        private static final String TAG_END_CONTENT = "</content>";
        private static final String INPUT_FILE = "/home/test/w2v/corpus.txt";
        private static final String OUTPUT_FILE = "/home/test/w2v/corpus_out.txt";
    
        public static void main(String[] args) throws Exception {
            BufferedReader reader = null;
            PrintWriter pw = null;
            try {
                System.out.println("开始处理分词...");
                reader = IOUtil.getReader(INPUT_FILE, "UTF-8");
                pw = new PrintWriter(OUTPUT_FILE);
                long start = System.currentTimeMillis();
                int totalCharactorLength = 0;
                int totalTermCount = 0;
                Set<String> set = new HashSet<String>();
                String temp = null;
                while ((temp = reader.readLine()) != null) {
                    temp = temp.trim();
                    if (temp.startsWith(TAG_START_CONTENT)) {
                        //System.out.println("处理文本:" + temp);
                        int end = temp.indexOf(TAG_END_CONTENT);
                        String content = temp.substring(TAG_START_CONTENT.length(), end);
                        totalCharactorLength += content.length();
                        Result result = ToAnalysis.parse(content);
                        for (Term term : result) {
                            String item = term.getName().trim();
                            totalTermCount++;
                            pw.print(item + " ");
                            set.add(item);
                        }
                        pw.println();
                    }
                }
    
                long end = System.currentTimeMillis();
                System.out.println("共" + totalTermCount + "个Term,共" 
                    + set.size() + "个不同的Term,共 " 
                    + totalCharactorLength + "个字符,每秒处理字符数:" 
                    + (totalCharactorLength * 1000.0 / (end - start)));
            } finally {
                // close reader and pw
            }
        }
    }
    

    分词处理之后的文件内容如下所示:


    分词之后的语料

    下载word2vec源码并编译

    这里我没有从官网下载而是从github上的svn2github/word2vec项目下载源码,下载之后执行make命令编译,这个过程很快就可以结束。

    开始word2vec处理

    编译成功后开始处理。我这里用的是CentOS 64位的虚拟机,八核CPU,32G内存,整个处理过程耗时大约4个小时。

    ./word2vec -train ../corpus_out.txt -output vectors.bin -cbow 0 -size 200 -window 5 -negative 0 -hs 1 -sample 1e-3 -threads 12 -binary 1
    
    // 参数解释
    -train 训练数据 
    -output 结果输入文件,即每个词的向量 
    -cbow 是否使用cbow模型,0表示使用skip-gram模型,1表示使用cbow模型,默认情况下是skip-gram模型,cbow模型快一些,skip-gram模型效果好一些 
    -size 表示输出的词向量维数 
    -window 为训练的窗口大小,5表示每个词考虑前5个词与后5个词(实际代码中还有一个随机选窗口的过程,窗口大小<=5) 
    -negative 表示是否使用负例采样方法0表示不使用,其它的值目前还不是很清楚 
    -hs 是否使用Hierarchical Softmax方法,0表示不使用,1表示使用 
    -sample 表示采样的阈值,如果一个词在训练样本中出现的频率越大,那么就越会被采样
    -binary 表示输出的结果文件是否采用二进制存储,0表示不使用(即普通的文本存储,可以打开查看),1表示使用,即vectors.bin的存储类型
    

    测试处理结果

    处理结束之后,使用distance命令可以测试处理结果,以下是分别测试【足球】和【改革】的效果:

    测试足球一词的词向量 测试改革一词的词向量

    学习小结

    • word2vec的模型是基于神经网络来训练词向量的工具;
    • word2vec通过一系列的模型和框架对原有的NNLM进行优化,简化了计算但准确度还是保持得很好;
    • word2vec的主要的应用还是自然语言的处理,通过训练出来的词向量,可以进行聚类等处理,或者作为其他深入学习的输入。另外,word2vec还适用于一些时序数据的挖掘,比如用户商品的浏览分析、用户APP的下载等,通过这些数据的分析,可以得到商品或者APP的向量表示,从而用于个性化搜索和推荐。

    参考材料

    扫一扫 关注我的微信公众号

    相关文章

      网友评论

      • Krystal_YI:嗯还可以,github地址有么!?
        ginobefun:看这里 https://github.com/ginobefun/learning_projects/tree/master/learning-w2v

      本文标题:word2vec学习小记

      本文链接:https://www.haomeiwen.com/subject/chamnttx.html