作者|Mauro Di Pietro 编译|VK 来源|Towards Data Science
在本文中,我将使用NLP和Python解释如何为机器学习模型分析文本数据和提取特征。
自然语言处理(NLP)是人工智能的一个研究领域,它研究计算机与人类语言之间的相互作用,特别是如何对计算机进行编程以处理和分析大量自然语言数据。
NLP常用于文本数据的分类。文本分类是根据文本数据的内容对其进行分类的问题。文本分类最重要的部分是特征工程:从原始文本数据为机器学习模型创建特征的过程。
在本文中,我将解释不同的方法来分析文本并提取可用于构建分类模型的特征。我将介绍一些有用的Python代码。
这些代码可以很容易地应用于其他类似的情况(只需复制、粘贴、运行),并且我加上了注释,以便你可以理解示例(链接到下面的完整代码)。
https://github.com/mdipietro09/DataScience_ArtificialIntelligence_Utils/blob/master/deep_learning_natural_language_processing/text_classification_example.ipynb
我将使用“新闻类别数据集”(以下链接),其中向你提供从赫芬顿邮报获得的2012年至2018年的新闻标题,并要求你使用正确的类别对其进行分类。
https://www.kaggle.com/rmisra/news-category-dataset
特别是,我将通过:
环境设置:导入包并读取数据。
语言检测:了解哪些自然语言数据在其中。
文本预处理:文本清理和转换。
长度分析:用不同的指标来衡量。
情绪分析:判断一篇文章是正面的还是负面的。
命名实体识别:带有预定义类别(如人名、组织、位置)的标识文本。
词频:找出最重要的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添加一个情感特征:
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”可以分解为:
我将以政治新闻为例说明如何计算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/