前言
軟件工程 | https://edu.cnblogs.com/campus/gdgy/informationsecurity1812 |
---|---|
作業要求 | https://edu.cnblogs.com/campus/gdgy/informationsecurity1812/homework/11155 |
作業目標 | 代碼實現、性能分析、單元測試、異常處理說明、記錄PSP表格 |
本文涉及代碼已上傳個人GitHub
題目:論文查重
描述如下:
設計一個論文查重算法,給出一個原文文件和一個在這份原文上經過了增刪改的抄襲版論文的文件,在答案文件中輸出其重復率。
原文示例:今天是星期天,天氣晴,今天晚上我要去看電影。
抄襲版示例:今天是周天,天氣晴朗,我晚上要去看電影。
要求輸入輸出采用文件輸入輸出,規范如下:從命令行參數給出:論文原文的文件的絕對路徑。
從命令行參數給出:抄襲版論文的文件的絕對路徑。
從命令行參數給出:輸出的答案文件的絕對路徑。
我們提供一份樣例,課堂上下發,上傳到班級群,使用方法是:orig.txt是原文,其他orig_add.txt等均為抄襲版論文。注意:答案文件中輸出的答案為浮點型,精確到小數點后兩位
查詢網上文章,總結出實現思路:
- 先將待處理的數據(中文文章)進行分詞,得到一個存儲若干個詞匯的列表
- 接着計算並記錄出列表中詞匯對應出現的次數,將這些次數列出來即可認為我們得到了一個向量
- 將兩個數據對應的向量代入夾角余弦定理
- 計算的值意義為兩向量的偏移度,這里也即對應兩個數據的相似度
除了余弦定理求相似度,還可以使用歐氏距離、海明距離等
所用接口
jieba.cut
用於對中文句子進行分詞,功能非常強大,詳細功能見GitHub
該方法提供多種分詞模式供選擇,這里只需用到默認最簡單的“精確模式”。
代碼:
seg_list = jieba.cut("他來到了網易杭研大廈") # 默認是精確模式
print(", ".join(seg_list))
運行結果:
他, 來到, 了, 網易, 杭研, 大廈
re.match
由於對比對象為中文或英文單詞,因此應該對讀取到的文件數據中存在的換行符\n
、標點符號過濾掉,這里選擇用正則表達式來匹配符合的數據。
代碼:
def filter(str):
str = jieba.lcut(str)
result = []
for tags in str:
if (re.match(u"[a-zA-Z0-9\u4e00-\u9fa5]", tags)):
result.append(tags)
else:
pass
return result
這里正則表達式為u"[a-zA-Z0-9\u4e00-\u9fa5]"
,也即對jieba.cut
分詞之后的列表中的值,只保留英文a-zA-z
、數字0-9
和中文\u4e00-\u9fa5
的結果。
gensim.dictionary.doc2bow
Doc2Bow是gensim中封裝的一個方法,主要用於實現Bow模型。
Bag-of-words model (BoW model) 最早出現在自然語言處理(Natural Language Processing)和信息檢索(Information Retrieval)領域.。該模型忽略掉文本的語法和語序等要素,將其僅僅看作是若干個詞匯的集合,文檔中每個單詞的出現都是獨立的。
例如:
text1='John likes to watch movies. Mary likes too.'
text2='John also likes to watch football games.'
基於上述兩個文檔中出現的單詞,構建如下一個詞典 (dictionary):
{"John": 1, "likes": 2,"to": 3, "watch": 4, "movies": 5,"also": 6, "football": 7, "games": 8,"Mary": 9, "too": 10}
上面的詞典中包含10個單詞, 每個單詞有唯一的索引, 那么每個文本我們可以使用一個10維的向量來表示。如下:
[1, 2, 1, 1, 1, 0, 0, 0, 1, 1]
[1, 1, 1, 1, 0, 1, 1, 1, 0, 0]
該向量與原來文本中單詞出現的順序沒有關系,而是詞典中每個單詞在文本中出現的頻率。
代碼:
def convert_corpus(text1,text2):
texts=[text1,text2]
dictionary = gensim.corpora.Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]
return corpus
gensim.similarities.Similarity
該方法可以用計算余弦相似度,但具體的實現方式官網似乎並未說清楚,這是我查找大量文章得到的一種實現方式:
def calc_similarity(text1,text2):
corpus=convert_corpus(text1,text2)
similarity = gensim.similarities.Similarity('-Similarity-index', corpus, num_features=len(dictionary))
test_corpus_1 = dictionary.doc2bow(text1)
cosine_sim = similarity[test_corpus_1][1]
return cosine_sim
當然也可以根據余弦公式實現計算余弦相似度:
from math import sqrt
def similarity_with_2_sents(vec1, vec2):
inner_product = 0
square_length_vec1 = 0
square_length_vec2 = 0
for tup1, tup2 in zip(vec1, vec2):
inner_product += tup1[1]*tup2[1]
square_length_vec1 += tup1[1]**2
square_length_vec2 += tup2[1]**2
return (inner_product/sqrt(square_length_vec1*square_length_vec2))
cosine_sim = similarity_with_2_sents(vec1, vec2)
print('兩個句子的余弦相似度為: %.4f。'%cosine_sim)
代碼實現
將上述方法匯總應用,得到代碼:
import jieba
import gensim
import re
#獲取指定路徑的文件內容
def get_file_contents(path):
str = ''
f = open(path, 'r', encoding='UTF-8')
line = f.readline()
while line:
str = str + line
line = f.readline()
f.close()
return str
#將讀取到的文件內容先進行jieba分詞,然后再把標點符號、轉義符號等特殊符號過濾掉
def filter(str):
str = jieba.lcut(str)
result = []
for tags in str:
if (re.match(u"[a-zA-Z0-9\u4e00-\u9fa5]", tags)):
result.append(tags)
else:
pass
return result
#傳入過濾之后的數據,通過調用gensim.similarities.Similarity計算余弦相似度
def calc_similarity(text1,text2):
texts=[text1,text2]
dictionary = gensim.corpora.Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]
similarity = gensim.similarities.Similarity('-Similarity-index', corpus, num_features=len(dictionary))
test_corpus_1 = dictionary.doc2bow(text1)
cosine_sim = similarity[test_corpus_1][1]
return cosine_sim
if __name__ == '__main__':
path1 = "E:\pythonProject1\test\orig_0.8_dis_10.txt" #論文原文的文件的絕對路徑(作業要求)
path2 = "E:\pythonProject1\test\orig_0.8_dis_15.txt" #抄襲版論文的文件的絕對路徑
save_path = "E:\pythonProject1\save.txt" #輸出結果絕對路徑
str1 = get_file_contents(path1)
str2 = get_file_contents(path2)
text1 = filter(str1)
text2 = filter(str2)
similarity = calc_similarity(text1, text2)
print("文章相似度: %.4f"%similarity)
#將相似度結果寫入指定文件
f = open(save_path, 'w', encoding="utf-8")
f.write("文章相似度: %.4f"%similarity)
f.close()
可以看出兩篇文章相似度很大:
運行結果:
更改路徑,
path1 = "E:\pythonProject1\test\orig.txt" ##論文原文的文件的絕對路徑
path2 = "E:\pythonProject1\test\orig_0.8_dis_15.txt" #抄襲版論文的文件的絕對路徑
可以看出兩篇文章相似度較小:
運行結果:
綜上,該程序基本符合判斷相似度的要求
性能分析
時間耗費
利用pycharm的插件可以得到耗費時間的幾個主要函數排名:
關注到filter
函數:由於cut
和lcut
暫時找不到可提到的其他方法(jieba庫已經算很強大了),暫時沒辦法進行改進,因此考慮對正則表達式匹配改進。
這里是先用lcut
處理后再進行匹配過濾,這樣做顯得過於臃腫,可以考慮先匹配過濾之后再用lcut
來處理
改進代碼:
def filter(string):
pattern = re.compile(u"[^a-zA-Z0-9\u4e00-\u9fa5]")
string = pattern.sub("", string)
result = jieba.lcut(string)
return result
再做一次運行時間統計:
可以看到總耗時快了0.5s,提升了時間效率
代碼覆蓋率
代碼覆蓋率100%,滿足要求:
單元測試
這里需要用到python的unittest單元測試框架,詳見官網介紹
為了方便進行單元測試,源碼的main()
應該修改一下:
import jieba
import gensim
import re
#獲取指定路徑的文件內容
def get_file_contents(path):
string = ''
f = open(path, 'r', encoding='UTF-8')
line = f.readline()
while line:
string = string + line
line = f.readline()
f.close()
return string
#將讀取到的文件內容先把標點符號、轉義符號等特殊符號過濾掉,然后再進行結巴分詞
def filter(string):
pattern = re.compile(u"[^a-zA-Z0-9\u4e00-\u9fa5]")
string = pattern.sub("", string)
result = jieba.lcut(string)
return result
#傳入過濾之后的數據,通過調用gensim.similarities.Similarity計算余弦相似度
def calc_similarity(text1, text2):
texts = [text1, text2]
dictionary = gensim.corpora.Dictionary(texts)
corpus = [dictionary.doc2bow(text) for text in texts]
similarity = gensim.similarities.Similarity('-Similarity-index', corpus, num_features=len(dictionary))
test_corpus_1 = dictionary.doc2bow(text1)
cosine_sim = similarity[test_corpus_1][1]
return cosine_sim
def main_test():
path1 = input("輸入論文原文的文件的絕對路徑:")
path2 = input("輸入抄襲版論文的文件的絕對路徑:")
str1 = get_file_contents(path1)
str2 = get_file_contents(path2)
text1 = filter(str1)
text2 = filter(str2)
similarity = calc_similarity(text1, text2) #生成的similarity變量類型為<class 'numpy.float32'>
result=round(similarity.item(),2) #借助similarity.item()轉化為<class 'float'>,然后再取小數點后兩位
return result
if __name__ == '__main__':
main_test()
為了使預期值更好確定,這里考慮只取返回的相似度值的前兩位,借助round(float,2)即可處理,由於生成的similarity類型為<class 'numpy.float32'>,因此應當先轉化為<class 'float'>,查找對應解決方法:通過xxx.item()
即可轉化。
再新建單元測試文件unit_test.py:
import unittest
from main import main_test
class MyTestCase(unittest.TestCase):
def test_something(self):
self.assertEqual(main_test(),0.99) #首先假設預測的是前面第一組運行的測試數據
if __name__ == '__main__':
unittest.main()
可以發現預測值為0.99正確:
相似度仍然預測為0.99,但路徑更改為之前測試的第二組數據:
可以發現預測失敗。
異常處理說明
在讀取指定文件路徑時,如果文件路徑不存在,程序將會出現異常,因此可以在讀取指定文件內容之前先判斷文件是否存在,若不存在則做出響應並且結束程序。
這里引入os.path.exists()
方法用於檢驗文件是否存在:
def main_test():
path1 = input("輸入論文原文的文件的絕對路徑:")
path2 = input("輸入抄襲版論文的文件的絕對路徑:")
if not os.path.exists(path1) :
print("論文原文文件不存在!")
exit()
if not os.path.exists(path2):
print("抄襲版論文文件不存在!")
exit()
······
PSP表格記錄
PSP | Personal Software Process Stages | 預估耗時(分鍾) | 實際耗時(分鍾) |
---|---|---|---|
Planning | 計划 | 120 | 150 |
· Estimate | · 估計這個任務需要多少時間 | 120 | 150 |
Development | 開發 | 480 | 300 |
· Analysis | · 需求分析 (包括學習新技術) | 120 | 100 |
· Design Spec | · 生成設計文檔 | 30 | 10 |
· Design Review | · 設計復審 | 30 | 10 |
· Coding Standard | · 代碼規范 (為目前的開發制定合適的規范) | 20 | 5 |
· Design | · 具體設計 | 10 | 5 |
· Coding | · 具體編碼 | 120 | 120 |
· Code Review | · 代碼復審 | 20 | 5 |
· Test | · 測試(自我測試,修改代碼,提交修改) | 20 | 20 |
Reporting | 報告 | 30 | 20 |
· Test Repor | · 測試報告 | 20 | 10 |
· Size Measurement | · 計算工作量 | 5 | 5 |
· Postmortem & Process Improvement Plan | · 事后總結, 並提出過程改進計划 | 5 | 5 |
Total | 總計 | 1150 | 915 |
參考文章
https://titanwolf.org/Network/Articles/Article?AID=26627f5e-1ce9-40cb-a091-b771ae91d69d
http://www.ruanyifeng.com/blog/2013/03/cosine_similarity.html