- 深入理解汉语分词的基本概念。
- 掌握并实现前向最大匹配算法、后向最大匹配算法和最少分词法。
- 掌握分词的评价指标,学会计算正确率、召回率和F-测度值。
利用人民日报语料库或自己构建的语料库(30词以上)作为词典,任选五个句子,并基于正向最大匹配算法和最短路径法分别对这五个句子进行分词,并分别计算分词结果的正确率,召回率和F-测度值。输出句子,基于两种算法的分词结果和其对应的评价指标值。
三、 实验环境操作系统:macOS Monterey 12.4
IDE:CLion 语言:C++
中文编码:UTF-8
4.1 建立词典 定义unordered_set类型全局变量dictionary,用于判断子串是否是词典中的词语,词典中的词语来自pku_training.txt。m表示词典中最长单词的字数*2,为什么要乘以2呢?因为在UTF-8编码中一个汉字占两个char。首先创建ifstream对象ifs,使用getlion方法按行读入字符串至read,再用read实例化stringstream 对象str,这样就可以根据空格提取该句子的每个词了,储存至dictionary中。 图1 建立词典 前向最大匹配 前向最大匹配算法思路较简单,int L表示现在遍历到的左端点,len表示现在考虑的字符串长度。每次通过L和len,调用substr( )函数得到子串,判断是否在词典中。若不在词典中,则len--;若在词典中,则在该词末尾添加分词标记,并更新L = L+len。直到L 等于句子长度后结束算法。注意,由于在UTF-8编码中一个汉字占两个char,部分操作需要考虑乘2。 图2 前向最大匹配 后向最大匹配 后向最大匹配算法与前向类似,int R表示现在遍历到的右端点len表示现在考虑的字符串长度。每次通过R和len,调用substr( )函数得到子串,判断是否在词典中。若不在词典中,则len--;若在词典中,则在该词末尾添加分词标记,并更新R = R - len。 图3 后向最大匹配 最小分词 最小分词算法可分为两步进行。 第一步是建图,需要建立一个结点数为n+1的有限无环图,其中n为输入字符串的长度。首先在相邻节点之间建立有向边,再依次枚举j和k(这里k从j开始枚举到min(字符串长度,m+j ) 结束,m为最长的词语的长度。这样能有效优化时间复杂度。),如果下标从j到k的子串在词典中,那么就连一条从j到k+1的有向边。 第二步是计算最短路,这里我使用了堆优化的迪杰斯特拉算法,时间复杂度为O(n logn)。在计算过程中需要记录最短路径,我定义了pre[]来记录某点是由哪个点转移过来的。 图4 最小分词算法 图5 堆优化Dijkstra求最短路 结果展示与评价指标计算 我使用了pku_training.txt中的词语作为词典,最终的结果如下。 图6 实验结果展示 5.1 最大匹配算法验证 输入:他是研究生命科学的一位科学家。 前向最大匹配分词结果:他/是/研究生/命/科学/的/一/位/科学家/。 后向最大匹配分词结果:他/是/研究/生命科学/的/一/位/科学家/。 结果符合预期。 输入:青年大学习还没完成的同学抓紧时间完成。 前向最大匹配分词结果:青年/大学/习/还/没完/成/的/同学/抓紧/时间/完成/。 后向最大匹配分词结果:青年/大/学习/还/没/完成/的/同学/抓紧/时间/完成/。 结果符合预期。 5.2 最小分词算法验证 输入:他是研究生命科学的一位科学家。 最小分词算法结果:他/是/研究/生命科学/的/一/位/科学家/。(词个数:9) 另一种候选结果:他/是/研究生/命/科学/的/一/位/科学家/。 (词个数:10) 结果符合预期。 5.3 评价指标计算 正确的分词结果应该是: 1 他/是/研究/生命科学/的/一/位/科学家/。 2 青年大学习/还/没/完成/的/同学/抓紧/时间/完成/。 3 今天/是/肯德基/疯狂/星期四/。 4 明日/起/将/全面/恢复/全市/正常/生产/生活/秩序/。 5 在/北京大学/生活区/喝/进口/白酒/。 前向最大匹配分词结果: 1 他/是/研究生/命/科学/的/一/位/科学家/。 2 青年/大学/习/还/没完/成/的/同学/抓紧/时间/完成/。 3 今天/是/肯德基/疯狂/星期四/。 4 明日/起/将/全面/恢复/全市/正常/生产/生活/秩序/。 5 在/北京大学/生活区/喝/进口/白酒/。 后向最大匹配分词结果: 1 他/是/研究/生命科学/的/一/位/科学家/。 2 青年/大/学习/还/没/完成/的/同学/抓紧/时间/完成/。 3 今天/是/肯德基/疯狂/星期四/。 4 明日/起/将/全面/恢复/全市/正常/生产/生活/秩序/。 5 在/北京大学/生活区/喝/进口/白酒/。 最小分词算法结果: 1 他/是/研究/生命科学/的/一/位/科学家/。 2 青年/大/学习/还/没/完成/的/同学/抓紧/时间/完成/。 3 今天/是/肯德基/疯狂/星期四/。 4 明日/起/将/全面/恢复/全市/正常/生产/生活/秩序/。 5 在/北京大学/生活区/喝/进口/白酒/。 对于FMM,正确切分的个数有38个,系统所有输出结果有46个;标准答案个数有43个。BMM和最小分词法输出结果相同,正确切分的个数有42个,系统所有输出结果有45个;标准答案个数有43个。其中F-测度值的beta=1,计算结果如下表。 表1 评价指标值 % 分词方法 正确率 召回率 F-测度值 FMM 82.61 88.37 85.39 BMM 93.33 97.67 95.45 最小分词法 93.33 97.67 95.45 复杂度分析 建立词典需要遍历文件,C++ 中unordered_set由哈希表实现,每次操作时间复杂度O(1),故建立词典需要O(n)的时间复杂度,n为数据集大小。 最大匹配法在最坏的情况下时间复杂度为O(nm),其中m为词典中最长词语的长度,n为输入的句子长度。 最小分词在建图的过程中,时间复杂度为O(nm),其中m为词典中最长词语的长度,n为输入的句子长度。求最短路用到了堆优化的迪杰斯特拉,时间复杂度为O(m logn),n 表示点数,m表示边数。 整个算法的空间复杂度为O(n),n为数据集大小。代码
#include#include #include #include #include #include using namespace std; typedef long long LL; typedef pair PII; const int n=5; string str[5]; unordered_set dictionary;//词典 int m; //词典中最长单词的字数*2 void creatDic(){ ifstream ifs("/Users/a26012/Desktop/大二下/NLP/实验/实验4/pku_training.txt"); ifs.unsetf(ios_base::skipws); string read,word; while(getline(ifs, read)) { stringstream mystr(read); while(mystr>>word){ if(!dictionary.count(word)) dictionary.insert(word); m = max(m,(int)word.size()); } } } void maximumMatching(){ vector ans; string temp; cout<<"The result of FMM:n"; for(int i=0;i if(!ans.empty()) ans.clear(); int sz=str[i].size(),l=0,len; while(l for(len=min(m,sz-l);len!=0;len-=2){ temp=str[i].substr(l,len); if(len==2||dictionary.count(temp)){ ans.push_back(temp); l=l+len; break; } } } for(int j=0;j if(!ans.empty()) ans.clear(); int sz=str[i].size(),r=sz-1,len; while(r>0){ for(len=min(m,r+1);len!=0;len-=2){ temp=str[i].substr(r-len+1,len); if(len==2||dictionary.count(temp)){ ans.push_back(temp); r=r-len; break; } } } for(int j=ans.size()-1;j>=0;j--) cout< mp[510]; bool st[510]; int pre[510],nextt[510],dis[510]; void Dijkstra(int sz){ priority_queue , greater > heap; memset(st,false,sizeof (st)); memset(dis,0x3f,sizeof (dis)); memset(pre,0x3f,sizeof (pre)); memset(nextt,0,sizeof (nextt)); dis[0]=0; heap.push({0, 0}); while (!heap.empty()) { auto t = heap.top(); heap.pop(); int ver = t.second, distance = t.first; if (st[ver]) continue; st[ver] = true; for (int nx:mp[ver]) { if (dis[nx] > distance + 1) { dis[nx] = distance + 1; pre[nx] = ver; heap.push({dis[nx], nx}); } } } int nw=sz-1; while(nw!=0){ int nx=pre[nw]; nextt[nx]=nw; nw=nx; } } vector res; void leastWord(){ cout<<"nThe result of Least word segmentation:n"; for(int i=0;i if(!res.empty()) res.clear(); int sz=(int)str[i].size()/2; //建图 for(int j=0; j <= sz; j++){ if(!mp[j].empty()) mp[j].clear(); if(j!=sz) mp[j].push_back(j+1); } for(int j=0;j<=sz;j++){ for(int k=j+1;k string temp = str[i].substr(j*2,(k-j+1)*2); if(dictionary.count(temp)){ mp[j].push_back(k+1); } } } Dijkstra(sz+1); //调用Dijkstra算法 int nw=0; while(nw!=sz){ int nx=nextt[nw]; res.push_back(str[i].substr(nw*2,(nx-nw)*2)); nw=nx; } for(int j=0;j cout< cout<<"Please input five sentences:n"; creatDic(); //建立词典 for(int i=0;i cin>>str[i]; } maximumMatching(); //最大匹配算法 leastWord(); //最小分词法 return 0; } //他是研究生命科学的一位科学家。 //青年大学习还没完成的同学抓紧时间完成。 //今天是肯德基疯狂星期四。 //明日起将全面恢复全市正常生产生活秩序。 //在北京大学生活区喝进口白酒。



