摘要
在本文中,我將使用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 pdimport collectionsimport json## 繪圖import matplotlib.pyplot as pltimport seaborn as snsimport wordcloud## 文本處理import reimport nltk## 語言檢測import langdetect ## 情感分析from textblob import TextBlob## 命名實體識別import spacy## 詞頻from sklearn import feature_extraction, manifold## word embeddingimport 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)。
## 創建dtfdtf = 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 = 3tags_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"] = 1dtf_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 = txtfor 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)## unigramsdic_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)## bigramsdic_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)## 將單詞映射到idid2word = gensim.corpora.Dictionary(lst_corpus)## 創建詞典 word:freqdic_corpus = [id2word.doc2bow(word) for word in lst_corpus] ## 訓練LDAlda_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'])## plotfig, 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基礎知識。