語言檢測,文本清理,長度測量,情緒分析,命名實體識別,n字頻率,詞向量,主題建模
前言
在本文中,我將使用NLP和Python解釋如何分析文本數據并為機器學習模型提取特征。
NLP(自然語言處理)是人工智能的一個領域,研究計算機和人類語言之間的交互,特別是如何編程計算機來處理和分析大量的自然語言數據。NLP經常被應用于文本數據的分類。文本分類是根據文本數據的內容給文本數據分配類別的問題。文本分類最重要的部分是特征工程:從原始文本數據為機器學習模型創建特征的過程。
在本文中,我將解釋分析文本和提取可用于構建分類模型的特征的不同方法。我將展示一些有用的Python代碼,它們可以很容易地應用于其他類似的情況(只是復制、粘貼、運行),并帶注釋遍歷每一行代碼,以便復制這個示例(鏈接到下面的完整代碼)。
我將使用“新聞類別數據集”(鏈接如下),在該數據集中,你將獲得從《赫芬頓郵報》獲得的2012年至2018年的新聞標題,并要求你按照正確的類別對它們進行分類。
https://www.kaggle.com/rmisra/news-category-dataset
具體來說,主要講的是:
• 環境設置:導入包并讀取數據。
• 語言檢測:了解數據屬于哪種自然語言。
• 文本預處理:文本清洗和轉換。
• 長度分析:用不同的度量方法測量。
• 情緒分析:確定文本是積極的還是消極的。
• 命名實體識別:帶有預定義類別(如人名、組織、位置)的標記文本。
• 詞頻:找出最重要的n字。
• 字向量:把字轉換成數字。
• 主題建模:從語料庫中提取主要主題。
環境設置
首先,我需要導入以下庫。
## 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'[^ws]', '', 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'[^ws]', '', 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模型encoreweb_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翻譯組