作者介紹:blmoistawinde, 西南某高校學(xué)生一枚,喜歡有意思的數(shù)據(jù)挖掘分析。希望給世界帶來(lái)些清新空氣~ 個(gè)人博客地址:https://blog.csdn.net/blmoistawinde。 本文首發(fā)于:https://blog.csdn.net/blmoistawinde 前言 一直以來(lái)對(duì)自然語(yǔ)言處理和社交網(wǎng)絡(luò)分析都很感興趣,前者能幫助我們從文本中獲得很多發(fā)現(xiàn),而后者能夠讓我們對(duì)人們和各個(gè)事物之間普遍存在的網(wǎng)絡(luò)般的聯(lián)系有更多認(rèn)識(shí)。當(dāng)二者結(jié)合,又會(huì)有怎樣的魔力呢? 作為一個(gè)三國(guó)迷,我就有了這樣的想法:能不能用文本處理的方法,得到《三國(guó)演義》中的人物社交網(wǎng)絡(luò),再進(jìn)行分析呢?Python 中有很多好工具能夠幫助我實(shí)踐我好奇的想法,現(xiàn)在就開(kāi)始動(dòng)手吧。 準(zhǔn)備工作 獲得《三國(guó)演義》的文本。 chapters = get_sanguo() # 文本列表,每個(gè)元素為一章的文本print(chapters[0][:106]) 第一回 宴桃園豪杰三結(jié)義 斬黃巾英雄首立功滾滾長(zhǎng)江東逝水,浪花淘盡英雄。是非成敗轉(zhuǎn)頭空。青山依舊在,幾度夕陽(yáng)紅。白發(fā)漁樵江渚上,慣看秋月春風(fēng)。一壺濁酒喜相逢。古今多少事,都付笑談中 《三國(guó)演義》并不是很容易處理的文本,它接近古文,我們會(huì)面對(duì)古人的字號(hào)等一系列別名。比如電腦怎么知道“玄德”指的就是“劉備”呢?那就要我們給它一些知識(shí)。我們?nèi)送ㄟ^(guò)學(xué)習(xí)知道“玄德”是劉備的字,電腦也可以用類(lèi)似的方法完成這個(gè)概念的連接。我們需要告訴電腦,“劉備”是實(shí)體(類(lèi)似于一個(gè)對(duì)象的標(biāo)準(zhǔn)名),而“玄德”則是“劉備”的一個(gè)指稱(chēng),告訴的方式,就是提供電腦一個(gè)知識(shí)庫(kù)。 entity_mention_dict, entity_type_dict = get_sanguo_entity_dict()print('劉備的指稱(chēng)有:',entity_mention_dict['劉備']) 劉備的指稱(chēng)有:['劉備', '劉玄德', '玄德', '使君'] 除了人的實(shí)體和指稱(chēng)以外,我們也能夠包括三國(guó)勢(shì)力等別的類(lèi)型的指稱(chēng),比如“蜀”又可以叫“蜀漢”,所以知識(shí)庫(kù)里還可以包括實(shí)體的類(lèi)型信息來(lái)加以區(qū)分。 print('劉備的類(lèi)型為',entity_type_dict['劉備'])print('蜀的類(lèi)型為',entity_type_dict['蜀'])print('蜀的指稱(chēng)有',entity_mention_dict['蜀'])劉備的類(lèi)型為 人名蜀的類(lèi)型為 勢(shì)力蜀的指稱(chēng)有 ['蜀', '蜀漢'] 有了這些知識(shí),理論上我們就可以編程聯(lián)系起實(shí)體的各個(gè)綽號(hào)啦。不過(guò)若是要從頭做起的話,其中還會(huì)有不少的工作量。而 HarvestText[注1] 是一個(gè)封裝了這些步驟的文本處理庫(kù),可以幫助我們輕松完成這個(gè)任務(wù)。 ht = HarvestText()ht.add_entities(entity_mention_dict, entity_type_dict) # 加載模型print(ht.seg('誓畢,拜玄德為兄,關(guān)羽次之,張飛為弟。',standard_name=True))['誓畢', ',', '拜', '劉備', '為兄', ',', '關(guān)羽', '次之', ',', '張飛', '為弟', '。'] 社交網(wǎng)絡(luò)建立 成功地把指稱(chēng)統(tǒng)一到標(biāo)準(zhǔn)的實(shí)體名以后,我們就可以著手挖掘三國(guó)的社交網(wǎng)絡(luò)了。具體的建立方式是利用鄰近共現(xiàn)關(guān)系。每當(dāng)一對(duì)實(shí)體在兩句話內(nèi)同時(shí)出現(xiàn),就給它們加一條邊。那么建立網(wǎng)絡(luò)的整個(gè)流程就如同下圖所示: 我們可以使用 HarvestText 提供的函數(shù)直接完成這個(gè)流程,讓我們先在第一章的小文本上實(shí)踐一下: # 準(zhǔn)備工作doc = chapters[0].replace('操','曹操') # 由于有時(shí)使用縮寫(xiě),這里做一個(gè)微調(diào)ch1_sentences = ht.cut_sentences(doc) # 分句doc_ch01 = [ch1_sentences[i]+ch1_sentences[i+1] for i in range(len(ch1_sentences)-1)] #獲得所有的二連句ht.set_linking_strategy('freq')# 建立網(wǎng)絡(luò)G = ht.build_entity_graph(doc_ch01, used_types=['人名']) # 對(duì)所有人物建立網(wǎng)絡(luò),即社交網(wǎng)絡(luò)# 挑選主要人物畫(huà)圖important_nodes = [node for node in G.nodes if G.degree[node]>=5]G_sub = G.subgraph(important_nodes).copy()draw_graph(G_sub,alpha=0.5,node_scale=30,figsize=(6,4)) 他們之間具體有什么關(guān)系呢?我們可以利用文本摘要得到本章的具體內(nèi)容: stopwords = get_baidu_stopwords() #過(guò)濾停用詞以提高質(zhì)量for i,doc in enumerate(ht.get_summary(doc_ch01, topK=3, stopwords=stopwords)): print(i,doc)玄德見(jiàn)皇甫嵩、朱儁,具道盧植之意。嵩曰:“張梁、張寶勢(shì)窮力乏,必投廣宗去依張角。時(shí)張角賊眾十五萬(wàn),植兵五萬(wàn),相拒于廣宗,未見(jiàn)勝負(fù)。植謂玄德曰:“我今圍賊在此,賊弟張梁、張寶在潁川,與皇甫嵩、朱儁對(duì)壘。次日,于桃園中,備下烏牛白馬祭禮等項(xiàng),三人焚香再拜而說(shuō)誓曰:“念劉備、關(guān)羽、張飛,雖然異姓,既結(jié)為兄弟,則同心協(xié)力, 本章的主要內(nèi)容,看來(lái)就是劉關(guān)張?zhí)覉@三結(jié)義,并且共抗黃巾賊的故事。 三國(guó)全網(wǎng)絡(luò)繪制 有了小范圍實(shí)踐的基礎(chǔ),我們就可以用同樣的方法,整合每個(gè)章節(jié)的內(nèi)容,畫(huà)出一張橫跨三國(guó)各代的大圖。 G_chapters = []for chapter in chapters: sentences = ht.cut_sentences(chapter) # 分句 docs = [sentences[i]+sentences[i+1] for i in range(len(sentences)-1)] G_chapters.append(ht.build_entity_graph(docs, used_types=['人名']))# 合并各張子圖G_global = nx.Graph()for G0 in G_chapters: for (u,v) in G0.edges: if G_global.has_edge(u,v): G_global[u][v]['weight'] += G0[u][v]['weight'] else: G_global.add_edge(u,v,weight=G0[u][v]['weight'])# 忽略游離的小分支只取最大連通分量largest_comp = max(nx.connected_components(G_global), key=len)G_global = G_global.subgraph(largest_comp).copy()print(nx.info(G_global))Name:Type: GraphNumber of nodes: 1290Number of edges: 10096Average degree: 15.6527 整個(gè)社交網(wǎng)絡(luò)有 1290 個(gè)人那么多,還有上萬(wàn)條邊!那么我們要把它畫(huà)出來(lái)幾乎是不可能的,那么我們就挑選其中的關(guān)鍵人物來(lái)畫(huà)出一個(gè)子集吧。 important_nodes = [node for node in G_global.nodes if G_global.degree[node]>=30]G_main = G_global.subgraph(important_nodes).copy() 用 pyecharts進(jìn)行可視化 from pyecharts import Graphnodes = [{'name': '結(jié)點(diǎn)1', 'value':0, 'symbolSize': 10} for i in range(G_main.number_of_nodes())]for i,name0 in enumerate(G_main.nodes): nodes[i]['name'] = name0 nodes[i]['value'] = G_main.degree[name0] nodes[i]['symbolSize'] = G_main.degree[name0] / 10.0links = [{'source': '', 'target': ''} for i in range(G_main.number_of_edges())]for i,(u,v) in enumerate(G_main.edges): links[i]['source'] = u links[i]['target'] = v links[i]['value'] = G_main[u][v]['weight']graph = Graph('三國(guó)人物關(guān)系力導(dǎo)引圖')graph.add('', nodes, links)graph.render('./images/三國(guó)人物關(guān)系力導(dǎo)引圖.html')graph 博客上不能顯示交互式圖表,這里就給出截圖:顯示了劉備的鄰接結(jié)點(diǎn) 整個(gè)網(wǎng)絡(luò)錯(cuò)綜復(fù)雜,背后是三國(guó)故事中無(wú)數(shù)的南征北伐、爾虞我詐。不過(guò)有了計(jì)算機(jī)的強(qiáng)大算力,我們依然可以從中梳理出某些關(guān)鍵線索,比如: 人物排名-重要性 對(duì)這個(gè)問(wèn)題,我們可以用網(wǎng)絡(luò)中的排序算法解決。PageRank 就是這樣的一個(gè)典型方法,它本來(lái)是搜索引擎利用網(wǎng)站之間的聯(lián)系對(duì)搜索結(jié)果進(jìn)行排序的方法,不過(guò)對(duì)人物之間的聯(lián)系也是同理。讓我們獲得最重要的 20 大人物: page_ranks = pd.Series(nx.algorithms.pagerank(G_global)).sort_values()page_ranks.tail(20).plot(kind='barh')plt.show() 《三國(guó)演義》當(dāng)仁不讓的主角就是他們了,哪怕你對(duì)三國(guó)不熟悉,也一定會(huì)對(duì)這些人物耳熟能詳。 人物排名-權(quán)力值 這個(gè)問(wèn)題看上去跟上面一個(gè)問(wèn)題很像,但其實(shí)還是有區(qū)別的。就像人緣最好的人未必是領(lǐng)導(dǎo)一樣,能在團(tuán)隊(duì)中心起到凝聚作用,使各個(gè)成員相互聯(lián)系合作的人才是最有權(quán)力的人。中心度就是這樣的一個(gè)指標(biāo),看看三國(guó)中最有權(quán)力的人是哪些吧? between = pd.Series(nx.betweenness_centrality(G_global)).sort_values()between.tail(20).plot(kind='barh')plt.show() 結(jié)果的確和上面的排序有所不同,我們看到劉備、曹操、孫權(quán)、袁紹等主公都名列前茅。而另一個(gè)有趣的發(fā)現(xiàn)是,司馬懿、司馬昭、司馬師父子三人同樣榜上有名,而曹氏的其他后裔則不見(jiàn)其名,可見(jiàn)司馬氏之權(quán)傾朝野。司馬氏之心,似乎就這樣被大數(shù)據(jù)揭示了出來(lái)! 社群發(fā)現(xiàn) 人物關(guān)系有親疏遠(yuǎn)近,因此往往會(huì)形成一些集團(tuán)。社交網(wǎng)絡(luò)分析里的社區(qū)發(fā)現(xiàn)算法就能夠讓我們發(fā)現(xiàn)這些集團(tuán),讓我使用 community [注2] 庫(kù)中的提供的算法來(lái)揭示這些關(guān)系吧。 import community # python-louvainpartition = community.best_partition(G_main) # Louvain算法劃分社區(qū)comm_dict = defaultdict(list)for person in partition: comm_dict[partition[person]].append(person) 在下面 3 個(gè)社區(qū)里,我們看到的主要是魏蜀吳三國(guó)重臣們。(只有一些小“問(wèn)題”,有趣的是,電腦并不知道他們的所屬勢(shì)力,只是使用算法。) draw_community(2) community 2: 張遼 曹仁 夏侯?lèi)?徐晃 曹洪 夏侯淵 張郃 許褚 樂(lè)進(jìn) 李典 于禁荀彧 劉曄 郭嘉 滿寵 程昱 荀攸 呂虔 典韋 文聘 董昭 毛玠 draw_community(4) community 4: 曹操 諸葛亮 劉備 關(guān)羽 趙云 張飛 馬超 黃忠 許昌 孟達(dá)[魏] 孫乾曹安民 劉璋 關(guān)平 龐德 法正 伊籍 張魯 劉封 龐統(tǒng) 孟獲 嚴(yán)顏 馬良 簡(jiǎn)雍 蔡瑁陶謙 孔融 劉琮[劉表子] 劉望之 夏侯楙 周倉(cāng) 陳登 draw_community(3) community 3: 孫權(quán) 孫策 周瑜 陸遜 呂蒙 丁奉 周泰 程普 韓當(dāng) 徐盛 張昭[吳] 馬相 黃蓋[吳] 潘璋 甘寧 魯肅 凌統(tǒng) 太史慈 諸葛瑾 韓吳郡 蔣欽 黃祖 闞澤 朱桓 陳武 呂范 draw_community(0) community 0: 袁紹 呂布 劉表 袁術(shù) 董卓 李傕 賈詡 審配 孫堅(jiān) 郭汜 陳宮 馬騰袁尚 韓遂 公孫瓚 高順 許攸[袁紹] 臧霸 沮授 郭圖 顏良 楊奉 張繡 袁譚 董承文丑 何進(jìn) 張邈[魏] 袁熙 還有一些其他社區(qū)。比如在這里,我們看到三國(guó)前期,孫堅(jiān)、袁紹、董卓等主公們?nèi)盒壑鹇?,好不熱鬧。 draw_community(1) community 1: 司馬懿 魏延 姜維 張翼 馬岱 廖化 吳懿 司馬昭 關(guān)興 吳班 王平鄧芝 鄧艾 張苞[蜀] 馬忠[吳] 費(fèi)祎 譙周 馬謖 曹真 曹丕 李恢 黃權(quán) 鐘會(huì) 蔣琬司馬師 劉巴[蜀] 張嶷 楊洪 許靖 費(fèi)詩(shī) 李嚴(yán) 郭淮 曹休 樊建 秦宓 夏侯霸 楊儀 高翔 張南[魏] 華歆 曹爽 郤正 許允[魏] 王朗[司徒] 董厥 杜瓊 霍峻 胡濟(jì) 賈充 彭羕 吳蘭 諸葛誕 雷銅 孫綝 卓膺 費(fèi)觀 杜義 閻晏 盛勃 劉敏 劉琰 杜祺 上官雝 丁咸 爨習(xí) 樊岐 曹芳 周群 這個(gè)社區(qū)是三國(guó)后期的主要人物了。這個(gè)網(wǎng)絡(luò)背后的故事,是司馬氏兩代三人打敗姜維率領(lǐng)的蜀漢群雄,又掃除了曹魏內(nèi)部的曹家勢(shì)力,終于登上權(quán)力的頂峰。 動(dòng)態(tài)網(wǎng)絡(luò) 研究社交網(wǎng)絡(luò)隨時(shí)間的變化,是個(gè)很有意思的任務(wù)。而《三國(guó)演義》大致按照時(shí)間線敘述,且有著極長(zhǎng)的時(shí)間跨度,順著故事線往下走,社交網(wǎng)絡(luò)會(huì)發(fā)生什么樣的變化呢? 這里,我取 10章 的文本作為跨度,每 5 章記錄一次當(dāng)前跨度中的社交網(wǎng)絡(luò),就相當(dāng)于留下一張快照,把這些快照連接起來(lái),我們就能夠看到一個(gè)社交網(wǎng)絡(luò)變化的動(dòng)畫(huà)。快照還是用 networkx 得到,而制作動(dòng)畫(huà),我們可以用 moviepy。 江山代有才人出,讓我們看看在故事發(fā)展的各個(gè)階段,都是哪一群人活躍在舞臺(tái)中央呢? import moviepy.editor as mpyfrom moviepy.video.io.bindings import mplfig_to_npimagewidth, step = 10,5range0 = range(0,len(G_chapters)-width+1,step)numFrame, fps = len(range0), 1duration = numFrame/fpspos_global = nx.spring_layout(G_main)def make_frame_mpl(t): i = step*int(t*fps) G_part = nx.Graph() for G0 in G_chapters[i:i+width]: for (u,v) in G0.edges: if G_part.has_edge(u,v): G_part[u][v]['weight'] += G0[u][v]['weight'] else: G_part.add_edge(u,v,weight=G0[u][v]['weight']) largest_comp = max(nx.connected_components(G_part), key=len) used_nodes = set(largest_comp) & set(G_main.nodes) G = G_part.subgraph(used_nodes) fig = plt.figure(figsize=(12,8),dpi=100) nx.draw_networkx_nodes(G,pos_global,node_size=[G.degree[x]*10 for x in G.nodes])# nx.draw_networkx_edges(G,pos_global) nx.draw_networkx_labels(G,pos_global) plt.xlim([-1,1]) plt.ylim([-1,1]) plt.axis('off') plt.title(f'第{i+1}到第{i+width+1}章的社交網(wǎng)絡(luò)') return mplfig_to_npimage(fig)animation = mpy.VideoClip(make_frame_mpl, duration=duration)animation.write_gif('./images/三國(guó)社交網(wǎng)絡(luò)變化.gif', fps=fps) 美觀起見(jiàn),動(dòng)畫(huà)中省略了網(wǎng)絡(luò)中的邊。 隨著時(shí)間的變化,曾經(jīng)站在歷史舞臺(tái)中央的人們也漸漸地會(huì)漸漸離開(kāi),讓人不禁唏噓感嘆。正如《三國(guó)演義》開(kāi)篇所言: 古今多少事,都付笑談中。 今日,小輩利用 python 做的一番笑談也就到此結(jié)束吧…… 源碼地址:https://github.com/blmoistawinde/hello_world/tree/master/sanguo_network 注: [1] harvesttext 是本人的作品~(*__*) ~,已在 Github 上開(kāi)源并可通過(guò) pip 直接安裝,旨在幫助使用者更輕易地完成像本文這樣的文本數(shù)據(jù)分析。除了本文涉及的功能以外,還有情感分析、新詞發(fā)現(xiàn)等功能。大家覺(jué)得有用的話,不妨親身嘗試下,看看能不能在自己感興趣的文本上有更多有趣有用的發(fā)現(xiàn)呢? [2] commutity 庫(kù)的本名是python-louvain,使用了和 Gephi 內(nèi)置相同的Louvain 算法進(jìn)行社區(qū)發(fā)現(xiàn) [3] 由于處理古文的困難性,本文中依然有一些比較明顯的錯(cuò)誤,希望大家不要介意~ |
|