提到資料科學,我們想到的都是數字的統計分析,但如今需要對很多非結構文字進行量化分析。本文將以《聖經》為例,用 spaCy Python 庫把三個最常見的 NLP 工具(理解詞性標註、依存分析、實體命名識別)結合起來分析文字,以找出《聖經》中的主要人物及其動作。
引言
在思考資料科學的時候,我們常常想起數字的統計分析。但是,各種組織機構越來越頻繁地生成大量可以被量化分析的非結構文字。一些例子如社交網路評論、產品評價、電子郵件以及面試記錄。
就文字分析而言,資料科學家們通常使用自然語言處理(NLP)。我們將在這篇部落格中涵蓋 3 個常見的 NLP 任務,並且研究如何將它結合起來分析文字。這 3 個任務分別是:
1. 詞性標註——這個詞是什麼型別?
2. 依存分析——該詞和句子中的其他詞是什麼關係?
3. 命名實體識別——這是一個專有名詞嗎?
我們將使用 spaCy Python 庫把這三個工具結合起來,以發現誰是《聖經》中的主要角色以及他們都幹了什麼。我們可以從那裡發現是否可以對這種結構化資料進行有趣的視覺化。
這種方法可以應用於任何問題,在這些問題中你擁有大量文件集合,你想了解哪些是主要實體,它們出現在文件中的什麼位置,以及它們在做什麼。例如,DocumentCloud 在其「View Entities」分析選項中使用了類似的方法。
分詞 & 詞性標註
從文字中提取意思的一種方法是分析單個單詞。將文字拆分為單詞的過程叫做分詞(tokenization)——得到的單詞稱為分詞(token)。標點符號也是分詞。句子中的每個分詞都有幾個可以用來分析的屬性。詞性標註就是一個例子:名詞可以是一個人,地方或者事物;動詞是動作或者發生;形容詞是修飾名詞的詞。利用這些屬性,通過統計最常見的名詞、動詞和形容詞,能夠直接地建立一段文字的摘要。
使用 spaCy,我們可以為一段文字進行分詞,並訪問每個分詞的詞性。作為一個應用示例,我們將使用以下程式碼對上一段文字進行分詞,並統計最常見名詞出現的次數。我們還會對分詞進行詞形還原,這將為詞根形式賦予一個單詞,以幫助我們跨單詞形式進行標準化。
from collections import Counter
import spacy
from tabulate import tabulate
nlp = spacy.load('en_core_web_lg')
text = """
One way to extract meaning from text is to analyze individual words.
The processes of breaking up a text into words is called tokenization --
the resulting words are referred to as tokens.
Punctuation marks are also tokens.
Each token in a sentence has several attributes we can use for analysis.
The part of speech of a word is one example: nouns are a person, place, or thing;
verbs are actions or occurences; adjectives are words that describe nouns.
Using these attributes, it's straightforward to create a summary of a piece of text
by counting the most common nouns, verbs, and adjectives.
"""
doc = nlp(text)
noun_counter = Counter(token.lemma_ for token in doc if token.pos_ == 'NOUN')
print(tabulate(noun_counter.most_common(5), headers=['Noun', 'Count']))
Noun Count
--------- -------
word 5
text 3
token 3
noun 3
attribute 2
依存分析
單詞之間也是有關係的,這些關係有好幾種。例如,名詞可以做句子的主語,它在句子中執行一個動作(動詞),例如「Jill 笑了」這句話。名詞也可以作為句子的賓語,它們接受句子主語施加的動作,例如「Jill laughed at John」中的 John。
依存分析是理解句子中單詞之間關係的一種方法。儘管在句子「Jill laughed at John」中,Jill 和 John 都是名詞,但是 Jill 是發出 laughing 這個動作的主語,而 John 是承受這個動作的賓語。依存關係是一種更加精細的屬性,可以通過句子中單詞之間的關係來理解單詞。
單詞之間的這些關係可能變得特別複雜,這取決於句子結構。對句子做依存分析的結果是一個樹形資料結構,其中動詞是樹根。
讓我們來看一下「The quick brown fox jumps over the lazy do」這句話中的依存關係。
nlp("The quick brown fox jumps over the lazy dog.")
spacy.displacy.render(doc, style='dep', options={'distance' : 140}, jupyter=True)
依存關係也是一種分詞屬性,spaCy 有專門訪問不同分詞屬性的強大 API(https://spacy.io/api/token)。下面我們會列印出每個分詞的文字、它的依存關係及其父(頭)分詞文字。
token_dependencies = ((token.text, token.dep_, token.head.text) for token in doc)
print(tabulate(token_dependencies, headers=['Token', 'Dependency Relation', 'Parent Token']))
Token Dependency Relation Parent Token
------- --------------------- --------------
The det fox
quick amod fox
brown amod fox
fox nsubj jumps
jumps ROOT jumps
over prep jumps
the det dog
lazy amod dog
dog pobj over
. punct jumps
作為分析的先導,我們會關心任何一個具有 nobj 關係的分詞,該關係表明它們是句子中的賓語。這意味著,在上面的示例句子中,我們希望捕獲到的是單詞「fox」。
命名實體識別
最後是命名實體識別。命名實體是句子中的專有名詞。計算機已經相當擅長分析句子中是否存在命名實體,也能夠區分它們屬於哪一類別。
spaCy 在文件水平處理命名實體,因為實體的名字可以跨越多個分詞。使用 IOB(https://spacy.io/usage/linguistic-features#section-named-entities)把單個分詞標記為實體的一部分,如實體的開始、內部或者外部。
在下面的程式碼中,我們在文件水平使用 doc.ents 列印出了所有的命名實體。然後,我們會輸出每個分詞,它們的 IOB 標註,以及它的實體型別(如果它是實體的一部分的話)
我們要使用的句子示例是「Jill laughed at John Johnson」。
doc = nlp("Jill laughed at John Johnson.")
entity_types = ((ent.text, ent.label_) for ent in doc.ents)
print(tabulate(entity_types, headers=['Entity', 'Entity Type']))
print()
token_entity_info = ((token.text, token.ent_iob_, token.ent_type_,) for token in doc)
print(tabulate(token_entity_info, headers=['Token', 'IOB Annotation', 'Entity Type']))
Entity Entity Type
------------ -------------
Jill PERSON
John Johnson PERSON
Token IOB Annotation Entity Type
------- ---------------- -------------
Jill B PERSON
laughed O
at O
John B PERSON
Johnson I PERSON
. O
例項:對《聖經》進行自然語言處理
上面提到的每個方法本身就很強大了,但如果將它們結合起來,遵循語言學的模式提取資訊,就能發揮自然語言處理的真正力量。我們可以使用詞性標註、依存分析、實體命名識別的一部分來了解大量文字中的所有角色及其動作。因其文字長度和角色範圍之廣,《聖經》是一個很好的例子。
我們正在匯入的資料每個《聖經》經文包含一個物件。經文被用作聖經部分的參考方案,通常包括一個或多個經文句子。我們會遍歷所有的經文,並提取其主題,確定它是不是一個人物,並提取這個人物所做的所有動作。
首先,讓我們從 GitHub 儲存庫中以 JSON 的形式載入聖經。然後,我們會從每段經文中抽取文字,通過 spaCy 傳送文字進行依存分析和詞性標註,並儲存生成的文件。
import requests
r = requests.get('https://github.com/tushortz/Bible/raw/master/json/kjv.json')
bible_json = [line['fields'] for line in r.json()]
print('Number of Verses:', len(bible_json))
text_generator = (line['text'] for line in bible_json)
%time verse_docs = [doc for doc in nlp.pipe(text_generator, n_threads=-1)]
我們已經用 3 分鐘多一點的時間將文字從 json 解析到了 verse_docs,大約每秒 160 個經文章節。作為參考,下面是 bible_json 前 3 行的內容。
[{'book_id': 1,
'chapter': 1,
'comment': '',
'text': 'In the beginning God created the heaven and the earth.',
'verse': 1},
{'book_id': 1,
'chapter': 1,
'comment': '',
'text': 'And the earth was without form, and void; and darkness was upon the face of the deep. And the Spirit of God moved upon the face of the waters.',
'verse': 2},
{'book_id': 1,
'chapter': 1,
'comment': '',
'text': 'And God said, Let there be light: and there was light.',
'verse': 3}]
使用分詞屬性
為了提取角色和動作,我們將遍歷一段經文中的所有分詞,並考慮 3 個因素:
1. 這個分詞是句子的主語嗎?(它的依存關係是不是 nsubj?)
2. 它的父分詞是不是動詞?(通常是這樣的,但是有時候 POS 標註和依存分析之間會存在衝突,我們會安全地使用它。此外,我並不是語言學家,所以這裡還會有一些奇怪的案例。)
3. 一個分詞的命名實體是否為一個人物?我們不想提取任何不是人物的名詞。(為了簡便,我們僅僅會提取名字)
如果我們的分詞滿足以上 3 種條件,我們將會收集以下的屬性:1. 名詞/實體分詞的文字。2. 包含名詞和動詞的範圍。3. 動詞。4. 動詞出現在標準英語文字中的對數概率(使用對數的原因是這裡的概率都很小)。5. 經文數量。
actors_and_actions = []
def token_is_subject_with_action(token):
nsubj = token.dep_ == 'nsubj'
head_verb = token.head.pos_ == 'VERB'
person = token.ent_type_ == 'PERSON'
return nsubj and head_verb and person
for verse, doc in enumerate(verse_docs):
for token in doc:
if token_is_subject_with_action(token):
span = doc[token.head.left_edge.i:token.head.right_edge.i+1]
data = dict(name=token.orth_,
span=span.text,
verb=token.head.lower_,
log_prob=token.head.prob,
verse=verse)
actors_and_actions.append(data)
print(len(actors_and_actions))
分析
我們已經獲得了提取到的所有角色及其動作的列表,現在我們做以下兩件事來快速分析:
1. 找出每個角色最常做出的動作(動詞)
2. 找出每個人最獨特的動作。我們將其確定為英文文字中出現概率最低的動詞。
import pandas as pd
action_df = pd.DataFrame(actors_and_actions)
print('Unique Names:', action_df['name'].nunique())
most_common = (action_df
.groupby(['name', 'verb'])
.size()
.groupby(level=0, group_keys=False)
.nlargest(1)
.rename('Count')
.reset_index(level=1)
.rename(columns={
'verb': 'Most Common'
})
)
# exclude log prob < -20, those indicate absence in the model vocabulary
most_unique = (action_df[action_df['log_prob'] > -20]
.groupby(['name', 'verb'])['log_prob']
.min()
.groupby(level=0, group_keys=False)
.nsmallest(1)
.rename('Log Prob.')
.reset_index(level = 1)
.rename(columns={
'verb': 'Most Unique'
})
)
# SO groupby credit
# https: //stackoverflow.com/questions/27842613/pandas-groupby-sort-within-groups
讓我們看一下前 15 個角色的動詞數及其最常用的動詞。
most_common.sort_values('Count', ascending=False).head(15)
貌似《聖經》裡面很多人都說了很多話,而所羅門簡直是個例外,他做了很多事情。
那麼從出現概率來看,最獨特的動詞是什麼呢?(我們將在此處刪去重複項,以便每個單詞都是唯一的)
(most_unique
.drop_duplicates('Most Unique')
.sort_values('Log Prob.', ascending=True)
.head(15)
)
看來我們要學習一些有趣的新詞彙了!我最喜歡的是 discomfited 和 ravin。
視覺化
接下來視覺化我們的結果。我們將選取行動最多、情節最多的前 50 個名字,這些行動發生在整篇文章中。我們還會在《聖經》每本書的開頭畫垂直線。姓名將按首次出現的順序排序。
這可以讓我們知道聖經中每個角色最活躍的時候。
我們將新增一些分隔符來分隔《聖經》的不同部分。我自己並非研究《聖經》學者,所以我參考瞭如下分隔法(https://www.thoughtco.com/how-the-books-of-the-bible-are-organized-363393):
《舊約》:
摩西五經或律法書:《創世紀》、《出埃及記》、《利未記》、《民數記》和《申命記》。
舊約歷史書:《約書亞記》、《士師記》、《路得記》、《撒慕耳記》上下、《列王記》上下、《歷代志》上下、《尼希米記》、《以斯拉記》、《以斯帖記》
詩歌智慧書:《約伯記》、《詩篇》、《箴言》、《傳道書》和《雅歌》;
大先知書:《以賽亞書》、《耶利米書》、《耶利米哀歌》、《以西結書》、《但以理書》、《何西阿書》、《約珥書》、《阿摩司書》、《俄巴底亞書》、《約拿書》、《彌迦書》、《那鴻書》、《哈巴谷書》、《西番雅書》、《哈該書》、《撒迦利亞書》、《瑪拉基書》。
《新約》:
福音書:《馬太福音》、《馬可福音》、《路加福音》、《約翰福音》
新約歷史書:《使徒行傳》
保羅書信:《羅馬書》、《哥林多前書》、《哥林多後書》、《加拉太書》、《以弗所書》、《腓立比書》、《歌羅西書》《帖撒羅尼迦前書》、《帖撒羅尼迦後書》、《提摩太前書》、《提摩太後書》、《提多書》、《腓利門書》、《希伯來書》、《雅各書》、《彼得前書》、《彼得後書》、《約翰壹書》、《約翰貳書》、《約翰叄書》和《猶大書》
語言/啟示錄:《啟示錄》
此外,我們還會用一條紅色的標誌線分割《舊約》和《新約》。
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
sns.set(context='notebook', style='dark')
most_frequent_actors = list(action_df['name'].value_counts().index[:50])
top_actors_df = action_df[action_df['name'].isin(most_frequent_actors)].copy()
book_locations = (pd.DataFrame(bible_json)
.reset_index()
.groupby('book_id')['index']
.min()
.to_dict()
)
fig, ax = plt.subplots(figsize=(8,12), dpi=144*2)
sns.stripplot(x='verse', y='name',
data=top_actors_df, ax=ax,
color='xkcd:cerulean',
size=3, alpha=0.25, jitter=0.25)
sns.despine(bottom=True, left=True)
for book, verse_num in book_locations.items():
ax.axvline(verse_num, alpha=1, lw=0.5, color='w')
divisions = [1, 6, 18, 23, 40, 44, 45, 65]
for div in divisions:
ax.axvline(book_locations[div], alpha=0.5, lw=1.5, color='grey')
ax.axvline(book_locations[40], alpha=0.5, lw=1.75, color='xkcd:coral')
ax.set_xlim(left=-150)
ax.set_title("Where Actions Occur in the Bible\nCharacters Sorted by First Appearance");
視覺化分析
在《聖經》開頭的《創世紀》中,上帝(God)被密集地提到。
在《新約》中,主(Lord)不再作為一個實體使用。
我們第一次看到保羅是在《使徒行傳》中被提及。(福音書後的第一本書)
在《詩歌智慧書》裡沒有提到很多實體。
耶穌的生活在《福音書》中被密集地記錄了下來。
彼拉多出現在《福音書》的末尾。
這種方法的問題
實體識別無法區分兩個名字相同的人
掃羅王(《舊約》)
直到《使徒行傳》的中途,保羅(使徒)一直被稱作掃羅
有些名詞不是實際的實體(如 Ye)
有些名詞可以使用更多的語境和全名(如 Pilate)
下一步
一如既往,有辦法擴充套件和改進這一分析。我在寫這篇文章的時候想到了以下幾點:
1. 使用依存關係來尋找實體之間的關係,通過網路分析的方法來理解角色。
2. 改進實體提取,以捕獲單個名稱之外的實體。
3. 對非人物實體及其語言關係進行分析——《聖經》中提到了哪些位置?
寫在結尾
僅僅通過使用文字中分詞級別的屬性我們就可以做一些很有趣的分析!在本文中,我們介紹了 3 種主要的 NLP 工具:
1. 詞性標註——這個詞是什麼型別?
2. 依存分析——該詞和句子中的其他詞是什麼關係?
3. 命名實體識別——這是一個專有名詞嗎?
我們結合這三個工具來發現誰是《聖經》中的主要角色,以及他們採取的動作。並且我們還繪製了這些角色和動作的圖表,以瞭解每個角色的主要動作發生在何處。