语言检测,文本清理,长度测量,情绪分析,命名实体识别,n字频率,词向量,主题建模
在本文中,我将使用NLP和Python解释如何分析文本数据并为机器学习模型提取特征。
NLP(自然语言处理)是人工智能的一个领域,研究计算机和人类语言之间的交互,特别是如何编程计算机来处理和分析大量的自然语言数据。NLP经常被应用于文本数据的分类。文本分类是根据文本数据的内容给文本数据分配类别的问题。文本分类最重要的部分是特征工程:从原始文本数据为机器学习模型创建特征的过程。
在本文中,我将解释分析文本和提取可用于构建分类模型的特征的不同方法。我将展示一些有用的Python代码,它们可以很容易地应用于其他类似的情况(只是复制、粘贴、运行),并带注释遍历每一行代码,以便复制这个示例(链接到下面的完整代码)。
我将使用“新闻类别数据集”(链接如下),在该数据集中,你将获得从《赫芬顿邮报》获得的2012年至2018年的新闻标题,并要求你按照正确的类别对它们进行分类。
https://www.kaggle.com/rmisra/news-category-dataset
具体来说,主要讲的是:
首先,我需要导入以下库。
## for data
import pandas as pd
import collections
import json## for plotting
import matplotlib.pyplot as plt
import seaborn as sns
import wordcloud## for text processing
import re
import nltk## for language detection
import langdetect ## for sentiment
from textblob import TextBlob## for ner
import spacy## for vectorizer
from sklearn import feature_extraction, manifold## for word embedding
import gensim.downloader as gensim_api## for topic modeling
import gensim
数据集包含在一个json文件中,因此我将首先将其读入一个带有json包的字典列表,然后将其转换为一个pandas Dataframe。
lst_dics = []
with open('data.json', mode='r', errors='ignore') as json_file:
for dic in json_file:
lst_dics.append( json.loads(dic) )## print the first one
lst_dics[0]
原始数据集包含30多个类别,但出于本教程的目的,我将使用其中3个类别的子集:娱乐、政治和技术。
## create dtf
dtf = pd.DataFrame(lst_dics)## filter categories
dtf = dtf[ dtf["category"].isin(['ENTERTAINMENT','POLITICS','TECH']) ][["category","headline"]]## rename columns
dtf = dtf.rename(columns={"category":"y", "headline":"text"})## print 5 random rows
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()
数据集是不平衡的:与其他新闻相比,科技新闻的比例真的很小。这可能是建模过程中的一个问题,数据集的重新取样可能会很有用。
现在已经设置好了,我将从清理数据开始,然后从原始文本中提取不同的见解,并将它们添加为dataframe的新列。这个新信息可以用作分类模型的潜在特征。
首先,我想确保我使用的是同一种语言,并且使用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()
dataframe现在有一个新列。使用相同的代码从以前,我可以看到有多少不同的语言:
即使有不同的语言,英语也是主要的。所以我打算用英语过滤新闻。
dtf = dtf[dtf["lang"]=="en"]
数据预处理是准备原始数据使其适合于机器学习模型的阶段。对于NLP,这包括文本清理、停止词删除、词干填塞和词元化。
文本清理步骤根据数据类型和所需任务的不同而不同。通常,字符串被转换为小写字母,并且在文本被标记之前删除标点符号。标记化是将一个字符串分割成一个字符串列表(或“记号”)的过程。
让我们以第一个新闻标题为例:
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”)。
既然我们有了所有有用的标记,我们就可以应用单词转换了。词根化和词元化都产生单词的词根形式。区别在于stem可能不是一个实际的单词,而lemma是一个实际的语言单词(词干词干通常更快)。这些算法都由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”中发生了变化,而“offici”不是一个单词,它是通过删除后缀“-al”而创建的。
我将把所有这些预处理步骤放入一个函数中,并将其应用于整个数据集。
'''
Preprocess a string.
:parameter
:param text: string - name of column containing text
:param lst_stopwords: list - list of stopwords to remove
:param flg_stemm: bool - whether stemming is to be applied
:param flg_lemm: bool - whether lemmitisation is to be applied
:return
cleaned text
'''
def utils_preprocess_text(text, flg_stemm=False, flg_lemm=True, lst_stopwords=None):
## clean (convert to lowercase and remove punctuations and characters and then strip)
text = re.sub(r'[^\w\s]', '', str(text).lower().strip())
## Tokenize (convert from string to list)
lst_text = text.split() ## remove Stopwords
if lst_stopwords is not None:
lst_text = [word for word in lst_text if word not in
lst_stopwords]
## Stemming (remove -ing, -ly, ...)
if flg_stemm == True:
ps = nltk.stem.porter.PorterStemmer()
lst_text = [ps.stem(word) for word in lst_text]
## Lemmatisation (convert the word into root word)
if flg_lemm == True:
lem = nltk.stem.wordnet.WordNetLemmatizer()
lst_text = [lem.lemmatize(word) for word in lst_text]
## back to string from list
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()
这3个类别的长度分布相似。这里,密度图非常有用,因为样本大小不同。
情绪分析是通过数字或类对文本数据进行主观情绪表征。由于自然语言的模糊性,情绪计算是自然语言处理的难点之一。例如,短语“这是如此糟糕,但它是好的”有不止一种解释。一个模型可以给“好”这个词赋予一个积极的信号,给“坏”这个词赋予一个消极的信号,从而产生中性的情绪。这是因为上下文是未知的。
最好的方法是训练你自己的情绪模型,让它适合你的数据。如果没有足够的时间或数据,可以使用预先训练好的模型,比如Textblob和Vader。基于NLTK的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])
类别和情绪之间是否存在某种模式?
除了政治新闻偏于负面,科技新闻偏于正面,大多数新闻标题的情绪都是中性的。
NER (named -entity recognition)是将非结构化文本中提到的命名实体用预定义的类别(如人名、组织、位置、时间表达式、数量等)标记的过程。
训练一个NER模型是非常耗时的,因为它需要一个非常丰富的数据集。幸运的是已经有人替我们做了这项工作。最好的开源NER工具之一是SpaCy。它提供了能够识别几种实体类别的不同NLP模型。
我将用SpaCy模型en_core_web_lg(训练于web数据的英语大模型)来举例说明我们通常的标题(原始文本,非预处理):
## call model
ner = spacy.load("en_core_web_lg")## tag text
txt = dtf["text"].iloc[0]
doc = ner(txt)## display result
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
## tag text and exctract tags into a list
dtf["tags"] = dtf["text"].apply(lambda x: [(tag.text, tag.label_)
for tag in ner(x).ents] )## utils function to count the element of a list
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
## count tags
dtf["tags"] = dtf["tags"].apply(lambda x: utils_lst_count(x))
## utils function create new column for each tag category
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
## extract features
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))
## print result
dtf.head()
现在我们可以有一个关于标签类型分布的宏视图。让我们以ORG标签(公司和组织)为例:
为了更深入地进行分析,我们需要解压缩在前面代码中创建的列“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 Smith”替换为“Will_Smith”,这样它就不会受到删除停止词的影响。因为遍历数据集中的所有文本以更改名称是不可能的,所以让我们使用SpaCy来实现这一点。我们知道,SpaCy可以识别一个人的名字,因此我们可以使用它进行名字检测,然后修改字符串。
## predict wit NER
txt = dtf["text"].iloc[0]
entities = ner(txt).ents## tag text
tagged_txt = txt
for tag in entities:
tagged_txt = re.sub(tag.text, "_".join(tag.text.split()),
tagged_txt) ## show result
print(tagged_txt)
到目前为止,我们已经了解了如何通过分析和处理整个文本来进行特征工程。现在我们来看看单个单词的重要性,通过计算n个字母的频率。n-gram是来自给定文本样本的n项连续序列。当n元数据的大小为1时,称为单元数据(大小为2时称为双元数据)。
例如,短语“I like this article”可以分解为:
4个字母:“I”,“like”,“this”,“article”
3双字母:“I like”、“like this”、“this article”
本文以政治新闻为样本,介绍如何计算单、双信息频数。
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个字母只出现在一个类别中,这些都可能成为新的特色。更费力的方法是对整个语料库进行向量化并使用所有单词作为特征(词包方法)。
现在我将向您展示如何将单词频率作为一个特性添加到您的dataframe中。我们只需要Scikit-learn中的CountVectorizer,这是Python中最流行的机器学习库之一。矢量化器将文本文档集合转换为令牌计数矩阵。我将用3个n-g来举个例子:“box office”(娱乐圈经常用)、“republican”(政治圈经常用)、“apple”(科技圈经常用)。
lst_words = ["box office", "republican", "apple"]## count
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)## add the new features as columns
dtf = pd.concat([dtf, dtf_X.set_index(dtf.index)], axis=1)
dtf.head()
可视化相同信息的一种好方法是使用单词云,其中每个标记的频率用字体大小和颜色显示。
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,我将加载一个预先训练好的Global vector模型。Global vector是一种无监督学习算法,用于获取大小为300的单词的向量表示。
nlp = gensim_api.load("glove-wiki-gigaword-300")
我们可以使用这个对象将单词映射到矢量:
word = "love"
nlp[word]
nlp[word].shape
现在让我们看看最接近的单词向量是什么,或者换句话说,是那些经常出现在相似上下文中的单词。为了在二维空间中画出向量,我需要把维数从300减少到2。我用的是scikit学习的t分布随机邻接嵌入。t-SNE是一种可视化高维数据的工具,它将数据点之间的相似性转换为联合概率。
## find closest vectors
labels, X, x, y = [], [], [], []
for t in nlp.most_similar(word, topn=20):
X.append(nlp[t[0]])
labels.append(t[0])## reduce dimensions
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])## plot
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')## add center
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(Latent Dirichlet Allocation)提取主题:生成统计模型,允许使用未观察到的组来解释观察集,这些组可以解释为什么数据的某些部分是相似的。基本上,文档被表示为潜在主题的随机混合,其中每个主题的特征是分布在单词上。
让我们看看我们可以从科技新闻中提取哪些主题。我需要指定模型必须聚类的主题数量,我将尝试使用3个:
y = "TECH"
corpus = dtf[dtf["y"]==y]["text_clean"]
## pre-process corpus
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)## map words to an id
id2word = gensim.corpora.Dictionary(lst_corpus)## create dictionary word:freq
dic_corpus = [id2word.doc2bow(word) for word in lst_corpus] ## train 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)
## output
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进行命名实体识别。最后,我解释了使用scikiti - learning的传统词频方法与使用Gensim的现代语言模型的区别。
作者:Mauro Di Pietro
deephub翻译组