萌新Learning-簡單的文本類似性檢測與抄襲判斷

前言

本文旨在記錄本萌新在作練手項目總結的心得體會,主要針對初學者,介紹的概念和技術會比較基礎,從而提供一些解決實際問題的思路(沒必要拘泥與其中使用到的概念和算法,在細節上徹底能夠作得更好,用其它更先進更前沿的技術替代),同時會重點介紹我認爲比較須要注意的技術細節。html

注意

  1. 本文的樣例數據恕不能分享,若有須要請本身動手爬取。
  2. 基本的操做在這裏不做討論,若有須要請本身查閱相關文檔。
  3. 相關概念:TF-IDF樸素貝葉斯(naive bayes)k-means聚類

問題描述

假如如今你是國內某新聞社的工做人員,如今發現其它媒體抄襲你平臺的文章,如今你接到一個任務,須要把其它媒體懷疑抄襲的文章找出來,並與原文對比定位抄襲的地方。python

解決流程

1. 數據清洗

咱們首先讀取數據命名爲news的dataframe,數據字段大概以下git

id author source content feature title url
89617 NaN 快科技 此外,自本週(6月12日)起,除小米手機6等15款機型外,其他機型已暫停更新發布(含開發版/ {"type":"科技","site":"cnbeta","commentNum":"37"... 小米MIUI 9首批機型曝光:共計15款 http://www.cnbeta.com/articles/tech/623597.htm
89616 NaN 快科技 驍龍835做爲惟一經過Windows 10桌面平臺認證的ARM處理器,高通強調,不會由於只考.. {"type":"科技","site":"cnbeta","commentNum":"15"... 驍龍835在Windows 10上的性能表現有望改善 http://www.cnbeta.com/articles/tech/623599.htm
89613 胡淑麗_MN7479 深圳大件事 (原標題:44歲女子跑深圳約會網友被拒,暴雨中裸身奔走……)\r\n@深圳交警微博稱:昨日清.. {"type":"新聞","site":"網易熱門","commentNum":"978",.. 44歲女子約網友被拒暴雨中裸奔 交警爲其披衣相隨 http://news.163.com/17/0618/00/CN617P3Q0001875...

咱們須要根據content字段來訓練模型,所以查看content字段爲NaN的樣本,經查看不是不少,所以能夠直接去掉。github

#show nans in the dataset
news[news.content.isna()].head(5)
#drop the nans
news=news.dropna(subset=['content'])
複製代碼

而後定義一個簡單的函數(使用jieba分詞)準備對content進行分詞,在分詞前去掉一些符號和中文標點,分詞後過濾掉一些停用詞,其中punctuation包含全部中文標點,stopwords是一個列表包含了一些停用詞(百度搜索能夠下載,你也能夠根據須要編輯)。在此我只是展現一種可行的處理方法,若是以爲有提高空間你大可沒必要這樣作,或許你能夠用pos of tag 根據詞性過濾你想要的詞彙,或者須要pharse detection甚至用word2vec來表徵。算法

def split_text(text):return ' '.join([w for w in list(jieba.cut(re.sub('\s|[%s]' % (punctuation),'',text))) if w not in stopwords])
複製代碼

測試下函數大概是這樣的效果:架構

split_text(news.iloc[1].content)
#out:
'''驍龍 835 惟一 Windows10 桌面 平臺 認證 ARM 處理器 高通 強調 不會 只 考慮 性能 屏蔽掉 小 核心 相反 正 聯手 微軟 找到 一種 適合 桌面 平臺 兼顧 性能 功耗 完美 方案 報道 微軟 已經 拿到 一些 源碼 Windows10 更好 理解 big little 架構 資料 顯示 驍龍 835 一款 集成 CPUGPU 基帶 藍牙 Wi Fi SoC 傳統 Wintel 方案 節省 至少 30% PCB 空間 按計劃 今年 Q4 華碩 惠普 聯想 首發 驍龍 835Win10 電腦 預計 均 二合一 形態 產品 固然 高通 驍龍 將來 也許 見到 三星 Exynos 聯發科 華爲 麒麟 小米 澎湃 進入 Windows10 桌面 平臺'''
複製代碼

如今能夠把函數應用到整列content字段上面啦!在這裏展現使用pandas的方法,在完整代碼示例我使用了比較pythontic的方法。app

news['content_split'] = news['content'].apply(split_text)
複製代碼

相似地,咱們可使用類似的方法制造標籤(好比我如今假設新聞來源包含新華兩個字爲正例)框架

news['is_xinhua'] = np.where(news['source'].str.contains('新華'), 1, 0)
複製代碼

到此,咱們的數據清洗工做就完成啦!:Ddom

2. 數據預處理

要運用機器學習算法,咱們必須把文本轉化成算法可理解的形式,如今咱們須要使用sklearn構造TF-IDF矩陣來表徵文本,TF-IDF是表徵文本簡單有效的方式,若是你不知道這是什麼請戳連接。機器學習

tfidfVectorizer = TfidfVectorizer(encoding='gb18030',min_df=0.015)
tfidf = tfidfVectorizer.fit_transform(news['content_split'])
複製代碼

在建立TfidfVectorizer時候注意指定encoding參數(默認是utf-8),在這裏min_df=0.015表示建立詞庫時忽略文檔頻率低於設置閾值的詞彙,這樣設置是由於個人機器不能計算太多的feature,若是計算資源充足能夠設置max_features=30000這樣會取詞頻排列在前30000的詞彙做爲feature(tfidf矩陣的列),這樣模型效果會更加好。

3. 訓練預測模型

訓練模型以前咱們須要把數據分爲訓練集(70%)和測試集(30%)。

#split the data
lable = news['is_xinhua'].values
X_train, X_test, y_train, y_test = train_test_split(tfidf.toarray(),label,test_size = 0.3, random_state=42)
複製代碼

如今能夠用樸素貝葉斯訓練模型啦!

clf = MultinomialNB()
clf.fit(X=X_train,y=y_train)
複製代碼

如今,怎麼知道咱們的模型擬合得好很差呢?能夠應用交叉驗證(cross-validation)輸出你關注的衡量指標,在這裏我選擇了precision,recall,accuracy,f1這些指標進行3折(3-folds)交叉驗證(實際上你須要根據關注問題的不一樣選擇不一樣的衡量指標,若是你不知道這些指標,請務必查閱相關資料。),而且和測試集的表現進行對比。

scores=cross_validate(clf,X_train,y_train,scoring=('precision','recall','accuracy','f1',cv=3,return_train_score=True)
print(scores)
#out:
'''{'fit_time': array([0.51344204, 0.43621135, 0.40280986]), 'score_time': array([0.15626907, 0.15601063, 0.14357495]), 'test_precision': array([0.9599404 , 0.96233543, 0.96181975]), 'train_precision': array([0.96242476, 0.96172716, 0.96269257]), 'test_recall': array([0.91072205, 0.91409308, 0.90811222]), 'train_recall': array([0.91286973, 0.91129295, 0.91055894]), 'test_accuracy': array([0.88475361, 0.88981883, 0.88415715]), 'train_accuracy': array([0.88883419, 0.88684308, 0.88706462]), 'test_f1': array([0.93468374, 0.93759411, 0.9341947 ]), 'train_f1': array([0.93699249, 0.93583104, 0.9359003 ])}'''
 
 y_predict = clf.predict(X_test)
 
 def show_test_reslt(y_true,y_pred):
    print('accuracy:',accuracy_score(y_true,y_pred))
    print('precison:',precision_score(y_true,y_pred))
    print('recall:',recall_score(y_true,y_pred))
    print('f1_score:',f1_score(y_true,y_pred))
    
show_test_reslt(y_test,y_predict)
#out:
''' accuracy: 0.8904162040050542 precison: 0.9624150339864055 recall: 0.9148612694792855 f1_score: 0.9380358534684333 '''
複製代碼

首先看cv的結果,3折的衡量指標差異都不大比較穩定,並且測試集和cv的結果也很是相近,說明模型擬合效果尚可,在這個數據中若用更多的features,accuracy可接近1。

到此,咱們已經創建了一個給定文本,預測來源是否某新聞平臺的模型,下面咱們就能夠定位抄襲文章了。

4. 定位抄襲文章

到了這步,咱們能夠根據模型預測的結果來對全量文本(或者新加入的文本,使用時你可能須要封裝一個pipline,這裏不做演示)進行預測,對於那些預測爲正類可是實際上爲負類的文本,說明了他們的文本與你平臺寫做風格有類似之處才被錯判,這些文本極可能就係抄襲文本或原文引用,首先把這部分「候選者」拿出來。

prediction = clf.predict(tfidf.toarray())

labels = np.array(label)

compare_news_index = pd.DataFrame({'prediction':prediction,'labels':labels})

copy_news_index=compare_news_index[(compare_news_index['prediction'] == 1) & (compare_news_index['labels'] == 0)].index

xinhuashe_news_index=compare_news_index[(compare_news_index['labels'] == 1)].index
複製代碼

如今咱們必須把這些疑似抄襲的文本和原文進行對比,拿出類似度較高的文本進一步分析,可是若是使用蠻力搜索算法複雜度至關高,僅僅是兩重嵌套循環就已是O(n^2),這種作法效率過低。

所以咱們須要一種更高效的搜索類似文本的方法,在這裏我使用k-means聚類(固然還有更好的方法,你能夠改進)。首先對全部文本進行k-means聚類,咱們就能夠獲得一個id-cluster的字典,根據這個字典建立cluster-id字典,這樣給定一個特定文本我就能夠知道這個文本屬於哪一個cluster,再用它和cluster中的其它文本作對比,找出最類似的top n個文本再分析,這樣作大大減小了搜索範圍。

normalizer = Normalizer()
scaled_array = normalizer.fit_transform(tfidf.toarray())

kmeans = KMeans(n_clusters=25,random_state=42,n_jobs=-1)
k_labels = kmeans.fit_predict(scaled_array)

id_class = {index:class_ for index,class_ in enumerate(k_labels)}

class_id = defaultdict(set)
for index,class_ in id_class.items():
    if index in xinhuashe_news_index.tolist():
        class_id[class_].add(index)
複製代碼

在這裏須要注意的是,sklearn中的k-means算法只支持根據歐氏距離計算類似度,在文本與文本的類似度比較中咱們通常使用餘弦距離,在使用k-means以前咱們須要把tfidf矩陣normalize成單位長度(unit norm),由於這樣作以後歐氏距離和餘弦距離線性相關(爲何?看這裏),這樣聚類時就是用餘弦距離衡量類似度。

還有一點要談的就是k-means中心數量(n_clusters)的選擇,在這裏我選擇簡單地聚爲25類。實際上你能夠根據你對數據的瞭解,好比說你知道你的數據中大概包含體育,軍事,娛樂這幾類的新聞,你就能夠根據經驗選擇中心數量,固然前提是你對數據很是熟悉。還有一種方法就是根據一些指標例如SSE,silhouette等等這些指標觀察elbow值選取中心數量,這裏有詳細例子

如今咱們就能夠應用聚類的結果搜索類似文本

def find_similar_text(cpindex,top=10):
    dist_dict={i:cosine_similarity(tfidf[cpindex],tfidf[i]) for i in class_id[id_class[cpindex]]}
    return sorted(dist_dict.items(),key=lambda x:x[1][0],reverse=True)[:top]
    
print(copy_news_index.tolist())

#random choice a candidate to show some results
fst=find_similar_text(3352)
print(fst)
#out:
''' id , cosine_similarity [(3134, array([[0.96849349]])), (63511, array([[0.94619604]])), (29441, array([[0.94281928]])), (3218, array([[0.87620818]])), (980, array([[0.87535143]])), (29615, array([[0.86922775]])), (29888, array([[0.86194742]])), (64046, array([[0.85277668]])), (29777, array([[0.84882241]])), (64758, array([[0.73406445]]))] '''
複製代碼

找出類似文本後,更仔細地,你能夠根據某些特徵(特定的長度,特定的分隔符)分割文本的句子,或者在這裏我簡單以「。」分割文本,分別計算類似文本句子間的edit distance後排序定位具體類似的地方。

def find_similar_sentence(candidate,raw):
    similist = []
    cl = candidate.strip().split('。')
    ra = raw.strip().split('。')
    for c in cl:
        for r in ra:
            similist.append([c,r,editdistance.eval(c,r)])
    sort=sorted(similist,key=lambda x:x[2])
    for c,r,ed in sort:
        if c!='' and r!='':
            print('懷疑抄襲句:{0}\n類似原句:{1}\neditdistance:{2}\n'.format(c,r,ed))
            
find_similar_sentence(news.iloc[3352].content,news.iloc[3134].content)
複製代碼

總結

本文主要提供了一個解決實際問題的思路框架,把一個實際的抄襲檢測問題分解成一個文本分類問題和一個類似文本搜索問題,結合機器學習的思路解決實際問題的思路值得參考。

同時本文不少部分只採起了簡單的方法,受到啓發的同窗歡迎不斷優化,個人進一步優化理念和心得體會將會持續更新。

完整示例代碼戳這裏

致謝

感謝你耐心閱讀完個人文章,不足之處歡迎批評指正,但願和你共同交流進步。

感謝個人指導老師高老師,還有積極討論解決問題的同窗朋友們!

相關文章
相關標籤/搜索