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;
}