用正向和逆向最大匹配算法进行中文分词

原创 2014年07月14日 22:22:33

1.概述

        用正向和逆向最大匹配算法进行中文分词。

2.遇到的问题

        编码问题,Linux默认的编码是UTF-8编码,对于汉字,每个字占三个字节。而本文使用的语料为1998年1月的人民日报语料,为GB2312编码,每个汉字占两个字节。

        本文所用的Ubuntu Linux操作系统默认是不支持GB2312等中文编码的,因此需要对系统添加GB2312编码的支持。添加方式参见:

3.分词结果

        分词结果如下图所示:


        从图中可以看出,逆向最大匹配分词的准确率和召回率均大于正向最大匹配分词方法,但是幅度相差不是很大。

4.源代码

        源代码分为三个文件:

        segmentutil.cpp(对语料进行预处理),它是单独运行的,可以将原始语料制作成词典文件和测试文件。

        dictionary.h(词典头文件,初始化词典)和main.cpp(进行分词操作)这两个文件需要一起编译运行。可以对测试文件进行分词,该过程需要用到前面文件生成的词典文件和测试文件。

        (1)segmentutil.cpp(对语料进行预处理)

#include <string>
#include <iostream>
#include <fstream>
#include <cstdlib>

using namespace std;

/*
 * 工具类
 * 功能:进行各种字符串操作、语料预处理操作
 *      含有2个字符串操作的函数 
 *      含有5个语料预处理相关的函数 
 */
class SegmentUtil{
	public:
		string removeLetters(string str_in);	//去掉字符串中的英文字母
		string& replace_all(string &str, string old_str, string new_str);	//置换字符串的特定字串
		void removeNum();			//去掉语料中的编号
		void spareLine();			//将语料进行分行
		void spareFile();			//将语料分为词典和测试语料
		void makeDict();			//构造词典
		void makeDict_2();			//构造词典
};



/*
 * 函数功能:删除词性标记(即去掉字符串中的英文字母)
 * 函数输入:含有词性标记的字符串
 * 函数输出:不含词性标记的字符串
 */
string SegmentUtil::removeLetters(string str_in){
	char s[10000];
	int j = 0;
	for(int i = 0; i < str_in.length(); i++){
		if(!(str_in[i] >= 'a' && str_in[i] <= 'z' || str_in[i] >= 'A' && str_in[i] <= 'Z')){
			s[j] = str_in[i];
			j++;	
		}
	}
	s[j] = '\0';
	string str_out = s;
	return str_out;
}


/*
 * 函数功能:将字符串中的所有特定子串置换为新的字符串
 * 函数输入:str     需要进行操作的字符串
 *         old_str 旧的字符串
 *         new_str 新的字符串
 * 函数输出:置换完毕的字符串
 */
string& SegmentUtil::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;
}




/*
 * 函数功能:去掉语料中每各段落前的编号
 * 函数输入:待处理的文件
 * 函数输出:处理完的文件
 */
void SegmentUtil::removeNum(){
	ifstream fin("199801_1.txt");
	if(!fin){
		cerr << "removeNum : Unable open input file !" << endl;
		exit(-1);
	}
	
	ofstream fout("199801_2.txt");
	if(!fout){
		cerr << "removeNum : Unable open output file !" << endl;
		exit(-1);
	}

	string str_in = "";
	string str_out = "";
	while(getline(fin, str_in, '\n')){
		if(str_in.find('/') == 18){
			str_out = str_in.substr(22, str_in.length() - 22);
		}
		fout << str_out << endl;
	}
	
	fin.close();
	fout.close();
}


/*
 * 函数功能:将语料进行分行
 * 函数输入:待处理的文件,文件中多个句子在一个段落中,每个段落为一行
 * 函数输出:处理完的文件,每个句子为一行
 */
void SegmentUtil::spareLine(){
	ifstream fin("199801_2.txt");
	if(!fin){
		cerr << "makeLine : Unable open input file !" << endl;
		exit(-1);
	}
	
	ofstream fout("199801_3.txt");
	if(!fout){
		cerr << "makeLine : Unable open output file !" << endl;
		exit(-1);
	}

	string str_in = "";
	string str_out = "";
	while(getline(fin, str_in, '\n')){
		if(str_in.find('/') == 18){
			str_out = str_in.substr(22, str_in.length() - 22);
		}
		fout << str_out << endl;
	}
	
	fin.close();
	fout.close();
}


/*
 * 函数功能:按照一定的比例,将原始语料分为词典语料和测试语料,默认比例为9:1。
 * 函数输入:分好行的语料文件,每个句子为一行
 * 函数输出:两个文件,文件1用于构造词典,文件2用于测试
 */
void SegmentUtil::spareFile(){
	ifstream fin("199801_003.txt");
	if(!fin){
		cerr << "spareLine : Unable open input file !" << endl;
		exit(-1);
	}
	
	ofstream fout_1("dict_1.txt");
	if(!fout_1){
		cerr << "spareLine : Unable open output file : dict.txt !" << endl;
		exit(-1);
	}

	ofstream fout_2("test.txt");
	if(!fout_2){
		cerr << "spareLine : Unable open output file : test.txt !" << endl;
		exit(-1);
	}
	
	long count = 0;
	string str_in = "";
	string str_out = "";
	while(getline(fin, str_in, '\n')){
		//过滤掉词性标记,即英文字母
		str_out = removeLetters(str_in);
		//以句子为单位,将语料按比例分为两个文件
		if(count % 10 == 0){
			fout_2 << str_out << endl;
		}else{
			fout_1 << str_out << endl;
		}
		count++;
	}
	
	fin.close();
	fout_1.close();
	fout_2.close();
}


/*
 * 函数功能:构造词典,使每个词语为一行
 * 函数输入:分好行的语料文件,每个句子为一行
 * 函数输出:初步的词典文件,每个词语为一行,但可能有空行
 */
void SegmentUtil::makeDict(){
	ifstream fin("dict_1.txt");
	if(!fin){
		cerr << "makeDict : Unable open input file !" << endl;
		exit(-1);
	}
	
	ofstream fout("dict_2.txt");
	if(!fout){
		cerr << "makeDict : Unable open output file !" << endl;
		exit(-1);
	}

	string line = "";

	while(getline(fin, line, '\n')){
		//将分词标记(/)和中文标点置换为回车
		for(int i = 0; i < line.length(); i++){
			unsigned char ch = (unsigned char) line[i];
			if(ch == '/'){
				line[i] = '\n';	
			}
		}
		line = replace_all(line, ",", "\n");
		line = replace_all(line, "。", "\n");
		line = replace_all(line, "?", "\n");
		line = replace_all(line, "!", "\n");
		line = replace_all(line, "《", "\n");
		line = replace_all(line, "》", "\n");
		line = replace_all(line, "”", "\n");
		line = replace_all(line, "“", "\n");
		line = replace_all(line, ":", "\n");
		line = replace_all(line, "、", "\n");
		line = replace_all(line, "(", "\n");
		line = replace_all(line, ")", "\n");
		line = replace_all(line, "[", "\n");
		line = replace_all(line, "]", "\n");
		fout << line << endl;
	}
	
	fin.close();
	fout.close();
}


/*
 * 函数功能:构造词典,使每个词语为一行(去掉词典中的空行)
 * 函数输入:初步的词典文件,每个词语为一行,但可能有空行
 * 函数输出:最终的语料文件,每个词语为一行,去掉了空行
 */
void SegmentUtil::makeDict_2(){
	ifstream fin("dict_2.txt");
	if(!fin){
		cerr << "makeDict_2 : Unable open input file !" << endl;
		exit(-1);
	}
	
	ofstream fout("dict_3.txt");
	if(!fout){
		cerr << "makeDict_2 : Unable open output file !" << endl;
		exit(-1);
	}

	string line = "";
	//去掉空行
	while(getline(fin, line, '\n')){
		if(!line.empty()){
			fout << line << endl;
		}
	}

	fin.close();
	fout.close();
}

int main(){
	SegmentUtil seg;
	//1.将原始语料分为词典语料和测试语料
	seg.spareFile();
	//2.构造词典
	seg.makeDict();
	seg.makeDict_2();
}


        (2)dictionary.h(词典头文件,初始化词典)

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

using namespace std;

class Dictionary{
	private:
		string strline;		//保存每行内容
		string word;		//保存一个词语
		set<string> word_set;	//词典,用集合表示
	
	public:
		Dictionary();		//构造函数,初始化词典
		~Dictionary();
		int findWord(string word);	//在词典中查找特定的词语
};

Dictionary::Dictionary(){
	//读取词典文件
	fstream fin("dict_3.txt");
	if(!fin){
		cerr << "open file error !" << endl;
		exit(-1);
	}
	//将每个词语加入集合
	while(getline(fin, strline, '\n')){
		istringstream istr(strline);
		istr >> word;		//
		word_set.insert(word);	//
	}
}

Dictionary::~Dictionary(){
	
}

int Dictionary::findWord(string word){
	if(word_set.find(word) != word_set.end()){
		return 1;
	} else {
		return 0;
	}
}


        (3)main.cpp(进行分词操作)

#include <cstdlib>
#include "dictionary.h"
#include <vector>
#include <iomanip>
#include <map>

const int MaxWordLength = 10;	//最大词长为10个字节(即5个汉字)
const char Separator = '/';	//词界标记

Dictionary word_dict;		//初始化一个词典


/*
 * 函数功能:对字符串用最大匹配算法(正向)处理
 * 函数输入:汉字字符串
 * 函数输出:分好词的字符串
 */
string SegmentSentence_1(string s1){
	string s2 = "";		//用s2存放分词结果
	
	while(!s1.empty()){
		int len = s1.length();	//取输入串长度
		if(len > MaxWordLength){
			len = MaxWordLength;	//只在最大词长范围内进行处理
		}
		
		string w = s1.substr(0, len);
		int n = word_dict.findWord(w);	//在词典中查找相应的词
		while(len > 2 && n == 0){
			len -= 2;	//从候选词右边减掉一个汉字,将剩下的部分作为候选词
			w = s1.substr(0, len);
			n = word_dict.findWord(w);
		}

		s2 = s2 + w + Separator;
		s1 = s1.substr(w.length(), s1.length() - w.length());
	}
	
	return s2;
}


/*
 * 函数功能:对字符串用最大匹配算法(逆向)处理
 * 函数输入:汉字字符串
 * 函数输出:分好词的字符串
 */
string SegmentSentence_2(string s1){
	string s2 = "";		//用s2存放分词结果
	
	while(!s1.empty()){
		int len = s1.length();	//取输入串长度
		if(len > MaxWordLength){
			len = MaxWordLength;	//只在最大词长范围内进行处理
		}
		
		string w = s1.substr(s1.length() - len, len);
		int n = word_dict.findWord(w);	//在词典中查找相应的词
		while(len > 2 && n == 0){
			len -= 2;	//从候选词左边减掉一个汉字,将剩下的部分作为候选词
			w = s1.substr(s1.length() - len, len);
			n = word_dict.findWord(w);
		}

		w = w + Separator;
		s2 = w + s2;
		s1 = s1.substr(0, s1.length() - len);
	}
	
	return s2;
}


/*
 * 函数功能:对句子进行最大匹配法处理,包含对特殊字符的处理
 * 函数输入:1.含有汉字、英文符号的字符串
 *         2.flag=1调用正向最大匹配算法,flag=2调用逆向最大匹配算法
 * 函数输出:分好词的字符串
 */
string SegmentSentenceMM(string s1, int flag){
	string s2 = "";	//用s2存放分词结果
	int i;
	int dd;
	while(!s1.empty()){
		unsigned char ch = (unsigned char)s1[0];
		if(ch < 128){
			//处理西文字符
			i = 1;
			dd = s1.length();

			while(i < dd && ((unsigned char)s1[i] < 128) && (s1[i] != 10) && (s1[i] != 13)){
				//s1[i]不能是换行符或回车符
				i++;
			}//中止循环条件:出现中文字符、换行或者回车

			if(i == 1 && (ch == 10 || ch == 13)){
				//如果是换行或回车符,将它拷贝给s2输出
				s2 += s1.substr(0, i);
			}else{
				s2 += s1.substr(0, i) + Separator;
			}
			
			s1 = s1.substr(i, dd);
			continue;
		}else{
			if(ch < 176){
				//中文标点等非汉字字符
				i = 0;
				dd = s1.length();
			
				//获取中文双字节特殊字符(非汉字、非中文标点),中止循环条件:超过长度、出现中文标点符号、出现汉字
				while(i < dd && ((unsigned char)s1[i] < 176) && ((unsigned char)s1[i] >= 161)
					&& (!((unsigned char)s1[i] == 161 && ((unsigned char)s1[i+1] >= 162 && (unsigned char)s1[i+1] <= 168)))
					&& (!((unsigned char)s1[i] == 161 && ((unsigned char)s1[i+1] >= 171 && (unsigned char)s1[i+1] <= 191)))
					&& (!((unsigned char)s1[i] == 163 && ((unsigned char)s1[i+1] == 161 || (unsigned char)s1[i+1] == 168
					||   (unsigned char)s1[i+1] == 169 || (unsigned char)s1[i+1] == 172 || (unsigned char)s1[i+1] == 186 
					||   (unsigned char)s1[i+1] == 187 || (unsigned char)s1[i+1] == 191)))){
					//假定没有半个汉字
					i = i + 2;
				}
				
				//出现中文标点
				if(i == 0){
					i = i + 2;
				}

				//中文标点每个加一个分词标记;其他非汉字双字节字符连续输出,只加一个分词标记
				s2 += s1.substr(0, i) + Separator;
				

				s1 = s1.substr(i, dd);
				continue;
			}
		}
		
		//以下处理汉字串
		i = 2;
		dd = s1.length();
		while(i < dd && (unsigned char)s1[i] >= 176){
			i += 2;
		}

		if(flag == 1){
			//调用正向最大匹配
			s2 += SegmentSentence_1(s1.substr(0, i));
		}else{
			//调用逆向最大匹配
			s2 += SegmentSentence_2(s1.substr(0, i));
		}

		s1 = s1.substr(i, dd); 
	}

	return s2;
}


/*
 * 函数功能:删除分词标记(即去掉字符串中的/)
 * 函数输入:含有分词标记的字符串
 * 函数输出:不含分词标记的字符串
 */
string removeSeparator(string str_in){
	char s[10000];
	int j = 0;
	for(int i = 0; i < str_in.length(); i++){
		if(!(str_in[i] == '/')){
			s[j] = str_in[i];
			j++;
		}
	}
	s[j] = '\0';
	string str_out = s;
	return str_out;
}


/*
 * 函数功能:计算切分标记的位置
 * 函数输入: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;
}


/*
 * 函数功能:获取单个句子切分的结果统计
 * 函数输入:1.vec_right 正确的分词标记位置集合
 *         2.vec_out   函数切分得到的分词标记位置集合
 * 函数输出:返回一个veceor,含有两个元素
 *         1.不该切分而切分的数量
 *         2.该切分而未切分的数量
 */
vector<int> getCount(vector<int> vec_right, vector<int> vec_out){
	vector<int> vec;	//存放计算结果
	map<int, int> map_result;
	int length_1 = 0;	//map改变前的长度
	int length_2 = 0;	//map改变后的长度
	int count_1 = 0;	//不该切分而切分的数量
	int count_2 = 0;	//该切分而未切分的数量

	for(int i = 0; i < vec_right.size(); i++){
		map_result[vec_right[i]] = 0;
	}
	length_1 = map_result.size();

	for(int i = 0; i < vec_out.size(); i++){
		++map_result[vec_out[i]];
	}
	length_2 = map_result.size();

	count_1 = length_2 - length_1;

	for(int i = 0; i < vec_right.size(); i++){
		if(map_result[vec_right[i]] == 0){
			++count_2;
		}
	}
	
	vec.push_back(count_1);
	vec.push_back(count_2);
	return vec;
}

/*
 * 主函数:进行分词并统计分词结果
 *
 *
 *
 */
int main(int argc, char *argv[]){

	string strline_right;	//输入语料:用作标准分词结果
	string strline_in;	//去掉分词标记的语料(用作分词的输入)
	string strline_out_1;	//正向最大匹配分词完毕的语料
	string strline_out_2;	//逆向最大匹配分词完毕的语料
	
	ifstream fin("test.txt");	//打开输入文件
	if(!fin){
		cout << "Unable to open input file !" << argv[1] << endl;
		exit(-1);
	}
	
	ofstream fout("result.txt");	//确定输出文件
	if(!fout){
		cout << "Unable to open output file !" << endl;
		exit(-1);
	}
	
	long count = 0;	//句子编号
	long count_right_all = 0;	//准确的切分总数
	long count_out_1_all = 0;	//正向匹配切分总数
	long count_out_2_all = 0;	//逆向匹配切分总数
	long count_out_1_right_all = 0;	//正向匹配切分正确总数
	long count_out_2_right_all = 0;	//逆向匹配切分正确总数

	while(getline(fin, strline_right, '\n')){
		if(strline_right.length() > 1){
			
			//去掉分词标记
			strline_in = removeSeparator(strline_right);

			//正向最大匹配分词
			strline_out_1 = strline_right;
			strline_out_1 = SegmentSentenceMM(strline_in, 1);
			
			//逆向最大匹配分词
			strline_out_2 = strline_right;
			strline_out_2 = SegmentSentenceMM(strline_in, 2);

			//输出结果
			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;
			cout << "逆向匹配分词长度: " << strline_out_2.length() << "  句子:" << endl;
			cout << strline_out_2 << endl;
			cout << endl;

			//计算准确率、召回率
			//Rev()
			vector<int> vec_right = getPos(strline_right, strline_in);
			vector<int> vec_out_1 = getPos(strline_out_1, strline_in);
			vector<int> vec_out_2 = getPos(strline_out_2, 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;
			for(int i = 0; i < vec_out_2.size(); i++){
				cout << setw(4) << vec_out_2[i];
			}
			cout << endl;

			vector<int> vec_count_1 = getCount(vec_right, vec_out_1);
			vector<int> vec_count_2 = getCount(vec_right, vec_out_2);
			//准确的切分数量
			int count_right = vec_right.size();
			//切分得到的数量
			int count_out_1 = vec_out_1.size();
			int count_out_2 = vec_out_2.size();
			//切分正确的数量
			int count_out_1_right = count_out_1 - vec_count_1[0] - vec_count_1[1];
			int count_out_2_right = count_out_2 - vec_count_2[0] - vec_count_2[1];

			cout << "正向最大匹配:" << endl;	
			cout << "  不该切分而切分的数量:" << vec_count_1[0] << endl;
			cout << "  该切分而未切分的数量:" << vec_count_1[1] << endl;
			cout << "逆向最大匹配:" << endl;	
			cout << "  不该切分而切分的数量:" << vec_count_2[0] << endl;
			cout << "  该切分而未切分的数量:" << vec_count_2[1] << endl;
			
			count_right_all += count_right;
			count_out_1_all += count_out_1;
			count_out_2_all += count_out_2;
			count_out_1_right_all += count_out_1_right;
			count_out_2_right_all += count_out_2_right;
			
			
		}
	}
	
	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;	//正向召回率
	double kk_3 = (double)count_out_2_right_all / count_out_2_all;	//逆向准确率
	double kk_4 = (double)count_out_2_right_all / count_right_all;	//逆向召回率
	cout << "----------------------------------" << endl;
	cout << endl;
	cout << "统计结果:" << endl;
	cout << "正向准确率:" << kk_1*100 << "%    正向召回率:" << kk_2*100 << "%" << endl;
	cout << "逆向准确率:" << kk_3*100 << "%    逆向召回率:" << kk_4*100 << "%" << endl;

	return 0;
}




相关文章推荐

逆向最大匹配分词算法

目前对中文分词有两个方向,其中一个是利用概率的思想对文章分词。也就是如果两个字,一起出现的频率很高的话,我们可以假设这两个字是一个词。这里可以用一个公式衡量:M(A,B)=P(AB)/P(A)P(B)...

中文分词基础原则及正向最大匹配法、逆向最大匹配法、双向最大匹配法的分析

分词算法设计中的几个基本原则: 1、颗粒度越大越好:用于进行语义分析的文本分词,要求分词结果的颗粒度越大,即单词的字数越多,所能表示的含义越确切,如:“公安局长”可以分为“公安局长”、“公安局长”、...

深度解析中文分词器算法(最大正向/逆向匹配)

中文分词算法概述:  1:非基于词典的分词(nlp语义领域)     相当于人工智能领域计算。一般用于机器学习,特定领域等方法,这种在特定领域的分词可以让计算机在现有的规则模型中, 推理如何分词...

中文分词 正向最大匹配法 逆向最大匹配法 双向最大匹配法

今天一个朋友找到我,她的一个亲戚写毕业论文涉及中文分词部分该如何设计算法,我把我的理解稍稍整理了一下,写出来供大家探讨: 分词算法设计中的几个基本原则: 1、颗粒度越大越好:用于进行语义分析的文本...

中文分词--逆向最大匹配

上一篇文章中介绍了正向最大匹配,可以看到有时候效果不是很好,这里在介绍一种逆向最大匹配的算法。词典和匹配的字符串都和上一篇文章相同 只是本算法是从后到前搜索字符串,然后找到最长的匹配结果输出。上代码...

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

中文分词中的正向最大匹配与逆向最大匹配

http://josh-persistence.iteye.com/blog/2243380 我们都知道,英文的分词由于单词间是以空格进行分隔的,所以分词要相对的容易些,而中文就不同了,中文中一个句...

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

Delphi7高级应用开发随书源码

  • 2003年04月30日 00:00
  • 676KB
  • 下载

正向(逆向)最大匹配和最大概率法分词的错误分析

1.基本情况     从语料中选取了200个句子作为样本,分别用三种不同的分词方式进行分词,观察他们产生的错误, 从而得出其中的规律。将错误的结果过滤掉重复的内容之后,得到的错误结果如下所...
内容举报
返回顶部
收藏助手
不良信息举报
您举报文章:用正向和逆向最大匹配算法进行中文分词
举报原因:
原因补充:

(最多只允许输入30个字)