用 Python 分析《紅樓夢》
1 前言
兩個月以來,我通過互聯網自學了一些文本處理的知識,用自然語言處理和機器學習演算法對《紅樓夢》進行了一些分析。這個過程中我找到了一些有趣的發現,所以我想寫一篇文章,既?與大家分享和討論實驗結果,也順便做一個整理和總結。(其實雖說是兩個月,但是中間停頓了一段時間,真正在做的時間大概是兩周左右)
我開始做這件事情是因為之前看到了一篇挺好玩的文章,大概內容是,作者用「結巴分詞」這個開源軟體統計了紅樓夢中各辭彙的出現次數(也就是詞頻),然後用詞頻作為每個章回的特徵,最終用「主成份分析」演算法把每個章回映射到三維空間中,從而比較各個章回的用詞有多麼相似。(文章地址:用機器學習判定紅樓夢後40回是否曹雪芹所寫)作者的結論是後四十回的用詞和前八十回有明顯的差距。
看完文章之後,我覺得有兩個小問題:首先,作者用的結巴分詞里的詞典是根據現代文的語料獲得的(參見「結巴分詞」開發者之前對網友的回復:模型的數據是如何生成的? · Issue #7 · fxsjy/jieba),而《紅樓夢》的文字風格是半文半白的,這樣的分詞方法準確性存疑;其次,雖然作者用《三國演義》做了對比,但是依然沒有有力地證明用詞差異沒有受到情節變化的影響。於是我決定自己做一遍實驗,用無字典分詞的方法來分詞,並且嘗試剔除情節對分析的影響,看看結果會不會有所不同。
本來開始寫的時候覺得 5000 字就差不多了,結果最後成文的時候竟然達到了 1.3 萬字。即使這樣,我也只能解釋一下演算法的大致工作過程,至於詳細的原理,如果感興趣的話可以找其他資料去學習,我也會附上一些資料鏈接。不然如果我寫的面面俱到的話感覺可以出書了……至於結果如何?先賣個關子。(誒,不要直接滑到底啊!)
程序已在 GitHub 上開源,使用方法參見 README 文件:LouYu2015/analysis_on_the_story_of_a_stone。考慮到版權問題,我決定不提供《紅樓夢》原文。如果想復現實驗結果的話,可以去找小說網站下載。(更新:根據網友提醒,《紅樓夢》因為作者去世遠遠超過 100 年而進入公有領域,不受版許可權制。因此我把原文也補充了上去,現在按照說明運行程序即可復現結果。也可在這裡獲取《紅樓夢》全文:紅樓夢 - 維基文庫,自由的圖書館。)
2 文本預處理
這一步很基礎,就不贅述了。簡單來說,就是要根據標點符號,把每一個分句都切開,然後用統一的符號(這裡我用的是井號)來標記切分點。這樣對於後面的程序來說就好處理一些了。
雖然目標很簡單,然而,有些細節還是需要額外處理一下的。比如,我找到的文本里,所有「性」啊,「露」啊之類的字都被用 『』 框了起來(可能為了過濾少兒不宜的內容?我怎麼覺得框起來以後更奇怪了……),所以這種標點需要被刪掉,不能當作分割符號。另外,每章開頭的回目編號也需要去掉,因為這不算小說的內容。最後,文本中出現了一些電腦中沒有的罕見字,不過好在文本中這些罕見字都在括弧內用拆分字型的方法標了出來(比如「(左王右扁)」),所以理論上我可以把這些內容替換成一些原文中沒有的字元(比如特殊符號),最後再替換回去。不過我太懶了,所以沒有做這樣的替換。理論上罕見字對後面的分析也不會有很大,因為後面涉及到的都是出現頻率比較高的單詞。
處理後的效果是這個樣子:
#甄士隱夢幻識通靈#賈雨村風塵懷閨秀#此開卷第一回也#作者自雲#因曾歷過一番夢幻之後#故將真事隱去#而借#通靈#之說#撰此石頭記一書也#故曰#甄士隱#云云#但書中所記何事何人#自又雲#今風塵碌碌一事無成#忽念及當日所有之女子#一一細考較去#覺其行止見識皆出於我之上#何我堂堂鬚眉誠不若彼裙釵哉#實愧則有餘#悔又無益之大無可如何之日也……
3 構建全文索引
得到處理後的文本之後,我需要建立一個全文索引。這樣是為了快速地查找原文內容,加速後面的計算。我使用了後綴樹這個結構作為索引。這個數據結構比較複雜,所以我們可以先談談更簡單的字典樹。
3.1 字典樹
首先,我們看看字典樹的樣子:
Free Image on Pixabay - Landscape, Tree, Flowers, Book
啊錯了,這個才是字典樹……
Trie - Wikipedia
上圖中,每個圓圈是一個結點,代表著一個字元串(就是圓圈內的內容);結點之間的連線是邊,代表著一個字母。最上面的結點,也就是空著的那個結點,是根結點。如果我們從根結點不斷向下走到某個結點,那麼把經過的每一條邊上的字母拼起來,就是這個結點代表的字元串了。這就是字典樹的特點。
那麼字典樹是幹什麼用的呢?舉個例子來說,假如我們想在這棵字典樹里查找 「to」 這個單詞,就可以先從根結點下面的邊里找到第一個字母,也就是 「t」 這條邊,從而找到 「t」 這個結點。然後我們再從 「t」 結點下面的邊里找到第二個字母,也就是 「o」 這條邊,就找到 「to」 這個結點了。假如 「to」 這個結點裡儲存了 「to」 的中文解釋,那麼我們只通過兩次操作就找到了 to 的中文意思。這樣比一個詞一個詞地找的方法快多了。這很像我們查字典的時候,先看第一個字母在字典中的位置,然後再看第二個字母……最終找到單詞,因此被稱為字典樹。
3.2 後綴樹
說完字典樹,我們再說說後綴樹的前身:後綴字典樹。後綴字典樹其實就是字典樹,只不過裡面的內容不是單詞,而是一個字元串的所有後綴:從第一個字母到最後一個字母的內容,從第二個字母到最後一個字母的內容……以此類推。比如說,"banana" 的所有後綴就是 banana, anana, nana, ana, na 和 a。把這些內容都加到字典樹里,就構成了後綴字典樹。下面左圖就是 banana 的後綴字典樹:
https://www.slideshare.net/farseerfc/ukks-algorithm-of-suffix-tree
而後綴樹和後綴字典樹的區別就是,在後綴樹中,我們要把下面只有一條邊的結點去掉,然後把這個結點連接的兩條邊壓縮成一條。比如,左圖後綴字典樹中的 b-a-n-a-n-a,在右圖的後綴樹中被壓縮成了 banana 這一條邊。此外,後綴樹還使用了一個技巧,就是不儲存邊的內容,而是儲存這些內容在原文中的位置。因為後綴樹中的很多內容都是重複的,所以這個小技巧可以大大減少索引的大小(用專業的語言描述,它的空間複雜度是 O(n))。
後綴樹又有什麼用呢?它最大的用途就是檢索字元串中間的內容。比如,假如我想查找 an 在 banana 中哪裡出現過,只需要查找代表 an 的結點,就找到了所有以 an 開頭的結點: anana 和 ana。由於每次出現 an 的地方都一定會產生一個以 an 開頭的後綴,而所有的後綴都在後綴樹中,所以這樣一定能夠找到所有 an 出現的位置。後綴樹的強大之處在於,即使我們把 banana 換成一篇很長很長的文章,我們也能很快地進行這樣的檢索。
最後,我使用了 Ukkonen 演算法快速地創建了整篇《紅樓夢》的後綴樹(用專業的語言描述 Ukkonen 演算法的速度:它的時間複雜度是 O(n))。Ukkonen 演算法比較複雜,所以這裡我不會講解 Ukkonen 演算法,感興趣的同學可以看看這些資料:
Ukkonen"s suffix tree algorithm in plain English
後綴樹的構造方法-Ukkonen詳解 - 懶人小何的日誌 - 網易博客
Ukkonen"s Suffix Tree Construction - Part 6 - GeeksforGeeks
有了全文索引以後,後面的程序就好做了。
4 製作字典
等等,我們不是要無字典分詞嗎,為什麼還要製作字典?其實無字典分詞並不是完全不用字典,只是說字典是根據原文生成的,而不是提前製作的。為了進行分詞,我們還是需要先找出文章中哪些內容像是單詞,才能確定如何進行切分。
那麼怎麼確定哪些內容像單詞呢?最容易想到的方法就是:把所有出現次數高的片段都當成單詞。聽上去很有道理,所以我們可以試一試,用後綴樹查詢紅樓夢中的所有重複的片段,然後按出現次數排個序:
寶玉(3983)、笑道(2458)、太太(1982)、什麼(1836)、鳳姐(1741)、了一(1697)、賈母(1675)、一個(1520)、也不(1448)、夫人(1437)、黛玉(1370)、我們(1233)、那裡(1182)、襲人(1144)、姑娘(1142)、去了(1090)、寶釵(1079)、不知(1074)、王夫人(1061)、起來(1059)
上面是出現頻率前 20 的片段,括弧內是出現次數。可以看到效果還不錯,很多片段都是單詞。然而,排在第六名的「了一」明明不是個單詞,出現次數卻比賈母還要高。可見這樣的篩選方法還是有一定問題的。而且,這樣被誤當成單詞的片段還有很多,例如「了的」、「的一」之類的。究其原因,是因為出現次數 TOP 5 的單字由高到低分別是「了、的、不、一、來」,所以它們的組合也會經常出現。為了排除這樣的組合,我們可以用「凝固度」來進行進一步地篩選。
4.1 凝固度
凝固度的定義是:一個片段出現的頻率比左右兩部分分別出現的頻率的乘積高出多少倍(注意,頻率表示的是出現的比例,而頻數表示的是出現的次數)。不過這句話太拗口了,還是用公式描述比較好。如果 P(AB) 是片段出現的頻率,P(A) 是片段左邊的字的出現的頻率, P(B) 是右邊的字出現的頻率,那麼凝固度 co 就是:
公式中, 就是左右部分在完全隨機組合的情況下被組合到一起的概率。凝固度的思想是:如果片段實際出現的概率比被隨機組合出來的概率高出很多倍,就說明這樣的組合應該不是意外產生的,而是有一些關聯的。這個關聯很可能就是因為這個片段是一個不可分割的整體,也就是單詞。
對於超過兩個字的片段,可以嘗試每一種拆分方法(比如「賈寶玉」有「賈/寶玉」和「賈寶/玉」兩種拆分方法),然後取各種方法的凝固度的最小值。
現在我選出《紅樓夢》中出現次數大於 5 的片段,對它們的凝固度做個排序:
翡翠(171415.92)、茉莉(171415.92)、砒霜(171415.92)、逶迤(142846.60)、誹謗(142846.60)、徘徊(142846.60)、繾綣(142846.60)、乜斜(142846.60)、戥子(142846.60)、籰子(142846.60)、姽嫿(122439.94)、蝴蝶(122439.94)、囟門(122439.94)、檳榔(122439.94)、琵琶(122439.94)、孌童(119038.83)、筏子(119038.83)、牲口(119038.83)、躊躇(107134.95)、隄防(107134.95)
這是凝固度排名前 20 的組合,括弧內是凝固度。可以看到效果還是不錯的。
接著往下看,在 Top 20~100 里也基本沒有不是單詞的條目:
靦腆、隄防、甬路、趔趄、蚊子、獅子、瓔珞、療治、羔子、跛足、堯舜、嫦娥、陛見、簸籮、梆子、粳米、竭力、栗子、攛掇、葵官、芭蕉、玲瓏、俞祿、妯娌、嘁嘁喳喳、玷辱、奚落、互相、譬如、腕上、禱告、攮的、鑰匙、覷著、恣意、磯上、饅頭、閻王、椒房、茯苓、琪官、牡丹、恆王、凹晶、翰林、畸角、淌眼、籃子、滋味、韶華、爆竹、漲了、芍藥、估量、拷問、杌子、嗓子、搪塞、晦氣、麒麟、玫瑰、葫蘆、躬身、懇切、崽子、盹兒、皂白、謠言、凸碧、唯唯、赫赫、簌簌、蔭堂、嗤的、嘮叨、努嘴、吆喝、荳官、茯苓霜、艱難
然而凝固度也有一定的局限性。再往後看的話,會發現裡面還有很多片段是半個詞,而它們的凝固度也挺高的。例如:「香院」(完整的詞應該是「梨香院」)、「太太太太」(完整的詞應該是「老太太太太」)。想想也有道理,這些片段雖然是半個詞,但是它們確實也跟完整的單詞一樣是「凝固」在一起的。所以,光看凝固度是不夠的,還要通過上下文判斷這個詞是否完整。
4.2 自由度
為了排除掉不完整的單詞,我們可以使用自由度這個概念來繼續過濾。自由度的思想是這樣的:如果一個組合是一個不完整的單詞,那麼它總是作為完整單詞的一部分出現,所以相鄰的字就會比較固定。比如說,「香院」在原文中出現了 23 次,而「梨香院」出現了 22 次,也就是說「梨」在「香院」的左邊一起出現的頻率高達 95.7%,所以我們有把握認為」香院」不是完整的單詞。而自由度描述的就是一個片段的相鄰字有多麼的多樣、不固定。如果片段的自由度比較高,就說明這個詞應該是完整的。
因為相鄰字分為左側和右側,所以自由度也分為左右兩部分。以左側的自由度為例,計算公式就是左側相鄰字的每一種字的頻率的總信息熵。也就是說,如果 是左側自由度, 到 是每種左側相鄰字出現的頻率,那麼:
(對於沒學過信息熵的同學來說這個公式可能很晦澀,反正記住左側自由度體現了左側相鄰字的多樣性就可以了。)
我們把左側自由度最低的 20 個組合拿出來,可以看到確實過濾出來了很多不是單詞的內容:
淚來(0.111)、在話(0.112)、慢的(0.116)、頭們(0.117)、今我(0.121)、雲笑(0.122)、以我(0.141)、王二(0.146)、里知道(0.146)、己也(0.151)、會子又(0.154)、太和(0.156)、用說(0.159)、嘻的(0.165)、今且(0.169)、么東西(0.187)、苦來(0.187)
(括弧內為左側自由度)
右側也同理,有些片段明顯是半個單詞:
有什(0.034)、周瑞家(0.053)、老太(0.065)、薛姨(0.072)、也罷(0.085)、老祖(0.093)、哭起(0.100)、在話(0.112)、聽下(0.113)、些東(0.118)、林之(0.121)、個婆(0.126)、我告(0.129)、老嬤(0.139)、二夫(0.144)、邢王二(0.149)、就罷(0.154)、到自(0.169)、這會(0.175)、大嫂(0.179)
(括弧內為右側自由度)
4.3 最終的單詞表
有了這些明確的評判標準,我們就可以把單詞篩選出來了。我最終選擇的判斷標準是:出現次數大於等於 5,且凝固度、左側自由度、右側自由度都大於 1。然而這個標準還是太寬鬆了。於是,我又設計了一個公式,把這些數據綜合起來:
也就是說,我簡單粗暴地把凝固度和自由度乘了起來,作為每個片段的分數。這樣只要其中一個標準的值比較低,總分就會比較低。於是我的判斷標準里又多了一條:總分還要大於等於 100。
經過層層遴選之後,單詞表初步成型了。我從最終結果中隨機抽取了 100 個條目,其中有 47 個是單詞:
佩鳳、尋常、歇下、王公、不提、仍往、親熱、之後、犯事、小戲子、現今、兩三天、縫兒、彎著腰、魂飛、故典、海棠社、支使、發熱、感激、壓倒、一座、已到、洋漆、包勇、查抄、舅爺、石榴、報與、戥子、一匹、拐子、家裡、林黛玉、法子、空門、值錢、抿嘴、未娶、秋爽齋、發誓、明日、相伴、舒服、小幺兒、李紈、仙長
這意味單詞表的正確率只有一半左右。不過,在錯誤的條目里,很多條目的切分其實正確的,只是有好幾個詞粘到了一起:
趕不上、個字、料他、你快去、丫鬟婆子、無有不、抿著嘴笑、在外間睡、把我、一個小、叫我怎麼、飯來、句好話、忙命人、惱的、答應了、提那、告辭了、庵里、和二奶奶、謝他、個女人、領著、急忙進、池子里、捶著、手裡拿著一、五百兩、之為人、和姑娘們、不知怎麼樣、罵一、是那裡來的、家的小姐、十歲的、個眼睛、如今我、幾個小、丫鬟名、省一、俗了、一一的、聽了這話、攆出去、梳洗了、淡淡的、恨不能、可惜了、件大事、作詩的、與尤老、散與、究竟不
雖然正確率不高,但其實沒有必要通過調高篩選標準的方法來進行更嚴格的過濾了。隨後分詞演算法將會解決單詞沒有被切開的問題。如果繼續調高標準,可能會導致很多確實是單詞的條目被去除。
參考資料:
基於信息熵的無字典分詞演算法 - 成都笨笨 - 博客園
5 分詞
之前在篩選單詞的時候,思路就是用各種各樣的數值標準進行判斷。而對於「分詞」這個看似更加困難的問題,思路也是類似的:制定一個評價切分方案的評分標準,然後找出評分最高的切分方案。評分標準是什麼呢?最簡單的標準就是,把切分之後每個片段是單詞的概率都乘起來,作為這個切分方案正確的概率,也就是評分標準。我們假設,一個片段是單詞的概率,就是這個片段在原文中的出現頻率。
有了評分標準之後,還有一個問題:如何找出分數最高的切分方案呢?肯定不能一個一個地嘗試每一種方案,不然速度實在是太慢了。我們可以用一個數學方法來簡化計算:維特比演算法。
5.1 維特比演算法
維特比演算法本質上就是一個動態規劃演算法。它的想法是這樣的:對於句子的某個局部來說,這一部分的最佳切分方案是固定的,不隨上下文的變化而變化;如果把這個最佳切分方案保存起來,就能減少很多重複的計算。我們可以從第一個字開始,計算前兩個字,前三個字,前四個字……的最佳切分方案,並且把這些方案保存起來。因為我們是依次計算的,所以每當增加一個字的時候,我們只要嘗試切分最後一個單詞的位置就可以了。這個位置前面的內容一定是已經計算過的,所以通過查詢之前的切分方案即可計算出分數。這就是維特比演算法的工作原理。
舉個例子,這是計算「寶玉黛玉」每種切分方式的得分的過程:
寶: p = 0.0079487991(最佳切分)
寶玉: p = 0.0079427436(最佳切分)寶/玉: p = 0.00795 * 0.00827 = 0.0000657630寶玉黛: p = 0.0000077623寶/玉黛: p = 0.00795 * 0.00001 = 0.0000000511寶玉/黛: p = 0.00794 * 0.00189 = 0.0000149872(最佳切分)寶玉黛玉: p = 0.0000096861寶/玉黛玉: p = 0.00795 * 0.00001 = 0.0000000617
寶玉/黛玉: p = 0.00794 * 0.00273 = 0.0000216996(最佳切分)寶玉黛/玉: p = 0.00001 * 0.00827 = 0.0000001240(注意,這裡計算時使用的是「寶玉黛」的最佳切分的分數,而不是「寶玉黛」這個片段本身的分數)
這樣得到每種切分方式的得分之後,程序先根據最後一步的結果,把「黛玉」切分出去,剩下「寶玉」。然後程序再看「寶玉」的各種切分結果,發現不切分的得分最高,於是把「寶玉」也切分了出去。最後,程序發現沒有剩下的內容了,於是切分完成了。
5.2 一些的調整
在構造單詞表的時候,我計算了每個片段有多麼像單詞,也就是分數。然而,後面的分詞演算法只考慮了片段出現的頻率,而沒有用到片段的分數。於是,我簡單粗暴地把片段的分數加入到了演算法中:把片段的頻率乘上片段的分數,作為加權了的頻率。這樣那些更像單詞的片段具有更高的權重,就更容易被切分出來了。
此外,還有一個問題:如果一個片段不在字典中,怎樣計算它的頻率?在需要外界提供字典的分詞演算法中,這是一個比較棘手的問題。不過在無字典(準確的說是自動構造字典)的演算法中,這反而是一個比較容易解決的問題:任何要切分的片段一定會出現在後綴樹中,因為這個片段是原文的一部分!所以,我們只需要通過後綴樹查詢這個片段的頻數,就可以計算它在原文中的頻率了。
最後還有一個小優化。我們知道,一般中文單詞的長度不會超過四個字,因此在程序枚舉切分方法的時候,只需要嘗試最後四個切分位置就可以了。這樣就把最長的切分片段限制在了四個字以內,而且對於長句子來說也減少了很多不必要的嘗試。
5.3 分詞演算法的測試
我選擇了兩段原文內容來測試演算法的準確性。
這是第二回開頭的一段敘事性片段的機器分詞結果:
只見/封肅/方/回來,歡天喜地,眾人/忙問/端的,他/乃說道,原來/本府/新/升的/太爺,姓賈名化,本/湖州人氏,曾與/女婿/舊日/相交,方才/在/咱們/前/過去,因/看見/嬌杏/那/丫頭/買線,所以/他/只當/女婿/移/住/於此,我一/一/將原故/回明,那/太爺倒/傷感/嘆息/了一回,又問/外孫女兒,我說/看燈/丟了,太/爺說,不妨,我自/使/番役/務必/探/訪/回來,說/了一回/話,臨走/倒/送了/我/二兩銀子,甄家/娘子/聽了,不免/心中/傷感,一宿無話,至/次日/早有/雨村/遣人/送了/兩封/銀子,四匹/錦緞,答/謝甄家娘/子,又/寄/一封/密/書與/封肅,托他/向/甄家/娘子/要/那/嬌杏/作二房,封肅/喜的/屁滾尿流,巴不得/去/奉承,便在/女兒/前一/力/攛掇/成了,乘/夜/只/用一/乘小轎,便把/嬌杏/送/進去/了,雨村/歡喜/自不必說,乃/封/百金/贈/封肅,外/又謝/甄家/娘子/許多/物/事,令其/好生養/贍,以待/尋訪/女兒/下落,封肅/回家/無話,卻說/嬌杏/這/丫鬟,便是/那年/回顧/雨村/者,因/偶然/一/顧,便/弄出/這段事來,亦是/自己/意/料不/到之/奇緣,誰想/他/命運/兩/濟,不/承望/自/到/雨村/身邊,只/一年/便/生了一子,又/半載,雨村/嫡妻/忽染疾/下世,雨村/便將他/扶/側/作/正室/夫人/了,正是,偶因/一/著/錯,便/為人/上/人
這是人工分詞的結果:
只見/封肅/方/回來,歡天喜地,眾人/忙問/端的,他/乃/說道,原來/本府/新升/的/太爺,姓/賈/名/化,本/湖州/人氏,曾/與/女婿/舊日/相交,方才/在/咱們/前/過去,因/看見/嬌杏/那/丫頭/買線,所以/他/只當/女婿/移住/於/此,我/一一/將/原故/回明,那/太爺/倒/傷感/嘆息/了/一回,又/問/外孫女兒,我/說/看燈/丟了,太爺/說,不妨,我/自/使/番役/務必/探訪/回來,說/了/一回話,臨走/倒/送了/我/二兩/銀子,甄家/娘子/聽了,不免/心中/傷感,一宿/無話,至/次日/早有/雨村/遣人/送了/兩封/銀子,四匹/錦緞,答謝/甄家/娘子,又/寄/一封/密書/與/封肅,托/他/向/甄家/娘子/要/那/嬌杏/作/二房,封肅/喜的/屁滾尿流,巴不得/去/奉承,便/在/女兒/前/一力/攛掇/成了,乘夜/只用/一乘/小轎,便/把/嬌杏/送/進去/了,雨村/歡喜/自/不必/說,乃/封/百金/贈/封肅,外/又/謝/甄家/娘子/許多/物事,令/其/好生/養贍,以/待/尋訪/女兒/下落,封肅/回家/無話,卻說/嬌杏/這/丫鬟,便是/那年/回顧/雨村/者,因/偶然/一顧,便/弄出/這段/事/來,亦是/自己/意料/不到/之/奇緣,誰想/他/命運/兩濟,不/承望/自/到/雨村/身邊,只/一年/便/生了/一/子,又/半載,雨村/嫡妻/忽/染疾/下世,雨村/便/將/他/扶側/作/正室/夫人/了,正是,偶/因/一著/錯,便/為/人上人
經過統計,程序的準確率是 85.71%(意義是程序切開的位置有多少是應該切開的),召回率是 75.00%(意義是應該切開的位置有多少被程序切開了)。這個結果看上去不是很高,因為大部分開源的分詞軟體準確率都能達到 90% 以上,甚至能達到 97% 以上。不過,畢竟我用的是無字典的分詞,而且演算法也比較簡單,所以我還是比較滿意的。
下面再看看詩詞類片段的分詞效果。這是《葬花吟》的機器分詞結果:
花謝/花飛/花滿/天,紅/消/香/斷有/誰憐,遊絲/軟/系飄春榭,落/絮輕沾撲/綉簾,閨中/女兒/惜春/暮,愁緒/滿懷/無/釋/處,手/把花鋤/出/綉簾,忍/踏/落花/來/復去,柳絲榆莢/自/芳菲,不管/桃/飄/與李/飛,桃李/明年/能再/發,明年/閨中/知/有誰,三月/香/巢已壘成,梁間/燕子/太/無情,明年/花/發/雖可/啄,卻不/道/人去/梁空巢/也/傾,一年/三百六十/日,風/刀霜劍嚴/相/逼,明/媚/鮮妍/能/幾時,一朝/飄泊/難/尋覓,花開/易見/落/難尋,階前/悶/殺/葬花/人,獨/把花鋤/淚/暗灑,灑/上/空/枝/見/血/痕,杜/鵑/無語/正/黃昏,荷鋤歸/去掩/重門,青燈/照壁/人初/睡,冷/雨敲窗被/未/溫,怪/奴/底/事/倍傷/神,半/為/憐春/半/惱/春,憐春/忽至/惱/忽/去,至/又/無言/去/不聞,昨宵/庭外悲歌/發,知是/花魂/與/鳥魂,花魂/鳥魂/總難/留,鳥/自/無言/花自/羞,願/奴/脅下/生/雙翼,隨/花/飛到/天盡頭,天盡頭,何處/有/香/丘,未/若錦/囊收艷骨,一堆/凈/土掩/風流,質本潔/來/還/潔/去,強/於/污/淖陷渠溝,爾今/死去/儂收葬,未卜/儂/身/何日/喪,儂今葬/花人笑痴,他年葬儂/知是誰,試看/春/殘花/漸/落,便是/紅顏老/死時,一朝春盡/紅顏老,花落人亡/兩/不知
這是人工分詞結果:
花謝/花飛/花滿天,紅消/香斷/有/誰/憐,遊絲/軟系/飄/春榭,落絮/輕沾/撲/綉簾,閨中/女兒/惜/春暮,愁緒/滿懷/無/釋處,手/把/花/鋤/出/綉簾,忍/踏/落花/來/復/去,柳絲/榆莢/自/芳菲,不管/桃飄/與/李飛,桃李/明年/能/再發,明年/閨中/知/有/誰,三月/香巢/已/壘成,梁間/燕子/太/無情,明年/花發/雖/可啄,卻/不道/人去/梁空/巢/也/傾,一年/三百/六十/日,風刀/霜劍/嚴/相逼,明媚/鮮妍/能/幾時,一朝/飄泊/難/尋覓,花開/易見/落/難尋,階前/悶殺/葬花人,獨/把/花/鋤/淚/暗灑,灑上/空枝/見/血痕,杜鵑/無語/正/黃昏,荷鋤/歸去/掩/重門,青燈/照壁/人/初睡,冷雨/敲窗/被/未溫,怪/奴/底事/倍/傷神,半為/憐春/半/惱春,憐春/忽至/惱/忽去,至/又/無言/去/不聞,昨宵/庭外/悲歌/發,知是/花魂/與/鳥魂,花魂/鳥魂/總/難留,鳥/自/無言/花/自/羞,願/奴/脅下/生/雙翼,隨花/飛到/天/盡頭,天/盡頭,何處/有/香丘,未/若/錦囊/收/艷骨,一堆/凈土/掩/風流,質/本/潔/來/還/潔/去,強/於/污淖/陷/渠溝,爾/今/死去/儂/收葬,未卜/儂身/何日/喪,儂/今/葬花/人/笑痴,他年/葬/儂/知/是/誰,試看/春殘/花/漸/落,便是/紅顏/老/死/時,一朝/春盡/紅顏/老,花落/人亡/兩/不知
這下程序的準確率下降到了 74.07%,召回率也下降到了 67.04%,分別下降了將近 10%,可見詩歌的分詞更難一些。這也在情理之中,因為詩詞中有很多不常用詞,有些詞甚至只出現過一次,所以電腦很難從統計數據中發掘信息。
6 詞頻統計
完成分詞以後,詞頻統計就非常簡單了。我們只需要根據分詞結果把片段切分開,去掉長度為一的片段(也就是單字),然後數一下每一種片段的個數就可以了。
這是出現次數排名前 20 的單詞:
寶玉(3940)、笑道(2314)、鳳姐(1521)、什麼(1432)、賈母(1308)、襲人(1144)、一個(1111)、黛玉(1102)、我們(1068)、王夫人(1059)、如今(1016)、寶釵(1014)、聽了(938)、出來(934)、老太太(908)、你們(890)、去了(879)、怎麼(867)、太太(856)、姑娘(856)
(括弧內為頻數)
可以跟之前只統計出現次數,不考慮切分問題的排名做個對比:
寶玉(3983)、笑道(2458)、太太(1982)、什麼(1836)、鳳姐(1741)、了一(1697)、賈母(1675)、一個(1520)、也不(1448)、夫人(1437)、黛玉(1370)、我們(1233)、那裡(1182)、襲人(1144)、姑娘(1142)、去了(1090)、寶釵(1079)、不知(1074)、王夫人(1061)、起來(1059)
(括弧內為頻數)
通過分詞後的詞頻,我們發現《紅樓夢》中的人物戲份由多到少依次是寶玉、鳳姐、賈母、襲人、黛玉、王夫人和寶釵。然而,這個排名是有問題的,因為」林黛玉」這個詞的出現次數還有 267 次,需要加到黛玉的戲份里,所以其實黛玉的戲份比襲人多。同理,「老太太」一般是指賈母,所以賈母的戲份加起來應該比鳳姐多。正確的排名應該是寶玉、賈母、鳳姐、黛玉、襲人、王夫人和寶釵。
此外,我們還發現《紅樓夢》中的人物很愛笑,因為除了人名以外出現次數最多的單詞就是「笑道」 : )
我把完整的詞頻表做成了一個網頁,感興趣的話可以去看一下:紅樓詞表 第二版
最後,我隨機選擇了詞頻表中的 200 項條目,用來估計其中有多少是真正的單詞。其中有 82 條是單詞:
暗地、老君、男人、匈奴、病在垂危、聞名不如、追索、氣怔、神昏、照照、守著、檔子、送去、下山、玉皇、菲材獲譴、托他、本身、這番、大海、十載、記的、遵諭、芸哥兒、現買、專司其職、天上人間、法官、推就、階上、所知、別物、朝陽、懼罪、入塾、前代、當地、神瑛、名利、嘩喇、句句、辮梢、端上、駙馬、按理、開金、以下、清官、香甜、猿背、避人、開眼、殊不知、笞杖、祭弔、藥方、色紅、鐵鎖、看見、逗蜂軒、不勝、上樓、正官祿馬、國中、入會、轉步、魄化、等等兒、公侯、代善、排律、只見、晝夜、外國、日月、蒓噎滿喉、誇獎、禮儀、自稱、王妃、千秋、買棺盛殮
而 118 條不是單詞:
上值日的、聽我說、帶的、到館來、餘之、搛了、一兩點、管這、誰先、料也、且喜、一兩天、糯五十斛、命坐、殺了、在你、痛的、等都說、現吃、怔了、這金、個個是、我原、氏忙、也錯不得、子本、毀僧謗、遮著、了手、眼淚直、菜已、己有、別理、涼著、遂不、仍復、的差役、們把、太爺的、你只怨、同你去、忙進去、腌髒話、在後面、又驚又、隊隊、名的、去睬他、與平、一個錢、沒聽、株枯木、正二刻、靜了、已醒了、釅釅的沏、細說與、醉中、個年高有、鬆了、了兩聲、賈珍也、讓至、早晚才、描鸞刺、那裡肯、松的、秋閨怨、張花梨、榮寧兩、待你、再多言、反嚇、里調、如若、庶不、是顆、到書、當此、點上燈來、去托、又寬、又聯道、你特、行一、卻難、薔大爺芸、其真、人情等、才吃了葯、再揀、而誕、兩手抱、便福、被劫、家大小、病就、各按、排穗褂、喝了幾杯、再說、面皆、天氣和暖、了對半、明日還、隨往、聽著、狹窄猶、沒好、兒給、有說有、傍晚得、而智者憂、不留心、聽著怪、不依我呢、而自、玉來
也就是說,單詞的正確率只有 41 %。這比字典的準確率還低,並沒有因為採用了分詞演算法而提高了正確率。不過這也可以理解,因為生成字典的時候我只考慮了出現次數大於 5 的片段,而分詞的時候有些單詞只出現了一次,所以難度確實應該更大一些。
詞頻表中總計有 3.99 萬個條目。根據估算的詞頻表中正確單詞的比例,我估計《紅樓夢》的辭彙量大約是 1.6 萬。有人用其他程序估計《紅樓夢》的辭彙量是 0.45 萬(http://bbs.creaders.net/politics/bbsviewer.php?trd_id=344894),不過作者沒有描述詳細的統計方法,所以我對其結果非常懷疑,因為《紅樓夢》中的單字就有 0.35 萬種了。
7 篩選特徵詞
終於做完了分詞,又離目標靠近了一大步。現在,我可以用之前看到的那篇文章里提到的 PCA 演算法來分析章回之間的差異了。不過在此之前,我想先反思一下,到底應該用哪些詞的詞頻來進行分析?
在很多用 PCA 分析《紅樓夢》的博文里,大家都是用出現頻率最高的詞來分析的。然而問題是,萬一頻率最高的詞是和情節變化相關的呢?為了剔除情節變化的影響,我決定選出詞頻隨情節變化最小的單詞來作為每一章的特徵。而我衡量詞頻變化的方法就是統計單詞在每一回的詞頻,然後計算標準方差。為了消除單詞的常用程度對標準方差的影響,我把標準方差除以該單詞在每一回的平均頻數,得到修正後的方差,然後利用這個標準來篩選特徵詞。
按照這個標準,與情節最無關的 20 個詞是:
下回分解(0.27)、也不(0.50)、不知(0.51)、一個(0.52)、起來(0.55)、如今(0.55)、自己(0.55)、聽了(0.55)、那裡(0.56)、什麼(0.57)、出來(0.58)、說著(0.58)、話說(0.59)、這裡(0.61)、來了(0.63)、只得(0.63)、我們(0.64)、只是(0.64)、怎麼(0.65)、就是(0.66)
(括弧內為修正後的方差)
有趣的是,處在排名末尾的詞,也就是詞頻變化最大的詞,大部分都是人名:
丫鬟、請安、平兒、家的、薛姨媽、家人、光景、二奶奶、賈璉、賈政、李紈、林姑娘、父親、探春、邢夫人、奴才、哥兒、母親、女兒、媽媽、麝月、惜春、晴雯、鳳姐兒、賈珍、林黛玉、鴛鴦、湘雲、尤氏、迎春、林之孝、紫鵑、薛蟠、寶琴、趙姨娘、香菱、周瑞、雨村、雪雁、妙玉、鶯兒、劉姥姥、芳官、秦鍾、金桂、寶蟾
可見這個篩選方法確實能去掉我們不想要的特徵詞。
最終,我選擇了詞頻變化最小的 50 個詞作為特徵,每個詞的修正後標準方差都小於 0.85。這 50 個詞如下:
下回分解、也不、不知、一個、起來、如今、自己、聽了、那裡、什麼、出來、說著、話說、這裡、來了、只得、我們、只是、怎麼、就是、去了、進來、知道、只見、這樣、出去、一時、還有、不得、都是、你們、寶玉、見他、不能、聽見、不是、兩個、說道、一面、咱們、這個、不敢、的人、沒有、還不、又不、笑道、所以、不過、叫他
8 主成份分析(PCA)
理論上,有了特徵之後,我們就可以比較各個章節的相似性了。然而問題是,現在我們有 50 個特徵,也就是說現在的數據空間是 50 維的,這對於想像四維空間都難的人類來說是很難可視化的。對於高維數據的可視化問題來說,PCA 是一個很好用的數學工具。
9.1 何謂是主成份分析
因為高維的數據空間很難想像,所以我們可以先想像一下低維的情況。比如說,假設下圖中的每個點都是一個數據,橫坐標和縱坐標分別代表兩個特徵的值:
https://zh.wikipedia.org/wiki/%E4%B8%BB%E6%88%90%E5%88%86%E5%88%86%E6%9E%90#/media/File:GaussianScatterPCA.png
現在,如果我們讓 PCA 程序把這兩個特徵壓縮成一個特徵的話,演算法就會尋找一條直線,使得數據點都投影到這條直線上後損失的信息最少(如果投影不好理解的話,可以想像用兩塊平行於直線的板子把數據點都擠壓到一條線上)。在這個例子中,這條線損失信息最少的線就是圖中較長的那個箭頭。這樣,如果我們知道了一個數據點在直線上投影的位置,我們就能大致知道數據點在壓縮之前的二維空間的位置了(比如是在左上角還是右下角)。
以上是把二維數據空間壓縮到一維的情況。三維壓縮到二維的情況也是類似的:尋找一個二維平面,使得數據點投影到平面後損失的信息最少,然後把所有數據點投影到這個平面上去。三維壓縮到一維就是把尋找平面改成尋找直線。更高維度的情況以此類推,雖然難以想像,但是在數學上是一樣的。
至於演算法如何找到損失信息最少的二維平面(或者直線、三維平面等等),這會涉及到一些數學知識,感興趣的同學可以去查找一下相關的數學公式和證明。這裡只要把這個演算法當成一個黑箱就可以了。
9.2 重大發現?
現在我們可以利用 PCA,把五十個詞的詞頻所構成的五十個維度壓縮到二維平面上了。我把壓縮後的數據點畫出來,發現是這個樣子的:
(圖中每個圓圈代表一個回目。圓圈內是回目編號,從 1 開始計數。紅色圓圈是 1-40 回,綠色圓圈是 41-80回,藍色圓圈是 81-120 回。)
80 回以後的內容(藍色)大部分都集中在左下角的一條狹長的區域內,很明顯地和其他章回區分開來了!莫非《紅樓夢》的最後 40 回真的不是同一個作者寫的?!
別著急,分析還沒結束。PCA 的一個很重要的優點就是,它的分析結果具有很強的可解釋性,因為我們可以知道每一個原始特徵在壓縮後的特徵(或者說成分)中的權重。從上圖中可以看到,後 40 回的主要區別在於成分二(component 2)的數值。因此我們可以看一看每一個詞的詞頻在成分 2 中的權重排名:
笑道(0.883)、我們(0.141)、一個(0.133)、你們(0.128)、兩個(0.113)、說著(0.079)、咱們(0.076)、這個(0.063)、聽了(0.052)、還有(0.046)、一面(0.045)、來了(0.037)、都是(0.032)、不過(0.028)、去了(0.027)、又不(0.025)、出去(0.021)、這樣(0.018)、如今(0.016)、這裡(0.016)、還不(0.011)、見他(0.011)、出來(0.010)、就是(0.010)、一時(0.008)、起來(0.005)、只見(0.002)、不是(0.002)、下回分解(0.000)、不得(-0.001)、也不(-0.001)、話說(-0.002)、的人(-0.005)、不知(-0.007)、那裡(-0.009)、叫他(-0.011)、不敢(-0.011)、自己(-0.011)、不能(-0.017)、什麼(-0.019)、所以(-0.020)、只是(-0.023)、知道(-0.026)、進來(-0.036)、說道(-0.046)、怎麼(-0.050)、只得(-0.056)、沒有(-0.077)、聽見(-0.092)、寶玉(-0.312)
(括弧內為權重)
我發現,「笑道」這個詞不僅是除了人名以外出現次數最多的單詞,而且在 PCA 結果中的權重也異常地高(0.88),甚至超過了「寶玉」的權重的絕對值(0.31)!為了搞明白這個詞為什麼有這麼大的權重,我把「笑道」的詞頻變化畫了出來:
(圖中橫坐標是章回編號,縱坐標是「笑道」的詞頻)
可以發現,「笑道」的詞頻是先增加再減少的,這不禁讓我聯想到了賈府興衰的過程。莫非「笑道」的詞頻和賈府的發展狀況有關?有趣的是,「笑道」的詞頻頂峰出現在第 50 回左右,而有些人從劇情的角度分析認為賈府的鼎盛時期開始於第 48、49 回,恰好重合:
《紅樓夢》之「釵黛合一」帶來大觀園鼎盛_風之子9881198198_新浪博客
[轉載]白坤峰講紅樓夢(172)賈府鼎盛:該來的都來了_史鼎說紅樓_新浪博客
也許「笑道」這一看似平常的辭彙確實側面反應了賈府的興衰史呢。雖然因果關係有待考證,不過想想也有一點道理,畢竟只有日子過的好的時候人們才會愛笑。
9.3 再次分析
在之前的分析中我們發現,「笑道」這個詞似乎和情節的關係比較大,並且嚴重影響到了我們的分析。此外,「寶玉」作為一個人名,它的權重的絕對值也比較大,也可能是受到了情節的影響。因此,我決定把這兩個詞「拉黑」,用剩下的 48 個詞的詞頻做特徵,再次進行 PCA 分析。這次結果如下:
這次我需要把特徵壓縮到三維空間而非二維空間了。這是因為之前我們得到的兩個成分的方差貢獻率(可以理解為成分提供的信息量)分別為 44.6% 和 19.0 %,總貢獻率 63.6%,算是比較高了。而現在,即使是三個成分,方差貢獻率也只有 23.9%,10.6% 和 6.9% 了,總貢獻率才 41.4%。可見去掉「笑道」和「寶玉」以後,從詞頻中發掘信息的難度提高了很多。
從圖中可以看到,現在後 40 回已經不像之前那麼聚集了,不過還是可以看出一點聚集的趨勢。特別地,前 80 回和後 40 回在成分二和成分三上的區別比較明顯。和之前一樣,我們可以把在這兩個成分中權重的絕對值比較大的詞都找出來,看看它們的詞頻變化。
在成分三中,權重最小的五個單詞是:沒有(-0.41)、聽見(-0.25)、如今(-0.21)、所以(-0.18)、我們(-0.14)。(括弧內為權重)
而權重最大的五個單詞是:聽了(0.22)、兩個(0.26)、說著(0.30)、只見(0.37)、一面(0.39)
成分二中,權重最小的三個單詞是:什麼(-0.30)、怎麼(-0.26)、聽見(-0.22)
權重最大三個單詞是:一個(0.28)、你們(0.37)、我們(0.43)
(「聽見」在排名中出現了兩次。不過不知道這個發現有什麼用。)
可以發現,有些詞的詞頻確實有一些異常的變化。然而,這些變化到底有沒有受到劇情影響呢?感覺很難說。此外,在 PCA 結果中,似乎前 40 回和中間 40 回也分開了一些,只是沒有後 40 回那麼明顯而已。那麼這是不是說明 PCA 的結果也是受到了劇情的影響呢?
總之,我有點把握認為《紅樓夢》前 80 回和後 40 回的用詞是有一些差異的,不過因為難以排除劇情的影響,所以我對於作者是不是同一個人這個問題還不敢下定論。雖然沒有完全解決這個問題,不過這個過程中誤打誤撞產生的發現也是挺有意思的,比如「笑道」的詞頻變化和賈府興衰史的有趣重合。更重要的是,看似枯燥的數學公式可以做出這些好玩的分析,Math is fun!
=======================================================
如果覺得我的文章寫的不錯的話,打賞一塊錢鼓勵一下唄~
支付寶:
微信:
(手機上可以先保存到相冊,然後掃碼時選擇從相冊中識別二維碼)
未經授權禁止轉載!
(已授權名單:「Python 中文社區」微信公眾號,」知乎日報」,「Helloworld少兒編程」, 「DT 數據俠」)
推薦閱讀:
※Python 家族有多龐大
※Python數據分析及可視化實例之CentOS7.2+Python3x+Flask部署標準化配置流程
※Flask 實現小說網站 (二)
※Python實現3D建模工具
※Flask模板引擎:Jinja2語法介紹