隱馬爾科夫模型(HMM)分詞研究

時空霹靂發表於2014-07-28


第一部分 模型簡介

        隱馬爾可夫模型是馬爾可夫鏈的一種,它的狀態不能直接觀察到,但能通過觀測向量序列觀察到,每個觀測向量都是通過某些概率密度分佈表現為各種狀態,每一個觀測向量是由一個具有相應概率密度分佈的狀態序列產生。所以,隱馬爾可夫模型是一個雙重隨機過程 ----具有一定狀態數的隱馬爾可夫鏈和顯示隨機函式集。自20 世紀80年代以來,HMM被應用於語音識別,取得重大成功。到了90年代,HMM還被引入計算機文字識別和行動通訊核心技術“多使用者的檢測”。HMM在生物資訊科學、故障診斷等領域也開始得到應用。

        1.隱馬爾可夫模型(HMM)可以用一個五元組來描述,包括2個狀態集合和3個概率矩陣:
        (1)隱含狀態S集合
        這些狀態之間滿足馬爾可夫性質,是馬爾可夫模型中實際所隱含的狀態。這些狀態通常無法通過直接觀測而得到。(例如S1、S2、S3等等)
        (2)可觀測符號O集合
        在模型中與隱含狀態相關聯,可通過直接觀測而得到。(例如O1、O2、O3等等,可觀測狀態的數目不一定要和隱含狀態的數目一致。)
        (3)初始狀態概率矩陣 π
        表示隱含狀態在初始時刻t=1的概率矩陣,(例如t=1 時,P(S1)=p1、P(S2)=P2、P(S3)=p3,則初始狀態概率矩陣 π=[ p1 p2 p3 ].
        (4)隱含狀態轉移概率矩陣 A。
        描述了HMM模型中各個狀態之間的轉移概率。其中Aij = P( Sj | Si ),1≤i,,j≤N。表示在 t 時刻、狀態為 Si 的條件下,在 t+1 時刻狀態是 Sj 的概率。
        (5)觀測狀態轉移概率矩陣 B (英文名為Confusion Matrix,直譯為混淆矩陣不太易於從字面理解)。
        令N代表隱含狀態數目,M代表可觀測狀態數目,則:Bij = P( Oi | Sj ), 1≤i≤M,1≤j≤N.表示在 t 時刻、隱含狀態是 Sj 條件下,觀察狀態為 Oi 的概率。

        總結:一般的,可以用λ=(A,B,π)三元組來簡潔的表示一個隱馬爾可夫模型。隱馬爾可夫模型實際上是標準馬爾可夫模型的擴充套件,新增了可觀測狀態集合和這些狀態與隱含狀態之間的概率關係。


第二部分 基本問題

        1. 評估問題。
        給定觀測序列 O=O1O2O3…Ot和模型引數λ=(A,B,π),怎樣有效計算某一觀測序列的概率,進而可對該HMM做出相關評估。例如,已有一些模型引數各異的HMM,給定觀測序列O=O1O2O3…Ot,我們想知道哪個HMM模型最可能生成該觀測序列。通常我們利用forward 演算法分別計算每個HMM產生給定觀測序列O的概率,然後從中選出最優的HMM模型。
        這類評估的問題的一個經典例子是語音識別。在描述語言識別的隱馬爾科夫模型中,每個單詞生成一個對應的HMM,每個觀測序列由一個單詞的語音構成,單詞的識別是通過評估進而選出最有可能產生觀測序列所代表的讀音的HMM而實現的。


        2.解碼問題
        給定觀測序列 O=O1O2O3…Ot 和模型引數λ=(A,B,π),怎樣尋找某種意義上最優的隱狀態序列。在這類問題中,我們感興趣的是馬爾科夫模型中隱含狀態,這些狀態不能直接觀測但卻更具有價值,通常利用Viterbi演算法來尋找。
        這類問題的一個實際例子是中文分詞,即把一個句子如何劃分其構成才合適。例如,句子“發展中國家”是劃分成“發展-中-國家”,還是“發展-中國-家”。這個問題可以用隱馬爾科夫模型來解決。句子的分詞方法可以看成是隱含狀態,而句子則可以看成是給定的可觀測狀態,從而通過建HMM來尋找出最可能正確的分詞方法。


        3. 學習問題。
        即HMM的模型引數λ=(A,B,π)未知,如何調整這些引數以使觀測序列O=O1O2O3…Ot的概率儘可能的大。通常使用Baum- Welch演算法以及Reversed Viterbi演算法解決。
怎樣調整模型引數λ=(A,B,π),使觀測序列 O=O1O2O3…Ot的概率最大?


        4.針對每個問題,人們提出了相應的演算法:
        (1)評估問題: 前向演算法
        (2)解碼問題: Viterbi演算法
        (3)學習問題: Baum-Welch演算法(向前向後演算法)


第三部分 實驗結果

        對於二階馬爾科夫過程來說,由於訓練語料的規模所限,符號發射矩陣會存在資料稀疏的問題,因此要在程式中進行資料平滑處理。實驗選取了兩種方式進行資料平滑,其一是Good-Turing(古德-圖靈)平滑方法,其二為加一平滑方式。

        語料來源:《人民日報》1998年一月語料。

        下面是運用Good-Turing(古德-圖靈)平滑方法處理資料,最終獲得的結果為:


        下面是運用加一平滑方法(詞頻都加一)處理資料,最終獲得的結果為:


        從上面的結果中看出,加一平滑方法結果更好一些。


第四部分 原始碼

        (1)檔名:util.h。下面好幾個檔案都要用到該檔案,如將測試檔案中的/去掉。

#include <string>

using namespace std;

/*
 * 函式功能:將字串中的所有特定子串置換為新的字串
 * 函式輸入:str     需要進行操作的字串
 *         old_str 舊的字串
 *         new_str 新的字串
 * 函式輸出:置換完畢的字串
 */
string& replace_all(string &str, string old_str, string new_str){
	while(1){
		string::size_type pos(0);
		if((pos = str.find(old_str)) != string::npos){
			str.replace(pos, old_str.length(), new_str);
		}else{
			break;
		}
	}
	return str;
}


        (2)檔名:prehmm.cpp。對檔案進行預處理工作,函式的功能請參見程式碼中的註釋。

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <cstdlib>
#include <map>
#include "util.h"

using namespace std;


/*
 * 函式功能:將訓練語料和測試語料中出現的漢字進行編碼,將他們的對應關係存入檔案
 *         格式為:漢字-編碼,編碼從0開始
 * 函式輸入:infile_1 訓練語料檔名
 *         infile_2 測試語料檔名
 *         outfile  指定的輸出檔名
 * 函式輸出:名為outfile的檔案
 */
void makeDB(string infile_1, string infile_2, string outfile){
	//讀取輸入檔案
	ifstream fin_1(infile_1.c_str());
	ifstream fin_2(infile_2.c_str());
	if(!(fin_1 && fin_2)){
		cerr << "makeDB : Open input file fail !" << endl;
		exit(-1);
	}
	//開啟輸出檔案
	ofstream fout(outfile.c_str());
	if(!fout){
		cerr << "makeDB : Open output file fail !" << endl;
		exit(-1);
	}
	
	map<string, int> map_cchar;
	int id = -1;
	string line = "";
	string cchar = "";
	//讀取輸入檔案內容
	while(getline(fin_1, line)){
		line = replace_all(line, "/", "");
		if(line.size() >= 2){
			//逐字讀取
			for(int i = 0; i < line.size() - 1; i += 2){
				cchar = line.substr(i, 2);
				if(map_cchar.find(cchar) == map_cchar.end()){
					++id;
					map_cchar[cchar] = id;
				}
			}
		}
	}
	while(getline(fin_2, line)){
		line = replace_all(line, "/", "");
		if(line.size() >= 2){
			//逐字讀取
			for(int i = 0; i < line.size() - 1; i += 2){
				cchar = line.substr(i, 2);
				if(map_cchar.find(cchar) == map_cchar.end()){
					++id;
					map_cchar[cchar] = id;
				}
			}
		}
	}
	
	//輸出到檔案
	map<string, int>::iterator iter;
	for(iter = map_cchar.begin(); iter != map_cchar.end(); ++iter){
		//cout << iter -> first << " " << iter -> second << endl;
		fout << iter -> first << " " << iter -> second << endl;
	}

	fin_1.close();
	fin_2.close();
	fout.close();
}


/*
 * 函式功能:將訓練語料每個漢字後面加入對應的BMES狀態
 * 函式輸入:infile  訓練語料檔名
 *         outfile 指定的輸出檔名
 * 函式輸出:名為outfile的檔案
 */
void makeBMES(string infile, string outfile){

	ifstream fin(infile.c_str());
	ofstream fout(outfile.c_str());
	if(!(fin && fout)){
		cerr << "makeBMES : Open file failed !" << endl;
		exit(-1);
	}
	
	string word_in = "";
	string word_out = "";	
	string line_in = "";
	string line_out = "";

	while(getline(fin, line_in)){
		if(line_in.size() > 1){
			line_out.clear();
			line_in = replace_all(line_in, "/", " ");
			istringstream strstm(line_in);
			while(strstm >> word_in){
				word_out.clear();
				if(word_in.size()%2 != 0){
					cout << "單詞不符合要求:" << word_in << endl;
					continue;
				}
				int num = word_in.size()/2;	//單詞中包含多少個漢字
				if(num == 0){
					continue;
				}

				if(num == 1){
					word_out = word_in;
					word_out += "/S";
				}else{
					//複製單詞中的第一個字
					word_out.insert(word_out.size(), word_in, 0, 2);
					word_out += "/B";
					//逐個複製單詞中間的字
					for(int i = 1; i < num - 1; i++){
						word_out.insert(word_out.size(), word_in, 2*i, 2);
						word_out += "/M";
					}
					//複製單詞中最後的漢字
					word_out.insert(word_out.size(), word_in, 2*num - 2, 2);
					word_out += "/E";
				}

				line_out += word_out;
			}
			
			//cout << line_out << endl;
			fout << line_out << endl;
		}
	}

}


/*
 * 主函式
 */
int main(int argc, char *argv[]){
	if(argc < 5){
		cout << "Usage: " << argv[0] << " train_file test_file db_file bmes_file" << endl;
		exit(-1);
	}
	//構造DB檔案,輸入訓練語料、測試語料、輸出檔名
	makeDB(argv[1], argv[2], argv[3]);

	//構造BMES檔案,輸入訓練語料、輸出檔名
	makeBMES(argv[1], argv[4]);

}

        (3)檔名:db.h。將漢字和編碼的對映檔案記憶體,構造為map,供其他程式使用。

#include <iostream>
#include <fstream>
#include <map>
#include <cstdlib>

using namespace std;

/*
 * 轉換類,獲取編號
 */
class DB{
	private:
		map<string, int> cchar_map;	//漢字-編碼對映
		map<int, string> index_map;	//編碼-漢字對映
	public:
		DB();
		DB(string file);
		string getCchar(int id);		//根據編碼獲得漢字
		int getObservIndex(string cchar);	//根據漢字獲得編碼
		int getStateIndex(char state);		//根據狀態獲得狀態編號
};

//無參建構函式
DB::DB(){

}

//有參建構函式
DB::DB(string file){
	ifstream fin(file.c_str());
	if(!fin){
		cout << "Open input file fail ! Can't init Trans !" << endl;
		exit(-1);
	}
	string line = "";
	string word = "";
	string cchar = "";
	int id = 0;
	while(getline(fin, line)){
		istringstream strstm(line);
		strstm >> word;
		cchar = word;
		strstm >> word;
		id = atoi(word.c_str());
		//加入map
		cchar_map[cchar] = id;
		index_map[id] = cchar;
	}
	cout << "cchar_map大小: " << cchar_map.size() << endl;
	cout << "index_map大小: " << index_map.size() << endl;
}

//將狀態轉換為數字編號
int DB::getStateIndex(char state){
	switch(state){
		case 'B' :
			return 0;
			break;
		case 'M' :
			return 1;
			break;
		case 'E' :
			return 2;
			break;
		case 'S' :
			return 3;
			break;
		default :
			return -1;
			break;
	}
}

//將漢字轉換為數字編號
int DB::getObservIndex(string cchar){
	map<string, int>::iterator iter = cchar_map.find(cchar);
	if(iter != cchar_map.end()){
		return iter -> second;
	}else{
		return -1;
	}
}

//將數字編號轉換為漢字
string DB::getCchar(int id){
	map<int, string>::iterator iter = index_map.find(id);
	if(iter != index_map.end()){
		return iter -> second;
	}else{
		return NULL;
	}
}

        (4)檔名:matrix.cpp。用最大似然估計的方法建立HMM的模型引數。

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <iomanip>
#include <cmath>
#include <list>
#include "db.h"

using namespace std;

const int N = 4;		//隱藏狀態的數目
const int M = 5236;		//漢字的個數
const double VALUE = 1.0;	//平滑演算法增加的值


//定義字典物件
DB db("db.txt");


/*
 * 模型訓練,將頻數轉換為頻率(加1平滑)
 */
void turingAdd(const int count[], double prob[], int len){
	double sum = 0.0;
	for(int i = 0; i < len; ++i){
		sum += count[i];
	}
	if(sum == 0.0){
		for(int i = 0; i < len; ++i){
			prob[i] = 0.0;
		}
	}else{
		sum = sum + VALUE * len;
		for(int i = 0; i < len; ++i){
			prob[i] = -log((count[i] + VALUE) / sum);//取對數
		}
	}
}


/*
 * 模型訓練,將發射頻數轉換為頻率(古德-圖靈平滑)
 */
void turingGood(const int count[], double prob[], int len){
	map<int, list<int> > freq_map;			//key為詞頻,value為該詞頻對應的漢字列表
	map<int, list<int> >::iterator iter;		//迭代器
	int sum = 0;					//詞頻總和

	//初始化freq_map
	for(int i = 0; i < len; i++){
		int freq = count[i];			//詞頻
		sum += freq;

		iter = freq_map.find(freq);
		if(iter != freq_map.end()){
			//該詞頻已經存在,把當前詞加入相應的list
			freq_map[freq].push_back(i);
		}else{
			//該詞頻不存在,建立對應的漢字list
			list<int> lst;
			lst.push_back(i);
			freq_map[freq] = lst;
		}
	}

	//若sum=0,則結果初始化為0.0即可
	if(sum == 0){
		for(int i = 0; i < len; i++){
			prob[i] = 0.0;
		}
		return;
	}
	
	//資料平滑處理
	iter = freq_map.begin();
	while(iter != freq_map.end()){
		double pr;	//頻率
		int freq = iter -> first;
		int freqsize = iter -> second.size();
		if(++iter != freq_map.end()){
			int freq_2 = iter -> first;
			if(freq_2 = freq + 1){
				int freqsize_2 = iter -> second.size();
				pr = ((1.0 + freq) * freqsize_2) / (sum * freqsize);
			}else{
				pr = 1.0 * freq / sum;
			}
		}else{
			pr = 1.0 * freq / sum;
		}

		//計算結果
		list<int> lst = (--iter) -> second;
		list<int>::iterator iter_in = lst.begin();
		while(iter_in != lst.end()){
			int index = *iter_in;
			prob[index] = pr;
			++iter_in;
		}

		//準備下次迭代
		++iter;
	}

	//概率歸一化
	double total = 0.0;
	for(int i = 0; i < len; i++){
		total += prob[i];
	}
	for(int i = 0; i < len; i++){
		prob[i] = -log((double)prob[i] / total);//取對數
	}
}


/*
 * 主函式,生成HMM模型的引數
 * 狀態轉移概率矩陣、初始狀態概率矩陣、符號發射概率矩陣
 */
int main(int argc, char *argv[]){
	if(argc < 2){
		cout << "Usage: " << argv[0] << " bmes_file !" << endl;
		exit(-1);
	}

	ifstream fin(argv[1]);
	if(!fin){
		cerr << "Open input file " << argv[1] << "filed !" << endl;
		exit(-1);
	}

	int Pi[N] = {0};		//初始狀態出現的次數
	int A[N][N] = {0};		//狀態轉移的次數
	int B[N][M] = {0};		//符號發射次數

	//抽取檔案中的狀態和觀察值
	string line = "";			//存放每一行的內容
	int line_num = 0;			//句子編號
	while(getline(fin, line)){
		line_num++;
		char state;			//狀態
		string cchar = "";		//一個漢字
		int i, j, k;
		string::size_type pos = 0;	//當前處理位置
		if((pos = line.find("/", pos + 1)) != string::npos){
			//抽取句子的第一個狀態
			state = line.at(pos + 1);
			i = db.getStateIndex(state);
			Pi[i]++;
			//抽取句子的第一個觀察值
			cchar = line.substr(pos - 2, 2);
			k = db.getObservIndex(cchar);
			B[i][k]++;
			while((pos = line.find("/", pos + 1)) != string::npos){
				//抽取句子的其他狀態
				state = line.at(pos + 1);
				j = db.getStateIndex(state);
				//Pi[j]++;
				A[i][j]++;
				//抽取句子的其他觀察值
				cchar = line.substr(pos - 2, 2);
				k = db.getObservIndex(cchar);
				B[j][k]++;
				
				//準備下次迭代
				i = j;
			}
		}
	}
	fin.close();

	//開啟輸出流
	ofstream fout_1("Pi.mat");	//初始概率矩陣
	ofstream fout_2("A.mat");	//狀態轉移矩陣
	ofstream fout_3("B.mat");	//發射概率矩陣
	if(!(fout_1 && fout_2 && fout_3)){
		cerr << "Create Matrix file failed !" << endl;
		return 1;
	}

	fout_1 << setprecision(8);
	fout_2 << setprecision(8);
	fout_3 << setprecision(8);

	//初始狀態矩陣寫入檔案
	double arr_pi[N] = {0.0};
	//turingGood(Pi, arr_pi, N);
	turingAdd(Pi, arr_pi, N);
	for(int i = 0; i < N; i++){
		fout_1 << arr_pi[i] << "\t";
	}
	fout_1 << endl;

	//狀態轉移矩陣寫入檔案
	double arr_a[N] = {0.0};
	for(int i = 0; i < N; i++){
		//turingGood(A[i], arr_a, N);
		turingAdd(A[i], arr_a, N);
		for(int j = 0; j < N; j++){
			fout_2 << arr_a[j] << "\t";
		}
		fout_2 << endl;
	}
	
	//發射概率矩陣寫入檔案
	double arr_b[M] = {0.0};
	for(int i = 0; i < N; i++){
		//turingGood(B[i], arr_b, M);
		turingAdd(B[i], arr_b, M);
		for(int j = 0; j < M; j++){
			fout_3 << arr_b[j] << "\t";
		}
		fout_3 << endl;
	}

	fout_1.close();
	fout_2.close();
	fout_3.close();

	return 0;
}

        (5)檔名:hmm.h。將儲存在檔案中的HMM的模型引數讀取到記憶體中,構造為一個HMM物件,供其他程式使用。

#include <fstream>
#include <sstream>
#include <string>
#include <cstdlib>

const int N = 4;
const int M = 5236;

//定義HMM模型
class HMM{

	public:
		int n;		//狀態數目
		int m;		//可能的觀察符號數目
		double A[N][N];	//狀態轉移概率矩陣
		double B[N][M];	//符號發射概率矩陣
		double Pi[N];	//初始狀態概率
		HMM();
		HMM(string f1, string f2, string f3);
};

//無參建構函式
HMM::HMM(){

}

//有參建構函式
HMM::HMM(string f1, string f2, string f3){
	ifstream fin_1(f1.c_str());
	ifstream fin_2(f2.c_str());
	ifstream fin_3(f3.c_str());
	if(!(fin_1 && fin_2 && fin_3)){
		exit(-1);
	}

	string line = "";
	string word = "";

	//讀取Pi
	getline(fin_1, line);
	istringstream strstm_1(line);
	for(int i = 0; i < N; i++){
		strstm_1 >> word;
		Pi[i] = atof(word.c_str());
	}
	
	//讀取A
	for(int i = 0; i < N; i++){
		getline(fin_2, line);
		istringstream strstm_2(line);
		for(int j = 0; j < N; j++){
			strstm_2 >> word;
			A[i][j] = atof(word.c_str());
		}
	}

	//讀取B
	for(int i = 0; i < N; i++){
		getline(fin_3, line);
		istringstream strstm_3(line);
		for(int j = 0; j < M; j++){
			strstm_3 >> word;
			B[i][j] = atof(word.c_str());
		}
	}
	
	fin_1.close();
	fin_2.close();
	fin_3.close();
}

        (6)檔名:viterbi.cpp。維特比演算法,用於分詞。

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <stack>
#include "hmm.h"
#include "db.h"

using namespace std;

HMM hmm("Pi.mat", "A.mat", "B.mat");	//初始化HMM模型
DB db("db.txt");			//初始化字典

/*
 * Viterbi演算法進行分詞
 */
string viterbi(string str_in){
	string str_out = "";
	if(str_in.size() == 0){
		return str_out;
	}

	//分配矩陣空間
	int row = str_in.size() / 2;	//輸入句子中的漢字個數

	double **delta = new double *[row];
	for(int i = 0; i < row; i++){
		delta[i] = new double[N]();
	}

	int **path = new int *[row];
	for(int i = 0; i < row; i++){
		path[i] = new int[N]();
	}

	//中間變數
	string cchar = "";	//存放漢字
	int max_path = -1;
	double val = 0.0;
	double max_val = 0.0;

	//初始化矩陣,給delta和path矩陣的第一行賦初值
	cchar = str_in.substr(0, 2);
	int cchar_num = db.getObservIndex(cchar);
	for(int i = 0; i < N; i++){
		delta[0][i] = hmm.Pi[i] + hmm.B[i][cchar_num];	//對數
		path[0][i] = -1;
	}

	//給delta和path的後續行賦值(對數)
	for(int t = 1; t < row; t++){
		cchar = str_in.substr(2*t, 2);
		cchar_num = db.getObservIndex(cchar);
		for(int j = 0; j < N; j++){
			max_val = 100000.0;
			//max_path = -1;
			max_path = 0;
			for(int i = 0; i < N; i++){
				val = delta[t-1][i] + hmm.A[i][j];
				if(val < max_val){
					max_val = val;
					max_path = i;
				}
			}

			delta[t][j] = max_val + hmm.B[j][cchar_num];
			path[t][j] = max_path;
		}
	}

	//找delta矩陣最後一行的最大值
	max_val = 100000.0;
	//max_path = -1;
	max_path = 0;
	for(int i = 0; i < N; i++){
		if(delta[row-1][i] < max_val){
			max_val = delta[row-1][i];
			max_path = i;
		}
	}

	//從max_path出發,回溯得到最可能的路徑
	stack<int> path_st;
	path_st.push(max_path);
	for(int i = row - 1; i > 0; i--){
		max_path = path[i][max_path];
		path_st.push(max_path);
	}
	
	//釋放二維陣列
	for(int i = 0; i < row; i++){
		delete []delta[i];
		delete []path[i];
	}
	delete []delta;
	delete []path;

	//根據標記好的狀態序列分詞
	int pos = 0;
	int index = -1;
	while(!path_st.empty()){
		index = path_st.top();
		path_st.pop();
		str_out.insert(str_out.size(), str_in, pos, 2);
		if(index == 2 || index == 3){
			//狀態為E或S
			str_out.append("/");
		}
		pos += 2;
	}
}


        (7)檔名:main.cpp。主函式,呼叫維特比演算法進行分詞工作,並對分詞結果進行比對,統計後輸出結果。

#include <cstdlib>
#include <vector>
#include <iomanip>
#include <map>
#include <algorithm>
#include <sys/time.h>
#include <sys/stat.h>
#include "util.h"
#include "viterbi.cpp"


const long MaxCount = 50000;	//需要切分的最大句子數量,若該值大於檔案中
				//實際的句子數量,以實際句子數量為準。

//獲取當前時間(ms)
long getCurrentTime(){
	struct timeval tv;
	gettimeofday(&tv, NULL);
	return tv.tv_sec*1000 + tv.tv_usec/1000;
}

//獲取檔案大小
unsigned long getFileSize(string file_path){
	unsigned long filesize = -1;
	struct stat statbuff;
	if(stat(file_path.c_str(), &statbuff) < 0){
		return filesize;
	}else{
		filesize = statbuff.st_size;
	}
		return filesize;
}


/*
 * 函式功能:計算切分標記的位置
 * 函式輸入:1.strline_in未進行切分的漢字字串
           2.strline_right進行切分後的漢字字串
 * 函式輸出:vecetor,其中存放了strline_in中哪些位置放置了分詞標記
 *         注意:vector中不包含最後標記的位置,但是包含位置0。
 */
vector<int> getPos(string strline_right, string strline_in){
	int pos_1 = 0;
	int pos_2 = -1;
	int pos_3 = 0;
	string word = "";
	vector<int> vec;

	int length = strline_right.length();
	while(pos_2 < length){
		//前面的分詞標記
		pos_1 = pos_2;
		
		//後面的分詞標記
		pos_2 = strline_right.find('/', pos_1 + 1);

		if(pos_2 > pos_1){
			//將兩個分詞標記之間的單詞取出
			word  = strline_right.substr(pos_1 + 1, pos_2 - pos_1 - 1);
			//根據單詞去輸入序列中查出出現的位置
			pos_3 = strline_in.find(word, pos_3);
			//將位置存入陣列
			vec.push_back(pos_3);
			pos_3 = pos_3 + word.size();
		}else{
			break;
		}
	}
	
	return vec;
}


/*
 * 獲取標準切分和程式切分的結果
 */
string getString(string word, int pos, vector<int> vec_right){
	char ss[1000];
	int i = 0;
	int k = 0;
	if(vec_right.size() == 0){
		return word;
	}
	while(vec_right[i] < pos){
		i++;
	}
	for(int j = 0; j < word.size(); j++){
		if(j == vec_right[i] - pos){
			if(j != 0){
				ss[k] = '/';
				++k;
			}
			++i;
		}
		ss[k] = word[j];
		++k;
	}
	ss[k] = '\0';
	string word_str = ss;

	return word_str;
}

/*
 * 函式功能:獲取單個句子切分的結果統計
 * 函式輸入:1.vec_right 正確的分詞標記位置集合
 *           2.vec_out   函式切分得到的分詞標記位置集合
 * 函式輸出:返回一個veceor,含有4個元素,分別為:
 *          切分正確、組合型歧義、未登入詞、交集型歧義的數量
 *
 */
vector<int> getCount_2(string strline, vector<int> vec_right, vector<int> vec_out, vector<string> &vec_err){
	vector<int> vec(4, 0);	//存放計算結果
	//建立map
	map<int, int> map_result;
	for(int i = 0; i < vec_right.size(); i++){
		map_result[vec_right[i]] += 1;
	}
	for(int i = 0; i < vec_out.size(); i++){
		map_result[vec_out[i]] += 2;
	}

	//統計map中的資訊
	//若value=1,只在vec_right中
	//若value=2,只在vec_out中
	//若value=3,在vec_right和vec_out中都有
	map<int, int>::iterator p_pre, p_cur;
	int count_value_1 = 0;
	int count_value_2 = 0;
	int count_value_3 = 0;
	p_pre = map_result.begin();
	p_cur = map_result.begin();
	while(p_cur != map_result.end()){
		while(p_cur != map_result.end() && p_cur -> second == 3){
			p_pre = p_cur;
			++count_value_3;	//切分正確的數目
			++p_cur;		//迭代器後移
		}
		
		while(p_cur != map_result.end() && p_cur -> second != 3){
			if(p_cur -> second == 1){
				++count_value_1;
			}else if(p_cur -> second == 2){
				++count_value_2;
			}
			++p_cur;
		}
		
		//確定切分錯誤的字串
		if(p_cur == map_result.end() && p_cur == (++p_pre)){
			continue;
		}
		int pos_1 = p_pre -> first;
		int pos_2 = p_cur -> first; 
		string word = strline.substr(pos_1, pos_2 - pos_1);	//切分錯誤的單詞
		string word_right = getString(word, pos_1, vec_right);	//正確的切分方式
		string word_out = getString(word, pos_1, vec_out);	//得到的切分方式
 
		string str_err = "";
		//不同的錯誤型別		
		if(count_value_1 > 0 && count_value_2 == 0){
			str_err = "  組合型歧義: " + word + "    正確切分: " + word_right + "    錯誤切分: " + word_out;
			vec_err.push_back(str_err);
			cout << str_err << endl;
			vec[1] += count_value_1;		
		}else if(count_value_1 == 0 && count_value_2 > 0){
			str_err = "  未登入詞語: " + word + "    正確切分: " + word_right + "    錯誤切分: " + word_out;
			vec_err.push_back(str_err);
			cout << str_err << endl;
			vec[2] += count_value_2;
		}else if(count_value_1 > 0 && count_value_2 > 0){
			str_err = "  交集型歧義: " + word + "    正確切分: " + word_right + "    錯誤切分: " + word_out;
			vec_err.push_back(str_err);
			cout << str_err << endl;
			vec[3] += count_value_2;	
		}

		//計數器復位
		count_value_1 = 0;
		count_value_2 = 0;
	}

	vec[0] += count_value_3;	

	return vec;
}


/*
 * 主函式:進行分詞並統計分詞結果
 *
 */
int main(int argc, char *argv[]){
	if(argc < 3){
		cout << "Usage: " << argv[0] << " test_file result_file" << endl;
		exit(-1);
	}

	long time_1 = getCurrentTime();
	
	string strline_right;	//輸入語料:用作標準分詞結果
	string strline_in;	//去掉分詞標記的語料(用作分詞的輸入)
	string strline_out_1;	//隱馬爾科夫模型分詞完畢的語料
	
	ifstream fin(argv[1]);	//開啟輸入檔案
	if(!fin){
		cout << "Unable to open input file !" << argv[1] << endl;
		exit(-1);
	}

	ofstream fout(argv[2]);	//確定輸出檔案
	if(!fout){
		cout << "Unable to open output file !" << endl;
		exit(-1);
	}
	
	long count = 0;			//句子編號
	long count_1 = 0;		//隱馬爾科夫模型切分完全正確的句子總數
	long count_right_all = 0;	//準確的切分總數
	long count_out_1_all = 0;	//隱馬爾科夫模型切分總數
	long count_out_1_right_all = 0;	//隱馬爾科夫模型切分正確總數
	long count_out_1_fail_1_all = 0;//隱馬爾科夫模型(組合型歧義)
	long count_out_1_fail_2_all = 0;//隱馬爾科夫模型(未登入詞語)
	long count_out_1_fail_3_all = 0;//隱馬爾科夫模型(交集型歧義)


	vector<string> vec_err_1;	//隱馬爾科夫模型切分錯誤的詞

	while(getline(fin, strline_right, '\n') && count < MaxCount){
		if(strline_right.length() > 1){
			
			//去掉分詞標記
			strline_in = strline_right;
			strline_in = replace_all(strline_in, "/", "");

			//隱馬爾科夫模型分詞
			strline_out_1 = strline_right;
			istringstream strstm(strline_in);
			string sentence;
			string result;
			string line_out;
			while(strstm >> sentence){
				result = viterbi(sentence);
				line_out += result;
			}
			strline_out_1 = line_out;

			//輸出分詞結果
			count++;
			cout << "----------------------------------------------" << endl;
			cout << "句子編號:" << count << endl;
			cout << endl;
			cout << "待分詞的句子長度: " << strline_in.length() << "  句子:" << endl;
			cout << strline_in << endl;
			cout << endl;
			cout << "標準比對結果長度: " << strline_right.length() << "  句子:" << endl;
			cout << strline_right << endl;
			cout << endl;
			cout << "隱馬爾科夫模型分詞長度: " << strline_out_1.length() << "  句子:" << endl;
			cout << strline_out_1 << endl;
			cout << endl;
			

			//輸出分詞結果的數字序列表示
			vector<int> vec_right = getPos(strline_right, strline_in);
			vector<int> vec_out_1 = getPos(strline_out_1, strline_in);
			
			cout << "標準結果:" << endl;
			for(int i = 0; i < vec_right.size(); i++){
				cout << setw(4) << vec_right[i];
			}
			cout << endl;

			cout << "隱馬爾科夫模型分詞結果:" << endl;
			for(int i = 0; i < vec_out_1.size(); i++){
				cout << setw(4) << vec_out_1[i];
			}
			cout << endl;
			

			//輸出匹配的錯誤列表
			cout << endl;
			if(vec_right == vec_out_1){
				cout << "隱馬爾科夫模型分詞完全正確!" << endl;
				count_1++;
			}else{
				cout << "隱馬爾科夫模型分詞錯誤列表:" << endl;
			}
			vector<int> vec_count_1 = getCount_2(strline_in, vec_right, vec_out_1, vec_err_1);

						
			//準確的切分數量
			int count_right = vec_right.size();
			//切分得到的數量
			int count_out_1 = vec_out_1.size();			
			//切分正確的數量
			int count_out_1_right = vec_count_1[0];
			cout << "切分得到:" << count_out_1 << endl;
			cout << "切分正確:" << count_out_1_right << endl;			

			cout << "隱馬爾科夫模型:" << endl;	
			cout << "  組合型歧義:" << vec_count_1[1] << endl;
			cout << "  未登入詞語:" << vec_count_1[2] << endl;
			cout << "  交集型歧義:" << vec_count_1[3] << endl;
			
			
			count_right_all += count_right;
			count_out_1_all += count_out_1;
			count_out_1_right_all += count_out_1_right;
			count_out_1_fail_1_all += vec_count_1[1];
			count_out_1_fail_2_all += vec_count_1[2];
			count_out_1_fail_3_all += vec_count_1[3];
			
			
		}
	}
	
	long time_2 = getCurrentTime();
	unsigned long file_size = getFileSize("test.txt");


	//列印錯誤的切分內容	
	cout << endl;
	cout << "---------------------------------" << endl;
	cout << "錯誤樣例(已排序):" << endl;


	//對錯誤切分內容進行排序並掉重複的
	sort(vec_err_1.begin(), vec_err_1.end());
	
	vector<string>::iterator end_unique_1 = unique(vec_err_1.begin(), vec_err_1.end());
	

	int num_1 = end_unique_1 - vec_err_1.begin();
	

	cout << "----------------------------------" << endl;
	cout << "隱馬爾科夫模型切分錯誤數量:" << num_1 << endl;
	for(int i = 0; i < num_1; i++){
		cout << vec_err_1[i] << endl;
	}
	cout << endl;

	

	//計算準確率和召回率
	double kk_1 = (double)count_out_1_right_all / count_out_1_all;	//隱馬爾科夫模型準確率
	double kk_2 = (double)count_out_1_right_all / count_right_all;	//隱馬爾科夫模型召回率
	

	//集中輸出結果
	cout << endl;
	cout << "---------------------------------" << endl;
	cout << "分詞消耗時間:" << time_2 - time_1 << "ms" << endl;
	cout << "測試檔案大小:" << file_size/1024 << " KB" << endl;
	cout << "分詞速度為:  " << (double)file_size*1000/((time_2 - time_1)*1024) << " KB/s" << endl;

	

	cout << endl;
	cout << "句子總數:" << count << endl;
	
	cout << "隱馬爾科夫模型切分完全正確的句子數目: " << count_1 << "\t ( " << (double)count_1*100/count << " % )" << endl;
	
	cout << endl;

	cout << "準確的切分總數:" << count_right_all << endl;		//準確的切分總數
	cout << "隱馬爾科夫模型切分總數:" << count_out_1_all << endl;		//隱馬爾科夫模型切分總數	
	cout << "隱馬爾科夫模型切分正確總數:" << count_out_1_right_all << endl;	//隱馬爾科夫模型切分正確總數
	

	cout << endl;
	cout << "隱馬爾科夫模型:" << endl;
	long count_out_1_fail_all = count_out_1_fail_1_all + count_out_1_fail_2_all + count_out_1_fail_3_all;	
	cout << "  組合型歧義:" << count_out_1_fail_1_all << "\t ( " << (double)count_out_1_fail_1_all*100/count_out_1_fail_all << " % )" << endl;
	cout << "  未登入詞語:" << count_out_1_fail_2_all << "\t ( " << (double)count_out_1_fail_2_all*100/count_out_1_fail_all << " % )" << endl;
	cout << "  交集型歧義:" << count_out_1_fail_3_all << "\t ( " << (double)count_out_1_fail_3_all*100/count_out_1_fail_all << " % )" << endl;
	
	cout << endl;		
	cout << "統計結果:" << endl;
	cout << "隱馬爾科夫模型    準確率:" << kk_1*100 << "%  \t召回率:" << kk_2*100 << "%" << endl;
	

	return 0;
}



相關文章