電影《芳華》在春節重映了一波,加上之前的熱映,最終取得了14億票房的好成績。嚴歌苓的原著也因此被更多的人細細品讀。用文字分析的一些技術肢解小說向來是自然語言處理領域的一大噱頭,這次當然也不能放過,本篇達成的成就有:
1、提取兩大主角劉峰和何小嫚(萍)的關鍵詞並繪製好看的人物詞雲;
2、以章節為單位探索小說的主題分佈並畫圖展示。
主要功能包:
jieba
lda
wordcloud
seaborn
安裝命令: pip install ***
複製程式碼
需要的外部檔案:
1、小說全文, 芳華-嚴歌苓.txt
2、中文停用詞,stopwords.txt
3、小說人物名稱,person.txt,作為jieba的使用者自定義詞典
4、兩個人物的png圖片
5、你喜歡的中文字型的ttf檔案,我用的楷體
複製程式碼
一、文字預處理
1、分詞,並過濾無意義詞
文字挖掘的必備步驟,畢竟理解中文的最小單位是詞彙。這裡沒有使用簡單的jieba.cut進行分詞,因為我們需要知道單詞的詞性,便於稍後根據詞性過濾不重要的詞。
採用jieba.posseg.cut分詞可以輸出詞性。我們並不能拍腦門決定是要動詞還是名詞等等,詞性有非常多個,我把全部分詞結果按照詞性分好類,看了一下每個詞性對應哪些詞,最後決定保留詞性為[“a”, “v”, “x”, “n”, “an”, “vn”, “nz”, “nt”, “nr”]的詞,例如圖中,m代表量詞,這是對語義沒有幫助的詞,應該捨棄。
import jieba.posseg
jieba.load_userdict("data/person.txt")
STOP_WORDS = set([w.strip() for w in open("data/stopwords.txt").readlines()])
def cut_words_with_pos(text):
seg = jieba.posseg.cut(text)
res = []
for i in seg:
if i.flag in ["a", "v", "x", "n", "an", "vn", "nz", "nt", "nr"] and is_fine_word(i.word):
res.append(i.word)
return list(res)
# 過濾詞長,過濾停用詞,只保留中文
def is_fine_word(word, min_length=2):
rule = re.compile(r"^[u4e00-u9fa5]+$")
if len(word) >= min_length and word not in STOP_WORDS and re.search(rule, word):
return True
else:
return False
複製程式碼
2、劃分章節
我們按照“第*章”這樣的字眼將小說的不同章節分割開來,作為獨立的文件,用於之後的主題分析。定義了一個名為MyChapters的生成器,儲存每章分好的詞彙,是為了避免章節過多帶來的一些程式執行問題。其實《芳華》僅有15章,用一個簡單的列表也是可以的。
class MyChapters(object):
def __init__(self, chapter_list):
self.chapter_list = chapter_list
def __iter__(self):
for chapter in self.chapter_list:
yield cut_words_with_pos(chapter)
def split_by_chapter(filepath):
text = open(filepath).read()
chapter_list = re.split(r`第.{1,3}章
`, text)[1:]
return chapter_list
複製程式碼
二、人物關鍵詞提取
要提取人物關鍵詞,首先要解決的問題是,在不借助外部的人物描述(比如百度百科和豆瓣電影上的角色介紹)的情況下,如何確定跟這個人物相關的內容。這裡採用的比較簡單的策略是,對小說檔案中的每一行,如果該人物的名稱存在,則將該行加入到此人的相關語料中去。再以此為基礎統計詞頻,結果大致ok,為了人物詞雲更精確的展示,我將詞頻輸出到了檔案,手動刪除了一些詞,並簡單調整了一些詞的詞頻,下圖是調整過後的詞和詞頻,左為何小嫚,右為劉峰。
import pandas as pd
def person_word(name):
lines = open("data/芳華-嚴歌苓.txt", "r").readlines()
word_list = []
for line in lines:
if name in line:
words = cut_words_with_pos(line)
word_list += words
# 統計詞頻並按照詞頻由大到小排序,取top500
cnt = pd.Series(word_list).value_counts().head(500)
# 可以把結果輸出到檔案,進行一些手動調整
# cnt.to_csv("data/cntliu.csv")
# 返回字典格式
return cnt.to_dict()
複製程式碼
三、詞雲繪製
python有wordcloud包可以用於詞雲繪製,在使用過程中需要注意:
1、用於定義形狀的外部圖片必須是png格式,預設純白色部分為非影像區域;
2、中文詞雲必須載入一個字型檔案;
3、字的顏色可以自己定義,也可以使用圖片本身的底色。本例中何小嫚的圖片 底色很鮮豔明晰,可以用本身的底色(ImageColorGenerator);而劉峰的圖片是單色,且色淺,我使用了自定義顏色(my_color_func);
4、繪製詞雲需要用到的資料格式為dict,key為詞,value為詞頻,詞頻越大,在圖片中的字型越大。
import matplotlib.pyplot as plt
from wordcloud import WordCloud, ImageColorGenerator
from scipy.misc import imread
from random import choice
# 定義顏色,方法很多,這裡用到的方法是在四個顏色中隨機抽取
def my_color_func(word, font_size, position, orientation, random_state=None, **kwargs):
return choice(["rgb(94,38,18)", "rgb(41,36,33)", "rgb(128,128,105)", "rgb(112,128,105)"])
def draw_cloud(mask_path, word_freq, save_path):
mask = imread(mask_path) #讀取圖片
wc = WordCloud(font_path=`data/kaiti.TTF`, # 設定字型
background_color="white", # 背景顏色
max_words=500, # 詞雲顯示的最大詞數
mask=mask, # 設定背景圖片
max_font_size=80, # 字型最大值
random_state=42,
)
# generate_from_frequencies方法,從詞頻產生詞雲輸入
wc.generate_from_frequencies(word_freq)
plt.figure()
# 劉峰, 採用自定義顏色
plt.imshow(wc.recolor(color_func=my_color_func), interpolation=`bilinear`)
# 何小嫚, 採用圖片底色
# image_colors = ImageColorGenerator(mask)
# plt.imshow(wc.recolor(color_func=image_colors), interpolation=`bilinear`)
plt.axis("off")
wc.to_file(save_path)
plt.show()
複製程式碼
# 獲取關鍵詞及詞頻
input_freq = person_word("劉峰")
# 經過手動調整過的詞頻檔案,供參考
# freq = pd.read_csv("data/cntliu.csv", header=None, index_col=0)
# input_freq = freq[1].to_dict()
draw_cloud("data/liu.png", input_freq, "output/liufeng.png")
複製程式碼
對人物進行摳圖,背景設定為純白,儲存為png格式。
為了使形狀更鮮明,對小嫚的辮子還有腰的部分做了加白處理,可以對比文章開頭原圖感受一下。
如果你看過這部作品,不知道印象最深的是不是像詞雲顯示的那樣?小嫚在精神病院的月下獨舞,劉峰對丁丁的深深眷戀,在戰爭中失去手臂,與不愛的人結婚又離婚,和小嫚以朋友的姿態相伴終老… …
四、主題分析
lda 方法的原理不做介紹了,假設你設定了這15章講了k個主題,那麼它的輸出是:1、每個主題都由哪些詞構成,概率幾何; 2、每章內容中,k個主題各佔多大比例,佔比越大,該章內容與該主題越貼切。
1、首先,整理模型輸入
lda要求的輸入格式為文件-詞彙頻次矩陣,也就是各詞語在個章節中出現了多少次,我們用CountVectorizer可以一步實現。
CountVectorizer要求的輸入格式為:[“word1 word2”, “word3 word4”, …]
即一個章節作為一個完整的字串,其中的詞用空格隔開
from sklearn.feature_extraction.text import CountVectorizer
def get_lda_input(chapters):
corpus = [" ".join(word_list) for word_list in chapters]
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(corpus)
return X.toarray(), vectorizer
複製程式碼
2、訓練模型
我們設定主題個數為20個,並列印如下內容:
每個主題列印最能描述該主題的前20個詞
每章列印佔比最高的前3個主題
def lda_train(weight, vectorizer):
model = lda.LDA(n_topics=20, n_iter=500, random_state=1)
model.fit(weight)
doc_num = len(weight)
topic_word = model.topic_word_
vocab = vectorizer.get_feature_names()
titles = ["第{}章".format(i) for i in range(1, doc_num + 1)]
n_top_words = 20
for i, topic_dist in enumerate(topic_word):
topic_words = np.array(vocab)[np.argsort(topic_dist)][:-(n_top_words + 1):-1]
print(`Topic {}: {}`.format(i, ` `.join(topic_words)))
doc_topic = model.doc_topic_
print(doc_topic, type(doc_topic))
plot_topic(doc_topic)
for i in range(doc_num):
print("{} (top topic: {})".format(titles[i], np.argsort(doc_topic[i])[:-4:-1]))
複製程式碼
def main():
chapter_list = split_by_chapter("data/芳華-嚴歌苓.txt")
chapters = MyChapters(chapter_list)
weight, vectorizer = get_lda_input(chapters)
lda_train(weight, vectorizer)
複製程式碼
輸出結果:
Topic 0: 小惠 郝淑雯 少俊 好人 啤酒 看著 生意 老闆 劉大哥 老戰友 髮廊 老公 鄰居 汽車 背叛 城管 出賣 眼線 惠雅玲 路燈
Topic 1: 年輕 女人 照片 眼睛 想到 生命 跟著 來到 笑笑 院子 回去 房間 好看 軍區 結婚 接受 開啟 聽說 坐在 關係
Topic 2: 劉峰 紅苕 老百姓 老太太 紅樓 括弧 落後 大娘 打靶 子彈 練功 板凳 榔頭 文工團 男孩兒 地板 大勝 打著 剩下 姨太太
Topic 3: 母親 父親 女兒 犧牲 善良 名字 前線 丈夫 看著 壞話 幹事 生活 觸控 碰到 妻子 家庭 手指尖 繼父 脊樑 手指
Topic 4: 丁丁 林丁丁 幹事 人格 小林 出賣 噁心 報告 聲樂 回答 老師 攝影 庫房 物件 演員 男女 喜歡 王老師 組織 權利
Topic 5: 女人 侄子 事兒 女朋友 電話 想象 紅樓 手機 回來 地方 公司 酒店 日子 轎車 叔叔 皮包 戰友 電梯 化療 客廳
Topic 6: 丁丁 丈夫 食堂 妹妹 王家 肯定 故事 文工團 條件 函授 機器 笑笑 老闆 夫人 工作 老三 姐妹 考試 姨媽 虛榮
Topic 7: 父親 標兵 女兵 爸爸 父母 茶缸 政治 包裹 招待所 看成 學雷鋒 蕭穗子 送來 編造 捎來 檔案 放進 行李袋 友誼商店 真話
Topic 8: 看著 黑色 眼睛 紅色 郝淑雯 故事 學校 懷疑 毛衣 走出 鬧鐘 帽子 玩兒 老兵 柔軟 軍帽 起床 熱愛 冷水 新兵
Topic 9: 何小嫚 頭髮 襯衫 演出 感覺 同志 所有人 輕傷 掌上明珠 傷員 分隊 小何 潛意識 表演 體溫 下放 連隊 溫度 退回 對話
Topic 10: 小時 跟頭 毯子 看著 同屋 同情 小郝 領導 危險 甜餅 自由 炊事班 中提琴手 伙食 邀請 辦法 目光 女孩兒 巷子 梆子
Topic 11: 團長 駕駛員 衛生員 騎兵 戰士 護士 包紮 士兵 獎品 裝病 流傳 紅蟻 卡車 體溫計 服裝 舞蹈 溫度計 彈藥 離開 軍馬
Topic 12: 護士 服務員 報導 醫院 首長 戰友 政治部 報紙 天使 住院 只能 剪斷 包紮 標語 伏擊 媽媽 學習 歌聲 遲到 護理員
Topic 13: 母親 弟弟 繼父 女兒 拖油瓶 廳長 毛衣 弄堂 妹妹 絨線衫 高燒 亭子間 跟著 餃子 討厭 小時 姆媽 蟲蛀 衛生 姐姐
Topic 14: 劉峰 郝淑雯 點兒 林丁丁 告訴 女兒 時間 發生 男人 老家 好像 醫院 拿出 無恥 等待 所有人 世界 不錯 幫忙 不知
Topic 15: 丁丁 沙發 表弟 林丁丁 祕密 蕭穗子 排長 膽石 眼睛 參觀 吉普 排球場 專注 肌膚 衛生帶 脫下 成功 距離 合算 襯衫
Topic 16: 身體 發現 孩子 回到 找到 不知 記得 說話 意識 見到 搖搖頭 所有人 漂亮 樣子 機會 顯得 毛巾 我會 發言 不見
Topic 17: 老師 朱克 頭髮 郝淑雯 身體 走廊 乳罩 承認 藤椅 衛生員 軍帽 撒謊 哨兵 地板 活兒 範兒 男舞者 眼淚 襯衣 海綿
Topic 18: 女兵 男兵 首長 明白 發現 地方 排練 回來 部隊 舞蹈 動作 觸控 演出 事件 祕密 軍裝 生病 舞臺 結束 接下去
Topic 19: 劉倩 平凡 追悼會 新兵 堂叔 靈臺 操場 小林 靈堂 鑰匙 冬青 通知 薩其馬 老頭兒 烈士陵園 小徐 看望 皮膚 土黃色 成就
複製程式碼
下面展示的章節所包括的主題,對照上面相應主題序號的詞語,是否能大致判斷每章在講些什麼呢。
第1章 (top topic: [ 2 18 16])
第2章 (top topic: [ 7 14 18])
第3章 (top topic: [10 14 7])
第4章 (top topic: [ 4 18 14])
第5章 (top topic: [15 14 1])
第6章 (top topic: [ 4 14 15])
第7章 (top topic: [ 3 14 16])
第8章 (top topic: [13 16 1])
第9章 (top topic: [ 8 13 18])
第10章 (top topic: [17 9 18])
第11章 (top topic: [11 9 18])
第12章 (top topic: [12 1 3])
第13章 (top topic: [ 6 14 18])
第14章 (top topic: [ 0 14 5])
第15章 (top topic: [19 14 1])
複製程式碼
3、畫圖
對於各章節的不同主題的分佈,我們可以畫個圖來展示一下。
利用lda輸出的doc_topic畫熱力圖,doc_topic是一個二維陣列,值為某主題在某章節的佔比,剛剛列印的內容只可以看到每章包括的前三個主題,從下圖中則可以看到全部主題在各章的分佈情況,參考圖例,顏色越深代表佔比越大。
def plot_topic(doc_topic):
f, ax = plt.subplots(figsize=(10, 4))
cmap = sns.cubehelix_palette(start=1, rot=3, gamma=0.8, as_cmap=True)
sns.heatmap(doc_topic, cmap=cmap, linewidths=0.05, ax=ax)
ax.set_title(`proportion per topic in every chapter`)
ax.set_xlabel(`topic`)
ax.set_ylabel(`chapter`)
plt.show()
f.savefig(`output/topic_heatmap.jpg`, bbox_inches=`tight`)
複製程式碼
五、結語
中文的自然語言處理技術是一項特別繁雜的工作,需要注意非常多的細節,在分析的過程中,我也花了足夠的精力做資料視覺化,好看的圖不僅可以吸引人的眼球,更可以加深我們對資料的理解。此外,探索一本小說,除了關鍵詞和主題,還有很多別的思路,比如利用pagerank演算法自動提取文字摘要,以及利用深度學習的模型自動續寫情節… …期待看到更多相關的作品,enjoy。
完整程式碼和資料:https://github.com/scarlettgin/novel_analysis