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

重炮手東方未明發表於2018-05-08

前言

本文旨在記錄本萌新在做練手專案總結的心得體會,主要針對初學者,介紹的概念和技術會比較基礎,從而提供一些解決實際問題的思路(不必拘泥與其中使用到的概念和演算法,在細節上完全可以做得更好,用其它更先進更前沿的技術替代),同時會重點介紹我認為比較需要注意的技術細節。

注意

  1. 本文的樣例資料恕不能分享,如有需要請自己動手爬取。
  2. 基本的操作在這裡不作討論,如有需要請自己查閱相關文件。
  3. 相關概念:TF-IDF樸素貝葉斯(naive bayes)k-means聚類

問題描述

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

解決流程

1. 資料清洗

我們首先讀取資料命名為news的dataframe,資料欄位大概如下

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的樣本,經檢視不是很多,因此可以直接去掉。

#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的方法。

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

類似地,我們可以使用相似的方法制造標籤(比如我現在假設新聞來源包含新華兩個字為正例)

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

到此,我們的資料清洗工作就完成啦!:D

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]]))]
'''
複製程式碼

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

找出相似文字後,更仔細地,你可以根據某些特徵(特定的長度,特定的分隔符)分割文字的句子,或者在這裡我簡單以“。”分割文字,分別計算相似文字句子間的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)
複製程式碼

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

總結

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

同時本文很多部分只採取了簡單的方法,受到啟發的同學歡迎不斷優化,我的進一步優化理念和心得體會將會持續更新。

完整示例程式碼戳這裡

致謝

感謝你耐心閱讀完我的文章,不足之處歡迎批評指正,希望和你共同交流進步。

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

相關文章