圖機器學習(17)——基于文檔語料庫構建知識圖譜
- 0. 前言
- 1. 基于文檔語料庫構建知識圖譜
- 2. 知識圖譜
- 3. 文檔-實體二分圖
0. 前言
文本數據的爆炸性增長,直接推動了自然語言處理 (Natural Language Processing
, NLP
) 領域的快速發展。在本節中,通過從文檔語料庫中提取的信息,基于文檔語料庫提取的信息,介紹如何利用這些信息構建知識圖譜。
1. 基于文檔語料庫構建知識圖譜
在本節中,我們將使用自然語言處理一節中提取的信息,構建關聯不同知識要素的網絡圖譜。我們將重點探討兩種圖結構:
- 知識圖譜 (
Knowledge-based graph
):通過句子的語義推斷實體間關系 - 二分圖 (
Bipartite graph
):建立文檔與文本實體間的連接,后可投影為僅含文檔節點或實體節點的同質圖
2. 知識圖譜
知識圖譜的價值在于不僅能關聯實體,更能賦予關系方向與語義內涵。例如比較以下兩種關系:
I (->) buy (->) a book
I (->) sell (->) a book
除 “buy/sell” 的動作差異外,關系方向性同樣關鍵——需區分動作執行者(主語)與承受者(賓語)的非對稱關系。
構建知識圖譜需要實現主謂賓 (Subject-Verb-Object
, SVO
) 三元組提取函數,該函數將應用于語料所有句子,聚合三元組后即可生成對應圖譜。
SVO
提取器可基于 spaCy
模型提供的依存句法分析實現:依存樹標注既能區分主從句子結構,又可輔助識別 SVO
三元組。實際業務邏輯需處理若干特殊情況(如連詞、否定結構、介詞短語等),這些均可通過規則集編碼實現,且規則可根據具體用例調整優化。使用這個輔助函數,可以計算語料庫中的所有三元組,并將它們存儲在語料庫 DataFrame
中。
from subject_object_extraction import findSVOs
corpus["triplets"] = corpus["parsed"].apply(lambda x: findSVOs(x, output="obj"))
連接類型(由句子核心謂語決定)存儲在 edge
列中。查看出現頻率最高的 10
種關系:
edges["edge"].value_counts().head(10)
最常見的邊類型對應基礎謂詞結構。除通用動詞(如 be、have、tell、give )外,我們還發現更多金融語境相關的謂詞(如 buy、sell、make)。利用這些邊數據,可以通過 networkx
工具函數構建知識圖譜:
G=nx.from_pandas_edgelist(edges, "source", "target", edge_attr=True, create_using=nx.MultiDiGraph())
通過篩選邊數據表并創建子網絡,可分析特定關系類型,如 “lend” 邊:
G=nx.from_pandas_edgelist(edges[edges["edge"]=="lend"], "source", "target",edge_attr=True, create_using=nx.MultiDiGraph()
)
下圖顯示了基于 “lend” 關系的子圖。
我們也可以通過篩選其他關系類型來靈活調整上述代碼進行探索。接下來,我們將介紹另一種將文本信息編碼為圖結構的方法,該方法將運用特殊圖結構——二分圖。
3. 文檔-實體二分圖
雖然知識圖譜能有效揭示實體間的聚合關系,但在某些場景下其他圖表示法可能更具優勢。例如當需要進行文檔語義聚類時,知識圖譜并非最優數據結構;同樣,對于識別未共現于同一句子但頻繁出現在同一文檔中的間接關聯(如競品分析、相似產品發現等),知識圖譜也存在局限性。
為解決這些問題,我們將采用二分圖對文檔信息進行編碼:為每篇文檔提取最具代表性的實體,建立文檔節點與對應實體節點的連接。這種結構中,單個文檔節點會關聯多個實體節點,而實體節點也可被多篇文檔引用(形成交叉引用網絡)。這種交叉引用關系可衍生出實體間、文檔間的相似度度量,進而支持將二分圖投影為純文檔節點或純實體節點的同質圖。
為了構建二分圖,需要提取文檔中的相關實體。“相關實體”在當前的上下文中,可以視為命名實體(例如,由命名實體識別引擎識別的組織、人物或地點)或關鍵詞;即,能夠識別并通常描述文檔及其內容的詞(或詞的組合)。
關鍵詞提取算法眾多,其中基于 TF-IDF
評分的方法較為經典:該算法通過計算詞元(或詞元組合 n-gram
)的得分進行篩選,得分與文檔內詞頻 (Term Frequency
) 成正比,與語料庫中出現該詞的文檔頻率 (Inverse Document Frequency
) 成反比:
ci,j∑ci,j?logN1+Di\frac{c_{i,j}}{\sum c_{i,j}}\cdot log\frac N{1+D_i} ∑ci,j?ci,j???log1+Di?N?
其中,ci,jc_{i,j}ci,j? 表示文檔 jjj 中詞語 iii 的計數,NNN 表示語料庫中的文檔數量,DiD_iDi? 表示詞 iii 出現的文檔。因此,TF-IDF
評分機制會提升文檔中高頻出現的詞匯權重,同時降低常見詞匯(往往缺乏代表性)的得分。
TextRank
算法同樣基于文檔的圖結構表示。TextRank
算法構建的網絡中,節點是單個詞元,邊則由特定滑動窗口內共現的詞元對形成。網絡構建完成后,通過 PageRank
算法計算各詞元的中心度得分,據此對文檔內詞匯進行重要性排序。最終選取中心度最高的節點(通常占文檔總詞量的 5%-20%
)作為候選關鍵詞。當多個候選關鍵詞相鄰出現時,它們會被合并為多詞復合關鍵詞。
(1) 利用 gensim
庫可以直接使用 TextRank
算法:
from gensim.summarization import keywords
text = corpus["clean_text"][0]
keywords(text, words=10, split=True, scores=True, pos_filter=('NN', 'JJ'), lemmatize=True)
輸出結果如下所示:
[('trading', 0.4615130639538529),('said', 0.3159855693494515),('export', 0.2691553824958079),('import', 0.17462010006456888),('japanese electronics', 0.1360932626379031),('industry', 0.1286043740379779),('minister', 0.12229815662000462),('japan', 0.11434500812642447),('year', 0.10483992409352465)]
這里的評分代表中心度 (centrality
),反映了特定詞元的重要性。可以看到,算法還可能生成復合詞元。我們可以實現關鍵詞提取功能來計算整個語料庫的關鍵詞,并將結果存儲到語料 DataFrame
中:
corpus["keywords"] = corpus["clean_text"].apply(lambda text: keywords(text, words=10, split=True, scores=True, pos_filter=('NN', 'JJ'), lemmatize=True)
)
(2) 除了關鍵詞,構建二分圖還需要解析 NER
引擎提取的命名實體,并以與關鍵詞相似的數據格式進行編碼:
def extractEntities(ents, minValue=1, typeFilters=["GPE", "ORG", "PERSON"]):entities = pd.DataFrame([{"lemma": e.lemma_, "lower": e.lemma_.lower(), "type": e.label_}for e in ents if hasattr(e, "label_")])if len(entities)==0:return pd.DataFrame()g = entities.groupby(["type", "lower"])summary = pd.concat({"alias": g.apply(lambda x: x["lemma"].unique()), "count": g["lower"].count()}, axis=1)return summary[summary["count"]>1].loc[pd.IndexSlice[typeFilters, :, :]]def getOrEmpty(parsed, _type):try:return list(parsed.loc[_type]["count"].sort_values(ascending=False).to_dict().items())except:return []def toField(ents):typeFilters=["GPE", "ORG", "PERSON"]parsed = extractEntities(ents, 1, typeFilters)
return pd.Series({_type: getOrEmpty(parsed, _type) for _type in typeFilters})
(3) 解析 spacy
標簽:
entities = corpus["parsed"].apply(lambda x: toField(x.ents))
(4) 通過 pd.concat
函數可以將實體 DataFrame
與語料 DataFrame
合并,從而將所有信息整合到單一數據結構中:
merged = pd.concat([corpus, entities], axis=1)
(5) 現在我們已經具備構建二分圖的所有要素,可以通過循環遍歷所有文檔-實體或文檔-關鍵詞對來創建邊列表:
edges = pd.DataFrame([{"source": _id, "target": keyword, "weight": score, "type": _type}for _id, row in merged.iterrows()for _type in ["keywords", "GPE", "ORG", "PERSON"] for (keyword, score) in row[_type]
])
(6) 邊列表創建完成后,即可使用 networkx API
生成二分圖:
G = nx.Graph()
G.add_nodes_from(edges["source"].unique(), bipartite=0)
G.add_nodes_from(edges["target"].unique(), bipartite=1)
G.add_edges_from([(row["source"], row["target"])for _, row in edges.iterrows()
])
接下來,我們將把這個二分圖投影到任意一組節點(實體或文檔)上。使我們能夠探索兩種圖之間的差異,并使用無監督技術對術語和文檔進行聚類。然后,我們將回到二分圖,通過利用網絡信息進行監督分類任務。
2.2.1 實體-實體圖
我們首先將圖投影到實體節點集合上。NetworkX
提供了一個專門處理二分圖的子模塊 networkx.algorithms.bipartite
,其中已實現了多種算法,networkx.algorithms.bipartite.projection
子模塊提供了若干實用函數,可將二分圖投影到特定節點子集上。在執行投影之前,我們需要利用創建圖時設置的 “bipartite” 屬性來提取特定集合(文檔或實體)的節點:
document_nodes = {n for n, d in G.nodes(data=True) if d["bipartite"] == 0}
entity_nodes = {n for n, d in G.nodes(data=True) if d["bipartite"] == 1}
圖投影本質上會創建一個由選定節點組成的新圖。節點之間邊的建立取決于它們是否擁有共同鄰居節點。基礎的 projected_graph
函數會生成一個邊未加權的網絡。但通常更具信息量的做法是根據共同鄰居的數量為邊賦予權重,投影模塊提供了基于不同權重計算方式的多種函數。在下一節中,我們將使用 overlap_weighted_projected_graph
函數,該函數基于共同鄰居采用 Jaccard
相似度計算邊權重。我們也可以探索其它函數,根據具體應用場景選擇最適合的方案。
2.2.2 維度問題
在進行圖投影時還需特別注意一個問題:投影后圖的維度。在某些情況下,投影可能會產生數量龐大的邊,導致圖難以分析。在我們的使用場景中,按照我們用來創建網絡的邏輯,一個文檔節點至少連接到 10
個關鍵詞和若干實體節點。在最終的實體-實體圖中,這些實體之間都會因為至少擁有一個共同鄰居(包含它們的文檔)而相互連接。因此單文檔就會生成約 15×142≈100\frac {15×14}2\approx 100215×14?≈100 條邊。如果我們將這個數字乘以文檔的數量 (~105\sim 10^5~105),即便在本節的簡單場景中也會產生數百萬條邊,幾乎無法處理。雖然這顯然是個保守上限(因為某些實體共現關系會出現在多個文檔中而不會重復計算),但已能反映可能面臨的復雜度量級。因此建議,根據底層網絡的拓撲結構和圖規模,實施二分圖投影前務必謹慎。
為了降低復雜度使投影可行,可以僅保留達到特定度數的實體節點。大多數復雜性來自于那些僅出現一次或少數次、卻仍在圖中形成團結構的實體節點。這類實體對模式捕捉和洞見發現的貢獻度很低,且可能受到統計波動性的強烈干擾。相反,我們應聚焦于那些高頻出現、能提供更可靠統計結果的強相關性實體。
為此,我們將僅保留度數 ≥5
的實體節點,具體方法是生成經過過濾的二分子圖:
nodes_with_low_degree = {n for n, d in nx.degree(G, nbunch=entity_nodes) if d<5}
對該子圖進行投影,而不會生成邊數過多的圖:
entityGraph = overlap_weighted_projected_graph(subGraph, {n for n, d in subGraph.nodes(data=True) if d["bipartite"] == 1}
)
盡管我們已應用過濾條件,但圖的邊數和平均節點度數仍然相當大。下圖展示了節點度數和邊權重的分布情況,可以觀察到,度數分布在較低值處出現一個峰值,但向高值方向呈現長尾分布;邊權重也表現出類似趨勢,峰值出現在較低值區間,但右側同樣具有長尾特征。這些分布特征表明圖中存在多個小型社區(即團結構),它們通過某些中心節點相互連接。
邊權重的分布情況也表明可以應用第二個過濾器。在二分圖上實施的實體度數過濾,幫助我們篩除了僅出現在少數文檔中的低頻實體。但由此得到的圖結構可能會面臨相反的問題:高頻實體之間可能僅僅因為共同出現在多個文檔中就產生連接,即便它們之間并不存在有意義的因果關系。以江蘇和蘇州為例,這兩個實體幾乎必然存在連接,因為極有可能存在至少一個或多個文檔同時提及它們。但若二者之間缺乏強因果關聯,它們的Jaccard相似度就不太可能達到較高值。僅保留權重最高的邊,能讓我們聚焦于最相關且可能穩定的關系。邊權重分布表明,將閾值設定為 0.05
較為合適:
filteredEntityGraph = entityGraph.edge_subgraph([edge for edge in entityGraph.edges if entityGraph.edges[edge]["weight"]>0.05]
)
該閾值能顯著減少邊的數量,使網絡分析具備可操作性。
上圖展示了過濾后圖的節點度數與邊權重的分布情況,可以看到當前分布顯示節點度數在 10
左右出現峰值。
2.2.3 圖分析
通過 Gephi
生成的網絡全景如下所示:
(1) 為深入理解網絡拓撲特性,我們計算了平均最短路徑長度、聚類系數和全局效率等宏觀指標。雖然該圖包含五個連通分量,但最大分量幾乎涵蓋整個網絡——在 2265
個節點中占據了 2254
個:
components = nx.connected_components(filteredEntityGraph)
pd.Series([len(c) for c in components])
(2) 獲取最大連通分量的全局屬性:
comp = components[0]
global_metrics = pd.Series({"shortest_path": nx.average_shortest_path_length(comp),"clustering_coefficient": nx.average_clustering(comp),"global_efficiency": nx.global_efficiency(comp)
})
輸出結果如下所示:
{'shortest_path': 4.715073779178782,'clustering_coefficient': 0.21156314975836915,'global_efficiency': 0.22735551077454275
}
從這些指標的數值(最短路徑約 5
,聚類系數約 0.2
) 結合度數分布可以看出,該網絡由多個規模有限的社區組成。局部屬性(如度數、PageRank
和中介中心性分布)如下圖所示,這些指標之間往往存在相互關聯性:
在完成網絡局部/全局指標的描述及整體可視化后,我們將應用無監督學習技術來挖掘網絡中的深層信息。
(3) 首先使用 Louvain
社區檢測算法,該算法通過模塊度優化,旨在將節點劃分至互不重疊的最佳社區結構中:
import community
communities = community.best_partition(filteredEntityGraph)
大約可以得到 30
個社區,其成員數量分布與下圖所示,其中較大規模的社區一般包含 130-150
篇文檔。
下圖展示了其中一個社區的細節視圖,從中可識別特定主題。左側除實體節點外,還可看到文檔節點,由此揭示了關聯二分圖的結構:
(4) 通過節點嵌入技術可以提取關于實體間拓撲關系與相似性的深層信息。我們可以采用 Node2Vec
方法——該方法通過將隨機生成的游走序列輸入 skip-gram
模型,將節點映射至向量空間,使拓撲相近的節點在嵌入空間中位置鄰近:
from node2vec import Node2Vec
node2vec = Node2Vec(filteredEntityGraph, dimensions=5)
model = node2vec.fit(window=10)
embeddings = model.wv
在嵌入向量空間中,我們可以應用傳統聚類算法(如高斯混合模型、K-Means
或 DBSCAN
)。我們還能通過 t-SNE
將嵌入向量投影至二維平面,實現聚類與社區的可視化。Node2Vec
不僅為圖中社區識別提供了另一種解決方案,還能像經典 Word2Vec
那樣計算詞語相似度。例如,我們可以查詢 Node2Vec
嵌入模型,找出與 “turkey” 最相似的詞語,從而獲取語義關聯詞:
[('turkish', 0.9975333213806152),
('lira', 0.9903393983840942),
('rubber', 0.9884852170944214),
('statoil', 0.9871745109558105),
('greek', 0.9846569299697876),
('xuto', 0.9830175042152405),
('stanley', 0.9809650182723999),
('conference', 0.9799597263336182),
('released', 0.9793018102645874),
('inra', 0.9775203466415405)]
盡管 Node2Vec
與 Word2Vec
在方法上存在相似性,但兩種嵌入方案的信息來源截然不同:Word2Vec
直接基于文本構建,捕捉句子層面的詞語關系;而 Node2Vec
源自實體-文檔二分圖,其編碼的特征更傾向于文檔層面的描述。
2.2.4 文檔-文檔圖
接下來,我們將二分圖投影至文檔節點集,構建可分析的文檔關聯網絡。與創建實體關聯網絡類似,這里采用 overlap_weighted_projected_graph
函數生成帶權圖,并通過過濾保留顯著性邊。需要注意的是,網絡的拓撲結構以及構建二分圖時使用的業務邏輯并不鼓勵形成團(即完全圖),正如在實體-實體圖中看到的那樣:只有當兩個節點至少共享一個關鍵詞、組織、地點或人時,它們才會連接。在 10-15
個節點組成的群組中(如實體節點群組),這種情況雖可能發生,但概率較低。
(1) 接下來,快速構建網絡:
documentGraph = overlap_weighted_projected_graph(G,document_nodes
)
下圖展示了節點度數與邊權重的分布情況,這有助于我們確定過濾邊時所采用的閾值。值得注意的是,與實體-實體圖的度數分布相比,當前節點度數分布明顯向高值區域偏移,表明存在多個具有超高連接度的"超級節點"。同時,邊權重分布顯示杰卡德指數傾向于接近1的數值,遠高于實體-實體圖中的觀測值。這兩項發現揭示了兩個網絡的本質差異:實體-實體圖以大量緊密連接的社區(即完全子圖)為特征,而文檔-文檔圖則呈現"核心-邊緣"結構——高度連接的大度數節點構成網絡核心,外圍則分布著弱連接或孤立節點:
(2) 將所有邊存儲至 DataFrame
中,這樣既能實現可視化呈現,又可基于該數據集進行過濾以生成子圖:
allEdgesWeights = pd.Series({(d[0], d[1]): d[2]["weight"]for d in documentGraph.edges(data=True)
})
觀察上圖可知,將邊權重閾值設定為 0.6
較為合理,這樣可通過 networkx
的 edge_subgraph
函數生成更易處理的網絡:
filteredDocumentGraph = documentGraph.edge_subgraph(allEdgesWeights[(allEdgesWeights>0.6)].index.tolist()
)
下圖展示了精簡后的網絡在節點度數與邊權重上的分布情況:
文檔-文檔圖與實體-實體圖在拓撲結構上的本質差異,在下文完整的網絡可視化圖中更為明顯。文檔-文檔網絡呈現"核心-衛星"結構——由緊密連接的核心網絡與多個弱連接的衛星節點構成。這些衛星節點代表那些未共享或僅共享少量關鍵詞/實體的文檔,其中完全孤立的文檔數量相當龐大,約占總量的 50%
:
(3) 提取該網絡的連通分量:
components = pd.Series({ith: componentfor ith, component in enumerate(nx.connected_components(filteredDocumentGraph))
})
下圖展示了連通分量的規模分布情況。可以清晰觀察到若干超大聚類(核心集群)與大量孤立或極小分量(邊緣衛星集群)并存的現象。這種結構與我們在實體-實體圖中觀察到的結構截然不同,在實體-實體圖中,所有節點歸屬于單一超大連通集群:
(4) 從完整圖中提取由最大連通分量構成的子圖:
coreDocumentGraph = nx.subgraph(filteredDocumentGraph,[nodefor nodes in components[components.apply(len)>8].valuesfor node in nodes]
)
觀察核心網絡的 Gephi
可視化結果(下圖右側),可以發現,該核心網絡由若干社區組成,其中包含多個彼此緊密連接的高度數節點。
與處理實體-實體網絡時類似,我們可以處理網絡以識別嵌入在圖中的社區。然而,不同之處在于,文檔-文檔圖現在提供了一個使用文檔標簽來判斷聚類的手段。實際上,屬于同一主題的文檔應當彼此鄰近且相互連接。此外,這種方法還能幫助我們識別不同主題之間的相似性。
(5) 首先,提取候選社區:
import community
communities = pd.Series(community.best_partition(filteredDocumentGraph)
)
(6) 隨后分析各社區內的主題分布,檢測是否存在同質性(所有文檔屬于同一類別)或主題間的相關性:
from collections import Counter
def getTopicRatio(df):return Counter([labelfor labels in df["label"]for label in labels])
communityTopics = pd.DataFrame.from_dict({cid: getTopicRatio(corpus.loc[comm.index])for cid, comm in communities.groupby(communities)
}, orient="index")
normalizedCommunityTopics = (communityTopics.T / communityTopics.sum(axis=1)
).T
normalizedCommunityTopics
是一個 DataFrame
結構,其中每一行代表一個社區 (community
),每一列對應不同主題的分布比例(以百分比形式呈現)。為了量化這些社區/集群內部主題混合的異質性,我們需要計算每個社區的香農熵 (Shannon entropy
):
Ic=?∑ilogtciIc=?∑_ilogt_{ci} Ic=?i∑?logtci?
其中,IcIcIc 表示社區 ccc 的熵值,$t_{ci} 表示社區 ccc 中主題 iii 的占比。接下來,我們需要為所有社區計算經驗香農熵:
normalizedCommunityTopics.apply(lambda x: np.sum(-np.log(x)), axis=1)
下圖展示了所有社區的熵值分布情況。大多數社區的熵值為零或接近零,這表明相同類別(標簽)的文檔傾向于聚集在一起:
盡管大多數社區在主題分布上呈現零變異或低變異,但當某些社區表現出異質性時,探究主題間的關聯仍具有意義。為此,我們計算主題分布之間的相關性
topicsCorrelation = normalizedCommunityTopics.corr().fillna(0)
然后,使用主題-主題網絡表示和可視化這些相關性:
topicsCorrelation[topicsCorrelation<0.8] = 0
topicsGraph = nx.from_pandas_adjacency(topicsCorrelation)
下圖左側展示了主題網絡的完整圖表示。與文檔-文檔網絡類似,主題網絡呈現出"核心-邊緣"結構——邊緣由孤立節點構成,核心則是高度連通的節點集群。右圖聚焦展示了核心網絡的細節,其中商品類主題之間的強相關性反映出明確的語義關聯:
本節我們分析了文檔及文本源分析中產生的各類網絡結構,通過全局與局部屬性統計描述網絡特征,并運用無監督算法揭示了圖中的潛在結構。