19
北京邮电大学计算机学院 《自然语言处理导论》 中文分词实验报告 名: 许伟林 号: 08211306 指导教师: 郑岩 期: 2010/12/22 1

自然语言处理 中文分词程序实验报告%28含源代码%29

  • Upload
    aemoe

  • View
    1.202

  • Download
    4

Embed Size (px)

Citation preview

Page 1: 自然语言处理 中文分词程序实验报告%28含源代码%29

北京邮电大学计算机学院

《自然语言处理导论》

中文分词实验报告

姓 名: 许伟林 学 号: 08211306 指导教师:  郑岩 日 期: 2010/12/22

1

Page 2: 自然语言处理 中文分词程序实验报告%28含源代码%29

内容目录

一、实验目的...............................................................3

二、实验环境...............................................................3

三、实验材料...............................................................3

四、实验设计...............................................................3

一、分词策略.............................................................3

词典逆向最大匹配法...................................................4

基于确定文法的分词法.................................................4

二、程序设计.............................................................4

查找算法:哈希表查找.................................................4

汉字编码格式:UTF-8...................................................5

程序流程图............................................................6

程序源代码............................................................8

五、结果和性能分析........................................................16

分词结果示例............................................................16

性能分析................................................................17

六、有待解决的问题........................................................18

七、实验总结..............................................................19

2

Page 3: 自然语言处理 中文分词程序实验报告%28含源代码%29

一、实验目的

了解中文分词的意义

掌握中文分词的基本方法

二、实验环境

UBUNTU 10.05 GCC v4.4.3

三、实验材料

中文常用词词典

《人民日报》1998 年合订本

四、实验设计

一、分词策略

据我的了解,目前较为成熟的中文分词方法主要有:

1、 词典正向最大匹配法

2、 词典逆向最大匹配法

3、 基于确定文法的分词法

4、 基于统计的分词方法

一般认为,词典的逆向匹配法要优于正向匹配法。基于确定文法和基于统计的方

法作为自然语言处理的两个流派,各有千秋。

3

Page 4: 自然语言处理 中文分词程序实验报告%28含源代码%29

由于时间仓促,且水平有限,本程序只实现了第 2 种和第 3 种分词法,即词典逆

向最大匹配法和基于确定文法的分词法。

词典逆向最大匹配法

词典逆向最大匹配法完成分词的大部分工作,设计思路是这样的:

1、 将词典的每个词条读入内存,最长是 4 字词,最短是 1 字词;

2、 从语料中读入一段(一行)文字,保存为字符串;

3、 如果字符串长度大于 4 个中文字符,则取字符串最右边的 4 个中文字符,作

为候选词;否则取出整个字符串作为候选词;

4、 在词典中查找这个候选词,如果查找失败,则去掉这个候选词的最左字,重

复这步进行查找,直到候选词为 1 个中文字符;

5、 将候选词从字符串中取出、删除,回到第 3 步直到字符串为空;

6、 回到第 2 步直到语料已读完。

基于确定文法的分词法

基于确定文法的分词法可以进行数字、西文、时间的分词,设计思路是这样的:

1、 增加一个词典,存储中文编码(全角)的大小写拉丁字母、中文小写数字、阿

拉伯数字、数字单位(百、千、万、亿、兆)、小数点、百分号、除号;词类型记为[D1];2、 增加一个词典,存储中文编码的时间单位,包括年、月、日、时、分、秒、点;词

类型记为[D2];3、 文法的正则表达式为[D1]*[D2]?。

二、程序设计

查找算法:哈希表查找

除了分词结果的准确性,程序的性能也是至关重要的。由于本程序采用了词典法

来分词,执行过程需要检索大量数据,因此查找效率成为程序性能的决定性因素。

据我的了解,目前比较成熟的查找算法主要有顺序查找、二分查找、哈希表查找等。

顺序查找的算法复杂度为 O(n); 二分查找的算法复杂度为 O(logn),但需要事先排序;

哈希表查找的复杂度为 O(1)。本程序采用效率最高的哈希表查找算法。

4

Page 5: 自然语言处理 中文分词程序实验报告%28含源代码%29

汉字编码格式:UTF-8

中文处理和英文处理的一个很大不同就在于编码格式的复杂性。常见的中文编码

格式有 GB2312,GB18030,GBK,Unicode等等。同样的中文字符,在不同的汉字

编码格式中,可能有不同的二进制表示。因此编程做字符串匹配时,通常要求统一的

编码格式。

在 linux下可以用 file命令查看文本文件的编码格式。经过检查,发现老师提供的

词典和语料属于不同的编码格式,直接做字符串匹配可能会有问题。考虑到UBUNTU以 UTF-8 作为默认编码格式,我索性使用 enconv命令将词典和语料都转换成 UTF-8格式。

下面简要介绍一下UTF-8 格式。

前面提到的 Unicode 的学名 是 "Universal Multiple-Octet Coded Character Set",简称为 UCS。UCS 可以看作是"Unicode Character Set"的缩写。

UCS只是规定如何编码,并没有规定如何传输、保存这个编码。UTF-8 是被广泛

接受的方案。UTF-8 的一个特别的好处是它与 ISO-8859-1 完全兼容。UTF “是 UCS Transformation Format”的缩写。

UTF-8就是以 8 位为单元对UCS 进行编码。

从 UCS-2 到 UTF-8 的编码方式如下:

UCS-2 编码(16 进制) UTF-8 字节流(二进制)

0000 – 007F 0xxxxxxx

0080 – 07FF 110xxxxx 10xxxxxx

0800 – FFFF 1110xxxx 10xxxxxx 10xxxxxx

“ ”例如 汉 字的 Unicode 编码是 6C49。6C49 在 0800-FFFF之间,所以肯定要用 3字节模板了:1110xxxx 10xxxxxx 10xxxxxx。将 6C49 写成二进制是:0110 110001 001001 , 用这个比特流依次代替模板中的 x ,得到: 11100110 10110001 10001001,即 E6 B1 89。所以,就有了这个结论:汉字的 UTF-8 编码是 3 字节的 。然而,语料库中出现的

字符并非都是汉字。比如半角空格、外国人名字间隔符等。如果把所有字单元都视为 3字节,就要出错了。这是编程时必须考虑的问题。

5

Page 6: 自然语言处理 中文分词程序实验报告%28含源代码%29

程序流程图

6

Page 7: 自然语言处理 中文分词程序实验报告%28含源代码%29

7

Page 8: 自然语言处理 中文分词程序实验报告%28含源代码%29

程序源代码

/*

* segment.cpp ---- 中文分词程序

* 

* 功能:1)中文词典分词(逆向最大匹配)

*    2)数字分词

*    3)西文分词

*    4)时间分词

* 用法:

*   ./segment ARTICAL

* 范例:

*    ./segment 1998-01-qiefen-file.txt.utf8

* 屏幕输出:

* 词典读入完毕,耗时 0.000776 s

* 分词完毕,耗时 3.18 s *

*    分词结果保存在 result.txt.utf8中。

*

* 注意:1)本程序只能处理 utf-8文本,其他格式的文本请用 iconv或 enconv转换成

utf-8格式。

*         2 ) 程 序 运 行 需 要

dict/CoreDict.txt.utf8,dict/number.txt.utf8,dict/unit.txt.utf8三个文件。

*

*  参考了以下文章,特此感谢!

*     http://www.52nlp.cn/maximum-matching-method-of-chinese-word-

segmentation/

*

* Created on: 2010-11-30

* Author: xuweilin <usa911 at bupt.edu.cn>

*/

#include <iostream>

#include <string>

#include <fstream>

#include <sstream>

#include <ext/hash_map>

#include <iomanip>

#include <stdio.h>

#include <time.h>

#define MaxWordLength 12 // 最大词长字节(即4个汉字)

8

Page 9: 自然语言处理 中文分词程序实验报告%28含源代码%29

#define Separator " " // 词界标记

#define UTF8_CN_LEN 3 // 汉字的UTF-8编码为3字节

using namespace std;

using namespace __gnu_cxx;

namespace __gnu_cxx

{

template<> struct hash< std::string >

{

size_t operator()( const std::string& x ) const

{

return hash< const char* >()( x.c_str() );

}

};

}

hash_map<string, int> wordhash; // 词典

hash_map<string, int> numberhash;// 数字和字母

hash_map<string, int> unithash;// 时间单位

//读入词典,以及数字、字母、时间单位集

void get_dict(void)

{

string strtmp; //读取词典的每一行

string word; //保存每个词

typedef pair<string, int> sipair;

ifstream infile("dict/CoreDict.txt.utf8");

if (!infile.is_open())

{

cerr << "Unable to open input file: " << "wordlexicon"

<< " -- bailing out!" << endl;

exit(-1);

}

while (getline(infile, strtmp)) // 读入词典的每一行并将其添加入哈希中

{

istringstream istr(strtmp);

istr >> word; //读入每行第一个词

wordhash.insert(sipair(word, 1)); //插入到哈希中

}

infile.close();

9

Page 10: 自然语言处理 中文分词程序实验报告%28含源代码%29

infile.open("dict/number.txt.utf8");

if (!infile.is_open())

{

cerr << "Unable to open input file: " << "wordlexicon"

<< " -- bailing out!" << endl;

exit(-1);

}

while (getline(infile, strtmp)) // 读入词典的每一行并将其添加入哈希中

{

istringstream istr(strtmp);

istr >> word; //读入每行第一个词

numberhash.insert(sipair(word, 1)); //插入到哈希中

}

infile.close();

infile.open("dict/unit.txt.utf8");

if (!infile.is_open())

{

cerr << "Unable to open input file: " << "wordlexicon"

<< " -- bailing out!" << endl;

exit(-1);

}

while (getline(infile, strtmp)) // 读入词典的每一行并将其添加入哈希中

{

istringstream istr(strtmp);

istr >> word; //读入每行第一个词

unithash.insert(sipair(word, 1)); //插入到哈希中

}

infile.close();

}

//删除语料库中已有的分词空格,由本程序重新分词

string eat_space(string s1)

{

int p1=0,p2=0;

int count;

string s2;

while(p2 < s1.length()){

//删除全角空格

// if((s1[p2]-0xffffffe3)==0 &&

// s1[p2+1]-0xffffff80==0 &&

// s1[p2+2]-0xffffff80==0){//空格

10

Page 11: 自然语言处理 中文分词程序实验报告%28含源代码%29

// if(p2 > p1){

// s2 += s1.substr(p1,p2-p1);

// }

// p2 += 3;

// p1 = p2;

// }

// else{

// p2 += 3;

// }

//删除半角空格

if(s1[p2] - 0x20 == 0){

if(p2>p1)

s2 += s1.substr(p1,p2-p1);

p2++;

p1=p2;

}

else{

p2++;

}

}

s2 += s1.substr(p1,p2-p1);

return s2;

}

//用词典做逆向最大匹配法分词

string dict_segment(string s1)

{

string s2 = ""; //用 s2存放分词结果

while (!s1.empty()) {

int len = (int) s1.length(); // 取输入串长度

if (len > MaxWordLength) // 如果输入串长度大于最大词长

{

len = MaxWordLength; // 只在最大词长范围内进行处理

}

//string w = s1.substr(0, len); // (正向用)将输入串左边等于最大词长

长度串取出作为候选词

string w = s1.substr(s1.length() - len, len); //逆向用

int n = (wordhash.find(w) != wordhash.end()); // 在词典中查找相应的词

while (len > UTF8_CN_LEN && n == 0) // 如果不是词

{

len -= UTF8_CN_LEN; // 从候选词左边减掉一个汉字,将剩下的部分作为候

选词

11

Page 12: 自然语言处理 中文分词程序实验报告%28含源代码%29

//w = w.substr(0, len); //正向用

w = s1.substr(s1.length() - len, len); //逆向用

n = (wordhash.find(w) != wordhash.end());

}

//s2 += w + Separator; // (正向用)将匹配得到的词连同词界标记加到输出

串末尾

w = w + Separator; // (逆向用)

s2 = w + s2; // (逆向用)

//s1 = s1.substr(w.length(), s1.length()); //(正向用)从 s1-w处开始

s1 = s1.substr(0, s1.length() - len); // (逆向用)

}

return s2;

}

//中文分词,先分出数字和字母以及时间,再交由词典分词,具有一定的智能。

string cn_segment(string s1)

{

//先分出数字和字母

string s2;

int p1,p2;

p1 = p2 = 0;

while(p2 < s1.length()){

while(p2 <= (s1.length()-3) && numberhash.find(s1.substr(p2,3)) ==

numberhash.end()){//不是数字或字母

p2 += 3;

}

s2 += dict_segment(s1.substr(p1,p2-p1));//之前的句子用词典分词

//将数字/字母和单位分出来

p1 = p2;

p2 += 3;

while(p2 <= (s1.length()-3) && numberhash.find(s1.substr(p2,3)) !=

numberhash.end()){//是数字或字母

p2 += 3;

}

if(p2 <= (s1.length()-3) && unithash.find(s1.substr(p2,3)) !=

unithash.end()){//是单位

p2 += 3;

}

s2 += s1.substr(p1,p2-p1) + Separator;

12

Page 13: 自然语言处理 中文分词程序实验报告%28含源代码%29

p1 = p2;

}

return s2;

}

//在执行中文分词前,过滤半角空格以及其他非 UTF-8字符

string seg_analysis(string s1)

{

string s2;

string s3 = "";

int p1 = 0;

int p2 = 0;

int count;

while(p2 < s1.length()){

if(((s1[p2]>>4)&0xe) ^ 0xe){//过滤非utf-8字符

count = 0;

do{

p2++;

count++;

}while((((s1[p2]>>4)&0xe) ^ 0xe) && p2 < s1.length());

s2 = s1.substr(p1,p2-count-p1);//特殊字符前的串

s3 += cn_segment(s2) + s1.substr(p2-count,count) + Separator;//特殊

字符,不要用汉字分词处理

if(p2 <= s1.length()){//这个等号!!!当特殊符号是最后一个字符时!

s1 = s1.substr(p2,s1.length()-p2);//剩余串

}

p1 = p2 = 0;

}

else

p2 += UTF8_CN_LEN;

}

if(p2 != 0){

s3 += cn_segment(s1);

}

return s3;

};

int main(int argc, char* argv[])

13

Page 14: 自然语言处理 中文分词程序实验报告%28含源代码%29

{

if(argv[1] == NULL){

cout <<

"\nsegment.cpp ---- 中文分词程序\n"

"* \n"

"* 功能:1)中文词典分词(逆向最大匹配)\n"

"*    2)数字分词\n"

"*    3)西文分词\n"

"*    4)时间分词\n"

"* 用法:\n"

"*   ./segment ARTICAL\n"

"* 范例:\n"

"*    ./segment 1998-01-qiefen-file.txt.utf8\n"

"* !!格外注意:本程序只能处理 utf-8文本,其他格式的文本请用 iconv 或 enconv 转

换成 utf-8格式。\n";

exit(0);

}

clock_t start, finish;

double duration;

start = clock();

get_dict();

finish = clock();

duration = (double)(finish - start) / CLOCKS_PER_SEC;

cout << "词典读入完毕,耗时 " << duration << " s" << endl;

string strtmp; //用于保存从语料库中读入的每一行

string line; //用于输出每一行的结果

ifstream infile(argv[1]); // 打开输入文件

if (!infile.is_open()) // 打开输入文件失败则退出程序

{

cerr << "Unable to open input file: " << argv[1] << " -- bailing out!"

<< endl;

exit(-1);

}

ofstream outfile1("result.txt.utf8"); //确定输出文件

if (!outfile1.is_open()) {

cerr << "Unable to open file:SegmentResult.txt" << "--bailing out!"

<< endl;

exit(-1);

14

Page 15: 自然语言处理 中文分词程序实验报告%28含源代码%29

}

start = clock();

cout << "正在分词并输出到文件,请稍候..." << endl;

while (getline(infile, strtmp)) //读入语料库中的每一行并用最大匹配法处理

{

line = eat_space(strtmp);

//cout << "NOSPACE with " << endl << line << endl;

line = seg_analysis(line); // 调用分词函数进行分词处理

//cout << "result " << endl << line << endl;

outfile1 << line << endl; // 将分词结果写入目标文件

}

finish = clock();

duration = (double)(finish - start) / CLOCKS_PER_SEC;

cout << "分词完毕,耗时 " << duration << " s" << endl;

cout << "分词结果保存在 result.txt.utf8中。" << endl;

return 0;

}

15

Page 16: 自然语言处理 中文分词程序实验报告%28含源代码%29

五、结果和性能分析

分词结果示例

程序编码基本正确,实现了程序设计中提到的两种分词策略,分词结果就在预料之中。

第100行原文:据最新统计,1997年1月至11月份,来华旅游人数达5236

万多人次,国际旅游收入达110.8亿多美元,分别较上年同期增长12.3%和18

7%,预计全年来华旅游入境人数约5400万人次,旅游创汇达115亿美元,再创新

纪录,国内旅游人数及收入也比上年有大幅增长。

分词结果:据 最新 统计 , 1997年 1月 至 11月 份 , 来华 旅游 人数 达

5236万 多 人次 , 国际 旅游 收入 达 110.8亿 多 美元 , 分别 较 上年 同

期 增长 12.3% 和 18.7% , 预计 全年 来华 旅游 入境 人数 约 5400万

人次 , 旅游 创汇 达 115亿 美元 , 再 创新纪录 , 国内 旅游 人数 及 收入 也

比 上年 有 大幅 增长 。

结果分析:程序实现了词典分词,还能识别出复杂数字、时间等词。

第1994行原文:CDMA数字移动通讯新阶段

分词结果:CDMA 数字 移动 通讯 新 阶段

分析:程序能识别英文单词。

16

Page 17: 自然语言处理 中文分词程序实验报告%28含源代码%29

第19096行原文:参加调查小组的世界卫生组织官员丹尼尔·拉万奇博士说,中国政府

对H5N1病毒高质量的监视活动给他留下了深刻的印象。他同时表示,今后对这种病毒的

监控仍不能放松。

分词结果:参加 调查 小组 的 世界 卫生 组织 官员 丹 尼 尔 · 拉 万 奇 博士

说 , 中国 政府 对 H5N1 病毒 高 质量 的 监视 活动 给 他 留下 了 深刻 的 印

象 。 他 同时 表示 , 今后 对 这种 病毒 的 监控 仍 不 能 放松 。

分析:程序能识别复杂英文词组

性能分析

$ ./segment 1998-01-qiefen-file.txt.utf8

词典读入完毕,耗时 0.22 s

正在分词并输出到文件,请稍候...

分词完毕,耗时 3.49 s

分词结果保存在 result.txt.utf8中。

程序运行输出结果如上所示。据显示,本机的运行时间为3.49s。在分词策略不变的情

况下,仍有优化空间。

影响性能的几个因素:

1、查词典。程序使用哈希表查词典,复杂度为 O(1),剩余优化空间不多。但是,若词条

数目太多,导致哈希表冲突经常发生,复杂度就不是 O(1)了。因此,当词典很大时,可以

把不同长度的词条分开到不同的哈希表存储,以减少冲突。经过测试本程序若采用这个优化

方案,可以减少数百毫秒的执行时间。为增加代码可读性,这个优化方案没有写入程序最终

版。

2、删除空格操作。 因为语料库是已经分好词的内容,所以完整的分词包括删除空格操

作。经过测试,若没有这项操作,可以减少数百毫秒的执行时间。

3、过程分遍执行。为了简化程序设计,删除空格、过滤非 UTF-8字符、、确定文法分词、词

典分词等四个过程设计为多遍执行,即前一项的输出作为后一项的输入。如果分词过程合并

为一遍,可以节省内存拷贝时间。

4、汉字编码格式。GB系列的汉字编码为2字节,而 UTF-8的汉字编码为3字节,采用

UTF-8编码时,文件I/O、字符匹配的时间都增加了50%。

17

Page 18: 自然语言处理 中文分词程序实验报告%28含源代码%29

六、有待解决的问题

本程序的分词结果并不完全令人满意。下面就是一些失败的分词结果。

第1行原文:迈向充满希望的新世纪——一九九八年新年讲话(附图片1张)

分词结果:迈向 充满 希望 的 新 世纪 — — 一九九八年 新年 讲话 ( 附 图片

1 张 )

问题:“——”是两条横线组成的破折号,是确定文法的词,暂未包含在程序

处理范围。

第4行原文:12月31日,中共中央总书记、国家主席江泽民发表1998年新年讲

话《迈向充满希望的新世纪》。(新华社记者兰红光摄)

分词结果:12月 31日 , 中共中央 总书记 、 国家 主席 江 泽 民 发表 199

8年 新年 讲话 《 迈向 充满 希望 的 新 世纪 》 。 ( 新华社 记者 兰 红 光 摄 )

问题:“江泽民”应为一个词。《人民日报》是党的机关报,党和国家领导人的名

字出现的频率很高,这种错误是不能容忍的。这可以通过丰富词典内容,也可以通过

统计方法来解决。

第 131 行原文:本报伊斯兰堡12月31日电记者王南报道:巴基斯坦穆斯林联盟

(谢里夫派)候选人、原最高法院大法官穆罕默德·拉斐克·塔拉尔,今天在巴国民议会、参

议院以及各省议会选举中,当选巴基斯坦第九任总统,任期5年。塔拉尔将于明天宣誓就职。

分词结果:本报 伊斯兰堡 12月 31日 电 记者 王 南 报道 : 巴基斯坦 穆斯林

联盟 ( 谢里夫派 ) 候选人 、 原 最高 法院 大法官 穆 罕 默 德 · 拉 斐 克 · 塔

拉 尔 , 今天 在 巴 国民 议会 、 参议院 以及 各省 议会 选举 中 , 当选 巴基斯坦

第 九 任 总统 , 任期 5年 。 塔 拉 尔 将 于 明天 宣誓 就职 。

问题:“穆罕默德·拉斐克·塔拉尔”是一个以“·”为隔断的人名,不能再分词。

因为“·”不是 UTF-8 字符,本程序暂时没有实现外国人名分词。

第9行片段:在这一年中,中国的外交工作取得了重要成果。

分词结果:在 这 一年 中 , 中国 的 外交 工作 取 得了 重要 成果 。

问题:“取得了”应分词为“取得 了”,而不是“取 得了”。未实现语义消歧。

18

Page 19: 自然语言处理 中文分词程序实验报告%28含源代码%29

七、实验总结

分词是中文自然语言处理的基础,在现实中已经得到广泛应用。比如,Google的

Chrome浏览器就内置了中文分词功能。如上图,我们可以注意到,在 Chrome中双击无链接

文本时,Chrome选中的不是一个字,也不是一句话,而是一个词。 当然,中文分词在数

据挖掘等方面的应用就更加明显了。掌握自然语言处理的基本知识,已经成为 IT行业对计

算机专业学生的基本要求。

虽然我曾经系统学习过《算法》《数据结构》等基础课程,但编写程序时仍然遇到了很多

问题,仅在汉字编码的问题上就纠缠了很久。幸而在搜索引擎和开源社区以及开源软件细致

的文档的帮助下,我攻克了一个又一个难题,最终做出了分词程序雏形。尽管它在功能和性

能上都没有达到国际先进水平,但确实是我在学习《自然语言处理》这门课中产生的阶段性

成果,是非常值得欣慰的。

自然语言处理是一个正在快速发展的学科,但愿我有朝一日能在这个领域大显身手。

19