【译文】基于Python的自然语言处理指南
作者 Shivam Bansal
译者 钱亦欣
根据业界估计,仅有约21%的数据是以结构化的形式出现的。现在,我们所说的话,发的微博,发送到各个APP上的信息都是重要的数据来源。这部分数据大部分以文本的形式存在,天生的非结构化。
抛开维度不谈,从这些据道采集的数据在手工加工或者用自动化的系统解析之前是没法直接建模分析的。
为了从文本数据得到显著而有用的信息,学习一些自然语言处理(NLP)方法尤为重要。
如果你今年计划编写一个聊天机器人,或者想要掌握文本预处理技术,本文是个不错的入门。本文将阐述NLP的基本原理、技术和应用方法。本文的目标就是让你能针对真实数据集应用这些方法。
目录
-
NLP简介
-
文本预处理
-
去除噪声
-
词汇规范化
-
词干提取
-
词形还原
-
-
对象标准化
-
-
文本特征化 (文本数据的特征工程)
-
句法分析
-
从属关系语法
-
词性标注
-
-
实例分析
-
短语检测
-
命名实体识别
-
主题建模
-
N元文法模型 (N-Grams)
-
-
统计特征
-
TF – IDF
-
频率/密度特征
-
可读性特征
-
-
单词嵌入
-
-
NLP重要任务
-
文本分类
-
文本匹配
-
编辑距离
-
语音匹配
-
柔性字符串匹配
-
-
指代消解
-
其他
-
-
重要的NLP库
1. NLP简介
NLP是数据科学的一大分支,旨在以智能而高效的方法来分析文本数据中潜藏的信息。采用适当的技术,你可以实现诸如自动总结,机器翻译,命名实体识别,情感分析等任务。
在进一步深入之前,让我先来解释下文会出现的一些术语:
-
标记化 – 把文本转化为标记的过程
-
标记 – 文本中出现的实体或词汇
-
文本对象 – 词语、词组、句子、文章
安装NLTK库和其数据集:
安装pip: 在终端中运行:
sudo easy_install pip
安装NLTK: 在终端中运行:
sudo pip install -U nltk
下载NLTK数据: 在python中运行如下代码:
import nltk
nltk.download()
按照屏幕上出现的指示,下载需要的包或者集合,其他库库可以通过pip下载
2. 文本预处理
由于文本是一种高度非结构化的数据,其中会蕴含这各种类型的噪音,如果不做预处理是无法分析的。将文本清洗并标准化,使得其噪音含量低可进行分析的全过程称为文本预处理。
上述过程一般包括如下步骤:
-
去除噪声
-
词汇规范化
-
对象标准化
下图展示来文本预处理工作流程
2.1 去除噪声
任何与文本数据内容不相关或是与最终输出无关的文本块被视作噪声。
举个例子,语言中的定冠词,一些介词,网页地址,社交媒体实例(标签等)和一些特有名词都是噪声。这个步骤目标就是把所有类型的噪声都移除。
去除噪声的通用最发是准备一个噪声实体的字典,然后把文本对象中的标记(或者词)逐个对照,如果它被包含在字典里,就移除。
下列python代码就实现来这个过程
# 移除文本噪声的示范代码
noise_list = ["is", "a", "this", "..."]
def _remove_noise(input_text):
words = input_text.split()
noise_free_words = [word for word in words if word not in
noise_list]
noise_free_text = " ".join(noise_free_words)
return noise_free_text_remove_noise("this is a sample text")
>>> "sample text"
另一种方法是利用正则表达式来处理有特殊模式的噪声。我在 先前的文章 中已经介绍过相关细节,按照下列代码就嗯能移除输入文本中特定模式的噪声。
# 移除固定模式噪声的示例代码
import re
def _remove_regex(input_text, regex_pattern):
urls = re.finditer(regex_pattern, input_text)
for i in urls:
input_text = re.sub(i.group().strip(), '', input_text)
return input_text
regex_pattern = "#[\w]*"
_remove_regex("remove this #hashtag from analytics vidhya", regex_pattern)
>>> "remove this from analytics vidhya"
2.2 词汇规范化
另一种文本噪声的形式是由多个单词形成的表达式
举个例子 - "play", "player", "played", "plays", "playing"同一个单词的不同时态或者词性变化,尽管形式不同但意义上相近。这步会把所有这一类的词汇规范化为同一范式(a.k.a. lemma)。规范化是文本数据特征工程的主要步骤,它把高维特征(N个不同特征)映射到来低维空间(1个特征),这对很多机器学习模型而言都是理想情况。
最常用的词汇规范化技术是:
-
词干提取: 词干提取是基于一些基本规则将词汇的后缀("ing", "ly", "es", "s"等)剥离的过程
-
词形还原: 词形还原是一种逐步还原词根的过程, 它基于词汇和词法分析实现这个目的。
下列是利用python中的NLTK库实现词法分析和词干提取的示例代码
from nltk.stem.wordnet import WordNetLemmatizer
lem = WordNetLemmatizer()
from nltk.stem.porter import PorterStemmer
stem = PorterStemmer()
word = "multiplying"
lem.lemmatize(word, "v")
>> "multiply"
stem.stem(word)
>> "multipli"
2.3 对象标准化
文本数据中时常有些词或者短语无法在标准词典里找到,这些内容不会被搜索引擎或者模型识别。
例子包括:一些缩写词,包含词语的标签和俚语词汇。好在利用正则表达式和手工收集的词典可以解决这一问题。下俩代码就能将社交媒体俚语进行替换。
lookup_dict = {'rt':'Retweet', 'dm':'direct message', "awsm" : "awesome", "luv" :"love", "..."}
def _lookup_words(input_text):
words = input_text.split()
new_words = []
for word in words:
if word.lower() in lookup_dict:
word = lookup_dict[word.lower()]
new_words.append(word)
new_text = " ".join(new_words)
return new_text
_lookup_words("RT this is a retweeted tweet by Shivam Bansal")
>> "Retweet this is a retweeted tweet by Shivam Bansal"
除了本文介绍的三种方法,其他的文本预处理内容发包括编码-解码噪声,语法检测和拼写纠正。相关细节可以看我以前写的 这篇文章 。
3.文本特征化 (文本数据的特征工程)
为了分析预处理后的数据,我们需要将其转化为特征。基于不同的用途,文本特征可以利用配套技术-句法分析,基于实体/N元文法/单词的特征,统计特征和单词嵌入。下文会详细阐述这些技术。
3.1 句法分析
句法分析包括对句中单词在语法和排列方式(与其他单词的相关关系)两方面的分析。从属关系语法和语音标签是句法分析的重要属性。
从属关系树(Dependency Trees) – 句子是单词的集合,句子中单词的相互关系依赖于基本的从属关系语法。从属关系语法专门处理(带标签的)两个词汇单元(单词)的非对称二元关系。每种关系都可以用三元表示法(relation, governor, dependent)关系显示 举个例子:考虑如下句子“ Bills on ports and immigration were submitted by Senator Brownback, Republican of Kansas. ” 该句单词的关系可以用如下的树状结构表示:
该树状结构表示单词“submitted”是这句话的根单词(root word),并且其与两棵子树相连(主题和客体子树)。 每颗子树又有自己的从属关系树结构,比如 – (“Bills” <-> “ports” <by> “proposition” relation), (“ports” <-> “immigration” <by> “conjugation” relation)。
当我们通过自上而下的迭代方法梳理好单词的从属关系后,就可以基于此将其作为特征解决NLP领域的很多问题了,比如情感分析、实体识别和文本分类等。Python的包装器 StanfordCoreNLP (由斯坦福NLP小组提供,仅供商业使用)和NLTK库中的相关语法可以用来生成从属关系树。
词性标注 – 除了语法关系,句中单词的位置(词性)标记也蕴含着信息,词的位置定义了它的用途和功能。宾夕法尼亚大学提供了一个完整的位置标记列表。下方代码则使用了NLTK库来对输入的文本进行词性标注。
from nltk import word_tokenize, pos_tag
text = "I am learning Natural Language Processing on Analytics Vidhya"
tokens = word_tokenize(text)
print pos_tag(tokens)
>>> [('I', 'PRP'), ('am', 'VBP'), ('learning', 'VBG'), ('Natural',
'NNP'),('Language', 'NNP'),('Processing', 'NNP'), ('on', 'IN'),
('Analytics', 'NNP'),('Vidhya', 'NNP')]
在NLP中,词性标注有个很多重要用途:
A.消除歧义: 一些词的不同用法代表不同的意思. 如下列两句:
I. “Please book my flight for Delhi”
II. “I am going to read this book in the flight”
“Book” 在这里代表不同的意义, 好在它在两句的位置也不同. 第一句“book”是的动词, 第二句中它是个名词。 ( Lesk Algorithm 也被用于类似目的)
B.强化基于单词的特征: 一个机器学习模型可以从一个词的很多方面提取信息,但如果一个词已经标注了词性,那么它作为特征就能提供更精准的信息。 例如:
句子 -“book my flight, I will read this book”
单词 – (“book”, 2), (“my”, 1), (“flight”, 1), (“I”, 1), (“will”, 1), (“read”, 1), (“this”, 1)
带标注的单词 – (“book_VB”, 1), (“my_PRP$”, 1), (“flight_NN”, 1), (“I_PRP”, 1), (“will_MD”, 1), (“read_VB”, 1), (“this_DT”, 1), (“book_NN”, 1)
译者注:如果不带词性标注,两个“book”就被认为是同义词,词频为2。这会在后续分析中引入误差。
C.标准化与词形还原 : 位置标注是词形还原的基础步骤之一,可以帮助把单词还原为基本形式.
D.有效移除停用词 : 利用位置标记可以有效地去除停用词。
举个例子,有一些标注可以知名某些语言中的低频词和不重要的词,像方位介词,数量词和语气助词等。
3.2 实体提取(实体作为特征)
实体指名词短语或者动词短语,被认为是句子最重要的组成部分。实体检测算法则是基于解析规则,词典搜索,位置标注和从属关系分析的一类模型的集合。实体检测算法可被应用于自动聊天机器人,内容分析和消费者行为分析等。
主题模型和命名实体识别是NLP中实体检测的两大主流方法。
A. 命名实体识别(NER)
检测诸如人名、地名和公司名之类的命名实体的过程被称为NER,比如:
句子 – Sergey Brin, the manager of Google Inc. is walking in the streets of New York.
命名实体 – ( “person” : “Sergey Brin” ), (“org” : “Google Inc.”), (“location” : “New York”)
一个典型的NER模型包括三个模块:
名词短语识别 该步骤会根据从属关系分析和词性标注提取文本中所有的名词短语
短语分类 该步骤把提取出的名词短语分到相应的类别(位置,姓名等)。Google地图的API提供了很好的位置归类方法,而利用dbpedia和wikipedia的数据库可以用来识别公司名和人名。如此之外,也可以认为把多渠道收集的数据整合为字典用于分类。
实体消除歧义 有时一个实体会被错误地分类,所以我们需要对分类结果加一层检验。使用知识图谱可以实现这一目的,使用较多的知识图谱包括Google知识图谱,IBM Watson和Wikipedia。
B. 主题模型
主题建模是识别文本所属主题的过程,它会以非监督技术识别单词间的潜在技术。主题被定义为语料库中以词组形式重复出现的模式。一个好的主题模型会把健康方面的文本主题总结为“health”,“doctor”,“patient”,“hospital”,把农耕方便的主题总结为“farm”,“crops”,“wheat”。
潜在狄利克雷分配模型(LDA)是最流行的主题建模技术,下列代码是在Python中建立LDA模型的过程。如果需要LDA模型的详细解释和技术细节,可以参考这篇 文章
doc1 = "Sugar is bad to consume. My sister likes to have sugar, but not my father."
doc2 = "My father spends a lot of time driving my sister around to dance practice."
doc3 = "Doctors suggest that driving may cause increased stress and blood pressure."
doc_complete = [doc1, doc2, doc3]
doc_clean = [doc.split() for doc in doc_complete]
import gensim from gensim
import corpora
# 建立一个语料库词典,其中每个词组都进行了编号
dictionary = corpora.Dictionary(doc_clean)
# 使用前一部构建的词典,把语料库词典从列表转换为文献-检索词矩阵
(DTM)doc_term_matrix = [dictionary.doc2bow(doc) for doc in doc_clean]
# 使用gensim库构建LDA模型
Lda = gensim.models.ldamodel.LdaModel
# 使用DTM构建并训练LDA模型
ldamodel = Lda(doc_term_matrix, num_topics=3, id2word = dictionary,
passes=50)
print(ldamodel.print_topics())
C. 把N元文法作为特征
N个单词构成的组合称为N元文法。N元文法(N > 1)通常比单词(一元文法)包含更多信息。此外,两元文法被认为是最重要的特征,下列代码能从文本中生成二元文法。
def generate_ngrams(text, n):
words = text.split()
output = []
for i in range(len(words)-n+1):
output.append(words[i:i+n])
return output
>>> generate_ngrams('this is a sample text', 2)
# [['this', 'is'], ['is', 'a'], ['a', 'sample'], , ['sample', 'text']]
3.3 统计特征
文本数据也可以借用一些技术量化:
A. 词组频率-逆文档频率 (TF – IDF)
TF-IDF是信息检索领域的常用模型。它的工作原理是基于单词在文档中频率(不考虑序关系)把文本文档转化为向量的形式。举个例子,假设有一个由N个文本文档组成的数据集,在任一文档“D”中,TF和IDF被定义为:
词组频率(TF) – 词组“t”的TF被定义为其在文档“D”中的出现次数
逆文档频率(IDF) – 词组的IDF被定义为其数据集中文档总数和包含该词组文档树的比值的自然对数。
TF - IDF - TF-IDF 公式给出了词组在语料库(文档列表)中的相对重要性的度量,公式如下:
下方代码使用python中的scikit-learn库来把文本转化为TF-IDF向量:
from sklearn.feature_extraction.text import TfidfVectorizer
obj = TfidfVectorizer()
corpus = ['This is sample document.', 'another random document.', 'third sample document text']
X = obj.fit_transform(corpus)
print X
(0, 1) 0.345205016865
(0, 4) ... 0.444514311537
(2, 1) 0.345205016865
(2, 4) 0.444514311537
该模型会建立一个词汇字典,再给每个单词分配一个编号。输出中的每一行包括一个元组(i,j)和文档i中标号为j的词组的TF-IDF值。
B. 频率 / 密度 / 可读性特征
基于频率或者密度的特征也被广泛用于建模和分析,这类特征可能看起来很简单却往往能发挥很大的作用。这类特征有:词频,句频,标点频率和行业术语频率。而另一类包括音节频率,SMOG指数和易读性指标的可读性特征,请参看 Textstat 库的相关内容。
3.4 单词嵌入 (文本向量)
单词嵌入是把词语向量化的现代方法,其目标是通过语料库的情景相似性把高维单词特征重新定义为低维向量。它们在深度学习(如卷积神经网络和循环神经网络)中被广泛使用。 Word2Vec 和 GloVe 是单词嵌入的常用库,这些模型把文本语料库作为输入,并把文本向量作为输出。
Word2Vec模型是预处理模型和一两种分别叫做连续词包和skip-gram浅层神经网络的集合体。这些模型在NLP的其他领域也持续发光发热。它首先通过训练预料库形成一个词汇表,然后再学习单词嵌入表达式。下列代码使用gensim库把单词嵌入到向量里:
from gensim.models import Word2Vec
sentences = [['data', 'science'], ['vidhya', 'science', 'data', 'analytics'],['machine', 'learning'], ['deep', 'learning']]
# 使用语料库训练模型
model = Word2Vec(sentences, min_count = 1)
print model.similarity('data', 'science')
>>> 0.11222489293
print model['learning']
>>> array([ 0.00459356 0.00303564 -0.00467622 0.00209638, ...])
它们可以作为机器学习模型的向量化输入,可以利用余弦定理来衡量文本相似性,还可以做词云和被用于文本分类。
4. NLP的重要任务
本部分会讨论NLP领域的不同应用场景和常见问题。
4.1 文本分类
文本分类是NLP的典型问题之一,具体的例子包括但不仅限于 - 垃圾邮件识别,新闻主题分类,情感分析和搜索引擎的网页排序。
文本分类通俗地说将文本对象(文档或句子)按照一定规律分配到某一固定类别中的过程。当数据总量很大时,从组织,信息过滤和存储方面考虑,分类就显得尤为重要。
一个典型的NLP分类器包含两部分:(a)训练 (b)预测。如下图所示,输入的文本会先经过预处理过程再被提取特征,之后相应的机器学习模型就会接受特征并对新文本作出类别预测。
这里的代码就使用了朴素贝叶斯分类器(包含在blob库中)来实现文本分类过程:
from textblob.classifiers import NaiveBayesClassifier as NBC
from textblob import TextBlob
training_corpus = [
('I am exhausted of this work.', 'Class_B'),
("I can't cooperate with this", 'Class_B'),
('He is my badest enemy!', 'Class_B'),
('My management is poor.', 'Class_B'),
('I love this burger.', 'Class_A'),
('This is an brilliant place!', 'Class_A'),
('I feel very good about these dates.', 'Class_A'),
('This is my best work.', 'Class_A'),
("What an awesome view", 'Class_A'),
('I do not like this dish', 'Class_B')]
test_corpus = [
("I am not feeling well today.", 'Class_B'),
("I feel brilliant!", 'Class_A'),
('Gary is a friend of mine.', 'Class_A'),
("I can't believe I'm doing this.", 'Class_B'),
('The date was good.', 'Class_A'),
('I do not enjoy my job', 'Class_B')]
model = NBC(training_corpus)
print(model.classify("Their codes are amazing."))
>>> "Class_A"
print(model.classify("I don't like their computer."))
>>> "Class_B"
print(model.accuracy(test_corpus))
Scikit.Learn也提供了文本分类的工作流程:
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics import classification_report
from sklearn import svm
# 为SVM准备数据 (和朴素贝叶斯分类器使用同样特征)
train_data = []
train_labels = []
for row in training_corpus:
train_data.append(row[0])
train_labels.append(row[1])
test_data = []
test_labels = []
for row in test_corpus:
test_data.append(row[0])
test_labels.append(row[1])
# 创建特征向量
vectorizer = TfidfVectorizer(min_df=4, max_df=0.9)
# 训练特征向量
train_vectors = vectorizer.fit_transform(train_data)
# 在测试集上应用模型
test_vectors = vectorizer.transform(test_data)
# 训练使用线性核的SVM模型
model = svm.SVC(kernel='linear')
model.fit(train_vectors, train_labels)
prediction = model.predict(test_vectors)
>>> ['Class_A' 'Class_A' 'Class_B' 'Class_B' 'Class_A' 'Class_A']
print (classification_report(test_labels, prediction))
文本分类器的效果和特征的质量与数量关系很大,使用任一机器学习模型输入更多的训练数据总是更好的。
4.2 文本匹配/相似度
NLP的另一重要领域是匹配文本对象来计算相似度。它可以应用在拼写纠正,数据去重和基因组分析等。
依据不同的需求,现在已有很多中匹配技术可供使用,这部分会详细介绍常用的几种。
A. Levenshtein 距离 – 两个字符串间的Levenshtein距离被定义为将一个字符串转化为另一字符串所需要变动的修正数的最小值。其中,修正指单个字符的插入、删除或替换。下列代码能使用一种很省内存的算法计算该距离.
def levenshtein(s1,s2):
if len(s1) > len(s2):
s1,s2 = s2,s1
distances = range(len(s1) + 1)
for index2,char2 in enumerate(s2):
newDistances = [index2+1]
for index1,char1 in enumerate(s1):
if char1 == char2:
newDistances.append(distances[index1])
else:
newDistances.append(1 + min((distances[index1], distances[index1+1], newDistances[-1])))
distances = newDistances
return distances[-1]
print(levenshtein("analyze","analyse"))
B. 语音匹配 – 语音匹配算法会把关键词(人名、地名等)作为输入,之后返回一个能大致代表输入类别的单词。在对大型语料库进行索引,纠正拼写错误和匹配相关姓名时很有作用。Soundex和Metaphone是这一领域的两大主要算法,Python中的Fuzzy库能实现Soundex算法,比如:
import fuzzy
soundex = fuzzy.Soundex(4)
print soundex('ankit')
>>> “A523”
print soundex('aunkit')
>>> “A523”
C. 柔性字符串匹配 – 一套完整的文本匹配系统会包含各种不同的算法以适应文本的多样性。正则表达式对此也能发挥很大的作用。而另一种常用技术则包括 - 精确字符串匹配,词形还原匹配和紧凑匹配等。
D. 余弦相似度 – 当文本以向量形式出现时,使用余弦定理就能衡量两个文本的相似度了。下列代码就把文本按照词频转换成向量并使用余弦值来衡量两个文本的相似度。
import math
from collections import Counter
def get_cosine(vec1, vec2):
common = set(vec1.keys()) & set(vec2.keys())
numerator = sum([vec1[x] * vec2[x] for x in common])
sum1 = sum([vec1[x]**2 for x in vec1.keys()])
sum2 = sum([vec2[x]**2 for x in vec2.keys()])
denominator = math.sqrt(sum1) * math.sqrt(sum2)
if not denominator:
return 0.0
else:
return float(numerator) / denominator
def text_to_vector(text):
words = text.split()
return Counter(words)
text1 = 'This is an article on analytics vidhya'
text2 = 'article on analytics vidhya is about natural language processing'