Java余弦計算文本相似度項目—第一次個人編程作業(軟件工程)


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

代碼鏈接(Java)

  • GitHub鏈接,若有幫助,可以點個Star~

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

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

整體流程

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

工程分包的截圖

image-20200921193739689

項目內的主要的類

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

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

org.odm 包內的UML圖

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(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/empty.txt","src/test/result/testEmptyResult.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試 輸入的對比文本路徑參數為錯誤參數的情況
     */
    @Test
    public void testForWrongOriginArgument(){
        try {
            MainApplication.process("src/test/testcase/123.txt","src/test/testcase/orig_0.8_add.txt","src/test/result/testAddResult.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試 輸出文件路徑參數為錯誤參數的情況
     */
    @Test
    public void testForWrongOutputArgument(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig.txt","src/test/result/testAWrongArgumentResult");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試20%文本添加情況:orig_0.8_add.txt
     */
    @Test
    public void testForAdd(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_add.txt","src/test/result/testAddResult.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試20%文本刪除情況:orig_0.8_del.txt
     */
    @Test
    public void testForDel(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_del.txt","src/test/result/testDelResult.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試20%文本亂序情況:orig_0.8_dis_1.txt
     */
    @Test
    public void testForDis1(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_dis_1.txt","src/test/result/testDis1Result.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試20%文本亂序情況:orig_0.8_dis_3.txt
     */
    @Test
    public void testForDis3(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_dis_3.txt","src/test/result/testDis3Result.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }

    }

    /**
     * 測試20%文本亂序情況:orig_0.8_dis_7.txt
     */
    @Test
    public void testForDis7(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_dis_7.txt","src/test/result/testDis7Result.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試20%文本亂序情況:orig_0.8_dis_10.txt
     */
    @Test
    public void testForDis10(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_dis_10.txt","src/test/result/testDis10Result.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試20%文本亂序情況:orig_0.8_dis_15.txt
     */
    @Test
    public void testForDis15(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_dis_15.txt","src/test/result/testDis15Result.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試20%文本格式錯亂情況:orig_0.8_mix.txt
     */
    @Test
    public void testForMix(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_mix.txt","src/test/result/testMixResult.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試20%文本錯別字情況:orig_0.8_rep.txt
     */
    @Test
    public void testForRep(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_0.8_rep.txt","src/test/result/testRepResult.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試相同文本:orig.txt
     */
    @Test
    public void testForSame(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig.txt","src/test/result/testSameResult.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試文本的子集文本:orig_sub.txt
     */
    @Test
    public void testForSub(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig_sub.txt","src/test/result/testSubResult.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

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

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

測試結果

image-20200921184407297

image-20200921185332034

測試覆蓋率截圖

image-20200921190022674

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

IOException以及FileNotFoundException,異常的場景是文件的寫入和讀取以及文件不存在仍要操作,可能會導致這些異常,所以要提前規避。

如下:

image-20200921190422573

對應的測試

    /**
     * 測試 文本為空文本的情況
     */
    @Test
    public void testForEmpty(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/empty.txt","src/test/result/testEmptyResult.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試 輸入的對比文本路徑參數為錯誤參數的情況
     */
    @Test
    public void testForWrongOriginArgument(){
        try {
            MainApplication.process("src/test/testcase/123.txt","src/test/testcase/orig_0.8_add.txt","src/test/result/testAddResult.txt");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

    /**
     * 測試 輸出文件路徑參數為錯誤參數的情況
     */
    @Test
    public void testForWrongOutputArgument(){
        try {
            MainApplication.process("src/test/testcase/orig.txt","src/test/testcase/orig.txt","src/test/result/testAWrongArgumentResult");
        }
        catch (Exception e) {
            e.printStackTrace();
            // 如果拋出異常,證明測試失敗,沒有通過,沒通過的測試計數在Failures中
            Assert.fail();
        }
    }

測試結果

image-20200921191839455

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

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

  • 類的內存消耗

    image-20200921192453340

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

image-20200921192037298

  • 堆內存情況

image-20200921183351607

  • 耗時操作情況

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

image-20200921183616707

image-20200921183758486

改進耗時的地方

摸索了大概20分鍾,最后發現由於最耗時的地方是在於分詞操作的函數,而如果一味提高速度就會損失精度,所以無法從Hanlp的分詞函數動刀。故只好從其他耗時地方(對象創建等)入手,例子如下:

image-20200921214403182

代碼質量檢查Code Quantity Analysis

使用了Github 上對公開項目托管的靜態代碼檢查工具——Codacy

修復了一些issue后,現在已經是一個干凈的項目了,截圖如下:

image-20200923110946219

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監控性能
  • 單元測試的話,感覺挺方便的,但是這次的結果都是不可預計的,所以得用白盒測試。使用了斷言Assert.fail()。
  • 異常處理部分,處理了主要的 IOExceptionFileNotFoundException
  • PSP,還是高估了自己的能力,從一開始的一頭霧水到最后解決了,還是看出自己的很多不足。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM