中文分詞引擎 java 實現 — 正向最大、逆向最大、雙向最大匹配法

正向最大匹配法

分詞目標:

在詞典中進行掃描,儘可能地選擇與詞典中最長單詞匹配的詞作爲目標分詞,然後進行下一次匹配。

算法流程

假設詞典中最長的單詞爲 5 個(MAX_LENGTH),那麼最大匹配的起始子串字數也爲 5 個

(1)掃描字典,測試讀入的子串是否在字典中

(2)如果存在,則從輸入中刪除掉該子串,重新按照規則取子串,重複(1)

(3)如果不存在於字典中,則從右向左減少子串長度,重複(1)

分詞實例:

比如說輸入 “北京大學生前來應聘”,

  1. 第一輪:取子串 “北京大學生”,正向取詞,如果匹配失敗,每次去掉匹配字段最後面的一個字
    • “北京大學生”,掃描 5 字詞典,沒有匹配,子串長度減 1 變爲“北京大學”
    • “北京大學”,掃描 4 字詞典,有匹配,輸出“北京大學”,輸入變爲“生前來應聘”
  2. 第二輪:取子串“生前來應聘”
    • “生前來應聘”,掃描 5 字詞典,沒有匹配,子串長度減 1 變爲“生前來應”
    • “生前來應”,掃描 4 字詞典,沒有匹配,子串長度減 1 變爲“生前來”
    • “生前來”,掃描 3 字詞典,沒有匹配,子串長度減 1 變爲“生前”
    • “生前”,掃描 2 字詞典,有匹配,輸出“生前”,輸入變爲“來應聘””
  3. 第三輪:取子串“來應聘”
    • “來應聘”,掃描 3 字詞典,沒有匹配,子串長度減 1 變爲“來應”
    • “來應”,掃描 2 字詞典,沒有匹配,子串長度減 1 變爲“來”
    • 顆粒度最小爲 1,直接輸出“來”,輸入變爲“應聘”
  4. 第四輪:取子串“應聘”
    • “應聘”,掃描 2 字詞典,有匹配,輸出“應聘”,輸入變爲“”
  5. 輸入長度爲0,掃描終止


正向匹配法最終的切分結果爲:”北京大學 / 生前 / 來 / 應聘”

正向匹配法實現代碼如下:

public List<String> leftMax(String str) {

        List<String> results = new ArrayList<String>();
        String input = str;

        while( input.length() > 0 ) {

            String subSeq;
            // 每次取小於或者等於最大字典長度的子串進行匹配
            if( input.length() < MAX_LENGTH) 
                subSeq = input;
            else
                subSeq = input.substring(0, MAX_LENGTH);

            while( subSeq.length() > 0 ) {
                // 如果字典中含有該子串或者子串顆粒度爲1,子串匹配成功
                if( dictionary.contains(subSeq) || subSeq.length() == 1) {
                    results.add(subSeq);
                    // 輸入中從前向後去掉已經匹配的子串
                    input = input.substring(subSeq.length());
                    break;      // 退出循環,進行下一次匹配
                } else {
                    // 去掉匹配字段最後面的一個字
                    subSeq = subSeq.substring(0, subSeq.length() - 1);
                }   
            }

        }
        return results;
    }


逆向最大匹配法

分詞目標:

在詞典中進行掃描,儘可能地選擇與詞典中最長單詞匹配的詞作爲目標分詞,然後進行下一次匹配。

在實踐中,逆向最大匹配算法性能優於正向最大匹配算法。

算法流程

假設詞典中最長的單詞爲 5 個(MAX_LENGTH),那麼最大匹配的起始子串字數也爲 5 個

(1)掃描字典,測試讀入的子串是否在字典中

(2)如果存在,則從輸入中刪除掉該子串,重新按照規則取子串,重複(1)

(3)如果不存在於字典中,則從左向右減少子串長度,重複(1)

分詞實例:

比如說輸入 “北京大學生前來應聘”,

  1. 第一輪:取子串 “生前來應聘”,逆向取詞,如果匹配失敗,每次去掉匹配字段最前面的一個字
    • “生前來應聘”,掃描 5 字詞典,沒有匹配,字串長度減 1 變爲“前來應聘”
    • “前來應聘”,掃描 4 字詞典,沒有匹配,字串長度減 1 變爲“來應聘”
    • “來應聘”,掃描 3 字詞典,沒有匹配,字串長度減 1 變爲“應聘”
    • “應聘”,掃描 2 字詞典,有匹配,輸出“應聘”,輸入變爲“大學生前來”
  2. 第二輪:取子串“大學生前來”
    • “大學生前來”,掃描 5 字詞典,沒有匹配,字串長度減 1 變爲“學生前來”
    • “學生前來”,掃描 4 字詞典,沒有匹配,字串長度減 1 變爲“生前來”
    • “生前來”,掃描 3 字詞典,沒有匹配,字串長度減 1 變爲“前來”
    • “前來”,掃描 2 字詞典,有匹配,輸出“前來”,輸入變爲“北京大學生”
  3. 第三輪:取子串“北京大學生”
    • “北京大學生”,掃描 5 字詞典,沒有匹配,字串長度減 1 變爲“京大學生”
    • “京大學生”,掃描 4 字詞典,沒有匹配,字串長度減 1 變爲“大學生”
    • “大學生”,掃描 3 字詞典,有匹配,輸出“大學生”,輸入變爲“北京”
  4. 第四輪:取子串“北京”
    • “北京”,掃描 2 字詞典,有匹配,輸出“北京”,輸入變爲“”
  5. 輸入長度爲0,掃描終止


逆向匹配法最終的切分結果爲:”北京/ 大學生/ 前來 / 應聘”

逆向匹配法實現如下:

public List<String> rightMax(String str) {
        // 採用堆棧處理結果,後進先出
        Stack<String> store=new Stack<String>();
        List<String> results = new ArrayList<String>();
        String input = str;

        while( input.length() > 0 ) {

            String subSeq;
            // 每次取小於或者等於最大字典長度的子串進行匹配
            if( input.length() < MAX_LENGTH)
                subSeq = input;
            else 
                subSeq = input.substring(input.length() - MAX_LENGTH);

            while( subSeq.length() > 0 ) {
                // 如果字典中含有該子串或者子串顆粒度爲1,子串匹配成功
                if( dictionary.contains(subSeq) || subSeq.length() == 1) {
                    store.add(subSeq);
                    // 輸入中從後向前去掉已經匹配的子串
                    input = input.substring(0, input.length() - subSeq.length());
                    break;
                } else {
                    // 去掉匹配字段最前面的一個字
                    subSeq = subSeq.substring(1);
                }
            }
        }
        // 輸出結果
        int size = store.size();
        for( int i = 0; i < size; i ++) {
            results.add(store.pop());
        }

        return results;
    }


雙向最大匹配法

分詞目標:

將正向最大匹配算法和逆向最大匹配算法進行比較,從而確定正確的分詞方法。

算法流程:

  1. 比較正向最大匹配和逆向最大匹配結果
  2. 如果分詞數量結果不同,那麼取分詞數量較少的那個
  3. 如果分詞數量結果相同
    • 分詞結果相同,可以返回任何一個
    • 分詞結果不同,返回單字數比較少的那個


分詞實例:

就上例來看,

正向匹配最終切分結果爲:北京大學 / 生前 / 來 / 應聘,分詞數量爲 4,單字數爲 1

逆向匹配最終切分結果爲:”北京/ 大學生/ 前來 / 應聘,分詞數量爲 4,單字數爲 0

逆向匹配單字數少,因此返回逆向匹配的結果。

雙向最大匹配法實現如下:

public List<String> segment() {
        List<String> fmm = this.leftMax();
        List<String> bmm = this.rightMax();

        // 如果分詞的結果不同,返回長度較小的
        if( fmm.size() != bmm.size()) {
            if ( fmm.size() > bmm.size())
                return bmm;
            else 
                return bmm;
        }
        // 如果分詞的詞數相同
        else {
            int fmmSingle = 0, bmmSingle = 0;
            boolean isEqual = true;
            for( int i = 0; i < bmm.size(); i ++) {
                if( !fmm.get(i).equals(bmm.get(i))) {
                    isEqual = false;
                }
                if( fmm.get(i).length() == 1)
                    fmmSingle ++;
                if( bmm.get(i).length() == 1)
                    bmmSingle ++;
            }
            // 如果正向、逆向匹配結果完全相等,返回任意結果
            if ( isEqual ) {
                return fmm;
            // 否則,返回單字數少的匹配方式
            } else if ( fmmSingle > bmmSingle)      
                return bmm;
            else 
                return fmm;     
        }

    }


載入字典和自定義添加詞

這裏的字典文件採用的是

http://download.csdn.net/download/yuanlulu/2380141

載入字典和自定義添加詞實現如下:

private static Set<String> dictionary;  
    // 初始化字典,採用 hashset 存儲
    public void getDictionary() {
        dictionary = new HashSet<String>();  
        String dicpath = "data/worddict2.txt";  
        String line = null;  

            BufferedReader br;
            try {
                // 按照 gbk 編碼讀入文件
                br = new BufferedReader(new InputStreamReader(new FileInputStream(dicpath),"gbk"));
                try {
                    while(((line = br.readLine())!=null)) {
                        // 按照空格切分,只讀取第二部分
                        String[] str = line.split("\\s+");
                        line = str[1];
                        dictionary.add(line);   
                    }
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } catch (UnsupportedEncodingException | FileNotFoundException e) {
                e.printStackTrace();
            }       
    }
    // 自定義添加詞彙
    public void addWord(String str) {
        dictionary.add(str);    
    }


歧義句測試

可以看到效果還不錯,最大匹配法的效果還是取決於字典的質量。

整體代碼如下:

package mm;
import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Stack;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException; 
public class MMSegment {

    private String request;
    private int MAX_LENGTH = 5;
    private static Set<String> dictionary;  

    public void getDictionary() {
        dictionary = new HashSet<String>();  
        String dicpath = "data/worddict2.txt";  
        String line = null;  

            BufferedReader br;
            try {
                br = new BufferedReader(new InputStreamReader(new FileInputStream(dicpath),"gbk"));
                try {
                    while(((line = br.readLine())!=null)) {
                        String[] str = line.split("\\s+");
                        line = str[1];
                        dictionary.add(line);   
                    }
                    br.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            } catch (UnsupportedEncodingException | FileNotFoundException e) {
                e.printStackTrace();
            }       
    }

    public void addWord(String str) {
        dictionary.add(str);    
    }

    public List<String> leftMax() {

        List<String> results = new ArrayList<String>();
        String input = request;

        while( input.length() > 0 ) {

            String subSeq;
            if( input.length() < MAX_LENGTH) 
                subSeq = input;
            else
                subSeq = input.substring(0, MAX_LENGTH);

            while( subSeq.length() > 0 ) {
                if( dictionary.contains(subSeq) || subSeq.length() == 1) {
                    results.add(subSeq);
                    input = input.substring(subSeq.length());
                    break;  
                } else {
                    subSeq = subSeq.substring(0, subSeq.length() - 1);
                }   
            }

        }
        return results;
    }
    public List<String> rightMax() {

        Stack<String> store=new Stack<String>();
        List<String> results = new ArrayList<String>();
        String input = request;

        while( input.length() > 0 ) {

            String subSeq;
            if( input.length() < MAX_LENGTH)
                subSeq = input;
            else 
                subSeq = input.substring(input.length() - MAX_LENGTH);

            while( subSeq.length() > 0 ) {
                if( dictionary.contains(subSeq) || subSeq.length() == 1) {
                    store.add(subSeq);
                    input = input.substring(0, input.length() - subSeq.length());
                    break;
                } else {
                    subSeq = subSeq.substring(1);
                }
            }
        }
        int size = store.size();
        for( int i = 0; i < size; i ++) {
            results.add(store.pop());
        }

        return results;
    }

    public List<String> segment() {
        List<String> fmm = this.leftMax();
        List<String> bmm = this.rightMax();

        if( fmm.size() != bmm.size()) {
            if ( fmm.size() > bmm.size())
                return bmm;
            else 
                return fmm;
        }

        else {
            int fmmSingle = 0, bmmSingle = 0;
            boolean isEqual = true;
            for( int i = 0; i < bmm.size(); i ++) {
                if( !fmm.get(i).equals(bmm.get(i))) {
                    isEqual = false;
                }
                if( fmm.get(i).length() == 1)
                    fmmSingle ++;
                if( bmm.get(i).length() == 1)
                    bmmSingle ++;
            }

            if ( isEqual ) {
                return fmm;
            } else if ( fmmSingle > bmmSingle)      
                return bmm;
            else 
                return fmm; 
        }
    }

    public void test(String str) {
        request = str;
        System.out.println(this.segment());
    }

    public static void main(String[] args) {
        MMSegment f = new MMSegment();
        f.getDictionary();
        f.test("研究生命科學");
        f.test("研究生命令本科生");
        f.test("我從馬上下來");
        f.test("北京大學生喝進口紅酒");
        f.test("美軍中將竟公然說");
        f.test("阿美首腦會議將討論巴以和平等問題");
        f.addWord("巴以和平");
        System.out.println("---------------------------");
        System.out.println("向字典中添加'巴以和平'後");
        f.test("阿美首腦會議將討論巴以和平等問題");
        f.test("我不想吃東西");
    }

}

參考資料

[1] http://blog.csdn.net/worldwindjp/article/details/18085725

[2] http://blog.csdn.net/hu948162999/article/details/43608107

[3] http://blog.csdn.net/xiaoyeyopulei/article/details/25194021

[4] http://blog.csdn.net/chenlei0630/article/details/40710441

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章