1. 自然语言处理简介
自然语言处理 (Natural Language Processing
, NLP
) 是人工智能领域最火热的研究方向之一,NLP
为计算机真正理解人类语言提供了基础。NLP
已成为现代计算机程序系统的重要组成部分,广泛用于搜索引擎、语音助手、文档处理等应用中。机器可以很好地处理结构化数据,但在处理非结构化的文本时,就变得相对困难了。NLP
的目标是开发使计算机能够理解非结构化文本及语言的算法。
处理非结构化自然语言最具挑战性的事情之一就是语言存在多变复杂的上下文,上下文在如何理解语言时起着非常重要的作用。虽然,人类善于理解语言,但仍尚不清楚人类为何能够非常容易地理解语言。即使没有明确是上下文,我们也可以使用过去的知识和经验来理解对话。
为了解决这一问题,研究人员使用机器学习方法开发各种算法模型,利用大量的文本语料库,然后根据这些数据训练算法以执行各种任务,例如文本分类、情绪分析和主题建模等。简单而言,NLP
算法经过训练以检测输入文本数据中的模式并从中获得所需信息。
2. 自然语言处理相关库
2.1 NLTK
NLTK
是构建 Python
程序以处理自然语言的库。它为 50 多个语料库和词汇资源(如 WordNet
)提供了易于使用的接口,以及一套用于分类、分词、词干、标记、解析和语义推理的文本处理库、工业级 NLP
库的包装器,可以使用以下命令安装:
pip install nltk
安装完成后,可以使用以下语句下载相关数据:
import nltk
nltk.download()
2.2 Gensim
Gensim
能够将文档根据 TF-IDF
,LDA
,LSI
等模型转换成向量模式,Gensim
同样还实现了 word2vec
,能够将单词转换为词向量,可以用于处理大型语料库,使用以下命令进行安装:
pip install gensim
2.3 Pattern
Pattern
是一个多用途的Python库,可以用于不同的任务,如分词、情感分析等,同时它的语法简单易懂,对需要处理文本数据的开发人员而言加油良好的学习曲线,使用以下命令进行安装:
pip install pattern
3. 自然语言处理基础
本节中,我们将介绍用于分析文本和构建 NLP
应用程序的基础概念。
3.1 文本分词
当我们处理文本时,我们需要将其分解成更小的部分进行分析。为此,需要使用分词 (Tokenizing
) 技术,分词是将文本划分为一组片段(例如单词或句子)的过程,分离出的每一单词或句子称为 token
,根据需要,我们可以不同分词技术将文本分成多个 token
,接下来,我们使用 NLTK
对输入文本进行分词。首先,导入以下包:
from nltk.tokenize import sent_tokenize, word_tokenize, WordPunctTokenizer
定义将用于分词的输入文本:
input_text = "If the implementation is hard to explain, it's a bad idea. \If the implementation is easy to explain, it may be a good idea."
将输入文本划分为句子 token
:
print("Sentence tokenizer:")
print(sent_tokenize(input_text))
将输入文本划分为单词 token
:
print("Word tokenizer:")
print(word_tokenize(input_text))
使用 WordPunct
分词器将输入文本划分为单词 token
:
print("Word punct tokenizer:")
print(WordPunctTokenizer().tokenize(input_text))
运行代码,得到的输出结果如下:
3.2 词干提取
文本数据通常包括大量变化,处理文本需要能够处理大量变化的文本数据。例如,我们需要处理同一个词的不同形式,并使计算机能够理解这些不同的词具有相同的基本形式。如 swim
这个词可以出现多种形式,包括:swims
、swam
、swum
、swiming
等,这组词具有相似的含义,将不同形式的单词转换为其基本形式的过程称为词干提取 (stemming
),词干提取是一种产生词根或基本词的形态转换方法。
在分析文本时,提取这些词根非常有用,这样做可以从输入文本中提取有用的统计数据。词干提取器 (stemmer
) 的目标是将单词从它们的不同形式转换为共同的基本形式,简单的词干提取是一个启发式过程,通过截断单词的末端来提取它们的基本形式。接下来,我们使用 NLTK
进行词干提取。
首先,导入以下包:
from nltk.stem.porter import PorterStemmer
from nltk.stem.lancaster import LancasterStemmer
from nltk.stem.snowball import SnowballStemmer
定义一些输入词:
input_words = ['are', 'be', 'begins', 'reading', 'swims', 'goes', 'done', 'had', 'reduced', 'horse', 'normalize', 'worked','terribly', 'move','operation', 'noisy', 'capital', 'swam']
实例化 Porter
、Lancaster
和 Snowball
词干提取器对象:
porter = PorterStemmer()
lancaster = LancasterStemmer()
snowball = SnowballStemmer('english')
为输出显示创建一个名称列表并相应地格式化输出文本:
stemmer_names = ['PORTER', 'LANCASTER', 'SNOWBALL']
formatted_text = '{:>20}' * (len(stemmer_names) + 1)
print('\n', formatted_text.format('INPUT WORD', *stemmer_names), '\n', '='*90)
遍历单词并使用三个词干提取器获取这些单词的词干:
for word in input_words:output = [word, porter.stem(word), lancaster.stem(word), snowball.stem(word)]print(formatted_text.format(*output))
运行代码,输出结果如下所示:
从上图可以看出,三种词干提取算法基本上能够实现目标,它们之间的区别在于词干提取的严格程度。Porter
词干提取器是最不严格的,而 Lancaster
是最严格的,仔细观察输出,可以看到它们之间的差异。当输入单词为 terribly
或 operation
之类的词时,词干提取得到的结果会有所不同。从 Lancaster
词干提取器获得的词干输出较为混乱,因为它会在最大程度上减少单词。通常情况下我们可以选择 Snowball
词干提取器,因为它可以在速度和严格性之间取得良好权衡。
3.3 词形还原
词形还原 (Lemmatization
) 是另一种将单词简化为基本形式的方法。上一节的词干提取器中获得的一些单词基本形式没有任何意义,词形还原则是将单词的不同变形形式组合在一起的过程,以便可以将它们作为单个单词进行分析。词形还原类似词干提取,但它为单词赋予了上下文意义。因此,它可以具有相似含义的单词链接到同一个单词。例如,三个词干提取器都将 leaves
的转换为 leav
,但 leav
并非真实的单词。词形还原采用更结构化的方法来解决这一问题。
词形还原过程使用单词的词汇和形态分析。它通过删除诸如 ing
或 ed
之类的后缀来获得其基本形式。如果将 leaves
这个词进行词形还原,可以得到输出 leaf
。需要注意的是,词形还原的输出取决于单词是动词还是名词。接下来,我们使用 NLTK
进行词形还原。
首先,导入以下包:
from nltk.stem import WordNetLemmatizer
定义一些输入词,我们使用与上一节中相同输入单词集,以便比较输出:
input_words = ['are', 'be', 'begins', 'reading', 'swims', 'goes', 'done', 'had', 'reduced', 'horse', 'normalize', 'worked','terribly', 'move','operation', 'noisy', 'capital', 'swam']
创建一个词形还原器对象:
lemmatizer = WordNetLemmatizer()
为输出显示创建一个名称列表并相应地格式化输出文本:
lemmatizer_names = ['NOUN LEMMATIZER', 'VERB LEMMATIZER']
formatted_text = '{:>25}' * (len(lemmatizer_names) + 1)
print('\n', formatted_text.format('INPUT WORD', *lemmatizer_names), '\n', '='*90)
遍历单词并使用名词和动词词形还原器对单词进行词形还原:
for word in input_words:output = [word, lemmatizer.lemmatize(word, pos='n'), lemmatizer.lemmatize(word, pos='v')]print(formatted_text.format(*output))
运行以上代码,可以得到如下输出结果:
可以看到,当涉及到诸如 swam
或 horse
等词时,名词词形还原器与动词词形还原器的工作方式不同,将这些输出与词干提取器输出进行比较,可以发现它们之间的差异。词形还原器得到的输出都是有意义的,而词干提取器的输出可能有意义,也可能没有意义。
需要注意的是,执行以上程序,必须首先执行以下代码:
import nltk
nltk.download()
否则会报类似如下错误:
During handling of the above exception, another exception occurred:Traceback (most recent call last):File "/python3.7/lib/python3.7/runpy.py", line 193, in _run_module_as_main
....File "/python3.7/lib/python3.7/site-packages/nltk/data.py", line 583, in findraise LookupError(resource_not_found)
LookupError:
**********************************************************************Resource omw-1.4 not found.Please use the NLTK Downloader to obtain the resource:
3.4 文本分块
文本数据通常需要分成几部分进行进一步分析,这一过程称为分块 (Chunking
)。将文本分块的条件可能根据不同问题而有所不同,在分块期间,除了需要保证输出块具有意义外,不需要遵守任何其它约束。当我们处理大型文本文档时,将文本分块以提取有意义的信息就变得十分重要了。本节中,我们使用 NLTK
将输入文本划分为多个块。
首先,导入以下所需包:
import numpy as np
from nltk.corpus import cmudict
定义函数 chunker
用于将输入文本分块,其中第一个参数是文本,第二个参数是每个块的单词数:
def chunker(input_data, n):input_words = input_data.split(' ')output = []
遍历文本并使用输入参数将它们分块,函数返回一个列表:
cur_chunk = []count = 0for word in input_words:cur_chunk.append(word)count += 1if count == n:output.append(' '.join(cur_chunk))count, cur_chunk = 0, []output.append(' '.join(cur_chunk))return output
本节使用 cmudict
语料库读取输入数据:
input_data = ' '.join(cmudict.words())
定义每个块中的单词数 chunk_size
:
# 定义每个区块中的单词数
chunk_size = 700
将输入文本分块并显示输出:
chunks = chunker(input_data, chunk_size)
print('\nNumber of text chunks =', len(chunks), '\n')
for i, chunk in enumerate(chunks):print('Chunk', i+1, '==>', chunk[:80])
运行以上代码,可以得到以下输出结果:
我们已经学习了对文本进行分割和分块的技术,接下来,我们综合应用以上技术研究如何执行文本分析。
4. 自然语言处理应用
4.1 使用词袋模型获取单词总数
使用词袋模型进行文本分析的主要目标之一是将非结构化的文本数据转换为计算机可以识别的数值形式,以便我们使用机器学习算法对其进行进一步分析。假设我们需要分析的文本文档中包含数百万个单词的,为了分析这些文档,我们需要提取文本并将其转换为数值表示形式。
机器学习算法通常需要使用数值数据作为输入,以便可以分析数据并提取有意义的信息,这就是词袋模型的用途所在。该模型能够从文档中的所有单词中提取不同单词,并使用文档矩阵 (Term-Document Matrix
) 构建模型,将每个文档表示为一个词袋 (Bag of Words
)。我们只需要记录单词数量,而忽略语法细节和单词顺序。
接下里,我们将具体介绍文档矩阵。文档矩阵本质上是一个表格,它提供了文档中出现的各种单词的计数。因此,文本文档可以表示为各种单词的加权组合,我们可以设置阈值并选择更有意义的单词。或者说,词袋模型可以构建文档中所有单词数量的频率直方图,并用作特征向量。我们考虑以下句子:
- The teacher assigned each of the children a different task
- They worked hard to give their children a good start in life
- The task was a formidable one
在以上三个句子中,我们可以得到以下 22 个不同的词:
the
teacher
assigned
each
of
children
a
different
task
they
worked
hard
to
give
their
good
start
in
life
was
formidable
one
接下来,我们使用每个句子中的单词数量为每个句子构建一个频率直方图,每个特征向量都是 22
维的,因为我们有 22
个不同的单词:
[2, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0]
[1, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1]
使用词袋模型提取到句子的特征,即文档矩阵后,我们就可以使用机器学习算法来分析这些数据。接下来,我们使用 NLTK
构建词袋模型。
首先,导入所需包,同时我们还将重用上节中定义的分块函数 chunker
:
import numpy as np
from sklearn.feature_extraction.text import CountVectorizer
from nltk.corpus import movie_reviewsdef chunker(input_data, n):input_words = input_data.split(' ')output = []cur_chunk = []count = 0for word in input_words:cur_chunk.append(word)count += 1if count == n:output.append(' '.join(cur_chunk))count, cur_chunk = 0, []output.append(' '.join(cur_chunk))return output
从 movie_reviews
语料库中读取输入数据,为了便于观察,我们仅使用前 5400
个单词:
input_data = ' '.join(movie_reviews.words()[:5400])
定义每个块中的单词数:
# 定义每个区块中的单词数
chunk_size = 800
使用 chunker
函数将输入文本分成块,并将块转换为字典项:
text_chunks = chunker(input_data, chunk_size)
# 将块转换为字典项
chunks = []
for count, chunk in enumerate(text_chunks):d = {'index': count, 'text': chunk}chunks.append(d)
提取文档矩阵,我们每个块中获得每个单词的计数,可以使用 CountVectorizer
方法来完成这一任务,该方法接受两个输入参数,第一个参数是最小文档频率,第二个参数是最大文档频率,这里的频率是指一个词在文本中出现的次数:
count_vectorizer = CountVectorizer(min_df=7, max_df=20)
document_term_matrix = count_vectorizer.fit_transform([chunk['text'] for chunk in chunks])
使用词袋模型提取词汇表并显示,词汇表是指在上一步中提取的单词列表:
vocabulary = np.array(count_vectorizer.get_feature_names())
print("\nVocabulary:\n", vocabulary)
为了便于观察,为每个块生成一个名称:
# 为每个分块编号
chunk_names = []
for i in range(len(text_chunks)):chunk_names.append('Chunk-' + str(i+1))
打印文档矩阵:
# 打印文档矩阵
print("\nDocument term matrix:")
formatted_text = '{:>12}' * (len(chunk_names) + 1)
print('\n', formatted_text.format('Word', *chunk_names), '\n')
for word, item in zip(vocabulary, document_term_matrix.T):output = [word] + [str(freq) for freq in item.data]print(formatted_text.format(*output))
运行以上代码,可以得到以下输出结果:
文本中出现的所有单词都可以在词袋模型文档矩阵中看到,同时可以观察到每个块中的不同单词出现的次数。我们已经完成了单词的计数,接下来,就可以在此基础上根据单词的频率使用机器学习算法完成一些预测任务。
4.2 文本分类
文本类别预测可以用于预测给定文本所属的类别,文本分类经常用于对文本文档进行分类。搜索引擎经常使用此工具按相关性对搜索结果进行排序。例如,假设我们要预测给定的句子是属于体育、娱乐还是科学等。为此,我们可以构建数据语料库并训练了机器学习算法,然后用训练完成的算法推断未知数据。
为了构建文本预测模型,我们将使用词频-逆文本频率 (Term Frequency - Inverse Document Frequency
, TF-IDF
) 指标。在一组文档中,TF-IDF
指标可以帮助我们了解给定单词对于一个文件集或一个语料库中的其中一个文件的重要程度,单词的重要性随着它在文件中出现的次数成正比增加,但同时会随着它在语料库中出现的频率成反比下降。
TF-IDF
指标的第一部分是词频 (Term Frequency
, TF
),其衡量每个单词在给定文档中出现的频率,由于不同的文档有不同数量的单词,直方图中的确切数字会有所不同。为了使这些直方图具有可比性,我们需要标准化直方图。因此,我们将每个单词的计数除以给定文档中的单词总数,以获得词频。
TF-IDF
指标的第二部分是逆文本频率 (Inverse Document Frequency
, IDF
),它衡量一个词在给定文档集中对文档的独特性。当我们计算词频时,假设所有词都同等重要,但是我们不能仅仅依赖于每个单词的出现频率,因为诸如 and
、or
、the
之类的单词出现了很多。为了平衡这些常用词的频率,我们需要降低它们的权重并增加稀有词的权重,这也有助于我们识别每个文档所独有的单词。
为了计算这个统计量,我们需要计算文档数量与给定单词的比率,然后将其除以文档总数。这个比率本质上是包含给定单词的文档的分数,然后通过利用该比率计算逆文档频率。
我们可以结合词频和逆文本频率来生成特征向量对文档进行分类,这是对文本进行更深入分析以获得更深层含义的基础,例如情感分析或主题分析等。接下来,我们使用 Scikit-learn
构建文本类别预测模型。
首先,我们导入以下所需包:
from sklearn.datasets import fetch_20newsgroups
from sklearn.naive_bayes import MultinomialNB
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer
定义将用于训练数据集的类别字典映射,我们使用五个类别。此字典对象中的键使用 Scikit-learn
数据集中的名称:
# 定义类别映射
category_map = {'talk.politics.misc': 'Politics', 'rec.autos': 'Autos', 'rec.sport.hockey': 'Hockey', 'sci.electronics': 'Electronics', 'sci.med': 'Medicine'}
使用 fetch_20newsgroups
获取训练数据集:
training_data = fetch_20newsgroups(subset='train', categories=category_map.keys(), shuffle=True, random_state=5)
使用 CountVectorizer
对象提取单词计数:
count_vectorizer = CountVectorizer()
train_tc = count_vectorizer.fit_transform(training_data.data)
print("\nDimensions of training data:", train_tc.shape)
创建 TfidfTransformer
实例计算 TF-IDF
并使用数据对其进行训练:
tfidf = TfidfTransformer()
train_tfidf = tfidf.fit_transform(train_tc)
定义用于测试的样本输入语句:
# 定义测试数据
input_data = ['You can wirelessly manipulate many devices','You must be careful when driving a slippery road','Political debate helps us understand both sides', 'Players must be careful when approaching the goal post'
]
使用训练数据训练多项式贝叶斯分类器:
classifier = MultinomialNB().fit(train_tfidf, training_data.target)
使用 CountVectorizer
对象转换输入数据:
input_tc = count_vectorizer.transform(input_data)
使用 TF-IDF
转换器转换向量数据,使其可以通过推理模型得到预测结果:
input_tfidf = tfidf.transform(input_tc)
使用 TF-IDF
变换后的向量数据预测输出:
predictions = classifier.predict(input_tfidf)
打印输入测试数据中每个样本的输出类别:
for sent, category in zip(input_data, predictions):print('\nInput:', sent, '\nPredicted category:', category_map[training_data.target_names[category]])
运行以上代码,我们可以得到以下输出,可以看到预测的类别是正确的:
4.3 情感分析
情感分析是确定一段文本所表达情感的过程。例如,它可以用来确定电影评论是积极 (positive
) 的还是消极 (negative
) 的,这个简单是示例是 NLP
最流行的基础教程应用之一,根据不同的任务,我们也可以添加更多类别。情感发现模型可用于了解人们对产品、品牌的感受,可以用于分析营销活动、民意调查、购物网站上的产品评论等。接下来,我们构建情感分析模型确定观众影评是积极的还是消极的。
本节,我们使用朴素贝叶斯分类器来构建情感分析模型。首先,从文本中提取所有不同单词词汇表,NLTK
分类器需要将这些数据以字典的形式排列,以便提取这些词汇。同时,我们将文本数据划分为训练和测试数据集,使用训练数据集训练朴素贝叶斯分类器将评论分为积极和消极。之后,可以计算并显示表示积极和消极信息量最高的词。接下来,我们使用 NLTK
实现上述情感分析模型。
首先,导入以下所需包:
from nltk.corpus import movie_reviews
from nltk.classify import NaiveBayesClassifier
from nltk.classify.util import accuracy as nltk_accuracy
定义函数 extract_features
,根据输入的单词构造字典对象并返回:
def extract_features(words):return dict([(word, True) for word in words])
加载带标签的电影评论:
fileids_pos = movie_reviews.fileids('pos')
fileids_neg = movie_reviews.fileids('neg')
从电影评论中提取特征并为它们构建相应的 Positive
或 Negative
标签:
features_pos = [(extract_features(movie_reviews.words(fileids=[f])), 'Positive') for f in fileids_pos]
features_neg = [(extract_features(movie_reviews.words(fileids=[f])), 'Negative') for f in fileids_neg]
将数据集划分为训练和测试数据集,我们将 80% 用于训练和 20% 用于测试:
threshold = 0.8
num_pos = int(threshold * len(features_pos))
num_neg = int(threshold * len(features_neg))
分离训练和测试的特征向量:
features_train = features_pos[:num_pos] + features_neg[:num_neg]
features_test = features_pos[num_pos:] + features_neg[num_neg:]
打印用于训练和测试的数据样本数量:
print('\nNumber of training datapoints:', len(features_train))
print('Number of test datapoints:', len(features_test))
使用训练数据集训练 NaiveBayesClassifier
并使用 NLTK
中内置度量准确率的方法计算模型准确率:
classifier = NaiveBayesClassifier.train(features_train)
print('\nAccuracy of the classifier:', nltk_accuracy(classifier, features_test))
打印前 n
个信息量最大的单词:
n = 15
print('\nTop ' + str(n) + ' most informative words:')
for i, item in enumerate(classifier.most_informative_features()):print(str(i+1) + '. ' + item[0])if i == n - 1:break
定义用于测试的输入文本数据:
input_reviews = ['The costumes in this film are very nice', 'The story was terrible, and I think the character was very weak','The film director is said to be wonderful', ' This is such a stupid movie. I wont recommend it to anyone'
]
遍历样本数据并预测输出:
print("\nMovie review predictions:")
for review in input_reviews:print("\nReview:", review)
计算每个输入样本的属于 Positive
和 Negative
的类别概率:
probabilities = classifier.prob_classify(extract_features(review.split()))
选择概率预测中的最大值:
predicted_sentiment = probabilities.max()
打印预测的输出类别( Positive
或 Negative
):
print("Predicted sentiment:", predicted_sentiment)print("Probability:", round(probabilities.prob(predicted_sentiment), 2))
执行以上代码,可以看到如下输出,可以看出模型在测试数据集上的预测是正确的:
总结
在本节中,我们了解了自然语言处理中的各种基本概念,首先介绍了分词技术以将输入文本分成多个 token
,然后学习了如何使用词干提取和词形还原将单词转换为基本形式,并介绍了文本分块技术,根据预定义的条件将输入文本分成多个块。
然后,综合使用上述方法基于词袋模型构建了一个文档矩阵,学习了如何使用机器学习技术对文本进行分类,最后,我们介绍了如何识别给定文本中的主题。
系列链接
使用Scikit-learn开启机器学习之旅
一文开启深度学习之旅
一文开启计算机视觉之旅
一文开启监督学习之旅
一文开启无监督学习之旅