1. 程式人生 > >基於Tire樹和最大概率法的中文分詞功能的Java實現

基於Tire樹和最大概率法的中文分詞功能的Java實現

對於分詞系統的實現來說,主要應集中在兩方面的考慮上:一是對語料庫的組織,二是分詞策略的制訂。

1.   Tire樹

Tire樹,即字典樹,是通過字串的公共字首來對字串進行統計、排序及儲存的一種樹形結構。其具有如下三個性質:

1)      根節點不包含字元(或漢字),除根節點以外的每個節點只能包含一個字元(漢字)

2)      從根節點到任一節點的路徑上的所有節點中的字元(漢字)按順序排列的字串(片語)就是該節點所對應的字串(片語)

3)      每個節點的所有直接子節點包含的字元(漢字)各不相同

上述性質保證了從Tire樹中查詢任意字串(片語)所需要比較的次數儘可能最少,以達到快速搜尋語料庫的目的。

如下圖所示的是一個由片語集<一,一萬,一萬多,一萬元,一上午,一下午,一下子>生成的Tire樹的子樹:


可見,從子樹的根節點“一”開始,任意一條路徑都能組成一個以“一”開頭的片語。而在實際應用中,需要給每個節點附上一些資料屬性,如詞頻,因而可以用這些屬性來區別某條路徑上的字串是否是一個片語。如,節點“上”的詞頻為-1,那麼“一上”就不是一個片語。

如下的程式碼是Tire樹的Java實現:

package chn.seg;

import java.util.HashMap;
import java.util.Map;

public class TireNode {
	
	private String character;
	private int frequency = -1;
	private double antilog = -1;
	private Map<String, TireNode> children;
	
	public String getCharacter() {
		return character;
	}
	
	public void setCharacter(String character) {
		this.character = character;
	}
	
	public int getFrequency() {
		return frequency;
	}
	
	public void setFrequency(int frequency) {
		this.frequency = frequency;
	}
	
	public double getAntilog() {
		return antilog;
	}
	
	public void setAntilog(double antilog) {
		this.antilog = antilog;
	}
	
	public void addChild(TireNode node) {
		if (children == null) {
			children = new HashMap<String, TireNode>();
		}
		
		if (!children.containsKey(node.getCharacter())) {
			children.put(node.getCharacter(), node);
		}
	}
	
	public TireNode getChild(String ch) {
		if (children == null || !children.containsKey(ch)) {
			return null;
		}
		
		return children.get(ch);
	}
	
	public void removeChild(String ch) {
		if (children == null || !children.containsKey(ch)) {
			return;
		}
		
		children.remove(ch);
	}
}

2.   最大概率法(動態規劃)

最大概率法是中文分詞策略中的一種方法。相較於最大匹配法等策略而言,最大概率法更加準確,同時其實現也更為複雜。

基於動態規劃的最大概率法的核心思想是:對於任意一個語句,首先按語句中片語的出現順序列出所有在語料庫中出現過的片語;將上述片語集中的每一個詞作為一個頂點,加上開始與結束頂點,按構成語句的順序組織成有向圖;再為有向圖中每兩個直接相連的頂點間的路徑賦上權值,如A→B,則AB間的路徑權值為B的費用(若B為結束頂點,則權值為0);此時原問題就轉化成了單源最短路徑問題,通過動態規劃解出最優解即可。

如句子“今天下雨”,按順序在語料庫中存在的片語及其費用如下:

今,a

今天,b

天,c

天下,d

下,e

下雨,f

雨,g

則可以生成如下的加權有向圖:


顯而易見,從“Start”到“End”的單源路徑最優解就是“今天下雨”這個句子的分詞結果。

那麼,作為權值的費用如何計算呢?對於最大概率法來說,要求的是片語集在語料庫中出現的概率之乘積最大。對應單源最短路徑問題的費用來說,

費用 = log( 總詞頻 / 某一片語詞頻 )

通過上述公式就可以把“最大”問題化為“最小”問題,“乘積”問題化為“求和”問題進行求解了。

如下的程式碼是基於動態規劃的最大概率法的Java實現:

package chn.seg;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.List;

public class ChnSeq {
	private TireNode tire = null;

	public void init() throws IOException, ClassNotFoundException {		
		File file = new File("data" + File.separator + "dict.txt");
		if (!file.isFile()) {
			System.err.println("語料庫不存在!終止程式!");
			System.exit(0);
		}
		
		BufferedReader in = new BufferedReader(
				new InputStreamReader(new FileInputStream(file), "utf-8"));
		String line = in.readLine();
		int totalFreq = Integer.parseInt(line);
		
		tire = new TireNode();
		
		while ((line = in.readLine()) != null) {
			String[] segs = line.split(" ");
			String word = segs[0];
			int freq = Integer.parseInt(segs[1]);
			
			TireNode root = tire;
			for (int i = 0; i < word.length(); i++) {
				String c = "" + word.charAt(i);
				TireNode node = root.getChild(c);
				if (node == null) {
					node = new TireNode();
					node.setCharacter(c);
					root.addChild(node);
				}
				root = node;
			}
			
			root.setFrequency(freq);
			root.setAntilog(Math.log((double)totalFreq / freq));
		}
		in.close();
	}

	public TireNode getTire() {
		return tire;
	}
	
	public TireNode getNodeByWord(String word) {
		if (tire == null) {
			System.err.println("需要先初始化ChnSeq物件!");
			return null;
		}
		
		TireNode node = tire;
		for (int i = 0; i < word.length(); i++) {
			String ch = word.charAt(i) + "";
			if (node == null) {
				break;
			} else {
				node = node.getChild(ch);
			}
		}
		
		return node;
	}
	
	private class Segment {
		public String word;
		public String endChar;
		public String lastChar;
		public double cost;
		
		public final static String START_SIGN = "<< STARTING >>";
		public final static String END_SIGN = "<< ENDING >>";
	}
	
	private List<Segment> preSegment(String sentence) {
		List<Segment> segs = new ArrayList<Segment>();
		
		Segment terminal = new Segment();
		terminal.word = Segment.START_SIGN;
		terminal.endChar = Segment.START_SIGN;
		terminal.lastChar = null;
		segs.add(terminal);
		for (int i = 0; i < sentence.length(); i++) {
			for (int j = i + 1; j <= sentence.length(); j++) {
				String word = sentence.substring(i, j);
				TireNode tnode = this.getNodeByWord(word);
				if (tnode == null) {
					break;
				}
				if (tnode.getFrequency() <= 0) {
					continue;
				}
				
				Segment seg = new Segment();
				seg.word = word;
				seg.endChar = word.substring(word.length() - 1, word.length());
				if (i == 0) {
					seg.lastChar = Segment.START_SIGN;
				} else {
					seg.lastChar = sentence.substring(i - 1, i);
				}
				seg.cost = tnode.getAntilog();
				segs.add(seg);
			}
		}
		terminal = new Segment();
		terminal.word = Segment.END_SIGN;
		terminal.endChar = Segment.END_SIGN;
		terminal.lastChar = sentence.substring(sentence.length() - 1, sentence.length());
		segs.add(terminal);
		
		return segs;
	}
	
	private String[] dynamicSegment(List<Segment> segs) {
		final double INFINITE = 9999999;
		
		if (segs == null || segs.size() == 0) {
			return null;
		}
			
		int n = segs.size();
		
		double[][] costs = new double[n][n];
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				costs[i][j] = INFINITE;
			}
		}
		
		for (int i = 0; i < n; i++) {
			String endChar = segs.get(i).endChar;
			for (int j = 0; j < n; j++) {
				String lastChar = segs.get(j).lastChar;
				
				if (lastChar != null && lastChar.equals(endChar)) {
					costs[i][j] = segs.get(j).cost;
				}
			}
		}
		
		int sp = 0; // starting point
		int fp = n - 1; // finishing point
		
		double[] dist = new double[n];
		List<List<Integer>> sPaths = new ArrayList<List<Integer>>();
		List<Integer> list = new ArrayList<Integer>();
		for (int i = 0; i < n; i++) {
			dist[i] = costs[sp][i];
			if (sp != i) {
				list.add(i);
			}
			if (dist[i] < INFINITE) {
				List<Integer> spa = new ArrayList<Integer>();
				sPaths.add(spa);
			} else {
				sPaths.add(null);
			}
		}
		
		while (!list.isEmpty()) {
			Integer minIdx = list.get(0);
			for (int i: list) {
				if (dist[i] < dist[minIdx]) {
					minIdx = i;
				}
			}
			
			list.remove(minIdx);
			
			for (int i = 0; i < n; i++) {
				if (dist[i] > dist[minIdx] + costs[minIdx][i]) {
					dist[i] = dist[minIdx] + costs[minIdx][i];
					List<Integer> tmp = new ArrayList<Integer>(sPaths.get(minIdx));
					tmp.add(minIdx);
					sPaths.set(i, tmp);
				}
			}
		}
		
		String[] result = new String[sPaths.get(fp).size()];
		for (int i = 0; i < sPaths.get(fp).size(); i++) {
			result[i] = segs.get(sPaths.get(fp).get(i)).word;
		}
		return result;
	}
	
	public String[] segment(String sentence) {
		return dynamicSegment(preSegment(sentence));
	}
}

3.   測試程式碼

package chn.seg;

import java.io.IOException;

public class Main {

	public static void main(String[] args) throws ClassNotFoundException, IOException {
		ChnSeq cs = new ChnSeq();
		cs.init();

		String sentence = "生活的決定權也一直都在自己手上";
		
		String[] segs = cs.segment(sentence);
		for (String s: segs) {
			System.out.print(s + "\t");
		}
	}

}