用 Python 实现 LDA

发布时间:2019-09-01 10:52:35编辑:auto阅读(1762)

    原文出处:Jordan Barber

    • LDA 是什么
    • LDA 演练
      • 需要用到的包
      • 导入文档
      • 清洗文档 
        • 分词
        • 移除停用词
        • 词干提取
      • 创建 document-term matrix
      • 应用 LDA 模型
      • 检查结果
      • LDA 原理
      • 完整代码

    LDA 是什么?

    隐含狄利克雷分布(以下简写为 LDA)是一种主题模型,它基于一组文档中的词频生成主题。对于在给定的文档集中准确合理地找到主题的混合,LDA 是一种非常有效的方法。

    LDA 演练

    这一部分,我会用一个高度简化过的文档集来演练生成一个 LDA 模型的过程。这并不是对 LDA 的全面讲解。这个演练的目的是为大家准备数据,以及用 LDA 模型得到相应输出的核心步骤提供指导。

    需要用到的包

    该演练当中使用的 Python 包有:

    • NLTK, Python 的一个自然语言处理工具包。对于任何一种自然语言的处理都非常有用。
      • 在 Mac/Unix 下使用 pip 安装:$ sudo pip install -U nltk.
    • stop_words,一个包含停用词的 Python 包。
      • 在 Mac/Unix 下使用 pip 安装:$ sudo pip install stop-words.
    • gensim,包含我们要用到的 LDA 模型的一个主题模型包。 
      • 在 Mac/Unix 下使用 pip 安装:$ sudo pip install gensim.

    导入文档

    这是我们的文档用例:

    doc_a = "Brocolli is good to eat. My brother likes to eat good brocolli, but not my mother."
    doc_b = "My mother spends a lot of time driving my brother around to baseball practice."
    doc_c = "Some health experts suggest that driving may cause increased tension and blood pressure."
    doc_d = "I often feel pressure to perform well at school, but my mother never seems to drive my brother to do better."
    doc_e = "Health professionals say that brocolli is good for your health."
    
    # compile sample documents into a list
    doc_set = [doc_a, doc_b, doc_c, doc_d, doc_e]  

    清洗文档

    数据清洗对于生成一个有效的主题模型是极其极其重要的:俗话说,“输入的是垃圾,得到的一定也是垃圾”(Garbage in, garbarge out.)。下面的步骤就是自然语言处理的常见方法:

    • 分词:将文档转化为其原子元素。
    • 停用词处理:移除无意义的词。
    • 词干提取:将同义词合并。

    分词

    分词即将一个文档分成其原子元素。在这个例子中,我们将其分为单词。分词有很多种方法,我们用的是 NLTK 的 tokenize.regexp 模块:

    from nltk.tokenize import RegexpTokenizer
    tokenizer = RegexpTokenizer(r'\w+') 

    上面的代码会匹配所有单字字符,直到其遇到像空格这样的非单字的字符。这是个很简单的方法,但是会出现一些问题,比如像“don't”这样的单词就会被分成两个词“don”和“t“。NLTK 提供了很多像 nltk.tokenize.simple 这样的预留的特定结构的词。对于特殊用例,最好还是用 regex 和不断的迭代来使你的文档精确地分词。

    注意:这个例子是对单个文档调用 tokenize()。你需要创建一个 for 循环来遍历所有文档。用下面这个脚本做个示例。

    raw = doc_a.lower()
    tokens = tokenizer.tokenize(raw)
    
    >>> print(tokens)
    ['brocolli', 'is', 'good', 'to', 'eat', 'my', 'brother', 'likes', 'to', 'eat', 'good', 'brocolli', 'but', 'not', 'my', 'mother'] 

    文档 doc_a 现在就是一个词的列表了。

    停用词

    英语中的一些特定组成部分,比如“for”,“or”这样的连词,或是“the”这种词对主题模型毫无意义。这些词叫做停用词,需要从我们的单词列表中移除。

    停用词的定义是非常灵活的,在不同种类的文档中对应的停用词的含义是不同的。比如,如果我们要为一系列音乐评论做主题模型,那像“The Who”(英国的一支摇滚乐队)这种词就会有点麻烦,因为“the”通常都会作为一个常见的停用词而被移除。你可以根据实际情况来创建自己的停用词列表,或者用其他的包。

    在我们的例子中,我们使用 Pypi 的 stop_words 包,这是一个相对比较保守的列表。我们可以调用 get_stop_words() 来创建一个停用词列表:

    from stop_words import get_stop_words
    
    # create English stop words list
    en_stop = get_stop_words('en') 

    现在,移除停用词只是一个循环遍历我们的单词的工作了,将每一个词都和 en_list 列表作比较。

    # remove stop words from tokens
    stopped_tokens = [i for i in tokens if not i in en_stop]
    
    >>> print(stopped_tokens)
    ['brocolli', 'good', 'eat', 'brother', 'likes', 'eat', 'good', 'brocolli', 'mother'] 

    词干提取

    词干提取是 NLP 的另一个常见技术,它用于将相似的单词去除词缀得到词根。例如:“stemming”,“stemmer”,“stemmed”都有相似的意思;词干提取就是去除这些词的词缀而得到词根“stem”。这对主题模型来说很重要,否则如果将这些单词看做不同的实体,会降低他们在模型中的重要程度。

    和停用词一样,词干提取也是非常灵活的,有些方法在特定的情形下可能会出问题。Porter stemming algorithm 是使用最广泛的方法。我们从 NLTK 中引入 Porter Stemmer 模块来实现这个算法:

    from nltk.stem.porter import PorterStemmer
    
    # Create p_stemmer of class PorterStemmer
    p_stemmer = PorterStemmer() 
    注意,p_stemmer 要求所有单词的类型都是 str。p_stemmer 以词干的形式返回字符串参数。

    # stem token
    texts = [p_stemmer.stem(i) for i in stopped_tokens]
    
    >>> print(stemmed_tokens)
    ['brocolli', 'good', 'eat', 'brother', 'like', 'eat', 'good', 'brocolli', 'mother'] 

    构建 document-term matrix

    (译注:document-term matrix 是一个描述文档词频的矩阵,每一行对应文档集中的一篇文档,每一列对应一个单词,这个矩阵可以根据实际情况,采用不同的统计方法来构建。)

    清洗阶段的结果就是文本(texts),从单个的文档中整理出来的分好词,去除了停用词而且提取了词干的单词列表。假设我们已经循环遍历了所有文档,将每份文档都整理成为了文本。现在,文本就是一个(单词)列表的列表了,每个单词列表就代表一份原文档。

    要生成一个 LDA model,我们需要知道每个词在文档中出现的频繁程度。为此我们需要用到一个叫 gensim 的包来构建 document-term matrix:

    from gensim import corpora, models
    
    dictionary = corpora.Dictionary(texts) 

    Dictionary() 方法遍历所有的文本,为每个不重复的单词分配一个单独的整数 ID,同时收集该单词出现次数以及相关的统计信息。试试用 print(dictionary.token2id) 来查看每个单词的id。

    接下来,我们要将 dictionary 转化为一个词袋:

    doc2bow() 方法将 dictionary 转化为一个词袋。得到的结果 corpus 是一个向量的列表,向量的个数就是文档数。在每个文档向量中都包含一系列元组。举个例子,print(corpus[0]) 结果如下:

    >>> print(corpus[0])
    [(0, 2), (1, 1), (2, 2), (3, 2), (4, 1), (5, 1)] 

    这个元组列表代表我们的第一个文档 doc_a。元组的形式是(单词 ID,词频)。所以如果 print(dictionary.roken2id) 显示 brocolli 的 id 是 0,那么第一个元组就代表 brocolli 这个词在 doc_a 里出现了两次。只有在文档中出现过的词才会包含在 doc2bow() 中,否则它将不会出现在文档向量之中。

    应用 LDA 模型

    corpus 是一个 document-term matrix,现在,我们已经为生成一个 LDA 模型做好准备了:

    ldamodel = gensim.models.ldamodel.LdaModel(corpus, num_topics=3, id2word = dictionary, passes=20) 

    LdaModel 类的详细描述可以在 gensim 文档中查看。我们的实例中用到的参数:

    参数:

    • num_topics: 必须。LDA 模型要求用户决定应该生成多少个主题。由于我们的文档集很小,所以我们只生成三个主题。
    • id2word:必须。LdaModel 类要求我们之前的 dictionary 把 id 都映射成为字符串。
    • passes:可选。模型遍历语料库的次数。遍历的次数越多,模型越精确。但是对于非常大的语料库,遍历太多次会花费很长的时间。

    检查结果

    我们的 LDA 模型已经用 ldamodel 储存好了。我们可以用 print_topic 和 print_topics 方法来查看主题:

    >>> print(ldamodel.print_topics(num_topics=3, num_words=3))
    ['0.141*health + 0.080*brocolli + 0.080*good', '0.060*eat + 0.060*drive + 0.060*brother', '0.059*pressur + 0.059*mother + 0.059*brother'] 

    这是什么意思呢?每一个生成的主题都用逗号分隔开。每个主题当中有三个该主题当中最可能出现的单词。即使我们的文档集很小,这个模型依旧是很可靠的。还有一些需要我们考虑的问题:

    - health, brocolli 和 good 在一起时有很好的含义。

    - 第二个主题有点让人疑惑,如果我们重新查看源文档,可以看到 drive 有很多种含义:driving a car 意思是开车,driving oneself to improve 是激励自己进步。这是我们在结果中需要注意的地方。

    - 第三个主题包含 mother 和 brother,这很合理。

    调整模型的主题数和遍历次数对于得到一个好的结果是很重要的。两个主题看起来更适合我们的文档。

    ldamodel = gensim.models.ldamodel.LdaModel(corpus, num_topics=2, id2word = dictionary, passes=20)
    
    >>> print(ldamodel.print_topics(num_topics=2, num_words=4))
    ['0.054*pressur + 0.054*drive + 0.054*brother + 0.054*mother', '0.070*brocolli + 0.070*good + 0.070*health + 0.050*eat'] 

    LDA 到底做了什么?

    这个解释有点长,但是对于理解我们千辛万苦生成的模型非常有帮助。

    LDA 假定文档是从主题的混合生成的。这些主题又是由一些单词的特定概率分布而生成的。就像我们演练的模型一样。换句话说,LDA 假定文档以以下步骤生成:

    1. 确定一个文档中的单词数。假设我们的文档有六个单词。 
    2. 确定该文档由哪些主题混合而来,例如,这个文档包含 1/2 的“健康”(health)主题和 1/2 的“蔬菜”(vegetables)主题。
    3. 用每个主题的多项分布生成的单词来填充文档中的单词槽。在我们的例子中,“健康”主题占文档的 1/2,或者说占三个词。“健康”主题有“diet”这个词的可能性是 20%,或者有“execise" 这个词的概率是 15%,单词槽就是基于这些概率来填充的。

    基于文档如何生成的假定,LDA 反其道而行之,并尝试找出最初哪些主题会创建这些文档。

    完整代码

    from nltk.tokenize import RegexpTokenizer
    from stop_words import get_stop_words
    from nltk.stem.porter import PorterStemmer
    from gensim import corpora, models
    import gensim
    
    tokenizer = RegexpTokenizer(r'\w+')
    
    # create English stop words list
    en_stop = get_stop_words('en')
    
    # Create p_stemmer of class PorterStemmer
    p_stemmer = PorterStemmer()
        
    # create sample documents
    doc_a = "Brocolli is good to eat. My brother likes to eat good brocolli, but not my mother."
    doc_b = "My mother spends a lot of time driving my brother around to baseball practice."
    doc_c = "Some health experts suggest that driving may cause increased tension and blood pressure."
    doc_d = "I often feel pressure to perform well at school, but my mother never seems to drive my brother to do better."
    doc_e = "Health professionals say that brocolli is good for your health." 
    
    # compile sample documents into a list
    doc_set = [doc_a, doc_b, doc_c, doc_d, doc_e]
    
    # list for tokenized documents in loop
    texts = []
    
    # loop through document list
    for i in doc_set:
        
        # clean and tokenize document string
        raw = i.lower()
        tokens = tokenizer.tokenize(raw)
    
        # remove stop words from tokens
        stopped_tokens = [i for i in tokens if not i in en_stop]
        
        # stem tokens
        stemmed_tokens = [p_stemmer.stem(i) for i in stopped_tokens]
        
        # add tokens to list
        texts.append(stemmed_tokens)
    
    # turn our tokenized documents into a id <-> term dictionary
    dictionary = corpora.Dictionary(texts)
        
    # convert tokenized documents into a document-term matrix
    corpus = [dictionary.doc2bow(text) for text in texts]
    
    # generate LDA model
    ldamodel = gensim.models.ldamodel.LdaModel(corpus, num_topics=2, id2word = dictionary, passes=20)​ 


    关于 LDA 原理的更多分析,推荐两份资料《LDA 数学八卦》和《LDA 漫游指南》。

关键字