一、 实验目的

  1. 深入理解汉语分词的基本概念。
  2. 掌握并实现前向最大匹配算法、后向最大匹配算法和最少分词法。
  3. 掌握分词的评价指标,学会计算正确率、召回率和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 <iostream>
#include <fstream>
#include <sstream>
#include <vector>
#include <unordered_set>
#include <queue>

using namespace std;
typedef long long LL;
typedef pair<int,int> PII;
const int n=5;
string str[5];
unordered_set<string> 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<string> ans;
    string temp;

    cout<<"The result of FMM:\n";
    for(int i=0;i<n;i++){
        if(!ans.empty()) ans.clear();
        int sz=str[i].size(),l=0,len;
        while(l<sz){
            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<ans.size();j++)  cout<<ans[j]<<(j==ans.size()-1?"\n":"/");
    }

    cout<<"\nThe result of BMM:\n";
    for(int i=0;i<n;i++){
        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<<ans[j]<<(j==0?"\n":"/");
    }
}

vector<int> mp[510];
bool st[510];
int pre[510],nextt[510],dis[510];
void Dijkstra(int sz){

    priority_queue<PII, vector<PII>, greater<PII>> 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<string> res;
void leastWord(){
    cout<<"\nThe result of Least word segmentation:\n";
    for(int i=0;i<n;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<sz && k-j<=m/2;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<res.size();j++){
            cout<<res[j]<<(j==res.size()-1?"\n":"/");
        }
    }
}
int main()

{   cout<<"Please input five sentences:\n";
    creatDic(); //建立词典
    for(int i=0;i<n;i++){
        cin>>str[i];
    }

    maximumMatching(); //最大匹配算法
    leastWord(); //最小分词法

    return 0;
}

//他是研究生命科学的一位科学家。
//青年大学习还没完成的同学抓紧时间完成。
//今天是肯德基疯狂星期四。
//明日起将全面恢复全市正常生产生活秩序。
//在北京大学生活区喝进口白酒。