本文重新整理的更詳細(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)化處理。