做者|Mauro Di Pietro
编译|VK
来源|Towards Data Science
python
摘要
在本文中,我将使用NLP和Python解释如何为机器学习模型分析文本数据和提取特征。git
天然语言处理(NLP)是人工智能的一个研究领域,它研究计算机与人类语言之间的相互做用,特别是如何对计算机进行编程以处理和分析大量天然语言数据。github
NLP经常使用于文本数据的分类。文本分类是根据文本数据的内容对其进行分类的问题。文本分类最重要的部分是特征工程:从原始文本数据为机器学习模型建立特征的过程。web
在本文中,我将解释不一样的方法来分析文本并提取可用于构建分类模型的特征。我将介绍一些有用的Python代码。算法
这些代码能够很容易地应用于其余相似的状况(只需复制、粘贴、运行),而且我加上了注释,以便你能够理解示例(连接到下面的完整代码)。编程
我将使用“新闻类别数据集”(如下连接),其中向你提供从赫芬顿邮报得到的2012年至2018年的新闻标题,并要求你使用正确的类别对其进行分类。api
https://www.kaggle.com/rmisra/news-category-dataset网络
特别是,我将经过:app
-
环境设置:导入包并读取数据。
-
语言检测:了解哪些天然语言数据在其中。
-
文本预处理:文本清理和转换。
-
长度分析:用不一样的指标来衡量。
-
情绪分析:判断一篇文章是正面的仍是负面的。
-
命名实体识别:带有预约义类别(如人名、组织、位置)的标识文本。
-
词频:找出最重要的n个字母。
-
词向量:把一个字转换成向量。
-
主题模型:从语料库中提取主题。
环境设置
首先,我须要导入如下库。
## 数据 import pandas as pd import collections import json ## 绘图 import matplotlib.pyplot as plt import seaborn as sns import wordcloud ## 文本处理 import re import nltk ## 语言检测 import langdetect ## 情感分析 from textblob import TextBlob ## 命名实体识别 import spacy ## 词频 from sklearn import feature_extraction, manifold ## word embedding import gensim.downloader as gensim_api ## 主题模型 import gensim
数据集包含在一个json文件中,所以我将首先使用json包将其读入字典列表,而后将其转换为pandas数据帧。
lst_dics = [] with open('data.json', mode='r', errors='ignore') as json_file: for dic in json_file: lst_dics.append( json.loads(dic) ) ## 打印第一个 lst_dics[0]
原始数据集包含30多个类别,但在本教程中,我将使用3个类别的子集:娱乐、政治和技术(Entertainment, Politics, Tech)。
## 建立dtf dtf = pd.DataFrame(lst_dics) ## 筛选类别 dtf = dtf[ dtf["category"].isin(['ENTERTAINMENT','POLITICS','TECH']) ][["category","headline"]] ## 重命名列 dtf = dtf.rename(columns={"category":"y", "headline":"text"}) ## 打印5个随机行 dtf.sample(5)
为了理解数据集的组成,我将经过用条形图显示标签频率来研究单变量分布(仅一个变量的几率分布)。
x = "y" fig, ax = plt.subplots() fig.suptitle(x, fontsize=12) dtf[x].reset_index().groupby(x).count().sort_values(by= "index").plot(kind="barh", legend=False, ax=ax).grid(axis='x') plt.show()
数据集是不平衡的:与其余数据集相比,科技新闻的比例确实很小。这多是建模过程当中的一个问题,对数据集从新采样可能颇有用。
如今已经设置好了,我将从清理数据开始,而后从原始文本中提取不一样的细节,并将它们做为数据帧的新列添加。这些新信息能够做为分类模型的潜在特征。
语言检测
首先,我想确保我使用的是同一种语言,而且使用langdetect包,这很是简单。为了举例说明,我将在数据集的第一个新闻标题上使用它:
txt = dtf["text"].iloc[0] print(txt, " --> ", langdetect.detect(txt))
咱们为整个数据集添加一个包含语言信息的列:
dtf['lang'] = dtf["text"].apply(lambda x: langdetect.detect(x) if x.strip() != "" else "") dtf.head()
数据帧如今有一个新列。使用以前的相同代码,我能够看到有多少种不一样的语言:
即便有不一样的语言,英语也是主要的语言。因此我要用英语过滤新闻。
dtf = dtf[dtf["lang"]=="en"]
文本预处理
数据预处理是准备原始数据以使其适合机器学习模型的阶段。对于NLP,这包括文本清理、删除停用词、词干还原。
文本清理步骤因数据类型和所需任务而异。一般,在文本被标识化以前,字符串被转换为小写,标点符号被删除。标识化(Tokenization)是将字符串拆分为字符串列表(或“标识”)的过程。
再以第一条新闻标题为例:
print("--- original ---") print(txt) print("--- cleaning ---") txt = re.sub(r'[^\w\s]', '', str(txt).lower().strip()) print(txt) print("--- tokenization ---") txt = txt.split() print(txt)
咱们要保留列表中的全部标识吗?咱们没有。事实上,咱们想删除全部不提供额外信息的单词。
在这个例子中,最重要的词是“song”,由于它能够将任何分类模型指向正确的方向。相比之下,像“and”、“for”、“the”这样的词并不有用,由于它们可能出如今数据集中几乎全部的观察中。
这些是停用词的例子。停用词一般指的是语言中最多见的单词,可是咱们没有一个通用的停用词列表。
咱们可使用NLTK(天然语言工具包)为英语词汇表建立一个通用停用词列表,它是一套用于符号和统计天然语言处理的库和程序。
lst_stopwords = nltk.corpus.stopwords.words("english") lst_stopwords
让咱们从第一个新闻标题中删除这些停用词:
print("--- remove stopwords ---") txt = [word for word in txt if word not in lst_stopwords] print(txt)
咱们须要很是当心的停用词,由于若是你删除了错误的标识,你可能会失去重要的信息。例如,删除了“Will”一词,咱们丢失了此人是Will Smith的信息。
考虑到这一点,在删除停用词以前对原始文本进行一些手动修改是颇有用的(例如,将“Will Smith”替换为“Will_Smith”)。
既然咱们有了全部有用的标识,就能够应用word转换了。词干化(Stemming)和引理化(Lemmatization)都产生了单词的词根形式。
他们的区别在于词干可能不是一个实际的单词,而引理是一个实际的语言单词(词干一般更快)。这些算法都是由NLTK提供的。
继续示例:
print("--- stemming ---") ps = nltk.stem.porter.PorterStemmer() print([ps.stem(word) for word in txt]) print("--- lemmatisation ---") lem = nltk.stem.wordnet.WordNetLemmatizer() print([lem.lemmatize(word) for word in txt])
如你所见,有些单词已经改变了:“joins”变成了它的根形式“join”,就像“cups”。另外一方面,“official”只随着词干的变化而变化,词干“offici”不是一个词,而是经过去掉后缀“-al”而产生的。
我将把全部这些预处理步骤放在一个函数中,并将其应用于整个数据集。
''' 预处理. :parameter :param text: string - 包含文本的列的名称 :param lst_stopwords: list - 要删除的停用词列表 :param flg_stemm: bool - 是否应用词干 :param flg_lemm: bool - 是否应用引理化 :return cleaned text ''' def utils_preprocess_text(text, flg_stemm=False, flg_lemm=True, lst_stopwords=None): ## 清洗(转换为小写并删除标点和字符,而后删除) text = re.sub(r'[^\w\s]', '', str(text).lower().strip()) ## 标识化(从字符串转换为列表) lst_text = text.split() ## 删除停用词 if lst_stopwords is not None: lst_text = [word for word in lst_text if word not in lst_stopwords] ## 词干化 if flg_stemm == True: ps = nltk.stem.porter.PorterStemmer() lst_text = [ps.stem(word) for word in lst_text] ## 引理化 if flg_lemm == True: lem = nltk.stem.wordnet.WordNetLemmatizer() lst_text = [lem.lemmatize(word) for word in lst_text] ## 从列表返回到字符串 text = " ".join(lst_text) return text
请注意,你不该该同时应用词干和引理化。在这里我将使用后者。
dtf["text_clean"] = dtf["text"].apply(lambda x: utils_preprocess_text(x, flg_stemm=False, flg_lemm=True, lst_stopwords))
和之前同样,我建立了一个新的列:
dtf.head()
print(dtf["text"].iloc[0], " --> ", dtf["text_clean"].iloc[0])
长度分析
查看文本的长度很重要,由于这是一个简单的计算,能够提供不少信息。
例如,也许咱们足够幸运地发现,一个类别系统地比另外一个类别长,而长度只是构建模型所需的惟一特征。不幸的是,因为新闻标题的长度类似,状况并不是如此,但值得一试。
文本数据有几种长度度量。我将举几个例子:
- 字数:统计文本中的标识数(用空格分隔)
- 字符数:将每一个标识的字符数相加
- 句子计数:计算句子的数量(用句点分隔)
- 平均字长:字长之和除以字数(字数/字数)
- 平均句子长度:句子长度之和除以句子数(字数/句子数)
dtf['word_count'] = dtf["text"].apply(lambda x: len(str(x).split(" "))) dtf['char_count'] = dtf["text"].apply(lambda x: sum(len(word) for word in str(x).split(" "))) dtf['sentence_count'] = dtf["text"].apply(lambda x: len(str(x).split("."))) dtf['avg_word_length'] = dtf['char_count'] / dtf['word_count'] dtf['avg_sentence_lenght'] = dtf['word_count'] / dtf['sentence_count'] dtf.head()
让咱们看看例子:
这些新变量相对于目标的分布状况如何?为了回答这个问题,我将研究二元分布(两个变量如何一块儿影响)。
首先,我将整个观察结果分红3个样本(政治、娱乐、科技),而后比较样本的直方图和密度。若是分布不一样,那么变量是可预测的,由于这三组有不一样的模式。
例如,让咱们看看字符数是否与目标变量相关:
x, y = "char_count", "y" fig, ax = plt.subplots(nrows=1, ncols=2) fig.suptitle(x, fontsize=12) for i in dtf[y].unique(): sns.distplot(dtf[dtf[y]==i][x], hist=True, kde=False, bins=10, hist_kws={"alpha":0.8}, axlabel="histogram", ax=ax[0]) sns.distplot(dtf[dtf[y]==i][x], hist=False, kde=True, kde_kws={"shade":True}, axlabel="density", ax=ax[1]) ax[0].grid(True) ax[0].legend(dtf[y].unique()) ax[1].grid(True) plt.show()
这三个类别具备类似的长度分布。在这里,密度图很是有用,由于样本有不一样的大小。
情感分析
情感分析是经过数字或类来表达文本数据的主观情感。因为天然语言的模糊性,计算情感是天然语言处理中最困难的任务之一。
例如,短语“This is so bad that it’s good”有不止一种解释。一个模型能够给“好”这个词分配一个积极的信号,给“坏”这个词分配一个消极的信号,从而产生一种中性的情绪。这是由于上下文未知。
最好的方法是训练你本身的情绪模型,使之适合你的数据。当没有足够的时间或数据时,可使用预训练好的模型,好比Textblob和Vader。
- Textblob创建在NLTK的基础上,是最流行的一种,它能够给单词赋予极性,并做为一个平均值来估计整个文本的情绪。
- 另外一方面,Vader(Valence-aware dictionary and mootion reasoner)是一个基于规则的模型,尤为适用于社交媒体数据。
我将使用Textblob添加一个情感特征:
dtf["sentiment"] = dtf[column].apply(lambda x: TextBlob(x).sentiment.polarity) dtf.head()
print(dtf["text"].iloc[0], " --> ", dtf["sentiment"].iloc[0])
分类和情绪之间有规律吗?
大多数的头条新闻都是中性的,除了政治新闻偏向于负面,科技新闻偏向于正面。
命名实体识别
命名实体识别(Named entity recognition,NER)是用预约义的类别(如人名、组织、位置、时间表达式、数量等)提取非结构化文本中的命名实体的过程。
训练一个NER模型是很是耗时的,由于它须要一个很是丰富的数据集。幸运的是有人已经为咱们作了这项工做。最好的开源NER工具之一是SpaCy。它提供了不一样的NLP模型,这些模型可以识别多种类型的实体。
我将在咱们一般的标题(未经预处理的原始文本)中使用SpaCy模型en_core_web_lg(网络数据上训练的英语的大型模型),给出一个例子:
## 调用 ner = spacy.load("en_core_web_lg") ## 打标签 txt = dtf["text"].iloc[0] doc = ner(txt) ## 展现结果 spacy.displacy.render(doc, style="ent")
这很酷,可是咱们怎么能把它变成有用的特征呢?这就是我要作的:
-
对数据集中的每一个文本观察运行NER模型,就像我在前面的示例中所作的那样。
-
对于每一个新闻标题,我将把全部被承认的实体以及同一实体出如今文本中的次数放入一个新的列(称为“tags”)。
在这个例子中:
{ (‘Will Smith’, ‘PERSON’):1,
(‘Diplo’, ‘PERSON’):1,
(‘Nicky Jam’, ‘PERSON’):1,
(“The 2018 World Cup’s”, ‘EVENT’):1 } -
而后,我将为每一个标识类别(Person、Org、Event,…)建立一个新列,并计算每一个标识类别找到的实体数。在上面的例子中,特征将是
tags_PERSON = 3
tags_EVENT = 1
## 标识文本并将标识导出到列表中 dtf["tags"] = dtf["text"].apply(lambda x: [(tag.text, tag.label_) for tag in ner(x).ents] ) ## utils函数计算列表元素 def utils_lst_count(lst): dic_counter = collections.Counter() for x in lst: dic_counter[x] += 1 dic_counter = collections.OrderedDict( sorted(dic_counter.items(), key=lambda x: x[1], reverse=True)) lst_count = [ {key:value} for key,value in dic_counter.items() ] return lst_count ## 计数 dtf["tags"] = dtf["tags"].apply(lambda x: utils_lst_count(x)) ## utils函数为每一个标识类别建立新列 def utils_ner_features(lst_dics_tuples, tag): if len(lst_dics_tuples) > 0: tag_type = [] for dic_tuples in lst_dics_tuples: for tuple in dic_tuples: type, n = tuple[1], dic_tuples[tuple] tag_type = tag_type + [type]*n dic_counter = collections.Counter() for x in tag_type: dic_counter[x] += 1 return dic_counter[tag] else: return 0 ## 提取特征 tags_set = [] for lst in dtf["tags"].tolist(): for dic in lst: for k in dic.keys(): tags_set.append(k[1]) tags_set = list(set(tags_set)) for feature in tags_set: dtf["tags_"+feature] = dtf["tags"].apply(lambda x: utils_ner_features(x, feature)) ## 结果 dtf.head()
如今咱们能够在标识类型分布上有一个视图。以组织标签(公司和组织)为例:
为了更深刻地分析,咱们须要使用在前面的代码中建立的列“tags”。让咱们为标题类别之一绘制最经常使用的标识:
y = "ENTERTAINMENT" tags_list = dtf[dtf["y"]==y]["tags"].sum() map_lst = list(map(lambda x: list(x.keys())[0], tags_list)) dtf_tags = pd.DataFrame(map_lst, columns=['tag','type']) dtf_tags["count"] = 1 dtf_tags = dtf_tags.groupby(['type', 'tag']).count().reset_index().sort_values("count", ascending=False) fig, ax = plt.subplots() fig.suptitle("Top frequent tags", fontsize=12) sns.barplot(x="count", y="tag", hue="type", data=dtf_tags.iloc[:top,:], dodge=False, ax=ax) ax.grid(axis="x") plt.show()
接着介绍NER的另外一个有用的应用程序:你还记得咱们从“Will Smith”的名称中删除了“Will”这个单词的停用词吗?解决这个问题的一个有趣的方法是将“Will Smith”替换为“Will_Smith”,这样它就不会受到停用词删除的影响。
遍历数据集中的全部文原本更更名称是不可能的,因此让咱们使用SpaCy。如咱们所知,SpaCy能够识别一我的名,所以咱们可使用它来检测姓名,而后修改字符串。
## 预测 txt = dtf["text"].iloc[0] entities = ner(txt).ents ## 打标签 tagged_txt = txt for tag in entities: tagged_txt = re.sub(tag.text, "_".join(tag.text.split()), tagged_txt) ## 结果 print(tagged_txt)
词频
到目前为止,咱们已经看到了如何经过分析和处理整个文原本进行特征工程。
如今,咱们将经过计算n-grams频率来研究单个单词的重要性。n-gram是给定文本样本中n个项的连续序列。当n-gram的大小为1时,称为unigram(大小为2是一个bigram)。
例如,短语“I like this article”能够分解为:
- 4个unigram: “I”, “like”, “this”, “article”
- 3个bigrams:“I like”, “like this”, “this article”
我将以政治新闻为例说明如何计算unigram和bigrams频率。
y = "POLITICS" corpus = dtf[dtf["y"]==y]["text_clean"] lst_tokens = nltk.tokenize.word_tokenize(corpus.str.cat(sep=" ")) fig, ax = plt.subplots(nrows=1, ncols=2) fig.suptitle("Most frequent words", fontsize=15) ## unigrams dic_words_freq = nltk.FreqDist(lst_tokens) dtf_uni = pd.DataFrame(dic_words_freq.most_common(), columns=["Word","Freq"]) dtf_uni.set_index("Word").iloc[:top,:].sort_values(by="Freq").plot( kind="barh", title="Unigrams", ax=ax[0], legend=False).grid(axis='x') ax[0].set(ylabel=None) ## bigrams dic_words_freq = nltk.FreqDist(nltk.ngrams(lst_tokens, 2)) dtf_bi = pd.DataFrame(dic_words_freq.most_common(), columns=["Word","Freq"]) dtf_bi["Word"] = dtf_bi["Word"].apply(lambda x: " ".join( string for string in x) ) dtf_bi.set_index("Word").iloc[:top,:].sort_values(by="Freq").plot( kind="barh", title="Bigrams", ax=ax[1], legend=False).grid(axis='x') ax[1].set(ylabel=None) plt.show()
若是有n个字母只出如今一个类别中(即政治新闻中的“Republican”),那么这些就可能成为新的特征。一种更为费力的方法是对整个语料库进行向量化,并使用全部的单词做为特征(单词包方法)。
如今我将向你展现如何在数据帧中添加单词频率做为特征。咱们只须要Scikit learn中的CountVectorizer,它是Python中最流行的机器学习库之一。
CountVectorizer将文本文档集合转换为计数矩阵。我将用3个n-grams来举例:“box office”(常常出如今娱乐圈)、“republican”(常常出如今政界)、“apple”(常常出如今科技界)。
lst_words = ["box office", "republican", "apple"] ## 计数 lst_grams = [len(word.split(" ")) for word in lst_words] vectorizer = feature_extraction.text.CountVectorizer( vocabulary=lst_words, ngram_range=(min(lst_grams),max(lst_grams))) dtf_X = pd.DataFrame(vectorizer.fit_transform(dtf["text_clean"]).todense(), columns=lst_words) ## 将新特征添加为列 dtf = pd.concat([dtf, dtf_X.set_index(dtf.index)], axis=1) dtf.head()
可视化相同信息的一个很好的方法是使用word cloud,其中每一个标识的频率用字体大小和颜色显示。
wc = wordcloud.WordCloud(background_color='black', max_words=100, max_font_size=35) wc = wc.generate(str(corpus)) fig = plt.figure(num=1) plt.axis('off') plt.imshow(wc, cmap=None) plt.show()
词向量
最近,NLP领域开发了新的语言模型,这些模型依赖于神经网络结构,而不是更传统的n-gram模型。这些新技术是一套语言建模和特征学习技术,将单词转换为实数向量,所以称为词嵌入。
词嵌入模型经过构建所选单词先后出现的标识的几率分布,将特定单词映射到向量。这些模型很快变得流行,由于一旦你有了实数而不是字符串,你就能够执行计算了。例如,要查找相同上下文的单词,能够简单地计算向量距离。
有几个Python库可使用这种模型。SpaCy是其中之一,但因为咱们已经使用过它,我将谈论另外一个著名的包:Gensim。
它是使用现代统计机器学习的用于无监督主题模型和天然语言处理的开源库。使用Gensim,我将加载一个预训练的GloVe模型。
GloVe是一种无监督学习算法,用于获取300个单词的向量表示。
nlp = gensim_api.load("glove-wiki-gigaword-300")
咱们可使用此对象将单词映射到向量:
word = "love" nlp[word]
nlp[word].shape
如今让咱们来看看什么是最接近的词向量,换句话说,就是大多数出如今类似上下文中的词。
为了在二维空间中绘制向量图,我须要将维数从300降到2。我将使用Scikit learn中的t-分布随机邻居嵌入来实现这一点。
t-SNE是一种可视化高维数据的工具,它将数据点之间的类似性转换为联合几率。
## 找到最近的向量 labels, X, x, y = [], [], [], [] for t in nlp.most_similar(word, topn=20): X.append(nlp[t[0]]) labels.append(t[0]) ## 降维 pca = manifold.TSNE(perplexity=40, n_components=2, init='pca') new_values = pca.fit_transform(X) for value in new_values: x.append(value[0]) y.append(value[1]) ## 绘图 fig = plt.figure() for i in range(len(x)): plt.scatter(x[i], y[i], c="black") plt.annotate(labels[i], xy=(x[i],y[i]), xytext=(5,2), textcoords='offset points', ha='right', va='bottom') ## 添加中心 plt.scatter(x=0, y=0, c="red") plt.annotate(word, xy=(0,0), xytext=(5,2), textcoords='offset points', ha='right', va='bottom')
主题模型
Genism包专门用于主题模型。主题模型是一种用于发现文档集合中出现的抽象“主题”的统计模型。
我将展现如何使用LDA(潜Dirichlet分布)提取主题:它是一个生成统计模型,它容许由未观察到的组解释观察结果集,解释为何数据的某些部分是类似的。
基本上,文档被表示为潜在主题上的随机混合,每一个主题的特征是在单词上的分布。
让咱们看看咱们能够从科技新闻中提取哪些主题。我须要指定模型必须簇的主题数,我将尝试使用3:
y = "TECH" corpus = dtf[dtf["y"]==y]["text_clean"] ## 预处理语料库 lst_corpus = [] for string in corpus: lst_words = string.split() lst_grams = [" ".join(lst_words[i:i + 2]) for i in range(0, len(lst_words), 2)] lst_corpus.append(lst_grams) ## 将单词映射到id id2word = gensim.corpora.Dictionary(lst_corpus) ## 建立词典 word:freq dic_corpus = [id2word.doc2bow(word) for word in lst_corpus] ## 训练LDA lda_model = gensim.models.ldamodel.LdaModel(corpus=dic_corpus, id2word=id2word, num_topics=3, random_state=123, update_every=1, chunksize=100, passes=10, alpha='auto', per_word_topics=True) ## 输出 lst_dics = [] for i in range(0,3): lst_tuples = lda_model.get_topic_terms(i) for tupla in lst_tuples: lst_dics.append({"topic":i, "id":tupla[0], "word":id2word[tupla[0]], "weight":tupla[1]}) dtf_topics = pd.DataFrame(lst_dics, columns=['topic','id','word','weight']) ## plot fig, ax = plt.subplots() sns.barplot(y="word", x="weight", hue="topic", data=dtf_topics, dodge=False, ax=ax).set_title('Main Topics') ax.set(ylabel="", xlabel="Word Importance") plt.show()
试图仅用3个主题捕捉6年的内容可能有点困难,但正如咱们所看到的,关于苹果公司的一切都以同一个主题结束。
结论
本文是演示如何使用NLP分析文本数据并为机器学习模型提取特征的教程。
我演示了如何检测数据所使用的语言,以及如何预处理和清除文本。而后我解释了长度的不一样度量,用Textblob进行了情绪分析,并使用SpaCy进行命名实体识别。最后,我解释了Scikit学习的传统词频方法与Gensim的现代语言模型之间的区别。
如今,你已经了解了开始处理文本数据的全部NLP基础知识。
原文连接:https://towardsdatascience.com/text-analysis-feature-engineering-with-nlp-502d6ea9225d
欢迎关注磐创AI博客站:
http://panchuang.net/
sklearn机器学习中文官方文档:
http://sklearn123.com/
欢迎关注磐创博客资源汇总站:
http://docs.panchuang.net/