第 6 章 分析文本数据

在这一章,我们将介绍以下主题:

  • 用标记解析的方法预处理数据

  • 提取文本数据的词干

  • 用词形还原的方法还原文本的基本形式

  • 用分块的方法划分文本

  • 创建词袋模型(bag-of-words model)

  • 创建文本分类器

  • 识别性别

  • 分析句子的情感

  • 用主题建模识别文本的模式

6.1 简介

文本分析和NLP(Natural Language Processing,自然语言处理)是现代人工智能系统不可分割的一部分。计算机擅长于用有限的多样性来理解结构死板的数据。然而,当我们用计算机处理非结构化的自由文本时,就会变得很困难。开发NLP应用程序是一种挑战,因为计算机很难理解隐含的概念,而且语言交流方式也有很多细微的差异。这些差异的形式可以是方言、语境、俚语等。

为了解决这个问题,基于机器学习的NLP应运而生。这些算法检测文本数据的模式,以便可以从中得到了解。人工智能公司大量地使用了NLP和文本分析来推送相关结果。NLP最常用的领域包括搜索引擎、情感分析、主题建模、词性标注、实体识别等。NLP的目标是开发出一组算法,以便可以用简单的英文和计算机交流。如果这一目标实现,将不再需要程序设计语言来命令计算机执行指令。这一章将主要介绍文本分析,以及如何从文本数据中提取有意义的信息。我们将大量用到Python中的NLTK(Natural Language Toolkit)包。在进行接下来的学习之前,先确保你已经安装了NLTK,安装步骤可以参考http://www.nltk.org/install.html。你还需要安装NLTK数据,这些数据中包含很多语料和训练模型,这也是文本分析不可分割的部分,安装步骤可以参考http://www.nltk.org/data.html

6.2 用标记解析的方法预处理数据

标记解析是将文本分割成一组有意义的片段的过程。这些片段被称作标记,例如可以将一段文字分割成单词或者句子。根据手头的任务需要,可以自定义将输入的文本分割成有意义的标记。接下来介绍如何实现这样的标记解析。

详细步骤

(1) 创建一个Python文件,在文件中加入以下内容。这里定义了一些用于分析的示例文本:

  1. text = "Are you curious about tokenization? Let's see how it works! We need to analyze a couple of sentences with punctuations to see it in action."

(2) 接下来做句子解析。NLTK提供了一个句子解析器,首先加载该模块:

  1. # 对句子进行解析
  2. from nltk.tokenize import sent_tokenize

(3) 对输入文本运行句子解析器,提取出标记:

  1. sent_tokenize_list = sent_tokenize(text)

(4) 打印出句子解析结果列表:

  1. print "\nSentence tokenizer:"
  2. print sent_tokenize_list

(5) 单词解析在NLP中是非常常用的。NLTK附带了几个不同的单词解析器。先从最基本的单词解析器开始:

  1. # 建立一个新的单词解析器
  2. from nltk.tokenize import word_tokenize
  3. print "\nWord tokenizer:"
  4. print word_tokenize(text)

(6) NLTK中另外一个可以使用的单词解析器叫PunktWord,它以标点符号分割文本,如果是单词中的标点符号,则保留不做分割:

  1. # 创建一个带标点的单词解析器
  2. from nltk.tokenize import PunktWordTokenizer
  3. punkt_word_tokenizer = PunktWordTokenizer()
  4. print "\nPunkt word tokenizer:"
  5. print punkt_word_tokenizer.tokenize(text)

(7) 如果需要将标点符号保留到不同的句子标记中,可以用WordPunct标记解析器:

  1. # 创建一个新的WordPunct标记解析器
  2. from nltk.tokenize import WordPunctTokenizer
  3. word_punct_tokenizer = WordPunctTokenizer()
  4. print "\nWord punct tokenizer:"
  5. print word_punct_tokenizer.tokenize(text)

(8) 全部代码已经在tokenizer.py文件中给出。运行该代码,可以在命令行工具中看到如图6-1所示的输出结果。

{%}

图 6-1

6.3 提取文本数据的词干

处理文本文档时,可能会碰到单词的不同形式。以单词“play”为例,这个单词可能以各种形式出现,例如“play”“plays”“player”“playing”等,这些是具有同样含义的单词家族。在文本分析中,提取这些单词的原形非常有用,它有助于我们提取一些统计信息来分析整个文本。词干提取的目标是将不同词形的单词都变成其原形。词干提取使用启发式处理方法截取单词的尾部,以提取单词的原形。接下来介绍如何在Python中完成词干提取。

6.3.1 详细步骤

(1) 创建一个Python文件,导入以下程序包:

  1. from nltk.stem.porter import PorterStemmer
  2. from nltk.stem.lancaster import LancasterStemmer
  3. from nltk.stem.snowball import SnowballStemmer

(2) 定义一些单词来进行词干提取:

  1. words = ['table', 'probably', 'wolves', 'playing', 'is','dog', 'the', 'beaches', 'grounded', 'dreamt', 'envision']

(3) 定义一个稍后会用到的词干提取器列表:

  1. # 对比不同的词干提取算法
  2. stemmers = ['PORTER', 'LANCASTER', 'SNOWBALL']

(4) 初始化3个词干提取器对象:

  1. stemmer_porter = PorterStemmer()
  2. stemmer_lancaster = LancasterStemmer()
  3. stemmer_snowball = SnowballStemmer('english')

(5) 为了以整齐的表格形式将输出数据打印出来,需要设定其正确的格式:

  1. formatted_row = '{:>16}' (len(stemmers) + 1)
  2. print '
  3. ', formatted_row.format('WORD', stemmers), '\n'

(6) 迭代列表中的单词,并用3个词干提取器进行词干提取:

  1. for word in words:
  2. stemmed_words = [stemmer_porter.stem(word),
  3. stemmer_lancaster.stem(word), stemmer_snowball.stem(word)]
  4. print formatted_row.format(word, *stemmed_words)

(7) 全部代码已经在stemmer.py文件中给出。运行该代码,可以在命令行工具中看到如图6-2所示的输出结果。从图6-2中可以看出,Lancaster词干提取器的输出结果与其他词干提取器的输出结果不同。

{%}

图 6-2

6.3.2 工作原理

以上3种词干提取算法的本质目标都是提取出词干,消除词形的影响。它们的不同之处在于操作的严格程度不同。观察输出结果可以看到,Lancaster词干提取器比其他两个词干提取器更严格。就严格程度来说,Porter词干提取器是最宽松的,而Lancaster词干提取器是最严格的。从Lancaster词干提取器得到的词干往往比较模糊,难以理解。Lancaster词干提取算法的速度很快,但是它会减少单词的很大部分,因此通常会选择Snowball词干提取器。

6.4 用词形还原的方法还原文本的基本形式

词形还原的目标也是将单词转换为其原形,但它是一个更结构化的方法。在前一节中,可以看到用词根还原得到的单词原形并不是有意义的,例如单词“wolves”被还原成“wolv”,还原出的单词根本不是一个真实的单词。词形还原通过对单词进行词汇和语法分析来实现,因此可以圆满解决这一问题。词形还原变形词的结尾,例如“ing”或“ed”,然后返回单词的原形形式,这个原形也就是词根(lemma)。如果对单词“wolves”做词根还原,可以得到“wolf”的输出。输出结果取决于标记是一个动词还是一个名词。下面看看如何做词形还原。

详细步骤

(1) 创建一个Python文件,导入以下程序包:

  1. from nltk.stem import WordNetLemmatizer

(2) 定义一组单词来进行词形还原:

  1. words = ['table', 'probably', 'wolves', 'playing', 'is',
  2. 'dog', 'the', 'beaches', 'grounded', 'dreamt', 'envision']

(3) 比较两个不同的词形还原器,NOUN词形还原器和VERB词形还原器:

  1. # 对比不同的词形还原器
  2. lemmatizers = ['NOUN LEMMATIZER', 'VERB LEMMATIZER']

(4) 基于WordNet词形还原器创建一个对象:

  1. lemmatizer_wordnet = WordNetLemmatizer()

(5) 为了以整齐的表格形式将输出数据打印出来,需要设定其正确的格式:

  1. formatted_row = '{:>24}' (len(lemmatizers) + 1)
  2. print '
  3. ', formatted_row.format('WORD', lemmatizers), '\n'

(6) 迭代列表中的单词,并用词形还原器进行词形还原:

  1. for word in words:
  2. lemmatized_words = [lemmatizer_wordnet.lemmatize(word, pos='n'),
  3. lemmatizer_wordnet.lemmatize(word, pos='v')]
  4. print formatted_row.format(word, *lemmatized_words)

(7) 全部代码已经在lemmatizer.py文件中给出。运行该代码,可以看到如图6-3所示的输出结果。观察图中对于单词“is”的词形还原,NOUN词形还原器和VERB词形还原器的还原结果有何不同。

{%}

图 6-3

6.5 用分块的方法划分文本

分块是指基于任意随机条件将输入文本分割成块。与标记解析不同的是,分块没有条件约束,分块的结果不需要有实际意义。分块在文本分析中经常使用。当处理非常大的文本文档时,就需要将文本进行分块,以便进行下一步分析。在这一节中,将输入文本分成若干块,每块都包含固定数目的单词。

详细步骤

(1) 创建一个Python文件,导入以下程序包:

  1. import numpy as np
  2. from nltk.corpus import brown

(2) 定义一个将文本分割成块的函数。第一步是将文本按照空格划分:

  1. # 将文本分割成块
  2. def splitter(data, num_words):
  3. words = data.split(' ')
  4. output = []

(3) 初始化一些后面需要用到的变量:

  1. cur_count = 0
  2. cur_words = []

(4) 对这些单词进行迭代:

  1. for word in words:
  2. cur_words.append(word)
  3. cur_count += 1

(5) 获得的单词数量与所需的单词数量相等时,重置相应的变量:

  1. if cur_count == num_words:
  2. output.append(' '.join(cur_words))
  3. cur_words = []
  4. cur_count = 0

(6) 将块添加到输出变量列表的最后,并返回该输出变量:

  1. output.append(' '.join(cur_words) )
  2. return output

(7) 定义一个main函数,从布朗语料库(Brown corpus)加载数据。用到前10 000个单词:

  1. if __name__=='__main__':
  2. # 从布朗语料库加载数据
  3. data = ' '.join(brown.words()[:10000])

(8) 定义每块包含的单词数目:

  1. # 每块包含的单词数目
  2. num_words = 1700

(9) 定义两个相关变量:

  1. chunks = []
  2. counter = 0

(10) 对这个文本数据调用splitter函数,并将其打印输出:

  1. text_chunks = splitter(data, num_words)
  2. print "Number of text chunks =", len(text_chunks)

(11) 全部代码已经在chunking.py文件中给出。运行该代码,可以看到生成的块的数目被打印在命令行工具中,并且块的数目应该是6。

6.6 创建词袋模型

如果需要处理包含数百万单词的文本文档,需要将其转化成某种数值表示形式,以便让机器用这些数据来学习算法。这些算法需要数值数据,以便可以对这些数据进行分析,并输出有用的信息。这里需要用到词袋(bag-of-words)。词袋是从所有文档的所有单词中学习词汇的模型。学习之后,词袋通过构建文档中所有单词的直方图来对每篇文档进行建模。

6.6.1 详细步骤

(1) 创建一个Python文件,导入以下程序包:

  1. import numpy as np
  2. from nltk.corpus import brown
  3. from chunking import splitter

(2) 定义一个main函数,从布朗语料库加载输入数据:

  1. if __name__=='__main__':
  2. # 布朗语料库读取数据
  3. data = ' '.join(brown.words()[:10000])

(3) 将文本数据分成5块:

  1. # 每块包含的单词数量
  2. num_words = 2000
  3. chunks = []
  4. counter = 0
  5. text_chunks = splitter(data, num_words)

(4) 创建一个基于这些文本块的词典:

  1. for text in text_chunks:
  2. chunk = {'index': counter, 'text': text}
  3. chunks.append(chunk)
  4. counter += 1

(5) 下一步是提取一个文档-词矩阵。文档-词矩阵记录了文档中每个单词出现的频次。下面将用scikit-learn来构建这样的矩阵,因为相比于NLTK,scikit-learn有更简洁的完成这一任务的方式。导入以下程序包:

  1. # 提取文档-词矩阵
  2. from sklearn.feature_extraction.text import CountVectorizer

(6) 定义对象,并提取文档-词矩阵:

  1. vectorizer = CountVectorizer(min_df=5, max_df=.95)
  2. doc_term_matrix = vectorizer.fit_transform([chunk['text'] for chunk in chunks])

(7) 从vectorizer对象中提取词汇,并打印出:

  1. vocab = np.array(vectorizer.get_feature_names())
  2. print "\nVocabulary:"
  3. print vocab

(8) 打印文档-词矩阵:

  1. print "\nDocument term matrix:"
  2. chunk_names = ['Chunk-0', 'Chunk-1', 'Chunk-2', 'Chunk-3', 'Chunk-4']

(9) 为了打印成表格形式,可以做如下格式设置:

  1. formatted_row = '{:>12}' (len(chunk_names) + 1)
  2. print '
  3. ', formatted_row.format('Word', chunk_names), '\n'

(10) 迭代所有单词,打印每个单词出现在不同块中的次数:

  1. for word, item in zip(vocab, doc_term_matrix.T):
  2. # “item”是压缩的稀疏矩阵(csr_matrix)数据结构
  3. output = [str(x) for x in item.data]
  4. print formatted_row.format(word, *output)

(11) 全部代码已经在bag_of_words.py文件中给出。运行该代码,可以看到命令行工具中有两部分输出,第一部分输出的是词汇,如图6-4所示。

{%}

图 6-4

(12) 第二部分输出的是文档-词矩阵,这个矩阵非常大,这里仅显示前几行,如图6-5所示。

{%}

图 6-5

6.6.2 工作原理

以下面的句子为例:

  • 句子1:The brown dog is running.

  • 句子2:The black dog is in the black room.

  • 句子3: Running in the room is forbidden.

以上3句话中有以下9个唯一的单词:

  • the

  • brown

  • dog

  • is

  • running

  • black

  • in

  • room

  • forbidden

我们利用这些单词在每个句子中出现的频次将每个句子转换成直方图。每一个特征向量均是9维的,因为唯一单词个数为9:

  • 句子1: [1, 1, 1, 1, 1, 0, 0, 0, 0]

  • 句子2: [2, 0, 1, 1, 0, 2, 1, 1, 0]

  • 句子3: [0, 0, 0, 1, 1, 0, 1, 1, 1]

提取出这样的特征向量后,就可以用机器学习的算法对这些向量进行分析了。

6.7 创建文本分类器

文本分类的目的是将文本文档分为不同的类,这是NLP中非常重要的分析手段。这里将使用一种技术,它基于一种叫作tf-idf的统计数据,它表示词频-逆文档频率(term frequency—inverse document frequency)。这个统计工具有助于理解一个单词在一组文档中对某一个文档的重要性。它可以作为特征向量来做文档分类,你可以在http://www.tfidf.com中找到更多详细介绍。

6.7.1 详细步骤

(1) 创建一个Python文件,导入以下程序包:

  1. from sklearn.datasets import fetch_20newsgroups

(2) 选择一个类型列表,并且用词典映射的方式定义。这些类型是加载的新闻组数据集的一部分:

  1. category_map = {'misc.forsale': 'Sales', 'rec.motorcycles': 'Motorcycles',
  2. 'rec.sport.baseball': 'Baseball', 'sci.crypt': 'Cryptography',
  3. 'sci.space': 'Space'}

(3) 基于刚刚定义的类型加载训练数据:

  1. training_data = fetch_20newsgroups(subset='train', categories=category_map.keys(), shuffle=True, random_state=7)

(4) 导入特征提取器:

  1. # 特征提取
  2. from sklearn.feature_extraction.text import CountVectorizer

(5) 用训练数据提取特征:

  1. vectorizer = CountVectorizer()
  2. X_train_termcounts = vectorizer.fit_transform(training_data.data)
  3. print "\nDimensions of training data:", X_train_termcounts.shape

(6) 接下来训练分类器。这里将用到多项式朴素贝叶斯(Multinomial Naive Bayes)分类器:

  1. # 训练分类器
  2. from sklearn.naive_bayes import MultinomialNB
  3. from sklearn.feature_extraction.text import TfidfTransformer

(7) 定义一些随机输入的句子:

  1. input_data = [
  2. "The curveballs of right handed pitchers tend to curve to the left",
  3. "Caesar cipher is an ancient form of encryption",
  4. "This two-wheeler is really good on slippery roads"
  5. ]

(8) 定义tf-idf变换器对象,并训练:

  1. # tf-idf变换器
  2. tfidf_transformer = TfidfTransformer()
  3. X_train_tfidf = tfidf_transformer.fit_transform(X_train_ termcounts)

(9) 得到特征向量后,用该数据训练多项式朴素贝叶斯分类器:

  1. # 多项式朴素贝叶斯分类器
  2. classifier = MultinomialNB().fit(X_train_tfidf, training_data. target)

(10) 用词频统计转换输入数据:

  1. X_input_termcounts = vectorizer.transform(input_data)

(11) 用tf-idf变换器变换输入数据:

  1. X_input_tfidf = tfidf_transformer.transform(X_input_termcounts)

(12) 用训练过的分类器预测这些输入句子的输出类型:

  1. # 预测输出类型
  2. predicted_categories = classifier.predict(X_input_tfidf)

(13) 打印输出:

  1. # 打印输出
  2. for sentence, category in zip(input_data, predicted_categories):
  3. print '\nInput:', sentence, '\nPredicted category:', \
  4. category_map[training_data.target_names[category]]

(14) 全部代码已经在tfidf.py文件中给出。运行该代码,可以在命令行工具中看到如图6-6所示的输出结果。

{%}

图 6-6

6.7.2 工作原理

tf-idf技术常用于信息检索领域,目的是了解文档中每个单词的重要性。如果想要识别在文档中多次出现的单词,同时,像“is”和“be”这样的普通词汇并不能真正反映内容的本质,因此仅需要提取出具有实际意义的那些单词。词频越大,则表示这个词越重要,同时,如果这个词经常出现,那么这个词的词频也会增加,这两个因素互相平衡。提取出每个句子的词频,然后将其转换为特征向量,用分类器来对这些句子进行分类。

词频(The term frequency, TF)表示一个单词在给定文档中出现的频次。由于不同文档的长度不同,这些频次的直方图看起来会相差很大,因此需要将其规范化,使得这些频次可以在平等的环境下进行对比。为了实现规范化,我们用频次除以文档中所有单词的个数。逆文档频率(inverse document frequency,IDF)表示给定单词的重要性。当需要计算词频时,假定所有单词是同等重要的。为了抗衡哪些经常出现的单词的频率,需要用一个系数将其权重变小。我们需要计算文档的总数目除以该单词出现的文档数目的比值。逆文档频率对该比值取对数值。

例如,“is”或“the”等简单的单词在各个文档中均大量出现,但是这并不意味着要用这些词作为该篇文档的特征。同时,如果一个单词仅出现一次,那这个单词也不是十分有意义。因此,我们寻找的是那些出现了一定次数,但不太频繁以至于变成噪声的单词。tf-idf值的计算可以挑选出符合要求的单词,并且可以用于分类文档。搜索引擎经常用tf-idf工具来对搜索结果进行相关度排序。

6.8 识别性别

在NLP中,通过姓名识别性别是一个很有趣的任务。这里将用到启发式方法,即姓名的最后几个字符可以界定性别特征。例如,如果某一个名字以“la”结尾,那么它很有可能是一个女性名字,如“Angela”或者“Layla”。另外,如果一个名字以“im”结尾,那么它很有可能是一个男性名字,例如“Tim”或者“Jim”。确定需要用到几个字符来确定性别后,可以来做这个实验。接下来介绍如何识别性别。

详细步骤

(1) 创建一个Python文件,导入以下程序包:

  1. import random
  2. from nltk.corpus import names
  3. from nltk import NaiveBayesClassifier
  4. from nltk.classify import accuracy as nltk_accuracy

(2) 定义一个用于提取输入单词的特征的函数:

  1. # 提取输入单词的特征
  2. def gender_features(word, num_letters=2):
  3. return {'feature': word[-num_letters:].lower()}

(3) 定义main函数,需要一些带标记的训练数据:

  1. if __name__=='__main__':
  2. # 提取标记名称
  3. labeled_names = ([(name, 'male') for name in names.words('male.txt')] +
  4. [(name, 'female') for name in names.words('female.txt')])

(4) 设置随机生成数的种子值,并混合搅乱训练数据:

  1. random.seed(7)
  2. random.shuffle(labeled_names)

(5) 定义一些输入的姓名:

  1. input_names = ['Leonardo', 'Amy', 'Sam']

(6) 因为不知道需要多少个末尾字符,这里将这个参数设置为1~5。每次循环执行,都会截取相应大小的末尾字符个数:

  1. # 搜索参数空间
  2. for i in range(1, 5):
  3. print '\nNumber of letters:', i
  4. featuresets = [(gender_features(n, i), gender) for (n, gender) in labeled_names]

(7) 将数据分为训练数据集和测试数据集:

  1. train_set, test_set = featuresets[500:], featuresets[:500]

(8) 用朴素贝叶斯分类器做分类:

  1. classifier = NaiveBayesClassifier.train(train_set)

(9) 用参数空间的每一个值评价分类器的效果

  1. # 打印分类器的准确性
  2. print 'Accuracy ==>', str(100 * nltk_accuracy(classifier, test_set)) + str('%')
  3. # 为新输入预测输出结果
  4. for name in input_names:
  5. print name, '==>', classifier.classify(gender_features(name, i))

(10) 全部代码已经在gender_identification.py文件中给出。运行该代码,可以看到命令行工具中如图6-7所示的输出结果。

{%}

图 6-7

6.9 分析句子的情感

情感分析是NLP最受欢迎的应用之一。情感分析是指确定一段给定的文本是积极还是消极的过程。有一些场景中,我们还会将“中性”作为第三个选项。情感分析常用于发现人们对于一个特定主题的看法。情感分析用于分析很多场景中用户的情绪,如营销活动、社交媒体、电子商务客户等。

6.9.1 详细步骤

(1) 创建一个Python文件,导入以下程序包:

  1. import nltk.classify.util
  2. from nltk.classify import NaiveBayesClassifier
  3. from nltk.corpus import movie_reviews

(2) 定义一个用于提取特征的函数:

  1. def extract_features(word_list):
  2. return dict([(word, True) for word in word_list])

(3) 我们需要训练数据,这里将用NLTK提供的电影评论数据:

  1. if __name__=='__main__':
  2. # 加载积极与消极评论
  3. positive_fileids = movie_reviews.fileids('pos')
  4. negative_fileids = movie_reviews.fileids('neg')

(4) 将这些评论数据分成积极评论和消极评论:

  1. features_positive = [(extract_features(movie_reviews.words(fileids=[f])),
  2. 'Positive') for f in positive_fileids]
  3. features_negative = [(extract_features(movie_reviews.words(fileids=[f])),
  4. 'Negative') for f in negative_fileids]

(5) 将数据分成训练数据集和测试数据集:

  1. # 分成训练数据集(80%)和测试数据集(20%)
  2. threshold_factor = 0.8
  3. threshold_positive = int(threshold_factor len(features_positive))
  4. threshold_negative = int(threshold_factor len(features_negative))

(6) 提取特征:

  1. features_train = features_positive[:threshold_positive] + features_negative[:threshold_negative]
  2. features_test = features_positive[threshold_positive:] + features_negative[threshold_negative:]
  3. print "\nNumber of training datapoints:", len(features_train)
  4. print "Number of test datapoints:", len(features_test)

(7) 我们将用到朴素贝叶斯分类器。定义该对象并训练:

  1. # 训练朴素贝叶斯分类器
  2. classifier = NaiveBayesClassifier.train(features_train)
  3. print "\nAccuracy of the classifier:", nltk.classify.util.accuracy(classifier, features_test)

(8) 该分类器对象包含分析过程中获得的最有信息量的单词。通过这些单词可以判定哪些可以被归类为积极评论,哪些可以被归类为消极评论。将其打印出来:

  1. print "\nTop 10 most informative words:"
  2. for item in classifier.most_informative_features()[:10]:
  3. print item[0]

(9) 生成一些随机输入句子:

  1. # 输入一些简单的评论
  2. input_reviews = [
  3. "It is an amazing movie",
  4. "This is a dull movie. I would never recommend it to anyone.",
  5. "The cinematography is pretty great in this movie",
  6. "The direction was terrible and the story was all over the place"
  7. ]

(10) 在这些输入句子上运行分类器,获得预测结果:

  1. print "\nPredictions:"
  2. for review in input_reviews:
  3. print "\nReview:", review
  4. probdist = classifier.prob_classify(extract_features(review.split()))
  5. pred_sentiment = probdist.max()

(11) 打印输出:

  1. print "Predicted sentiment:", pred_sentiment
  2. print "Probability:", round(probdist.prob(pred_sentiment), 2)

(12) 全部代码已经在sentiment_analysis.py文件中给出。运行该代码,可以看到命令行工具中的输出结果包括3个部分。第一个部分是准确度,如图6-8所示。

第 6 章 分析文本数据 - 图8

图 6-8

(13) 第二部分输出最有信息量的单词,如图6-9所示。

{%}

图 6-9

(14) 第三部分是对输入句子的预测列表,如图6-10所示。

{%}

图 6-10

6.9.2 工作原理

这个例子将用NLTK的朴素贝叶斯分类器进行分类。在特征提取函数中,我们基本上提取了所有的唯一单词。然而,NLTK分类器需要的数据是用字典的格式存放的,因此这里用到了字典格式,便于NLTK分类器对象读取该数据。

将数据分成训练数据集和测试数据集后,可以训练该分类器,以便将句子分为积极和消极。如果查看最有信息量的那些单词,可以看到例如单词“outstanding”表示积极评论,而“insulting”表示消极评论。这是非常有趣的信息,因为它告诉我们单词可以用来表示情绪。

6.10 用主题建模识别文本的模式

主题建模是指识别文本数据隐藏模式的过程,其目的是发现一组文档的隐藏主题结构。主题建模可以更好地组织文档,以便对这些文档进行分析。主题建模是NLP研究的一个活跃领域,你可以在http://www.cs.columbia.edu/~blei/topicmodeling.html查看更多介绍。本节将用到gensim库,在进行接下来的学习之前,请确保你已经安装了该库,具体安装步骤可以参考https://radimrehurek.com/gensim/install.html

6.10.1 详细步骤

(1) 创建一个Python文件,导入以下程序包:

  1. from nltk.tokenize import RegexpTokenizer
  2. from nltk.stem.snowball import SnowballStemmer
  3. from gensim import models, corpora
  4. from nltk.corpus import stopwords

(2) 定义一个函数来加载输入数据,本例将用到本书提供的data_topic_modeling.txt文本文件:

  1. # 加载输入数据
  2. def load_data(input_file):
  3. data = []
  4. with open(input_file, 'r') as f:
  5. for line in f.readlines():
  6. data.append(line[:-1])
  7. return data

(3) 定义一个预处理文本的类。这个预处理器处理相应的对象,并从输入文本中提取相关的特征:

  1. # 类预处理文本
  2. class Preprocessor(object):
  3. # 对各种操作进行初始化
  4. def __init__(self):
  5. # 创建正则表达式解析器
  6. self.tokenizer = RegexpTokenizer(r'\w+')

(4) 我们需要一个停用词列表,在分析过程中可以将这些停用词排除。这些停用词都是常用词,例如“in”“the”“is”等:

  1. # 获取停用词列表
  2. self.stop_words_english = stopwords.words('english')

(5) 定义一个Snowball词干提取器:

  1. # 创建Snowball词干提取器
  2. self.stemmer = SnowballStemmer('english')

(6) 定义一个处理函数,负责标记解析、停用词去除和词干还原:

  1. # 标记解析、移除停用词、词干提取
  2. def process(self, input_text):
  3. # 标记解析
  4. tokens = self.tokenizer.tokenize(input_text.lower())

(7) 从文本中去除停用词:

  1. # 移除停用词
  2. tokens_stopwords = [x for x in tokens if not x in self.stop_words_english]

(8) 对标记做词干提取:

  1. # 词干提取
  2. tokens_stemmed = [self.stemmer.stem(x) for x in tokens_stopwords]

(9) 返回处理后的标记:

  1. return tokens_stemmed

(10) 定义一个main函数。从文本文件中加载输入数据:

  1. if __name__=='__main__':
  2. # 包含输入数据的文件
  3. input_file = 'data_topic_modeling.txt'
  4. # 加载数据
  5. data = load_data(input_file)

(11) 用我们定义的类定义一个对象:

  1. # 创建预处理对象
  2. preprocessor = Preprocessor()

(12) 处理文件中的文本,并提取处理好的标记:

  1. # 创建一组经过预处理的文档
  2. processed_tokens = [preprocessor.process(x) for x in data]

(13) 创建一个基于标记文档的词典,用于主题建模:

  1. # 创建基于标记文档的词典
  2. dict_tokens = corpora.Dictionary(processed_tokens)

(14) 用处理后的标记创建一个文档-词矩阵:

  1. # 创建文档-词矩阵
  2. corpus = [dict_tokens.doc2bow(text) for text in processed_tokens]

(15) 假定文本可以分成两个主题。我们将用隐含狄利克雷分布(Latent Dirichlet Allocation,LDA)做主题建模。定义相关参数并初始化LDA模型对象:

  1. # 基于刚刚创建的语料库生成LDA模型
  2. num_topics = 2
  3. num_words = 4
  4. ldamodel = models.ldamodel.LdaModel(corpus,
  5. num_topics=num_topics, id2word=dict_tokens, passes=25)

(16) 识别出两个主题后,可以看到它是如何将两个主题分开来看的:

  1. print "\nMost contributing words to the topics:"
  2. for item in ldamodel.print_topics(num_topics=num_topics, num_words=num_words):
  3. print "\nTopic", item[0], "==>", item[1]

(17) 全部代码已经在topic_modeling.py文件中给出。运行该代码,可以看到命令行工具中的输出结果如图6-11所示。

{%}

图 6-11

6.10.2 工作原理

主题建模通过识别文档中最有意义、最能表征主题的词来实现主题分类。这些单词往往可以确定主题的内容。之所以使用正则表达式(regular expression)标记器,是因为只需要那些没有标点或其他标记的单词。停用词去除是另一个非常重要的步骤,因为停用词去除可以减小一些常用词(例如“is”和“the”)的噪声干扰。之后,需要对单词做词干提取,以获得其原形。以上所有步骤均被打包在一个文本分析工具的预处理模块中。在本例的代码中就是这样实现的!

本例用到了隐含狄利克雷分布技术来构建主题模型。隐含狄利克雷分布将文档表示成不同主题的混合,这些主题可以“吐出”单词。这些“吐出”的单词是有一定的概率的。隐含狄利克雷分布的目标是找到这些主题。隐含狄利克雷分布是一个生成主题的模型,该模型试图找到所有主题,而所有主题又负责生成给定主题的文档。你可以在http://blog.echen.me/2011/08/22/introduction-to-latent-dirichlet-allocation中找到更多详细介绍。

你可以在输出结果中看到,诸如单词“talent”和“train”表示运动主题,而单词“encrypt”表示密码主题。这里仅仅试验了非常小的文本文件,在这样小的文本文件中,有一些单词可能看起来不那么相关。显然,如果你用更大的数据集来运行该程序,精确度会更高。