如何用Python處理自然語言?(Spacy與Word Embedding)
來自專欄玉樹芝蘭
本文教你用簡單易學的工業級Python自然語言處理軟體包Spacy,對自然語言文本做詞性分析、命名實體識別、依賴關係刻畫,以及詞嵌入向量的計算和可視化。
盲維
我總愛重複一句芒格愛說的話:
To the one with a hammer, everything looks like a nail. (手中有錘,看什麼都像釘)
這句話是什麼意思呢?
就是你不能只掌握數量很少的方法、工具。
否則你的認知會被自己能力框住。不只是存在盲點,而是存在「盲維」。
你會嘗試用不合適的方法解決問題(還自詡「一招鮮,吃遍天」),卻對原本合適的工具視而不見。
結果可想而知。
所以,你得在自己的工具箱裡面,多放一些兵刃。
最近我又對自己的學生,念叨芒格這句話。
因為他們開始做實際研究任務的時候,一遇到自然語言處理(Natural Language Processing, NLP),腦子裡想到的就是詞雲、情感分析和LDA主題建模。
為什麼?
因為我的專欄和公眾號里,自然語言處理部分,只寫過這些內容。
你如果認為,NLP只能做這些事,就大錯特錯了。
看看這段視頻,你大概就能感受到目前自然語言處理的前沿,已經到了哪裡。
當然,你手頭擁有的工具和數據,尚不能做出Google展示的黑科技效果。
但是,現有的工具,也足可以讓你對自然語言文本,做出更豐富的處理結果。
科技的發展,蓬勃迅速。
除了咱們之前文章中已介紹過的結巴分詞、SnowNLP和TextBlob,基於Python的自然語言處理工具還有很多,例如 NLTK 和 gensim 等。
我無法幫你一一熟悉,你可能用到的所有自然語言處理工具。
但是咱們不妨開個頭,介紹一款叫做 Spacy 的 Python 工具包。
剩下的,自己舉一反三。
工具
Spacy 的 Slogan,是這樣的:
Industrial-Strength Natural Language Processing. (工業級別的自然語言處理)
這句話聽上去,是不是有些狂妄啊?
不過人家還是用數據說話的。
數據采自同行評議(Peer-reviewed)學術論文:
看完上述的數據分析,我們大致對於Spacy的性能有些了解。
但是我選用它,不僅僅是因為它「工業級別」的性能,更是因為它提供了便捷的用戶調用介面,以及豐富、詳細的文檔。
僅舉一例。
上圖是Spacy上手教程的第一頁。
可以看到,左側有簡明的樹狀導航條,中間是詳細的文檔,右側是重點提示。
僅安裝這一項,你就可以點擊選擇操作系統、Python包管理工具、Python版本、虛擬環境和語言支持等標籤。網頁會動態為你生成安裝的語句。
這種設計,對新手用戶,很有幫助吧?
Spacy的功能有很多。
從最簡單的詞性分析,到高階的神經網路模型,五花八門。
篇幅所限,本文只為你展示以下內容:
- 詞性分析
- 命名實體識別
- 依賴關係刻畫
- 詞嵌入向量的近似度計算
- 詞語降維和可視化
學完這篇教程,你可以按圖索驥,利用Spacy提供的詳細文檔,自學其他自然語言處理功能。
我們開始吧。
環境
請點擊這個鏈接(http://t.cn/R35fElv),直接進入咱們的實驗環境。
對,你沒看錯。
你不需要在本地計算機安裝任何軟體包。只要有一個現代化瀏覽器(包括Google Chrome, Firefox, Safari和Microsoft Edge等)就可以了。全部的依賴軟體,我都已經為你準備好了。
打開鏈接之後,你會看見這個頁面。
不同於之前的 Jupyter Notebook,這個界面來自 Jupyter Lab。
你可以將它理解為 Jupyter Notebook 的增強版,它具備以下特徵:
- 代碼單元直接滑鼠拖動;
- 一個瀏覽器標籤,可打開多個Notebook,而且分別使用不同的Kernel;
- 提供實時渲染的Markdown編輯器;
- 完整的文件瀏覽器;
- CSV數據文件快速瀏覽
- ……
圖中左側分欄,是工作目錄下的全部文件。
右側打開的,是咱們要使用的ipynb文件。
根據咱們的講解,請你逐條執行,觀察結果。
我們說一說樣例文本數據的來源。
如果你之前讀過我的其他自然語言處理方面的教程,應該記得這部電視劇。
對,就是"Yes, Minister"。
出於對這部80年代英國喜劇的喜愛,我還是用維基百科上"Yes, Minister"的介紹內容,作為文本分析樣例。
下面,我們就正式開始,一步步執行程序代碼了。
我建議你先完全按照教程跑一遍,運行出結果。
如果一切正常,再將其中的數據,替換為你自己感興趣的內容。
之後,嘗試打開一個空白 ipynb 文件,根據教程和文檔,自己敲代碼,並且嘗試做調整。
這樣會有助於你理解工作流程和工具使用方法。
實踐
我們從維基百科頁面的第一自然段中,摘取部分語句,放到text變數裡面。
text = "The sequel, Yes, Prime Minister, ran from 1986 to 1988. In total there were 38 episodes, of which all but one lasted half an hour. Almost all episodes ended with a variation of the title of the series spoken as the answer to a question posed by the same character, Jim Hacker. Several episodes were adapted for BBC Radio, and a stage play was produced in 2010, the latter leading to a new television series on UKTV Gold in 2013."
顯示一下,看是否正確存儲。
text
The sequel, Yes, Prime Minister, ran from 1986 to 1988. In total there were 38 episodes, of which all but one lasted half an hour. Almost all episodes ended with a variation of the title of the series spoken as the answer to a question posed by the same character, Jim Hacker. Several episodes were adapted for BBC Radio, and a stage play was produced in 2010, the latter leading to a new television series on UKTV Gold in 2013.
沒問題了。
下面我們讀入Spacy軟體包。
import spacy
我們讓Spacy使用英語模型,將模型存儲到變數nlp中。
nlp = spacy.load(en)
下面,我們用nlp模型分析咱們的文本段落,將結果命名為doc。
doc = nlp(text)
我們看看doc的內容。
doc
The sequel, Yes, Prime Minister, ran from 1986 to 1988. In total there were 38 episodes, of which all but one lasted half an hour. Almost all episodes ended with a variation of the title of the series spoken as the answer to a question posed by the same character, Jim Hacker. Several episodes were adapted for BBC Radio, and a stage play was produced in 2010, the latter leading to a new television series on UKTV Gold in 2013.
好像跟剛才的text內容沒有區別呀?不還是這段文本嗎?
別著急,Spacy只是為了讓我們看著舒服,所以只列印出來文本內容。
其實,它在後台,已經對這段話進行了許多層次的分析。
不信?
我們來試試,讓Spacy幫我們分析這段話中出現的全部詞例(token)。
for token in doc: print(" + token.text + ")
你會看到,Spacy為我們輸出了一長串列表。
"The""sequel"",""Yes"",""Prime""Minister"",""ran""from""1986""to""1988"".""In""total""there""were""38""episodes"",""of""which""all""but""one""lasted""half""an""hour"".""Almost""all""episodes""ended""with""a""variation""of""the""title""of""the""series""spoken""as""the""answer""to""a""question""posed""by""the""same""character"",""Jim""Hacker"".""Several""episodes""were""adapted""for""BBC""Radio"",""and""a""stage""play""was""produced""in""2010"",""the""latter""leading""to""a""new""television""series""on""UKTV""Gold""in""2013""."
你可能不以為然——這有什麼了不起?
英語本來就是空格分割的嘛!我自己也能編個小程序,以空格分段,依次列印出這些內容來!
別忙,除了詞例內容本身,Spacy還把每個詞例的一些屬性信息,進行了處理。
下面,我們只對前10個詞例(token),輸出以下內容:
- 文本
- 索引值(即在原文中的定位)
- 詞元(lemma)
- 是否為標點符號
- 是否為空格
- 詞性
- 標記
for token in doc[:10]: print("{0} {1} {2} {3} {4} {5} {6} {7}".format( token.text, token.idx, token.lemma_, token.is_punct, token.is_space, token.shape_, token.pos_, token.tag_ ))
結果為:
The 0 the False False Xxx DET DTsequel 4 sequel False False xxxx NOUN NN, 10 , True False , PUNCT ,Yes 12 yes False False Xxx INTJ UH, 15 , True False , PUNCT ,Prime 17 prime False False Xxxxx PROPN NNPMinister 23 minister False False Xxxxx PROPN NNP, 31 , True False , PUNCT ,ran 33 run False False xxx VERB VBDfrom 37 from False False xxxx ADP IN
看到Spacy在後台默默為我們做出的大量工作了吧?
下面我們不再考慮全部詞性,只關注文本中出現的實體(entity)辭彙。
for ent in doc.ents: print(ent.text, ent.label_)
1986 to 1988 DATE38 CARDINALone CARDINALhalf an hour TIMEJim Hacker PERSONBBC Radio ORG2010 DATEUKTV Gold ORG2013 DATE
在這一段文字中,出現的實體包括日期、時間、基數(Cardinal)……Spacy不僅自動識別出了Jim Hacker為人名,還正確判定BBC Radio和UKTV Gold為機構名稱。
如果你平時的工作,需要從海量評論里篩選潛在競爭產品或者競爭者,那看到這裡,有沒有一點兒靈感呢?
執行下面這段代碼,看看會發生什麼:
from spacy import displacydisplacy.render(doc, stylex=ent, jupyter=True)
如上圖所示,Spacy幫我們把實體識別的結果,進行了直觀的可視化。不同類別的實體,還採用了不同的顏色加以區分。
把一段文字拆解為語句,對Spacy而言,也是小菜一碟。
for sent in doc.sents: print(sent)
The sequel, Yes, Prime Minister, ran from 1986 to 1988.In total there were 38 episodes, of which all but one lasted half an hour.Almost all episodes ended with a variation of the title of the series spoken as the answer to a question posed by the same character, Jim Hacker.Several episodes were adapted for BBC Radio, and a stage play was produced in 2010, the latter leading to a new television series on UKTV Gold in 2013.
注意這裡,doc.sents並不是個列表類型。
doc.sents
<generator at 0x116e95e18>
所以,假設我們需要從中篩選出某一句話,需要先將其轉化為列表。
list(doc.sents)
[The sequel, Yes, Prime Minister, ran from 1986 to 1988., In total there were 38 episodes, of which all but one lasted half an hour., Almost all episodes ended with a variation of the title of the series spoken as the answer to a question posed by the same character, Jim Hacker., Several episodes were adapted for BBC Radio, and a stage play was produced in 2010, the latter leading to a new television series on UKTV Gold in 2013.]
下面要展示的功能,分析範圍局限在第一句話。
我們將其抽取出來,並且重新用nlp模型處理,存入到新的變數newdoc中。
newdoc = nlp(list(doc.sents)[0].text)
對這一句話,我們想要搞清其中每一個詞例(token)之間的依賴關係。
for token in newdoc: print("{0}/{1} <--{2}-- {3}/{4}".format( token.text, token.tag_, token.dep_, token.head.text, token.head.tag_))
The/DT <--det-- sequel/NNsequel/NN <--nsubj-- ran/VBD,/, <--punct-- sequel/NNYes/UH <--intj-- sequel/NN,/, <--punct-- sequel/NNPrime/NNP <--compound-- Minister/NNPMinister/NNP <--appos-- sequel/NN,/, <--punct-- sequel/NNran/VBD <--ROOT-- ran/VBDfrom/IN <--prep-- ran/VBD1986/CD <--pobj-- from/INto/IN <--prep-- from/IN1988/CD <--pobj-- to/IN./. <--punct-- ran/VBD
很清晰,但是列表的方式,似乎不大直觀。
那就讓Spacy幫我們可視化吧。
displacy.render(newdoc, stylex=dep, jupyter=True, options={distance: 90})
結果如下:
這些依賴關係鏈接上的辭彙,都代表什麼?
如果你對語言學比較了解,應該能看懂。
不懂?查查字典嘛。
跟語法書對比一下,看看Spacy分析得是否準確。
前面我們分析的,屬於語法層級。
下面我們看語義。
我們利用的工具,叫做詞嵌入(word embedding)模型。
之前的文章《如何用Python從海量文本抽取主題?》中,我們提到過如何把文字表達成電腦可以看懂的數據。
文中處理的每一個單詞,都僅僅對應著詞典裡面的一個編號而已。你可以把它看成你去營業廳辦理業務時領取的號碼。
它只提供了先來後到的順序信息,跟你的職業、學歷、性別統統沒有關係。
我們將這樣過於簡化的信息輸入,計算機對於詞義的了解,也必然少得可憐。
例如給你下面這個式子:
? - woman = king - queen
只要你學過英語,就不難猜到這裡大概率應該填寫「man」。
但是,如果你只是用了隨機的序號來代表辭彙,又如何能夠猜到這裡正確的填詞結果呢?
幸好,在深度學習領域,我們可以使用更為順手的單詞向量化工具——詞嵌入(word embeddings )。
如上圖這個簡化示例,詞嵌入把單詞變成多維空間上面的向量。
這樣,詞語就不再是冷冰冰的字典編號,而是具有了意義。
使用詞嵌入模型,我們需要Spacy讀取一個新的文件。
nlp = spacy.load(en_core_web_lg)
為測試讀取結果,我們讓Spacy列印「minister」這個單詞對應的向量取值。
print(nlp.vocab[minister].vector)
可以看到,每個單詞,用總長度為300的浮點數組成向量來表示。
順便說一句,Spacy讀入的這個模型,是採用word2vec,在海量語料上訓練的結果。
我們來看看,此時Spacy的語義近似度判別能力。
這裡,我們將4個變數,賦值為對應單詞的向量表達結果。
dog = nlp.vocab["dog"]cat = nlp.vocab["cat"]apple = nlp.vocab["apple"]orange = nlp.vocab["orange"]
我們看看「狗」和「貓」的近似度:
dog.similarity(cat)
0.80168545
嗯,都是寵物,近似度高,可以接受。
下面看看「狗」和「蘋果」。
dog.similarity(apple)
0.26339027
一個動物,一個水果,近似度一下子就跌落下來了。
「狗」和「橘子」呢?
dog.similarity(orange)
0.2742508
可見,相似度也不高。
那麼「蘋果」和「橘子」之間呢?
apple.similarity(orange)
0.5618917
水果間近似度,遠遠超過水果與動物的相似程度。
測試通過。
看來Spacy利用詞嵌入模型,對語義有了一定的理解。
下面為了好玩,我們來考考它。
這裡,我們需要計算詞典中可能不存在的向量,因此Spacy自帶的similarity()
函數,就顯得不夠用了。
我們從scipy中,找到相似度計算需要用到的餘弦函數。
from scipy.spatial.distance import cosine
對比一下,我們直接代入「狗」和「貓」的向量,進行計算。
1 - cosine(dog.vector, cat.vector)
0.8016855120658875
除了保留數字外,計算結果與Spacy自帶的similarity()
運行結果沒有差別。
我們把它做成一個小函數,專門處理向量輸入。
def vector_similarity(x, y): return 1 - cosine(x, y)
用我們自編的相似度函數,測試一下「狗」和「蘋果」。
vector_similarity(dog.vector, apple.vector)
0.2633902430534363
與剛才的結果對比,也是一致的。
我們要表達的,是這個式子:
? - woman = king - queen
我們把問號,稱為 guess_word
所以
guess_word = king - queen + woman
我們把右側三個單詞,一般化記為 words。編寫下面函數,計算guess_word
取值。
def make_guess_word(words): [first, second, third] = words return nlp.vocab[first].vector - nlp.vocab[second].vector + nlp.vocab[third].vector
下面的函數就比較暴力了,它其實是用我們計算的 guess_word
取值,和字典中全部詞語一一核對近似性。把最為近似的10個候選單詞列印出來。
def get_similar_word(words, scope=nlp.vocab): guess_word = make_guess_word(words) similarities = [] for word in scope: if not word.has_vector: continue similarity = vector_similarity(guess_word, word.vector) similarities.append((word, similarity)) similarities = sorted(similarities, key=lambda item: -item[1]) print([word[0].text for word in similarities[:10]])
好了,遊戲時間開始。
我們先看看:
? - woman = king - queen
即:
guess_word = king - queen + woman
輸入右側詞序列:
words = ["king", "queen", "woman"]
然後執行對比函數:
get_similar_word(words)
這個函數運行起來,需要一段時間。請保持耐心。
運行結束之後,你會看到如下結果:
[MAN, Man, mAn, MAn, MaN, man, mAN, WOMAN, womAn, WOman]
原來字典裡面,「男人」(man)這個辭彙有這麼多的變形啊。
但是這個例子太經典了,我們嘗試個新鮮一些的:
? - England = Paris - London
即:
guess_word = Paris - London + England
對你來講,絕對是簡單的題目。左側國別,右側首都,對應來看,自然是巴黎所在的法國(France)。
問題是,Spacy能猜對嗎?
我們把這幾個單詞輸入。
words = ["Paris", "London", "England"]
讓Spacy來猜:
get_similar_word(words)
[france, FRANCE, France, Paris, paris, PARIS, EUROPE, EUrope, europe, Europe]
結果很令人振奮,前三個都是「法國」(France)。
下面我們做一個更有趣的事兒,把詞向量的300維的高空間維度,壓縮到一張紙(二維)上,看看詞語之間的相對位置關係。
首先我們需要讀入numpy軟體包。
import numpy as np
我們把詞嵌入矩陣先設定為空。一會兒慢慢填入。
embedding = np.array([])
需要演示的單詞列表,也先空著。
word_list = []
我們再次讓Spacy遍歷「Yes, Minister」維基頁面中摘取的那段文字,加入到單詞列表中。注意這次我們要進行判斷:
- 如果是標點,丟棄;
- 如果辭彙已經在詞語列表中,丟棄。
for token in doc: if not(token.is_punct) and not(token.text in word_list): word_list.append(token.text)
看看生成的結果:
word_list
[The, sequel, Yes, Prime, Minister, ran, from, 1986, to, 1988, In, total, there, were, 38, episodes, of, which, all, but, one, lasted, half, an, hour, Almost, ended, with, a, variation, the, title, series, spoken, as, answer, question, posed, by, same, character, Jim, Hacker, Several, adapted, for, BBC, Radio, and, stage, play, was, produced, in, 2010, latter, leading, new, television, on, UKTV, Gold, 2013]
檢查了一下,一長串(63個)詞語列表中,沒有出現標點。一切正常。
下面,我們把每個辭彙對應的空間向量,追加到詞嵌入矩陣中。
for word in word_list: embedding = np.append(embedding, nlp.vocab[word].vector)
看看此時詞嵌入矩陣的維度。
embedding.shape
(18900,)
可以看到,所有的向量內容,都被放在了一個長串上面。這顯然不符合我們的要求,我們將不同的單詞對應的詞向量,拆解到不同行上面去。
embedding = embedding.reshape(len(word_list), -1)
再看看變換後詞嵌入矩陣的維度。
embedding.shape
(63, 300)
63個辭彙,每個長度300,這就對了。
下面我們從scikit-learn
軟體包中,讀入TSNE模塊。
from sklearn.manifold import TSNE
我們建立一個同名小寫的tsne,作為調用對象。
tsne = TSNE()
tsne的作用,是把高維度的詞向量(300維)壓縮到二維平面上。我們執行這個轉換過程:
low_dim_embedding = tsne.fit_transform(embedding)
現在,我們手裡擁有的 low_dim_embedding
,就是63個辭彙降低到二維的向量表示了。
我們讀入繪圖工具包。
import matplotlib.pyplot as plt%pylab inline
下面這個函數,用來把二維向量的集合,繪製出來。
如果你對該函數內容細節不理解,沒關係。因為我還沒有給你系統介紹過Python下的繪圖功能。
好在這裡我們只要會調用它,就可以了。
def plot_with_labels(low_dim_embs, labels, filename=tsne.pdf): assert low_dim_embs.shape[0] >= len(labels), "More labels than embeddings" plt.figure(figsize=(18, 18)) # in inches for i, label in enumerate(labels): x, y = low_dim_embs[i, :] plt.scatter(x, y) plt.annotate(label, xy=(x, y), xytext=(5, 2), textcoords=offset points, ha=right, va=bottom) plt.savefig(filename)
終於可以進行降維後的詞向量可視化了。
請執行下面這條語句:
plot_with_labels(low_dim_embedding, word_list)
你會看到這樣一個圖形。
請注意觀察圖中的幾個部分:
- 年份
- 同一單詞的大小寫形式
- Radio 和 television
- a 和 an
看看有什麼規律沒有?
我發現了一個有意思的現象——每次運行tsne,產生的二維可視化圖都不一樣!
不過這也正常,因為這段話之中出現的單詞,並非都有預先訓練好的向量。
這樣的單詞,被Spacy進行了隨機化等處理。
因此,每一次生成高維向量,結果都不同。不同的高維向量,壓縮到二維,結果自然也會有區別。
問題來了,如果我希望每次運行的結果都一致,該如何處理呢?
這個問題,作為課後思考題,留給你自行解答。
細心的你可能發現了,執行完最後一條語句後,頁面左側邊欄文件列表中,出現了一個新的pdf文件。
這個pdf,就是你剛剛生成的可視化結果。你可以雙擊該文件名稱,在新的標籤頁中查看。
看,就連pdf文件,Jupyter Lab也能正確顯示。
下面,是練習時間。
請把ipynb出現的文本內容,替換為你感興趣的段落和辭彙,再嘗試運行一次吧。
源碼
執行了全部代碼,並且嘗試替換了自己需要分析的文本,成功運行後,你是不是很有成就感?
你可能想要更進一步挖掘Spacy的功能,並且希望在本地復現運行環境與結果。
沒問題,請使用這個鏈接(http://t.cn/R35MIKh)下載本文用到的全部源代碼和運行環境配置文件(Pipenv)壓縮包。
如果你知道如何使用github,也歡迎用這個鏈接(http://t.cn/R35MEqk)訪問對應的github repo,進行clone或者fork等操作。
當然,要是能給我的repo加一顆星,就更好了。
謝謝!
小結
本文利用Python自然語言處理工具包Spacy,非常簡要地為你演示了以下NLP功能:
- 詞性分析
- 命名實體識別
- 依賴關係刻畫
- 詞嵌入向量的近似度計算
- 詞語降維和可視化
希望學過之後,你成功地在工具箱里又添加了一件趁手的兵器。
願它在以後的研究和工作中,助你披荊斬棘,馬到成功。
加油!
討論
你之前做過自然語言處理項目嗎?使用過哪些工具包?除了本文介紹的這些基本功能外,你覺得還有哪些NLP功能是非常基礎而重要的?你是如何學習它們的呢?歡迎留言,把你的經驗和思考分享給大家,我們一起交流討論。
如果你對我的文章感興趣,歡迎點贊,並且微信關注和置頂我的公眾號「玉樹芝蘭」(nkwangshuyi)。
如果本文可能對你身邊的親友有幫助,也歡迎你把本文通過微博或朋友圈分享給他們。讓他們一起參與到我們的討論中來。
延伸閱讀
如何高效入門數據科學?
推薦閱讀: