最近在做畢設的原因,使用到了 python 的 word2vec 模塊,將較大批量文本進行訓練生成的詞向量存在明顯的準確度太低的情況(例如查找與某個詞相似的 top10,給出的結果總是亂七八糟),這裏找到了原因,做一下簡單記錄。
目錄
一、原因
(一)錯誤的思路
首先簡單介紹一下我原本是怎麼做的吧!
1. 獲取停用詞(停用詞庫下載)
# 獲取停用詞
def get_stopwords(stopwords_path):
# 停用詞列表
stopwords = []
with open(stopwords_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
for j in lines:
line = j.replace('\n', '')
stopwords.append(line)
return stopwords
2. 選擇需要的詞性
這裏我只選擇了在詞向量訓練過程中具有明顯意義的詞性:名詞、地名、機構團體、英文、動詞、形容詞
# 詞性
# 名詞、地名、機構團體、英文、動詞、形容詞
my_flags = ('n', 'ns', 'nt', 'eng', 'v', 'a')
3. jieba 分詞
# 獲取結巴分詞
# file_path:文本路徑,stopwords:停用詞列表,car_path:自定義語料庫路徑,flags:詞性,corpus_path:保存路徑
def get_split(filenames, stopwords, car_path, flags, corpus_path):
split = ''
with open(filenames, 'r', encoding='utf-8') as f:
txt = f.read()
# 增加專業名詞
jieba.load_userdict(car_path)
words = [w.word for w in jp.cut(txt) if w.flag in flags and w.word not in stopwords]
text = ' '.join(words)
split += text
with open(corpus_path, 'w', encoding='utf-8') as f:
f.write(split)
這段代碼中最重要的部分其實就是下面這一行:
words = [w.word for w in jp.cut(txt) if w.flag in flags and w.word not in stopwords]
jp 來自於 import jieba.posseg as jp ,jp.cut() 的結果 w 具有兩個屬性:詞(w.word)和詞性(w.flag),這句話的含義就是:通過 jp.cut() 操作的 w ,如果其詞 w.word 不在停用詞 stopwords 中且詞性 w.flag 在 flags 中,則將該詞 w.word 保存在列表 words 中。
- 整體代碼:split.py
import jieba
import jieba.posseg as jp
# 獲取停用詞
def get_stopwords(stopwords_path):
# 停用詞列表
stopwords = []
with open(stopwords_path, 'r', encoding='utf-8') as f:
lines = f.readlines()
for j in lines:
line = j.replace('\n', '')
stopwords.append(line)
return stopwords
# 獲取結巴分詞
# file_path:文本路徑,stopwords:停用詞列表,car_path:自定義語料庫路徑,flags:詞性,corpus_path:保存路徑
def get_split(file_path, stopwords, car_path, flags, corpus_path):
split = ''
with open(file_path, 'r', encoding='utf-8') as f:
txt = f.read()
# 增加專業名詞
jieba.load_userdict(car_path)
words = [w.word for w in jp.cut(txt) if w.flag in flags and w.word not in stopwords]
text = ' '.join(words)
split += text
with open(corpus_path, 'w', encoding='utf-8') as f:
f.write(split)
if __name__ == '__main__':
# 停用詞路徑
my_stopwords_path = r'../../data/jieba_data/stopwords.txt'
# 文本路徑
my_file_path = r'../../data/result_data/mouth.txt'
# 專業名詞路徑
my_car_path = r'../../data/jieba_data/car_name.txt'
# 語料庫路徑
my_corpus_path = r'../../data/result_data/corpus.txt'
# 詞性
# 名詞、地名、機構團體、英文、動詞、形容詞
my_flags = ('n', 'ns', 'nt', 'eng', 'v', 'a')
# 獲取停用詞
my_stopwords = get_stopwords(my_stopwords_path)
# 分詞
get_split(my_file_path, my_stopwords, my_car_path, my_flags, my_corpus_path)
4. 訓練 word2vec
# 訓練詞向量
# corpus_path:語料庫路徑,vector_path:詞向量保存路徑,model_path:模型保存路徑
def train_word2vec(corpus_path, vector_path, model_path):
# 把語料變成句子集合
sentences = LineSentence(corpus_path)
# 訓練word2vec模型(size爲向量維度,window爲詞向量上下文最大距離,min_count需要計算詞向量的最小詞頻)
# (iter隨機梯度下降法中迭代的最大次數,sg爲3是Skip-Gram模型)
model = word2vec.Word2Vec(sentences, size=20, sg=3, window=5, min_count=1, workers=4, iter=5)
# 保存word2vec模型
model.save(model_path)
model.wv.save_word2vec_format(vector_path, binary=False)
讀者讀到這裏時,建議留意上述代碼中的一句“sg爲3是Skip-Gram模型”。
- 整體代碼:word2vec.py
word2vec 編碼參考了另一篇博客:《『詞向量』用Word2Vec訓練中文詞向量(一)—— 採用搜狗新聞數據集》,作者:來日憑君發遣。
import gensim.models as word2vec
from gensim.models.word2vec import LineSentence
# 訓練詞向量
# corpus_path:語料庫路徑,vector_path:詞向量保存路徑,model_path:模型保存路徑
def train_word2vec(corpus_path, vector_path, model_path):
# 把語料變成句子集合
sentences = LineSentence(corpus_path)
# 訓練word2vec模型(size爲向量維度,window爲詞向量上下文最大距離,min_count需要計算詞向量的最小詞頻)
# (iter隨機梯度下降法中迭代的最大次數,sg爲3是Skip-Gram模型)
model = word2vec.Word2Vec(sentences, size=20, sg=3, window=5, min_count=1, workers=4, iter=5)
# 保存word2vec模型
model.save(model_path)
model.wv.save_word2vec_format(vector_path, binary=False)
# 加載模型
def load_word2vec_model(model_path):
model = word2vec.Word2Vec.load(model_path)
return model
# 計算詞語最相似的詞
def calculate_most_similar(model, word):
similar_words = model.wv.most_similar(word)
print(word)
for j in similar_words:
print(j[0], j[1])
# 計算兩個詞相似度
def calculate_words_similar(model, word1, word2):
print(model.similarity(word1, word2))
# 找出不合羣的詞
def find_word_dis_match(model, lists):
print(model.wv.doesnt_match(lists))
if __name__ == '__main__':
# 語料庫路徑
my_corpus_path = r'../../data/result_data/corpus.txt'
# 語料向量路徑
my_vector_path = r'../../data/result_data/corpus.vector'
# 模型路徑
my_model_path = r'../../data/result_data/word2vec.model'
# 訓練模型
train_word2vec(my_corpus_path, my_vector_path, my_model_path)
# # 加載模型
# my_model = load_word2vec_model(my_model_path)
#
# # 找相近詞
# calculate_most_similar(my_model, "大衆")
# # 兩個詞相似度
# calculate_words_similar(my_model, "奔馳", "奧迪")
# # 詞向量
# print(my_model.wv.__getitem__('車'))
# my_lists = ["奔馳", "奧迪", "大衆", "唐"]
#
# find_word_dis_match(my_model, my_lists)
- 效果
與“大衆”相似的 top10:
血統 0.9097452163696289
低端 0.9042119979858398
大衆呢 0.8991310000419617
樣 0.8986610770225525
牌子 0.8938345909118652
個人愛好 0.8861078023910522
速騰 0.8810515403747559
韓國 0.8804070949554443
風格 0.8773608207702637
心意 0.875822901725769
(二)問題所在
按理說,上文中邏輯並沒有什麼問題,不就是文本預處理之後訓練詞向量嗎,那問題出在哪裏了?
爲了說明問題我們得先說明另一個概念,也就是上面訓練詞向量時出現的 Skip-Gram 模型(如果想較爲全面地瞭解的話,可看我的另一篇博客:《基於Hierarchical Softmax的CBOW模型》)。
簡答來說,Skip-Gram 模型(n-gram 模型)依據馬爾科夫假設:下一個詞的出現僅依賴於它前面的 個詞,實際應用中最常採用的是 的三元模型(即依賴於該詞之前的兩個詞)。
現在回想一下剛纔文本預處理時,我們去停用詞、根據詞性篩選特定詞等等,最終得到的語料庫已經失去了文本原有的語義,這裏以我實際的數據爲例:
- 原始文本
油耗令人滿意,看我的論壇5000公里油耗的文章,最低的時候到過7.2,現在在7.4。
- 文本預處理後的語料庫
油耗 論壇 油耗 文章 最低
使用 Skip-Gram 模型 來處理這樣的語料庫,自然而然會出現準確度太低的原因。
二、解決方法
解決方法其實很簡單,我們只需要在文本預處理時不再對文本進行去停用詞、根據詞性篩選特定詞,就可保留文本語義。
直接給出全部代碼:
- 分詞:split_without.py
import jieba
import re
# 獲取結巴分詞
def get_split(file_path, car_path, corpus_path):
split = ''
# 標點符號
remove_chars = '[·’!"#$%&\'()*+,-./:;<=>?@,。?★、…【】《》?“”‘’![\\]^_`{|}~]+'
with open(file_path, 'r', encoding='utf-8') as f:
txt = f.read()
# 去除標點符號
txt = re.sub(remove_chars, "", txt)
# 增加專業名詞
jieba.load_userdict(car_path)
words = [w for w in jieba.cut(txt, cut_all=False)]
text = ' '.join(words)
split += text
with open(corpus_path, 'w', encoding='utf-8') as f:
f.write(split)
if __name__ == '__main__':
# 源數據路徑
my_file_path = r'../../data/result_data/mouth.txt'
# 專業名詞路徑
my_car_path = r'../../data/jieba_data/car_name.txt'
# 語料庫路徑
my_corpus_path = r'../../data/result_data/corpus.txt'
# 分詞
get_split(my_file_path, my_car_path, my_corpus_path)
word2vec.py 代碼當然九不需要修改了。
- 原始文本
油耗令人滿意,看我的論壇5000公里油耗的文章,最低的時候到過7.2,現在在7.4。
- 文本預處理後的語料庫
油耗 令人滿意 看 我 的 論壇 5000 公里 油耗 的 文章 最低 的 時候 到 過 72 現在 在 74
- 效果
與“大衆”相似的 top10:
一如既往 0.9303665161132812
長處 0.9040323495864868
心中 0.9030961990356445
新鮮感 0.9028049111366272
德系 0.896550714969635
牌子 0.8938297629356384
耶 0.8930386304855347
德系車 0.8929986953735352
情懷 0.8924524784088135
下滑 0.8921371102333069