第一次個人編程作業


作業呈上

  • GitHub鏈接🔗點擊鏈接進入我的GitHub倉庫
  • 點擊博客左上角的那只貓進入我的GitHub首頁
  • 可運行的Jar包已發布至release
  • 開發平台、環境、工具以及開發者日志等詳情在倉庫中的README.md中查看
  • 博客寫的很潦草,我后續或許還會更新,代碼也是

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

整體流程

  • 首先呢,我先用土的不能再土的方式展現我的設計思路及流程

大白話

WTF???什么是黑...

咱先不管這個黑魔法是什么,總之就是我的核心算法,我們應該先集中精力把不屬於算法的部分(也就是框架)先搞定,不是嗎?這樣才有精力專心來搞算法。

emm...這樣或許真的太大白話了,那我具體一點,把它換上我的類和具體實現方法,也順便展示出我代碼中各個類、方法的關系

(0)先展示一下我的類(沒有屬性,主要還是靜態方法,當工具用了)

  • TextProcessor:文本處理器,輸入文本文件,轉字符串
  • AlgorithmProcessor:算法處理器,集中用來寫算法
  • AnswerProcessor:答案處理器,也是最大塊的,通過調用TextProcessor和AlgorithmProcessor中的方法,以及自己的一些私有靜態方法,集中處理各種字符串和集合,最后輸出

類

(1)輸入硬盤中的.txt → 內存中的String → 分詞后的List < String > 集合

1

(2)分詞后的List < String > 集合 → int結果 → 計算出查重率float → 輸出到硬盤中的.txt

2

我不知道我這一通狂說有沒有說清楚具體流程(除了算法),希望我的圖文能說明白,那我下面要說最關鍵的算法了

核心算法

我先亮方法了,我使用的算法主要是兩個的結合體:

  • LCS,也就是基於動態規划的最長公共子串
  • Tokenizer,是HanLP家的,用來做分詞操作
  • Jaro-Whinkle距離,是一個計算文本相似度的算法,是Apache家的

嗯,沒錯,我聽從柯老板,我選擇了擁抱開源

我們按順序來說吧!

  • 首先是LCS

LCS的基本實現思想是,取兩個字符串的字符一一比對,若兩者相同則令S [i] [j] = S [i] [j] + 1,否則S [i] [j] = max(S [i-1] [j] , S [i] [j-1]),這也是符合LCS的推導公式的。而得到最長相同子串的方法是通過數據結構的棧實現的,因為一般的實現會記錄字符的方向,而輸出字符串是要從S矩陣的右下角逆序輸出,因此很契合棧的結構,其規則是:若S1[i] = S2[j],將字符入棧,否則對比S [i] [j-1]和S [i-1] [j]的大小,若前者不大於后者則遍歷后者。(具體詳說可以參考這篇博文

LCS

  • 再來是HanLP家的Tokenizer,和jieba類似。

這個東西我不想說太多,因為這個不是算法關鍵所在,屬於核心處理前的預處理,分詞,大概呢就是基於字符串匹配、理解、統計、規則一系列的方式給他切好,我們這邊只是把它當工具來用,並不打算講太多。

  • 最后就是Jaro-Whinkle距離,也是我們的核心所在,雖然它是我調用的工具包,但是它是核心的一部分!所以我會多說點!

我們先說定義:

Jaro Distance也是字符串相似性的一種度量方式,也是一種編輯距離,Jaro 距離越高本文相似性越高;而Jaro–Winkler distance是Jaro Distance的一個變種。其定義如下:

img

其中

  • m是匹配數目(保證順序相同)
  • |s|是字符串長度
  • t是換位數目

其中t換位數目表示:兩個分別來自S1和S2的字符如果相距不超過

img

我們就認為這兩個字符串是匹配的;而這些相互匹配的字符則決定了換位的數目t,簡單來說就是不同順序的匹配字符的數目的一半即為換位的數目t,舉例來說,MARTHA與MARHTA的字符都是匹配的,但是這些匹配的字符中,T和H要換位才能把MARTHA變為MARHTA,那么T和H就是不同的順序的匹配字符,t=2/2=1。
而Jaro-Winkler則給予了起始部分就相同的字符串更高的分數,他定義了一個前綴p,給予兩個字符串,如果前綴部分有長度為 的部分相同,則Jaro-Winkler Distance為:img

  • dj是兩個字符串的Jaro Distance
  • l是前綴的相同的長度,但是規定最大為4
  • p則是調整分數的常數,規定不能超過0.25,不然可能出現dw大於1的情況,Winkler將這個常數定義為0.1

舉個簡單的例子:
計算s_1=DIXON,s_2=DICKSONX的距離

img

我們利用\lfloor \frac{max(|s_1|,|s_2|)}{2}-1 \rfloor可以得到一個匹配窗口距離為3,圖中黃色部分便是匹配窗口,其中1表示一個匹配,我們發現兩個X並沒有匹配,因為其超出了匹配窗口的距離3。我們可以得到:

img

img

img

img

其Jaro score為:

d_j=\frac{1}{3}(\frac{4}{5}+\frac{4}{8}+\frac{4-0}{4})=0.767

而計算Jaro–Winkler score,我們使用標准權重p=0.1,\ell=2,其結果如下:

img

疑問

你可能會問,為什么這邊有兩個算法好像都是在算文本相似度的?不,我們可以把它們兩個結合,具體分析可以看下面的性能改進部分。我們把LCS判斷相等的條件,換成計算兩個詞的Jaro距離得分,按經驗分析,如果大於0.6,我們就認為它相似,執行原來LCS相等條件的部分。

小總結

我再總結一下吧,大概就是這樣:

  • 讀進來,給他分詞,把標點抽掉,就留詞組
  • 比較兩個詞組的集合,計算Jaro距離得分,大於0.6,認為他們相似,執行LCS的相等邏輯
  • 計算結果,輸出去

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

我們先談算法准確度,先不談性能嗷!

我們知道Jaro和LCS都是計算文本相似度一類的算法,那我們可以各單獨跑一遍看看,咱們先跑純LCS:

  • 測試代碼
    /**
     * 測試全部樣例(純LCS)
     */
    @Test
    public void testForAllFilesOnlyLCS(){
        File f = new File("tests");
        String[] files = f.list();
        int cnt = 1;
        for(String file : files){
            if(!file.equals("orig.txt")){
                System.out.println("開始處理"+file);
                AnswerProcessor.processJustByLCS("tests/orig.txt","tests/"+file,"result/ans"+cnt+".txt");
                cnt++;
            }
        }
    }
  • 控制台輸出結果
開始處理orig_0.8_add.txt
查重率為:1.00
結果已寫入result/ans1.txt
開始處理orig_0.8_del.txt
查重率為:0.84
結果已寫入result/ans2.txt
開始處理orig_0.8_dis_1.txt
查重率為:0.97
結果已寫入result/ans3.txt
開始處理orig_0.8_dis_10.txt
查重率為:0.85
結果已寫入result/ans4.txt
開始處理orig_0.8_dis_15.txt
查重率為:0.70
結果已寫入result/ans5.txt
開始處理orig_0.8_dis_3.txt
查重率為:0.93
結果已寫入result/ans6.txt
開始處理orig_0.8_dis_7.txt
查重率為:0.90
結果已寫入result/ans7.txt
開始處理orig_0.8_mix.txt
查重率為:0.91
結果已寫入result/ans8.txt
開始處理orig_0.8_rep.txt
查重率為:0.83
結果已寫入result/ans9.txt

第一個抄襲文本是添加,因為其文本都是原文本存在的內容,結果竟然出來了1.00的結果,全抄??顯然不對,LCS的不足之處很快就體現出來了,它非常依賴原文本!我們再來試試純Jaro:

  • 測試代碼
    /**
     * 測試全部樣例(純Jaro)
     */
    @Test
    public void testForAllFilesOnlyJaro(){
        File f = new File("tests");
        String[] files = f.list();
        int cnt = 1;
        for(String file : files){
            if(!file.equals("orig.txt")){
                System.out.println("開始處理"+file);
                AnswerProcessor.processJustByJaro("tests/orig.txt","tests/"+file,"result/ans"+cnt+".txt");
                cnt++;
            }
        }
    }
  • 控制台輸出結果
開始處理orig_0.8_add.txt
查重結果為:0.79
結果已寫入result/ans1.txt
開始處理orig_0.8_del.txt
查重結果為:0.78
結果已寫入result/ans2.txt
開始處理orig_0.8_dis_1.txt
查重結果為:0.99
結果已寫入result/ans3.txt
開始處理orig_0.8_dis_10.txt
查重結果為:0.97
結果已寫入result/ans4.txt
開始處理orig_0.8_dis_15.txt
查重結果為:0.95
結果已寫入result/ans5.txt
開始處理orig_0.8_dis_3.txt
查重結果為:0.99
結果已寫入result/ans6.txt
開始處理orig_0.8_dis_7.txt
查重結果為:0.98
結果已寫入result/ans7.txt
開始處理orig_0.8_mix.txt
查重結果為:0.82
結果已寫入result/ans8.txt
開始處理orig_0.8_rep.txt
查重結果為:0.74
結果已寫入result/ans9.txt

誒,這個看着還不錯啊,但中間這幾個文本...那幾個文本是調換順序,怎么說呢,文本內容其實沒變,就是換了一些詞的順序,但是非常接近全抄,其實這個算法這樣已經很好了,可我感覺總是差點意思,它不依賴原文本,它靠的是差異;如果文本內容調換順序,還能維持原來的意思嗎?不見得。在某些情況下,經過排列后,其實不算是抄,那是有技巧地推陳出新;還有第一個add的部分,讓我想起來小學時候寫作文,不會就嗯抄,然后自己添一點,就是自己的了,顯然不對,我認為這就是大抄!“天下文章一大抄”這話固然沒錯,但是好的抄文是懂得排列組合,而非單靠一篇文章推陳出新,基於此,我覺得add的抄襲比重還不夠,應該給予更多!


而且不知道你們有沒有發現,Jaro有個獎勵機制,就是它會獎勵前綴相同。正因如此,如果是長文本的話,也就是整篇文章直接來,那它至多只會被獎勵一次,就看文章開頭,這似乎不太准確;若是加上分詞將它切開,放如LCS中,就會獲得很多的獎勵,這樣豈不是更准確,拉開“貧富差距”!


綜上考慮,我改進的思路和想法就是:把兩者結合!!!結合兩者的優勢,中和掉二者的不足!


LCS+Jaro:

  • 先看代碼
     /**
     * 基於最長公共子串算法,計算Jaro距離
     * @param word_a org.txt文件
     * @param word_b org_add.txt文件
     * @return
     */
    public static int JaroDisBasedOnLCS(List<String> word_a, List<String> word_b){
        int[][] cell = new int[word_a.size()+1][word_b.size()+1];
        int lena = word_a.size();
        int lenb = word_b.size();
        for(int i=0;i<lena;i++){
            for(int j=0;j<lenb;j++){
                if(StringUtils.getJaroWinklerDistance(word_a.get(i),word_b.get(j))>0.6){
                    cell[i+1][j+1] = cell[i][j] + 1;
                }else{
                    cell[i+1][j+1] = Math.max(cell[i][j+1],cell[i+1][j]);
                }
            }
        }
        return cell[lena][lenb];
    }
  • 再來看看運行結果:
開始處理orig_0.8_add.txt
查重率為:0.98
結果已寫入result/ans1.txt
開始處理orig_0.8_del.txt
查重率為:0.79
結果已寫入result/ans2.txt
開始處理orig_0.8_dis_1.txt
查重率為:0.98
結果已寫入result/ans3.txt
開始處理orig_0.8_dis_10.txt
查重率為:0.85
結果已寫入result/ans4.txt
開始處理orig_0.8_dis_15.txt
查重率為:0.67
結果已寫入result/ans5.txt
開始處理orig_0.8_dis_3.txt
查重率為:0.94
結果已寫入result/ans6.txt
開始處理orig_0.8_dis_7.txt
查重率為:0.90
結果已寫入result/ans7.txt
開始處理orig_0.8_mix.txt
查重率為:0.90
結果已寫入result/ans8.txt
開始處理orig_0.8_rep.txt
查重率為:0.79
結果已寫入result/ans9.txt

蕪湖,起飛!✈️它已經很貼近我的心理預期了!

但這樣結合帶來的后果就是...跑得很慢...似乎不是性能改進啊這...

其實我唯一的性能改進的地方就是加了抽去標點符號,那使我速度快了40%,但是這似乎是步驟中必要的一步,也不算是改進啦,后來想起來加上去的,所以性能改進part的話,暫時是沒有解決的。


扯完皮了,該回答一下要求的問題了!

(接下來我以添加20%文本的那條為例)

性能分析圖展示

  • 類的內存消耗

類

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

cpu

  • 堆內存情況

memeory

程序中消耗最大的方法

毫無疑問,當然是Jaro啊,在LCS中,它作為每一步的判斷條件,不得累死,直接吃掉75%左右,最氣的是它沒辦法優化,因為是我調的別人的,嗚嗚嗚😢

方法消耗

記錄在改進計算模塊性能上所花費的時間

其實並不多,主要花時間在查找算法資料上,一開始我就是用的LCS,但發現了它的穩定性、效率、准確性,都不盡如人意,想到要結合,或許會好些;硬要算的話,maybe 2小時用來找Jaro了,找到就用了,測試結果雖然不算很好,但也不差

計算模塊部分單元測試展示

展示單元測試代碼

import org.junit.Test;
import java.io.File;

/**
 * 測試
 */
public class TestCase {

    /**
     * 測試全部樣例(純Jaro)
     */
    @Test
    public void testForAllFilesOnlyJaro(){
        File f = new File("tests");
        String[] files = f.list();
        int cnt = 1;
        for(String file : files){
            if(!file.equals("orig.txt")){
                System.out.println("開始處理"+file);
                AnswerProcessor.processJustByJaro("tests/orig.txt","tests/"+file,"result/ans"+cnt+".txt");
                cnt++;
            }
        }
    }

    /**
     * 測試全部樣例(純LCS)
     */
    @Test
    public void testForAllFilesOnlyLCS(){
        File f = new File("tests");
        String[] files = f.list();
        int cnt = 1;
        for(String file : files){
            if(!file.equals("orig.txt")){
                System.out.println("開始處理"+file);
                AnswerProcessor.processJustByLCS("tests/orig.txt","tests/"+file,"result/ans"+cnt+".txt");
                cnt++;
            }
        }
    }


    /**
     * 測試全部樣例(Jaro+LCS)
     */
    @Test
    public void testAllFiles(){
        File f = new File("tests");
        String[] files = f.list();
        int cnt = 1;
        for(String file : files){
            if(!file.equals("orig.txt")){
                System.out.println("開始處理"+file);
                AnswerProcessor.process("tests/orig.txt","tests/"+file,"result/ans"+cnt+".txt");
                cnt++;
            }
        }
    }

    /**
     * 測試20%文本添加情況:orig_0.8_add.txt
     */
    @Test
    public void testForAdd(){
        AnswerProcessor.process("tests/orig.txt","tests/orig_0.8_add.txt","ans.txt");
    }

    /**
     * 測試20%文本刪除情況:orig_0.8_del.txt
     */
    @Test
    public void testForDel(){
        AnswerProcessor.process("tests/orig.txt","tests/orig_0.8_del.txt","ans.txt");
    }

    /**
     * 測試20%文本亂序情況:orig_0.8_dis_1.txt
     */
    @Test
    public void testForDis1(){
        AnswerProcessor.process("tests/orig.txt","tests/orig_0.8_dis_1.txt","ans.txt");
    }

    /**
     * 測試20%文本亂序情況:orig_0.8_dis_3.txt
     */
    @Test
    public void testForDis3(){
        AnswerProcessor.process("tests/orig.txt","tests/orig_0.8_dis_3.txt","ans.txt");
    }

    /**
     * 測試20%文本亂序情況:orig_0.8_dis_7.txt
     */
    @Test
    public void testForDis7(){
        AnswerProcessor.process("tests/orig.txt","tests/orig_0.8_dis_7.txt","ans.txt");
    }

    /**
     * 測試20%文本亂序情況:orig_0.8_dis_10.txt
     */
    @Test
    public void testForDis10(){
        AnswerProcessor.process("tests/orig.txt","tests/orig_0.8_dis_10.txt","ans.txt");
    }

    /**
     * 測試20%文本亂序情況:orig_0.8_dis_15.txt
     */
    @Test
    public void testForDis15(){
        AnswerProcessor.process("tests/orig.txt","tests/orig_0.8_dis_15.txt","ans.txt");
    }

    /**
     * 測試20%文本格式錯亂情況:orig_0.8_mix.txt
     */
    @Test
    public void testForMix(){
        AnswerProcessor.process("tests/orig.txt","tests/orig_0.8_mix.txt","ans.txt");
    }

    /**
     * 測試20%文本錯別字情況:orig_0.8_rep.txt
     */
    @Test
    public void testForRep(){
        AnswerProcessor.process("tests/orig.txt","tests/orig_0.8_rep.txt","ans.txt");
    }

    /**
     * 測試相同文本:orig.txt
     */
    @Test
    public void testForSame(){
        AnswerProcessor.process("tests/orig.txt","tests/orig.txt","ans.txt");
    }

}

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

  • 測試的方法就是分別跑LCS、Jaro、LCS+Jaro咯!就遍歷一下原文文本所在文件夾的所有文件,讀進來處理就行!其他就是分別對單個文件進行測試。為了防止出錯,我還加了個相同文件的,要是不為1就出事了!
  • 至於測試數據,我覺得給的樣例就很好了,涵蓋了不同情況:添加、刪除、錯別字、打亂順序、格式錯亂等...

測試結果

測試結果

測試覆蓋率截圖

覆蓋率

評價一下自己的測試

  • 沒有使用Assert斷言,不知是我還不太會用,還是確實用不上,感覺不需要啊我這個
  • 沒有試驗大數據文本來跑,是個缺憾(我盲猜會非常非常慢)

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

我設計的異常

/**
 * 非相同文本,查重率卻為1
 */
public class ResultEqualsOneException extends Exception{

    public ResultEqualsOneException() {
    }

    public ResultEqualsOneException(String message) {
        super(message);
    }

    @Override
    public String getMessage() {
        return super.getMessage();
    }
}

// 具體使用
if(ans/standardLength==1.00 && !orgFileName.equals(orgAddFileName)){
    try {
        throw new ResultEqualsOneException("非相同文本,查重率怎么會為1呢?");
    } catch (ResultEqualsOneException e) {
        e.printStackTrace();
    }
}

測試

我們試一下相同文本的情況,會不會報

test1

再試一下LCS的add文本看看

tets2

和前文提到的1.00的結果一致


其實還可以寫一個與之對應的異常和測試方法,就是那兩個完全不同的文本來測是否為0;這個其實我有測過了,是對的,但我覺得意義真的不大,又刪去了,這樣真的不是一個好的異常和測試的設計。(空文本測試同理)

總結

  • 接口的設計的話,我覺得這次其實看不出什么,因為只是個小小的小項目,寫的也基本上是工具類,體現不出設計模式一類的東西
  • 性能改進方面真的很抱歉啊,沒辦法,擁抱開源的我,還把自己寫的垃圾LCS和Apache的庫結合了,結果就是淦慢,希望下次能自己設計算法吧,自己的東西才方便優化
  • 單元測試的話,第一次使用,很遺憾沒能用上斷言(除了兩個完全相同和兩個完全不同的或許可以硬用上Assert的Equals,其他我真不知道怎么用,可能也和我的代碼有關系),希望下次有機會能用上,還有測試壓力部分,逃避了,下次盡量找大數據懟
  • 異常處理部分,說實話,真的不知道能寫什么異常,絞盡腦汁,就想到一個,還是沒去Override的,直接就等於Exception改個名兒,希望下次能提升自身邏輯嚴謹性,考慮盡量周全(也可能是項目還不夠大吧)

PSP表格

PSP2.1 Personal Software Process Stages 預估耗時(分鍾) 實際耗時(分鍾)
Planning 計划 30 40
Estimate 估計這個任務需要多少時間 20 25
Development 開發 480 300
Analysis 需求分析 (包括學習新技術) 300 180
Design Spec 生成設計文檔 60 20
Design Review 設計復審 30 15
Coding Standard 代碼規范 (為目前的開發制定合適的規范) 30 10
Design 具體設計 60 60
Coding 具體編碼 390 210
Code Review 代碼復審 30 30
Test 測試(自我測試,修改代碼,提交修改) 180 120
Reporting 報告 90 120
Test Repor 測試報告 60 100
Size Measurement 計算工作量 20 15
Postmortem & Process Improvement Plan 事后總結, 並提出過程改進計划 60 30
Total 合計 1840 1275


免責聲明!

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



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