以 jieba 與 gensim 探索文本主題:五月天人生無限公司歌詞分析 ( II)

Youngmi huang
PyLadies Taiwan
Published in
17 min readDec 17, 2017

--

本次主題著重於實現心目中想做的數據集的運用,下篇聚焦於 gensim 主題建模的使用。源起與應用請見上篇:以 jieba 與 gensim 探索文本主題:五月天人生無限公司歌詞分析 ( I )

gensim 簡介

gensim 是一個免費的 python module,致力於處理原始的、非結構化的文本,它可以從文檔中自動提取語義主題。模組是從以下三個概念展開:語料庫(corpus)、向量(vector)、模型(model)。

gensim official website
  1. 用到的算法如潛在語義分析( Latent Semantic Analysis ,LSA )、隱含狄利克雷分配(Latent Dirichlet Allocation ,LDA )或隨機預測(Random Projections )等,是透過檢查單詞在訓練語料庫的同一文檔中的統計共同出現模式,來發現文檔的語義結構。
  2. 這些算法都是非監督式算法(Unsupervised Learning),也就是無需人為給定正確答案的介入。透過統計共現,文檔當中的語料集就可以被用一個語義代號簡潔地表示,並用這個代號去查詢某一文本與其他文本的相似性。

資料前處理

在進入 gensim 建模以前,主要進行兩件事情:斷詞、同義字處理。

先將五月天人生無限公司演唱會所出現的 33 首歌詞進行斷詞後,引用 fukuball 讓中文斷詞不要悲劇當中使用的同義字列表 ( word_net.txt ) 處理同義字字詞,最後產生語料庫的資料集 lyrics_word_net_mayday.dataset (詳見文末有上傳至 github )。

# 同義字列表(舉例)
可是 但
但是 但
但 但
一下 一下
一下子 一下
一些 一些
一些些 一些

主題建模:LET’S START!!

接下來會從語料庫與向量空間開始( corpus、vector),將向量化的語料庫透過 tf-idf 進行轉換建立 LSI 模型,最後會進行歌詞間文本相似度的試驗( similarity )。

何謂 LSI 模型?

LSI (Latent Semantic Indexing、也可稱為 Latent Semantic Analysis),中文為潛在語義索引,是利用 SVD ( Singular Value Decomposition )把文檔從高維空間投影到低維空間,在這個空間內進行文本相似性的比較。與詞組之間語意上的關聯相比, LSI 更關注的是詞組之間「隱含」的關聯。

# see logging events 
import logging
logging.basicConfig(format='%(asctime)s : %(levelname)s : % (message)s', level=logging.INFO)
import os
from gensim import corpora, models, similarities

語料庫與向量空間 ( Corpora and Vector Spaces )

1. 語料庫建立

在 gensim 官方文件當中,最終輸出的語料庫為「字典」形式,並且會為字典當中出現的字詞( word ) 給予序號( tokenize),給予序號的概念有點像是,一個字詞會發給一個 id,有幾個 id 就代表有幾維的向量空間。

因此第一步是將字詞塞入字典,資料源為前面經過斷詞、同義字處理後的資料集 lyrics_word_net_mayday.dataset。接著從字典當中,移除停用字 (stop_word.txt 來源同樣引用自 讓中文斷詞不要悲劇 ) 以及只出現一次的字詞( optional ):

# 讀取停用字清單
with open("lyrics/stop_words.txt") as f:
stop_word_content = f.readlines()
stop_word_content = [x.strip() for x in stop_word_content]
stop_word_content = " ".join(stop_word_content)
# 建立本次文檔的語料庫(字典)
dictionary = corpora.Dictionary(document.split()
for document in open("lyrics/lyrics_word_net_mayday.dataset"))
# 找到停用字對應的序號、移除停用字
stoplist = set(stop_word_content.split())
stop_ids = [dictionary.token2id[stopword] for stopword in stoplist
if stopword in dictionary.token2id]
dictionary.filter_tokens(stop_ids)
dictionary.compactify()
# 讀取字詞
texts = [[word for word in document.split() if word not in stoplist]
for document in
open("lyrics/lyrics_word_net_mayday.dataset")]
# 移除只出現一次的字詞 (Optional)
from collections import defaultdict
frequency = defaultdict(int)
for text in texts:
for token in text:
frequency[token] += 1
texts = [[token for token in text if frequency[token] > 1]
for text in texts]
# 字典存檔
dictionary.save("lyrics/lyrics_mayday.dict")

如果我們好奇剛建好的字典所產生的內容,可用以下方式查看:

# 字典摘要
print(dictionary)
# Result
Dictionary(2221 unique tokens: ['會', '不會', '有', '一天', '時間']...)
# 字典內容
for word,index in dictionary.token2id.items():
print(word +" id:"+ str(index))
# Result
會 id:0
不會 id:1
有 id:2
一天 id:3
時間 id:4

然而,上述所存的字典,內容仍為字詞+序號的形式,這是人能看得懂的字典,因此還不是最終格式,必須將字典轉為序列化,也就是演算法才讀得懂的向量形式。

2. 將字典轉為向量空間模型格式的語料庫

根據官方文件是使用 Matrix Market format的方式作轉換,官方也有其他序列化方法可以參考: Joachim’s SVMlight formatBlei’s LDA-C formatGibbsLDA++ format 。經過轉換成 lyrics_mayday.mm形式之後,語料庫中的語料已變成數據流。

# 將corpus序列化
corpus = [dictionary.doc2bow(text) for text in texts]
corpora.MmCorpus.serialize("lyrics/lyrics_mayday.mm", corpus)

tf — idf 轉換與創建 LSI 模型 ( Topics and Transformations )

  1. 將 corpus 丟入tf-idf 模型 :將字典中的字詞向量轉換為字詞的重要性的向量。 ( tf — idf 模型詳見上集介紹:以 jieba 與 gensim 探索文本主題:五月天人生無限公司歌詞分析 ( I )
# 載入語料庫
if (os.path.exists("lyrics/lyrics_mayday.dict")):
dictionary = corpora.Dictionary.load("lyrics/lyrics_mayday.dict")
corpus = corpora.MmCorpus("lyrics/lyrics_mayday.mm")
print("Used files generated from first tutorial")
else:
print("Please run first tutorial to generate data set")
# 創建 tfidf model
tfidf = models.TfidfModel(corpus)
corpus_tfidf = tfidf[corpus]

2. LSI 模型建立:

LSI 模型的建立,需要給定 tf-idf 生成的語料庫、字典,以及制定主題數量:這邊是先設定主題 num_topics = 20,主要是從 33 首歌曲為思考的出發點,找接近的 30、20、10;以及 66 、99 等都可以去做嘗試。

# 創建 LSI model 潛在語義索引
lsi = models.LsiModel(corpus_tfidf, id2word=dictionary, num_topics=10)
corpus_lsi = lsi[corpus_tfidf] # LSI潛在語義索引
lsi.save('lyrics/lyrics_mayday.lsi')
corpora.MmCorpus.serialize('lyrics/lsi_corpus_mayday.mm', corpus_lsi)
print("LSI topics:")
lsi.print_topics(5)

制定主題數量可以來回調整,每個人制定的主題邏輯不同,甚至可以先隨機設定,去看相似度後進行調整。若主題數制定過少,則會有歌詞不相近的歌詞卻被分到同一個主題底下的情形;反之,若主題數制定過多,則會有歌詞相近的歌詞被分到不同主題底下。

# LSI topics
[(0,
'0.204*"噢" + 0.132*"一次" + 0.127*"我倆" + 0.113*"一天" + 0.106*"兄弟" + 0.105*"著" + 0.105*"最" + 0.098*"這" + 0.096*"和" + 0.094*"在"'),
(1,
'-0.725*"噢" + -0.282*"兄弟" + -0.130*"oh" + -0.123*"一次" + -0.103*"唔" + 0.089*"最" + 0.088*"突然" + -0.081*"最好" + -0.079*"一個" + -0.074*"這樣"'),
(2,
'0.194*"決定" + 0.187*"動次" + 0.186*"oh" + 0.171*"love" + 0.133*"ing" + 0.128*"真正" + -0.125*"我倆" + 0.119*"無望" + -0.112*"任意" + -0.109*"無限"'),
(3,
'-0.367*"oh" + -0.242*"動次" + 0.213*"love" + 0.177*"無望" + 0.146*"ing" + -0.141*"突然" + 0.128*"路" + 0.118*"這款" + 0.118*"I" + 0.118*"法度"'),
(4,
'-0.256*"oh" + 0.200*"一次" + -0.199*"love" + 0.198*"倔強" + -0.192*"動次" + -0.172*"ing" + 0.163*"啦" + -0.111*"無望" + -0.104*"噢" + 0.102*"自己"')]

制定完主題數後,上述所建立的 corpus_lsi 為 LSI 潛在語義索引,可以比喻成每一首歌在 10 個主題的權重,接下來,要透過歌曲在各主題隱含語義的權重去計算他們的相似度。

計算歌詞相似度

輸入目標歌曲後,透過 LSI 模型進行文本主題的相似度計算後,輸出另外四首與相近歌曲:

  1. 投入的目標歌曲:「好好」
# 「好好」這首歌詞在各主題的佔比
# 基於tf-idf-> lsi 的文本相似度分析
doc = "想 把 你 寫成 一首歌 想養 一隻 貓 想要 回到 每個 場景 撥慢 每 隻 錶 我們 在 小孩 和 大人 的 轉角 蓋 一座 城堡 我們 好好 好 到 瘋 掉 像 找回 失散多年 雙胞 生命 再長 不過 煙火 落下 了 眼角 世界 再大 不過 你 我 凝視 的 微笑 在 所有 流逝 風景 與 人群 中 你 對 我 最好 一切 好好 是否 太好 沒有 人 知道 你 和 我 背著 空空 的 書包 逃出 名為 日常 的 監牢 忘 了 要 長大 忘 了 要 變老 忘 了 時間 有腳 最 安靜 的 時刻 回憶 總是 最 喧囂 最 喧囂 的 狂歡 寂寞 包圍 著 孤島 還以 為 馴服 想念 能 陪伴 我 像 一隻 家貓 它 就 窩 在 沙發 一角 卻 不肯 睡著 你 和 我 曾 有 滿滿的 羽毛 跳 著名 為 青春 的 舞蹈 不 知道 未來 不 知道 煩惱 不知 那些 日子 會 是 那麼 少 時間 的 電影 結局 才 知道 原來 大人 已 沒有 童謠 最後 的 叮嚀 最後 的 擁抱 我們 紅著 眼笑 我們 都 要 把 自己 照顧 好 好 到 遺憾 無法 打擾 好好 的 生活 好好 的 變老 好好 假裝 我 已經 把 你 忘掉 "
# 把字典中的語料庫轉為詞包
vec_bow = dictionary.doc2bow(doc.split())
# 用前面建好的 lsi 去計算這一篇歌詞
vec_lsi = lsi[vec_bow]
print(vec_lsi)
# Result
[(0, 7.2705663600217534), (1, 2.7284780730201121), (2, 0.82233838373656243), (3, -2.146904827492619), (4, 0.65830306598148058), (5, 3.9017717755774819), (6, 2.5031445409607125), (7, -1.8386513217392966), (8, -1.7113198568985561), (9, -2.8521822914394201)]

2. 建立索引以及相似歌曲結果輸出

建立索引,透過 gensim 當中 similarities 套件算出每首歌與「好好」相似的 similarity 後,降冪排列,並輸出前五首相似歌詞。

# 建立索引
index = similarities.MatrixSimilarity(lsi[corpus])
index.save("lyrics/lyrics_mayday.index")
# 相似度
sims = index[vec_lsi]
sims = sorted(enumerate(sims), key=lambda item: -item[1])
print(sims[:5])
# Result
[(2, 0.95469892), (25, 0.81220996), (29, 0.77366918), (30, 0.58470833), (33, 0.56304359)]

3. 索引結果對應相似歌詞

輸出的結果是機器吐出來的,以索引表示,這裡的索引是將每一首歌詞作編號。接著串回歌詞文本,以找到前三名相似歌曲為例,變為人能判讀的文字歌詞:也就是序號 2 、序號 25 、序號 29 代表的歌詞分別就是「好好」、「自傳」、「溫柔」。因自己跟本身相似度會很高,因此最相似的為其本身,而這裡不是 100% 的原因在於,我們前面將歌詞做完斷詞後,還有進行同義字以及停用字的資料前處理,故文本算出來後並不是 100% 相似。

# 相似的前三首歌曲
lyrics = [];
fp = open("lyrics/lyrics_word_net_mayday.dataset") # 斷詞後的歌詞
for i, line in enumerate(fp):
lyrics.append(line)
fp.close()
for lyric in sims[:3]:
print("\n相似歌詞:", lyrics[lyric[0]])
print("相似度:", lyric[1])
# Result
相似歌詞: 想 把 你 寫成 一首歌 想養 一隻 貓 想要 回到 每個 場景 撥慢 每 隻 錶 我倆 在 小孩 和 大人 的 轉角 蓋 一座 城堡 我倆 好好 好 到 瘋 掉 像 找回 失散多年 雙胞 生命 再長 不過 煙火 落下 了 眼角 世界 再大 不過 你 我 凝視 的 微笑 在 所有 流逝 風景 與 人群 中 你 對 我 最好 一切 好好 是否 太好 沒有 人 知道 你 和 我 背著 空蕩 的 書包 逃避 名為 日常 的 監牢 忘記 了 要 長大 忘記 了 要 變老 忘記 了 時間 有腳 最 安靜 的 時刻 回憶 總是 最 喧囂 最 喧囂 的 狂歡 孤單 包圍 著 孤島 還以 為 馴服 想 能 陪伴 我 像 一隻 家貓 它 就 窩 在 沙發 一角 卻 不肯 睡著 你 和 我 曾經 有 滿滿的 羽毛 跳 著名 為 青鳥 的 舞蹈 不 知道 未來 不 知道 煩惱 不知道 那些 日子 會 是 那麼 少 時間 的 電影 結果 才 知道 原來 大人 已經 沒有 童謠 最後 的 叮嚀 最後 的 擁抱 我倆 紅著 眼笑 我倆 都 要 把 自己 照顧 好 好 到 遺憾 無法 打擾 好好 的 生活 好好 的 變老 好好 假裝 我 已經 把 你 忘記

相似度: 0.954699

相似歌詞: 轉眼 走到 了 自傳 最終 章 已經 瀏覽 所有 命運 的 風光 混濁 的 瞳孔 風乾 的 皮囊 也 曾經 那般 花漾 最愛 的 相片 讓 你 挑 一張 千萬個 片刻 誰 在 你 身邊 那 一年 的 我 曾經 和 你 一樣 飛揚 惶惶不安 念念不忘 還是 得 放開 雙掌 手心 曾握 著 誰 的 體溫 漸涼 有沒有 人 在 某個 地方 等 我 重回 當初 的 樣子 雙頰 曾經 光滑 夜色 曾沁涼 世界 曾經 瘋狂 愛情 曾經 綻放 有沒有 人 依偎 我 身邊 聽 我 傾訴 餘生 的 漫長 在 你 的 眼中 我 似乎 健忘 因為 我 腦海 已有 最 難忘 最難 忘記 在 我 的 時代 還有 唱片 行 如同 博物館 裝滿 了 希望 披頭 與 槍花 愛情 和 憂傷 永遠 驕傲 高唱 成就 如 沙堡 生命 如 海浪 浪花 會 掏盡 所有 的 幻象 存款 與 樓房 掙扎 與 渴望 散場 回憶 如窗 冷淚 盈眶 風景 模糊 如 天堂 孤單 的 大床 誰 貼近 我 臉頰 有沒有 人 也 笑憶 過往 跌跌撞撞 當初 的 蠢樣 最 平凡無奇 日子 最 卑微 夢想 何時 才 發現 最 值得 珍藏 有沒有 人 告訴 我 真相 時間 就是 最 巨大 的 謊 以為 的 日常 原來 是 無常 生命 的 具象 原來 只是 幻象 這是 我 自傳 最終 章 寫 這 首長 詩篇 用 一生 時光 軀殼 會 解脫 藥罐 和 空房 我 從 嬰兒 床 再 走 回 光芒 有沒有 人 知道 某種 秘方 不用 永生 只要 回憶 不 忘記 我 不怕 死亡 只 害怕 忘記 回憶 是 你 我 生存 的 地方 有沒有 人 知道 那個 地方 我能 回到 我 的 最愛 身邊 兒孫 們 都 忙 就讓 他們 忙 離開 的 時候 就當 我 飛翔 自由 飛翔

相似度: 0.81221

相似歌詞: 走 在 風中 今天 陽光 突然 好 溫柔 天 的 溫柔 地 的 溫柔 像 你 抱 著 我 然後 發現 你 的 改變 孤單 的 今後 如果 冷 該 怎麼 度過 天邊 風光 身邊 的 我 都 不在 你 眼中 你 的 眼中 藏著 什麼 我 從來 都 不 懂 沒有 關係 你 的 世界 就讓 你 擁有 不 打擾 是 我 的 溫柔 不 知道 不明 瞭 不 想要 為 什麼 我 的 心 明明 是 想 靠近 卻 孤單 到 黎明 不 知道 不明 瞭 不 想要 為 什麼 我 的 心 那 愛情 的 綺麗 總是 在 孤單 裡 再 把 我 的 最好 的 愛給 你 不知不覺 不情 不願 又 到 巷子口 我 沒有 哭 也 沒有 笑 因為 這是 夢 沒有 預兆 沒有 理由 你 真的 有 說過 如果 有 就讓 你 自由 我給 你 自由 我給 你 自由 我給 你 自由 我給 你 自由 我給 你 全部 全部 全部 全部 自由 oh 這是 我 的 溫柔 這是 我 的 溫柔 還你 你 的 自由 還你 你 的 自由 oh 不 知道 不明 瞭 不 想要 為 什麼 我 的 心 明明 是 想 靠近 卻 孤單 到 黎明 不 知道 不明 瞭 不 想要 為 什麼 我 的 心 那 愛情 的 綺麗 總是 在 孤單 裡 再 把 我 的 最好 的 愛給 你 不知不覺 不情 不願 又 到 巷子口 我 沒有 哭 也 沒有 笑 因為 這是 夢 沒有 預兆 沒有 理由 你 真的 有 說過 如果 有 就讓 你 自由 自由 這是 我 的 溫柔 這是 我 的 溫柔 這是 我 的 溫柔 這是 我 的 溫柔 讓 你 自由

相似度: 0.773669

小結

上篇與下篇的實作,主要就是參考 gensim 官方文件以及 fukuball 讓中文斷詞不要悲劇文章,我的方法是先去思考 input 和 output 是什麼,確定要完成的範疇後,開始實作的流程。在遇到第一次看到的語法的時候,回頭去找官方文件說明,這個指令執行的意義,同時搭配瞭解主題建模方法論 ( 像是 LSA 跟 LSI 其實是在講同一件事,而 LDA 同樣也是主題建模的一種方法,但它可解決 LSA 無法分辨多義字的缺點),最後再歸納整理成文章,以幫助日後遇到同樣問題時的釐清。

以本次為例,就是要找出五月天歌詞的主題字,與相似歌詞計算,可以精進的方向是:擴大五月天歌詞規模(例如從目前 33 首擴增到 100 首)、或是丟入更多樂團的歌曲(像是蘇打綠、五月天、玖壹壹等),也許會有有趣的發現。

本次完整程式碼

( jupyter notebook: Here)

如果這篇文章有幫助到你,可以幫我在下方綠色的拍手圖示按5下,只要登入Google或FB,不需任何花費就能【免費支持】youmgmi 繼續創作。

--

--

Youngmi huang
PyLadies Taiwan

Participate in data science field, fascinated by something new. (Current: fraud risk modeling with ML/DL, Past: NLP)