1. 程式人生 > >使用jieba和gensim模組判斷文字相似度

使用jieba和gensim模組判斷文字相似度

原文:

https://www.jianshu.com/p/cb978743f4d4

 碎冰op 

判斷文字的相似度在很多地方很有用,比如在爬蟲中判斷多篇已爬取的文章是否相似,只對不同文章進一步處理可以大大提高效率。
在Python中,可以使用gensim模組來判斷長篇文章的相似度。點這裡進官網

官方的文件部分內容實在太含糊了,網上也找不到很有用的文章,所以我現在寫下來記錄一下自己的踩坑史。
實際中我用的是資料庫抽取的批量文章,所以就不放上來了,只講程式碼本身使用。
假定最初給定的格式是內容為(content_id, content)cur資料庫遊標。

初步處理

在使用gensim模組之前,要對爬取的文章做一些清洗:

del_words = {
    '編輯', '責編', '免責宣告', '記者 ', '摘要 ', '風險自擔', '掃碼下載', '(原題為', '依法追究', '嚴正宣告',
    '關鍵詞 ', '原標題', '原文', '概不承擔', '轉載自', '來源:', '僅做參考', '僅供參考', '未經授權', 
    '禁止轉載', '閱後點贊', '研究員:', '本文首發', '微信公眾號', '個人觀點', '藍字關注', '微訊號:', '歡迎訂閱', '點選右上角分享', '加入我們'
}


def filter_words(sentences):
    '''
    過濾文章中包含無用詞的整條語句
    :sentences list[str]
    :return list[str]
    '''
    text = []
    for sentence in sentences:
        if sentence.strip() and not [word for word in del_words if word in sentence]:
            text.append(sentence.strip())
    return text


contents = []
for id_, content in cur:
    sentences = content.split('。')
    contents.append('。'.join(filter_words(sentences)).strip())

上面的程式碼中,sentences是文章的每句話構成的列表,如果爬取的結果僅僅是純文字的全文,就可以簡單的使用content.split('。')得到。
如果是使用readability模組得到的含html標籤的全文,還需通過lxml轉化再xpath提取純文字的全文。

分詞過濾

然後,用jieba模組進行分詞並去掉無用詞

from jieba import posseg as pseg


def tokenization(content):
    '''
    {標點符號、連詞、助詞、副詞、介詞、時語素、‘的’、數詞、方位詞、代詞}
    {'x', 'c', 'u', 'd', 'p', 't', 'uj', 'm', 'f', 'r'}
    去除文章中特定詞性的詞
    :content str
    :return list[str]
    '''
    stop_flags = {'x', 'c', 'u', 'd', 'p', 't', 'uj', 'm', 'f', 'r'}
    stop_words = {'nbsp', '\u3000', '\xa0'}
    words = pseg.cut(content)
    return [word for word, flag in words if flag not in stop_flags and word not in stop_words]


texts = [tokenization(content) for id_, content in contents]

相似度判斷

到重頭戲了。
匯入要使用的模組:

from gensim import corpora, models, similarities

為了把文章轉化成向量表示,這裡使用詞袋錶示,具體來說就是每個詞出現的次數。連線詞和次數就用字典表示。然後,用doc2bow()函式統計詞語的出現次數。

dictionary = corpora.Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]

準備需要對比相似度的文章

new_doc = contents[0][0]  # 假定用contents的第1篇文章對比,由於contents每個元素由id和content組成,所以是contents[0][0]
new_vec = dic.doc2bow(tokenization(new_doc))

然後,官方文件給的初步例子是tf-idf模型:

tfidf = models.TfidfModel(corpus)  # 建立tf-idf模型
index = similarities.MatrixSimilarity(tfidf[corpus], num_features=12)  # 對整個語料庫進行轉換並編入索引,準備相似性查詢
sims = index[tfidf[new_vec]]  # 查詢每個文件的相似度
print(list(enumerate(sims)))
# [(0, 1.0), (1, 0.19139354), (2, 0.24600551), (3, 0.82094586), (4, 0.0), (5, 0.0), (6, 0.0), (7, 0.0), (8, 0.0)]

上面的結果中,每個值由編號和相似度組成,例如,編號為0的文章與第1篇文章相似度為100%。
以上就是通過官方文件的入門示例判斷中文文字相似度的基本程式碼,由於某些原因,結果可能為負值或大於1,暫且忽略,這不是重點。

這個例子中,num_features的取值需要注意,官方文件沒有解釋為什麼是12,在大批量的判斷時還使用12就會報錯。實際上應該是num_features=len(dictionary)
此外,這個模型的準確度實在是令人堪憂,不知道為什麼官方使用這個模型作為入門示例,淺嘗輒止的話,可能就誤以為現在的技術還達不到令人滿意的程度。

下面我們換成lsi模型,實際體驗表現很好

lsi = models.LsiModel(corpus, id2word=dic, num_topics=500)
index = similarities.MatrixSimilarity(lsi[corpus])
sims = index[lsi[new_vec]]
res = list(enumerate(sims))

其實只是換了模型名稱而已,但還要注意幾個點:

  • 官方文件中LsiModel()引數用的是tfidf[corpus],實測會導致部分結果不對。
  • 官方文件中最初用的num_topics=2,後面又介紹了這個值最好在200-500之間即可。

好了。到這裡,初步的相似度判斷就完畢了。如果想要更好的顯示結果,例如按相似度排序,可以使用lambda語法

res = list(enumerate(sims))
res = sorted(res, key=lambda x: x[1])
print(res)

但是這樣也有問題,這隻能判斷單篇的結果,其他文章再對比的話,要用for迴圈一篇篇對比嗎?
此外,顯然這個方法是把文章都存在記憶體中,如果文章很多,每篇又很長,很容易擠爆記憶體。
眾所周知,Python的for迴圈效率很低。所以,不要這樣做。
gensim提供了一個類,來本地化儲存所有文章並直接互相對比,這也是我真正最後使用的方法。
點選原文
原文很多地方雲裡霧裡的,比如最基礎的這個similarities.Similarity類的引數,get_tmpfile("index")是什麼都沒講。
實際使用相當簡單:

# 'index'只是把文章儲存到本地後的檔名,所以可以隨便命名,結果儲存的檔名是index.0,不是文字檔案,無法直接檢視
index = similarities.Similarity('index', lsi[corpus], num_features=lsi.num_topics)
for i in enumerate(index):
    print(i)   # 輸出對整組的相似度
# 或者,直接輸出文章id分組
# percentage是相似度,可以手動設定0.9代表把90%相似度以上的輸出為1組等
for l, degrees in enumerate(index):
    print(contents[l][0], [contents[i][0] for i, similarity in enumerate(degrees) if similarity >= percentage])

對比原文,注意到num_features的值不一樣。原文給定的是num_features=len(dictionary),在實際使用中,碰到大量文章時會出錯:

mismatch between supplied and computed number of non-zeros

google之,在這裡得到的經驗,應該使用num_features=lsi.num_topics。文件給出的示例是tf-idf模型下的結果,在lsi模型下就因為傳遞的資料不對而可能出錯。
*似乎仍然會出錯,用tfidf模型轉換能避免這個錯誤。準確率就下去了。

錦上添花:用flask做post介面

在伺服器上接受post請求來執行就更加易用了,簡單起見用flask作一段程式碼示例

from flask import Flask, request

app = Flask(__name__)


@app.route('/similar', methods=['POST'])
def similar_lst():
    if request.method == 'POST':
        ids = request.form.get('ids')
        ids = [int(i.strip()) for i in ids.split(',')]
        if ids:
            percentage = float(request.form.get('percentage'))
            contents = get_content(ids)  # 包含從資料庫獲取id對應的文章程式碼,上面省略了
            res = similar(contents, percentage)
            return json.dumps(res)


if __name__ == '__main__':
    # main()
    app.run(host='0.0.0.0', port=80)

程式碼中的部分函式也就是上面介紹的程式碼本身,只是省略了通過批量id從資料庫獲取contents列表的部分。

在遠端執行後,本地請求可以像這樣

import requests

ids = '1, 2, 3'
data = {'ids': ids, 'percentage': 0.95}
url = 'http://IP:80/similar'  # 遠端的IP地址
r = requests.post(url, data=data)
for k, v in r.json().items():
    print(k, v)

來檢視結果。

最後

這篇文章只是完成了一個文字判斷的雛形,算是可以使用的地步而已,還可以對停用詞做檔案配置等來進一步優化處理。