使用jieba和gensim模塊判斷文本相似度

本文重新整理的更詳細(xì)規(guī)范的介紹見這里

判斷文本的相似度在很多地方很有用猴蹂,比如在爬蟲中判斷多篇已爬取的文章是否相似,只對不同文章進(jìn)一步處理可以大大提高效率搏存。
在Python中硝逢,可以使用gensim模塊來判斷長篇文章的相似度。點(diǎn)這里進(jìn)官網(wǎng)

官方的文檔部分內(nèi)容實(shí)在太含糊了润歉,網(wǎng)上也找不到很有用的文章模狭,所以我現(xiàn)在寫下來記錄一下自己的踩坑史。
實(shí)際中我用的是數(shù)據(jù)庫抽取的批量文章踩衩,所以就不放上來了嚼鹉,只講代碼本身使用。
假定最初給定的格式是內(nèi)容為(content_id, content)cur數(shù)據(jù)庫游標(biāo)驱富。

初步處理

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

del_words = {
    '編輯', '責(zé)編', '免責(zé)聲明', '記者 ', '摘要 ', '風(fēng)險(xiǎn)自擔(dān)', '掃碼下載', '(原題為', '依法追究', '嚴(yán)正聲明',
    '關(guān)鍵詞 ', '原標(biāo)題', '原文', '概不承擔(dān)', '轉(zhuǎn)載自', '來源:', '僅做參考', '僅供參考', '未經(jīng)授權(quán)', 
    '禁止轉(zhuǎn)載', '閱后點(diǎn)贊', '研究員:', '本文首發(fā)', '微信公眾號', '個人觀點(diǎn)', '藍(lán)字關(guān)注', '微信號:', '歡迎訂閱', '點(diǎn)擊右上角分享', '加入我們'
}


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是文章的每句話構(gòu)成的列表,如果爬取的結(jié)果僅僅是純文字的全文,就可以簡單的使用content.split('浑侥。')得到姊舵。
如果是使用readability模塊得到的含html標(biāo)簽的全文,還需通過lxml轉(zhuǎn)化再xpath提取純文字的全文寓落。

分詞過濾

然后括丁,用jieba模塊進(jìn)行分詞并去掉無用詞

from jieba import posseg as pseg


def tokenization(content):
    '''
    {標(biāo)點(diǎn)符號躏将、連詞祸憋、助詞、副詞肖卧、介詞蚯窥、時語素、‘的’塞帐、數(shù)詞拦赠、方位詞、代詞}
    {'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]

相似度判斷

到重頭戲了葵姥。
導(dǎo)入要使用的模塊:

from gensim import corpora, models, similarities

為了把文章轉(zhuǎn)化成向量表示荷鼠,這里使用詞袋表示,具體來說就是每個詞出現(xiàn)的次數(shù)榔幸。連接詞和次數(shù)就用字典表示允乐。然后,用doc2bow()函數(shù)統(tǒng)計(jì)詞語的出現(xiàn)次數(shù)牍疏。

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

準(zhǔn)備需要對比相似度的文章

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)  # 對整個語料庫進(jìn)行轉(zhuǎn)換并編入索引瞻惋,準(zhǔn)備相似性查詢
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)]

上面的結(jié)果中厦滤,每個值由編號和相似度組成,例如蹂匹,編號為0的文章與第1篇文章相似度為100%碘菜。
以上就是通過官方文檔的入門示例判斷中文文本相似度的基本代碼,由于某些原因,結(jié)果可能為負(fù)值或大于1忍啸,暫且忽略仰坦,這不是重點(diǎn)。

這個例子中计雌,num_features的取值需要注意悄晃,官方文檔沒有解釋為什么是12,在大批量的判斷時還使用12就會報(bào)錯凿滤。實(shí)際上應(yīng)該是num_features=len(dictionary)
此外妈橄,這個模型的準(zhǔn)確度實(shí)在是令人堪憂,不知道為什么官方使用這個模型作為入門示例翁脆,淺嘗輒止的話眷蚓,可能就誤以為現(xiàn)在的技術(shù)還達(dá)不到令人滿意的程度。

下面我們換成lsi模型反番,實(shí)際體驗(yàn)表現(xiàn)很好

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

其實(shí)只是換了模型名稱而已沙热,但還要注意幾個點(diǎn):

  • 官方文檔中LsiModel()參數(shù)用的是tfidf[corpus],實(shí)測會導(dǎo)致部分結(jié)果不對罢缸。
  • 官方文檔中最初用的num_topics=2篙贸,后面又介紹了這個值最好在200-500之間即可。

好了枫疆。到這里爵川,初步的相似度判斷就完畢了。如果想要更好的顯示結(jié)果息楔,例如按相似度排序寝贡,可以使用lambda語法

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

但是這樣也有問題,這只能判斷單篇的結(jié)果钞螟,其他文章再對比的話,要用for循環(huán)一篇篇對比嗎谎碍?
此外,顯然這個方法是把文章都存在內(nèi)存中,如果文章很多蹋半,每篇又很長若债,很容易擠爆內(nèi)存。
眾所周知熔任,Python的for循環(huán)效率很低褒链。所以,不要這樣做疑苔。
gensim提供了一個類甫匹,來本地化存儲所有文章并直接互相對比,這也是我真正最后使用的方法。
點(diǎn)擊原文
原文很多地方云里霧里的兵迅,比如最基礎(chǔ)的這個similarities.Similarity類的參數(shù)抢韭,get_tmpfile("index")是什么都沒講。
實(shí)際使用相當(dāng)簡單:

# 'index'只是把文章存儲到本地后的文件名恍箭,所以可以隨便命名刻恭,結(jié)果存儲的文件名是index.0,不是文本文件扯夭,無法直接查看
index = similarities.Similarity('index', lsi[corpus], num_features=lsi.num_topics)
for i in enumerate(index):
    print(i)   # 輸出對整組的相似度
# 或者鳍贾,直接輸出文章id分組
# percentage是相似度,可以手動設(shè)置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),在實(shí)際使用中藕筋,碰到大量文章時會出錯:

mismatch between supplied and computed number of non-zeros

google之纵散,在這里得到的經(jīng)驗(yàn),應(yīng)該使用num_features=lsi.num_topics隐圾。文檔給出的示例是tf-idf模型下的結(jié)果伍掀,在lsi模型下就因?yàn)閭鬟f的數(shù)據(jù)不對而可能出錯。
*似乎仍然會出錯暇藏,用tfidf模型轉(zhuǎn)換能避免這個錯誤蜜笤。準(zhǔn)確率就下去了。

錦上添花:用flask做post接口

在服務(wù)器上接受post請求來運(yùn)行就更加易用了盐碱,簡單起見用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)  # 包含從數(shù)據(jù)庫獲取id對應(yīng)的文章代碼把兔,上面省略了
            res = similar(contents, percentage)
            return json.dumps(res)


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

代碼中的部分函數(shù)也就是上面介紹的代碼本身,只是省略了通過批量id從數(shù)據(jù)庫獲取contents列表的部分瓮顽。

在遠(yuǎn)端運(yùn)行后县好,本地請求可以像這樣

import requests

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

來查看結(jié)果。

最后

這篇文章只是完成了一個文本判斷的雛形暖混,算是可以使用的地步而已缕贡,還可以對停用詞做文件配置等來進(jìn)一步優(yōu)化處理。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拣播,一起剝皮案震驚了整個濱河市晾咪,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌贮配,老刑警劉巖谍倦,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異泪勒,居然都是意外死亡昼蛀,警方通過查閱死者的電腦和手機(jī)宴猾,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來曹洽,“玉大人鳍置,你說我怎么就攤上這事∷拖” “怎么了税产?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長偷崩。 經(jīng)常有香客問我辟拷,道長,這世上最難降的妖魔是什么阐斜? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任衫冻,我火速辦了婚禮,結(jié)果婚禮上谒出,老公的妹妹穿的比我還像新娘隅俘。我一直安慰自己,他們只是感情好笤喳,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布为居。 她就那樣靜靜地躺著,像睡著了一般杀狡。 火紅的嫁衣襯著肌膚如雪蒙畴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天呜象,我揣著相機(jī)與錄音膳凝,去河邊找鬼。 笑死恭陡,一個胖子當(dāng)著我的面吹牛蹬音,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播休玩,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼著淆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了哥捕?” 一聲冷哼從身側(cè)響起牧抽,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤嘉熊,失蹤者是張志新(化名)和其女友劉穎遥赚,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體阐肤,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡凫佛,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年讲坎,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片愧薛。...
    茶點(diǎn)故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡晨炕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出毫炉,到底是詐尸還是另有隱情瓮栗,我是刑警寧澤,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布瞄勾,位于F島的核電站费奸,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏进陡。R本人自食惡果不足惜愿阐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望趾疚。 院中可真熱鬧缨历,春花似錦、人聲如沸糙麦。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喳资。三九已至觉吭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間仆邓,已是汗流浹背鲜滩。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留节值,地道東北人徙硅。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像搞疗,于是被迫代替她去往敵國和親嗓蘑。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,619評論 2 354