軟工我的做業—論文查重算法+單元測試+JProfiler+PSP表格+Git管理

軟件工程 https://edu.cnblogs.com/campus/gdgy/informationsecurity1812
做業要求 https://edu.cnblogs.com/campus/gdgy/informationsecurity1812/homework/11155
做業目標 論文查重算法+單元測試+JProfiler+PSP表格+Git管理

代碼連接(Java)

  • GitHub連接java

  • 可運行的Jar包已發佈至倉庫的release包內android

計算模塊接口的設計與實現過程

總體流程

  1. MainApplication.main()會接收到三個參數,接着執行process方法
  2. 將兩個等待對比的文本內容分別轉換爲字符串
  3. SimilarTextCalculator.getSimilarity(),對比這兩個字符串
  4. 將結果輸出到指定路徑文件

工程分包的截圖

image-20200921193739689

項目內的主要的類

  • MainApplication : 主程序,入口
  • AtomicFloat :可原子操做的Float類
  • SimilarTextCalculator : 類似文本計算工具類
  • ConvertUtil :轉換工具類,實現字符串與文本文件的互轉
  • TextUtil:文本處理工具類,執行文本分詞等操做

類、函數之間的關係經過IDEA自帶生成的UML圖直觀地呈現git

org.odm 包內的UML圖github

image-20200921193520044

utils包內的UML圖算法

image-20200921193658858

實際命令行運行效果框架

image-20200921194731464

算法的關鍵函數

基於一個概念——餘弦距離,也稱爲餘弦類似度,是用向量空間中兩個向量夾角的餘弦值做爲衡量兩個個體間差別的大小的度量。餘弦值越接近1,就代表夾角越接近0度,也就是兩個向量越類似,這就叫"餘弦類似性"。工具

獨到之處性能

複雜文本狀況下,速度能夠維持在2s內,簡單狀況下如文本高度相同,速度能夠達到20ms,同時識別準確率也很不錯。計算速度和準確度達到了相對均衡。單元測試

文本類似度算法—餘弦類似度算法

計算公式

img

  • 餘弦值越接近 1 ,也就是兩個向量越類似,這就叫"餘弦類似性"
  • 餘弦值越接近 0 ,也就是兩個向量越不類似,也能夠說這兩個字符串越不類似

實際例子

用餘弦類似度算法計算文本的類似性。

爲了簡單起見,先從句子着手。

句子A:這頂帽子尺寸大了。那頂尺寸合適。

句子B:這頂帽子尺寸不小,那頂更合適。

基本計算的思路是:若是這兩句話的用詞越類似,它們的內容就應該越類似。

所以,能夠從詞頻入手,計算它們的類似程度。

第一步,分詞

​ 句子A:這頂/帽子/尺寸/大了。那頂/尺寸/合適。

​ 句子B:這頂/帽子/尺寸/不/小,那頂/更/合適。

第二步,計算詞頻

​ 句子A:這頂(1),帽子(1),尺寸(2),大了(1),那頂(1),合適(1),不(0),小(0),更(0)

​ 句子B:這頂(1),帽子(1),尺寸(1),大了(0),那頂(1),合適(1),不(1),小(1),更(1)

第三步,寫出詞頻向量

  句子A:(1,1,2,1,1,1,0,0,0)

  句子B:(1,1,1,0,1,1,1,1,1)

第四步:運用上面的公式:計算以下:

img

計算結果中夾角的餘弦值爲0.81,很是接近於1。

因此,上面的句子A和句子B是基本類似的

算法總結

  1. 分詞:分詞固然要按必定規則,否則隨便分那也沒有意義,那這裏經過採用HanLP中文天然語言處理中標準分詞進行分詞。
  2. 統計詞頻:就統計上面詞出現的次數。
  3. 經過每個詞出現的次數,變成一個向量,經過向量公式計算類似率。

計算模塊的單元測試展現(白盒)

展現單元測試代碼(12種狀況)

public class MainApplicationTest {

    @BeforeClass
    public static void beforeTest(){
        System.out.println("測試即將開始");
    }

    @AfterClass
    public static void afterTest(){
        System.out.println("測試結束");
    }
    
    /**
     * 測試 文本爲空文本的狀況
     */
    @Test
    public void testForEmpty(){
        MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/empty.txt","src/test/result/testEmptyResult.txt");
    }

    /**
     * 測試 輸入的對比文本路徑參數爲錯誤參數的狀況
     */
    @Test
    public void testForWrongOriginArgument(){
        MainApplication.process("src/test/testcase/123.txt","src/test/testcase/orig_0.8_add.txt","src/test/result/testAddResult.txt");
    }

    /**
     * 測試 輸出文件路徑參數爲錯誤參數的狀況
     */
    @Test
    public void testForWrongOutputArgument(){
        MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig.txt","src/test/result/testAddResult.word");
    }

    /**
     * 測試20%文本添加狀況:orig_0.8_add.txt
     */
    @Test
    public void testForAdd(){
        MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_add.txt","src/test/result/testAddResult.txt");
    }

    /**
     * 測試20%文本刪除狀況:orig_0.8_del.txt
     */
    @Test
    public void testForDel(){
        MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_del.txt","src/test/result/testDelResult.txt");
    }

    /**
     * 測試20%文本亂序狀況:orig_0.8_dis_1.txt
     */
    @Test
    public void testForDis1(){
        MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_dis_1.txt","src/test/result/testDis1Result.txt");
    }

    /**
     * 測試20%文本亂序狀況:orig_0.8_dis_3.txt
     */
    @Test
    public void testForDis3(){
        MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_dis_3.txt","src/test/result/testDis3Result.txt");
    }

    /**
     * 測試20%文本亂序狀況:orig_0.8_dis_7.txt
     */
    @Test
    public void testForDis7(){
        MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_dis_7.txt","src/test/result/testDis7Result.txt");
    }

    /**
     * 測試20%文本亂序狀況:orig_0.8_dis_10.txt
     */
    @Test
    public void testForDis10(){
        MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_dis_10.txt","src/test/result/testDis10Result.txt");
    }

    /**
     * 測試20%文本亂序狀況:orig_0.8_dis_15.txt
     */
    @Test
    public void testForDis15(){
        MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_dis_15.txt","src/test/result/testDis15Result.txt");
    }

    /**
     * 測試20%文本格式錯亂狀況:orig_0.8_mix.txt
     */
    @Test
    public void testForMix(){
        MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_mix.txt","src/test/result/testMixResult.txt");
    }

    /**
     * 測試20%文本錯別字狀況:orig_0.8_rep.txt
     */
    @Test
    public void testForRep(){
        MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_rep.txt","src/test/result/testRepResult.txt");
    }

    /**
     * 測試相同文本:orig.txt
     */
    @Test
    public void testForSame(){
        MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig.txt","src/test/result/testSameResult.txt");
    }

    /**
     * 測試文本的子集文本:orig_sub.txt
     */
    @Test
    public void testForSub(){
        MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_sub.txt","src/test/result/testSubResult.txt");
    }
}

說明測試的方法,構造測試數據的思路

  • 單元測試,利用各類不一樣狀況的文本,與原文本進行類似度的計算,在控制檯輸出計算的結果,以及輸入錯誤的文件路徑參數。
  • 測試的文本涵蓋了不一樣狀況:在原文本上進行添加、刪除、錯別字、打亂順序、格式錯亂,節選文本原片斷等

測試結果

image-20200921184407297

image-20200921185332034

測試覆蓋率截圖

image-20200921190022674

計算模塊部分異常處理說明

IOException以及FileNotFoundException,異常的場景是文件的寫入和讀取以及文件不存在仍要操做,可能會致使這些異常,因此要提早規避。

以下:

image-20200921190422573

對應的測試

/**
 * 測試 文本內容爲空文本的狀況
 */
@Test
public void testForEmpty(){
    MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/empty.txt","src/test/result/testEmptyResult.txt");
}

/**
 * 測試 輸入的對比文本路徑參數爲錯誤參數的狀況
 */
@Test
public void testForWrongOriginArgument(){
    MainApplication.process("src/test/testcase/123.txt","src/test/testcase/orig_0.8_add.txt","src/test/result/testAddResult.txt");
}

/**
 * 測試 輸出文件路徑參數爲錯誤參數的狀況
 */
@Test
public void testForWrongOutputArgument(){
    MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig.txt","src/test/result/testAddResult.word");
}

測試結果

image-20200921191839455

計算模塊接口部分的性能改進

執行單元測試,對各類狀況進行測試的同時使用 JProfiler對性能進行監控

  • 類的內存消耗

    image-20200921192453340

  • CPU Load(運行時間:1.1 s,知足要求)

image-20200921192037298

  • 堆內存狀況

image-20200921183351607

  • 耗時操做狀況

由圖能夠看出,改進前的程序中時間平均耗時最大的方法——Hanlp的分詞操做

image-20200921183616707

image-20200921183758486

改進耗時的地方

摸索了大概20分鐘,最後發現因爲最耗時的地方是在於分詞操做的函數,而若是一味提升速度就會損失精度,因此沒法從Hanlp的分詞函數動刀。故只好從其餘耗時地方(對象建立等)入手,例子以下:

image-20200921214403182

PSP表格

PSP 各個階段 本身預估的時間(分鐘) 實際的記錄(分鐘)
計劃: 明確需求和其餘因素,估計如下的各個任務須要多少時間 30 45
開發 (包括下面 8 項子任務) (如下都填預估值) 218
· 需求分析 (包括學習新技術、新工具的時間) 20 30
· 生成設計文檔 (總體框架的設計,各模塊的接口,用時序圖,快速原型等方法) 15 5
· 設計複審 (和同事審覈設計文檔,或者本身複審) 15 20
· 代碼規範 (爲目前的開發制定或選擇合適的規範) 5 3
· 具體設計(用僞代碼,流程圖等方法來設計具體模塊) 20 30
· 具體編碼 60 75
· 代碼複審 15 20
· 測試(自我測試,修改代碼,提交修改) 30 35
報告 75 95
測試報告(發現了多少bug,修復了多少) 15 20
計算工做量 (多少行代碼,多少次簽入,多少測試用例,其餘工做量) 10 15
過後總結, 並提出改進計劃 (包括寫文檔、博客的時間) 50 60
總共花費的時間 (分鐘) 290 358

看來仍是對本身太自信了~將來儘可能實際追上計劃吧

總結

  • Java的項目不常寫,因而按照了平時寫android的分包和設計類的關係
  • 性能方面的話,用了比較主流的Hanlp的分詞和餘弦計算,因此速度和精度達到了均衡;學會了用JProfiler監控性能
  • 單元測試的話,感受挺方便的,可是此次的結果都是不可預計的,因此得用白盒測試,沒有使用斷言。
  • 異常處理部分,處理了主要的 IOExceptionFileNotFoundException
  • PSP,仍是高估了本身的能力,從一開始的一頭霧水到最後解決了,仍是看出本身的不少不足。