学习主题建模和LDA(Latent Dirichlet Allocation)

155 阅读8分钟

主题建模

主题建模是一种自然语言处理(NLP)技术,用于确定文档中的主题。同时,我们可以用它来发现文档中的词汇模式。通过分析文档中单词和短语的频率,它能够确定一个单词或短语属于某个主题的概率,并根据它们的相似性或接近性对文档进行分类。

首先,话题建模从一个大的文本语料库开始,将其减少到一个小得多的话题。话题是通过分析语料库中词语之间的关系而发现的。此外,话题建模还可以发现哪些词经常与其他词共同出现,以及它们一起出现的频率。

该模型试图找到那些比仅仅由于机会而出现的更频繁的共同出现的词群。这就给出了一个关于文档中的主题以及它们在其重要性层次上的排名的大致概念。
目前提取主题模型的方法包括Latent Dirichlet Allocation(LDA)、Latent Semantic Analysis(LSA)、Probabilistic Latent Semantic Analysis(PLSA)和Non-Negative Matrix Factorization(NMF)。在这篇文章中,我们将重点讨论Latent Dirichlet Allocation(LDA)。

主题建模之所以有用,是因为它不仅允许用户探索他们的语料库(文档)内部的内容,还可以在他们甚至没有意识到的主题之间建立新的联系。主题建模的一些应用还包括文本总结、推荐系统、垃圾邮件过滤器,以及类似的。

潜隐二分法分配(LDA)

Latent Dirichlet Allocation(LDA)是一种无监督的聚类技术,通常用于文本分析。它是一种主题建模,其中单词被表示为主题,而文档被表示为这些单词主题的集合。

为了这个目的,我们将通过话题建模来描述LDA。因此,让我们想象一下,我们有一个文档或文章的集合。每个文件都有一个主题,如计算机科学、物理学、生物学等。另外,有些文章可能有多个主题。问题是,我们只有文章,却没有它们的主题,我们希望有一种算法能够将文档分类到主题中。

抽取主题

我们可以想象,LDA会根据文档的主题将文档放在空间中。例如,在我们的案例中,如果有计算机科学、物理学和生物学的主题,LDA就会把文档放到一个三角形中,而三角形的四角就是这些主题。我们可以在下面的图片中看到这一点,每个橙色的圆圈代表一个文档。

正如我们所说,有些文件可能有几个主题,一个例子就是上图中计算机科学和生物学之间的文件。例如,如果该文件是关于生物技术的,这是有可能的。在概率论和统计学中,这种分布被称为Dirichlet分布,它由参数 \(alpha\)控制。

举例来说,如果参数(\alpha=1\)表示样本比较均匀地分布在空间上,如果参数(\alpha>1\)表示样本聚集在中间,如果参数(\alpha<1\)表示样本趋向于角落。另外,参数\(alpha\)通常是一个\(k\)维矢量,其中每个分量对应于每个角落或我们案例中的主题。我们可以在下面的图片中观察到这种行为。

接下来,如果我们考虑上面提到的关于生物技术的文件,它可能由50%的计算机科学、45%的生物学和5%的物理学组成。一般来说,我们可以把文档中的这种主题分布定义为参数为 \theta\ 的多叉分布。因此,参数(\theta\)是一个概率的(k\)维向量,其总和必须为1。之后,我们从多叉分布 \(N\)中抽取不同的主题。为了理解这个过程,我们可以观察下面的图片。

抽取字词

在选择了 \(N\)不同的主题之后,我们还需要对单词进行抽样。为了这个目的,我们还将使用Dirichlet分布和多指标分布。 第二种Dirichlet分布,用参数 \(\beta\)定义,在词的空间中映射主题。例如,一个三角形的角,在4维的情况下是四面体,或者对于 \(n\)维来说是单数,现在可能是诸如算法、遗传、速度或类似的词。

现在,我们将主题放入这个空间,而不是文件。例如,计算机科学的主题更接近于算法这个词,而不是遗传这个词,这个主题的多项式分布可能包括75%的算法、15%的遗传和10%的速度。同样地,我们可以将生物学主题的多项式分布定义为10%的算法、85%的遗传和5%的速度,将物理学主题定义为20%的算法、5%的遗传和75%的速度。

另外,在为话题定义了多项分布后,我们将从这些分布中抽取词语,对应于第一步中抽取的每个话题。这个过程可以通过下面的图示更容易理解。

例如,如果我们考虑一个代表计算机科学主题的蓝色圆圈,这个主题有它自己的分布,由我们使用的词。接下来,按照同样的抽样话题顺序,对于每个话题,我们根据话题分布选择一个词。例如,上面的第一个主题是计算机科学,基于概率0.75,0.15和0.1的单词算法,遗传,和速度,我们选择了单词算法。按照这个过程,LDA创建了一个新的文件。

LDA的定义

这样,对于每个输入文件,我们都会创建一个新的文件。毕竟,我们要最大限度地提高创建相同文档的概率,上述整个过程在数学上定义为

P(boldsymbolW,boldsymbolZ,boldsymboltheta,boldsymbolphi;alpha,beta)=prod_i=1MP(theta_j;αprod_i=1KPphi;betaprod_t=1NPZ_j,tθ_jPW_j,tphiz_j,t,P(\\boldsymbol{W}, \\boldsymbol{Z}, \\boldsymbol{theta}, \\boldsymbol{phi}; \\alpha, \\beta) = \\prod\_{i = 1}^{M}P(\\theta\_{j};\\α)\\prod\_{i = 1}^{K}P(\\phi; \\beta)\\prod\_{t = 1}^{N}P(Z\_{j, t} | θ\_{j})P(W\_{j, t} | \\phi z\_{j, t}),

\(\alpha\)和\(\beta\)定义了Dirichlet分布,\(\theta\)和\(phi\)定义了多叉分布,\(\boldsymbol{Z}\)是所有文档中所有词的主题向量。\(\boldsymbol{W}\)是所有文件中所有单词的向量, \(M\)文件数量, \(K\)主题数量和 \(N\)单词数量。
训练或最大化概率的整个过程可以用吉布斯抽样来完成,其总体思路是使每个文档和每个词都尽可能地单色化。基本上,这意味着我们希望每个文档有尽可能少的文章,每个词属于尽可能少的主题。

例子

在这个例子中,我们将使用20个新闻组的文本数据集。20个新闻组数据集包括大约12000个关于20个主题的新闻组帖子。让我们加载数据和所有需要的包。

import pandas as pd
import re
import numpy as np
from sklearn.datasets import fetch_20newsgroups
import nltk
from nltk.stem import WordNetLemmatizer
from nltk.corpus import stopwords

from gensim import corpora, models
from gensim.models.ldamulticore import LdaMulticore
from gensim.models.coherencemodel import CoherenceModel
import pyLDAvis.gensim

newsgroups_train = fetch_20newsgroups(subset='train')

df = pd.DataFrame({'post': newsgroups_train['data'], 'target': newsgroups_train['target']})
df['target_names'] = df['target'].apply(lambda t: newsgroups_train['target_names'][t])
df.head()

作为文本预处理步骤,我们将首先删除URL、HTML标签、电子邮件和非alpha字符。之后,我们将对其进行词法处理,并去除停顿词。

def remove_urls(text):
    " removes urls"
    url_pattern = re.compile(r'https?://\S+|www\.\S+')
    return url_pattern.sub(r'', text)
    
def remove_html(text):
    " removes html tags"
    html_pattern = re.compile('')
    return html_pattern.sub(r'', text)

def remove_emails(text):
    email_pattern = re.compile('\S*@\S*\s?')
    return email_pattern.sub(r'', text)

def remove_new_line(text):
    return re.sub('\s+', ' ', text)

def remove_non_alpha(text):
    return re.sub("[^A-Za-z]+", ' ', str(text))

def preprocess_text(text):
    t = remove_urls(text)
    t = remove_html(t)
    t = remove_emails(t)
    t = remove_new_line(t)
    t = remove_non_alpha(t)
    return t

def lemmatize_words(text, lemmatizer):
    return " ".join([lemmatizer.lemmatize(word) for word in text.split()])

def remove_stopwords(text, stopwords):
    return " ".join([word for word in str(text).split() if word not in stopwords])


df['post_preprocessed'] = df['post'].apply(preprocess_text).str.lower()

print('lemming...')
nltk.download('wordnet')
lemmatizer = WordNetLemmatizer()
df['post_final'] = df['post_preprocessed'].apply(lambda post: lemmatize_words(post, lemmatizer))

print('remove stopwors...')

nltk.download('stopwords')
swords = set(stopwords.words('english'))

df['post_final'] = df['post_preprocessed'].apply(lambda post: remove_stopwords(post, swords))
df.head()

接下来,我们将制作字典和语料库。同时,作为一个语料库,我们可以只使用术语频率或TF-IDF。

posts = [x.split(' ') for x in df['post_final']]
id2word = corpora.Dictionary(posts)
corpus_tf = [id2word.doc2bow(text) for text in posts]
print(corpus_tf[0])

tfidf = models.TfidfModel(corpus_tf)
corpus_tfidf = tfidf[corpus_tf]
print(corpus_tfidf[0])

之后,我们用LDA模型测试这两个语料库。我们将使用一致性分数UMass来衡量它们的性能,因为更常用的CV分数可能不会给出好的结果。关于一致性分数的更多信息可以在这篇文章中找到。

为了看到每个主题的关键词,我们使用`show_topics`方法。基本上,它显示了主题指数和每个关键词的权重。

model = LdaMulticore(corpus=corpus_tf,id2word = id2word, num_topics = 20,
                     alpha=.1, eta=0.1, random_state = 0)

coherence = CoherenceModel(model = model, texts = posts, dictionary = id2word, coherence = 'u_mass')

print(coherence.get_coherence())
print(model.show_topics())

可视化主题-关键词

在我们建立了LDA模型之后,下一步是使用pyLDAvis包将结果可视化。这是一个显示话题和关键词的交互式图表。

在左边,主题用圆圈表示。圆圈越大,说明该话题越普遍。一个好的主题模型会有大的、不重叠的圆圈,散布在整个图表中,而不是集中在一个象限。

在右边,我们可以观察到所选主题中最相关的关键词。

lda_display = pyLDAvis.gensim.prepare(model, corpus_tf, id2word, sort_topics = False)
pyLDAvis.display(lda_display)

最后,主导性话题和该话题的贡献率被提取出来。

data_dict = {'dominant_topic':[], 'perc_contribution':[], 'topic_keywords':[]}

for i, row in enumerate(model[corpus_tf]):
    #print(i)
    row = sorted(row, key=lambda x: x[1], reverse=True)
    #print(row)
    for j, (topic_num, prop_topic) in enumerate(row):
        wp = model.show_topic(topic_num)
        topic_keywords = ", ".join([word for word, prop in wp])
        data_dict['dominant_topic'].append(int(topic_num))
        data_dict['perc_contribution'].append(round(prop_topic, 3))
        data_dict['topic_keywords'].append(topic_keywords)
        #print(topic_keywords)
        break

df_topics = pd.DataFrame(data_dict)

contents = pd.Series(posts)

df_topics['post'] = df['post']
df_topics.head()

进一步的工作

本教程仅代表话题建模和LDA算法的理论背景和基线模型。因此,提出的结果可能不是最好的,还有很多空间可以改进。例如,我们可以尝试使用不同的文本预处理方法,调整LDA超参数,如话题数量、α、β,尝试不同的一致性指标,等等。