中文分词在中文信息处理中是最最基础的,无论机器翻译亦或信息检索还是其他相关应用,如果涉及中文,都离不开中文分词,因此中文分词具有极高的地位。中文分词入门最简单应该是最大匹配法了,当年师兄布置给我的第一个学习任务就是实现最大匹配法的分词算法(正向、逆向)。记得当时对自己参考学习最有帮助的是北大詹卫东老师“中文信息处理基础”的课件和源程序,不过他实现的是mfc程序,词表存储在数据库里。自己实现时用纯c++实现,利用hash_map存储词表。这里我介绍一下相关的知识和一个简单的程序示例,部分参考自詹老师的讲义。
正向最大匹配法算法如下所示:
最大匹配法图
(注:以上最大匹配算法图来自于詹老师讲义)
逆向匹配法思想与正向一样,只是从右向左切分,这里举一个例子:
输入例句:S1="计算语言学课程有意思" ;
定义:最大词长MaxLen = 5;S2= " ";分隔符 = “/”;
假设存在词表:…,计算语言学,课程,意思,…;
最大逆向匹配分词算法过程如下:
(1)S2="";S1不为空,从S1右边取出候选子串W="课程有意思";
(2)查词表,W不在词表中,将W最左边一个字去掉,得到W="程有意思";
(3)查词表,W不在词表中,将W最左边一个字去掉,得到W="有意思";
(4)查词表,W不在词表中,将W最左边一个字去掉,得到W="意思"
(5)查词表,“意思”在词表中,将W加入到S2中,S2=" 意思/",并将W从S1中去掉,此时S1="计算语言学课程有";
(6)S1不为空,于是从S1左边取出候选子串W="言学课程有";
(7)查词表,W不在词表中,将W最左边一个字去掉,得到W="学课程有";
(8)查词表,W不在词表中,将W最左边一个字去掉,得到W="课程有";
(9)查词表,W不在词表中,将W最左边一个字去掉,得到W="程有";
(10)查词表,W不在词表中,将W最左边一个字去掉,得到W="有",这W是单字,将W加入到S2中,S2=“ /有 /意思”,并将W从S1中去掉,此时S1="计算语言学课程";
(11)S1不为空,于是从S1左边取出候选子串W="语言学课程";
(12)查词表,W不在词表中,将W最左边一个字去掉,得到W="言学课程";
(13)查词表,W不在词表中,将W最左边一个字去掉,得到W="学课程";
(14)查词表,W不在词表中,将W最左边一个字去掉,得到W="课程";
(15)查词表,“意思”在词表中,将W加入到S2中,S2=“ 课程/ 有/ 意思/”,并将W从S1中去掉,此时S1="计算语言学";
(16)S1不为空,于是从S1左边取出候选子串W="计算语言学";
(17)查词表,“计算语言学”在词表中,将W加入到S2中,S2=“计算语言学/ 课程/ 有/ 意思/”,并将W从S1中去掉,此时S1="";
(18)S1为空,输出S2作为分词结果,分词过程结束。

相应程序示例:
准备文件:建立一个词表文件wordlexicon,格式如下
计算语言学
课程
意思
输入文件:test,格式如下
计算语言学课程有意思
编译后执行如下:SegWord.exe test
输出分词结果文件:SegmentResult.txt
源代码如下:

// Dictionary.h
#include <iostream>
#include <string>
#include <fstream>
#include <sstream>
#include <hash_map>

using namespace std;
using namespace stdext;

class CDictionary
{
public:
CDictionary(); //将词典文件读入并构造为一个哈希词典
~CDictionary();
int FindWord(string w); //在哈希词典中查找词

private:
string strtmp; //读取词典的每一行
string word; //保存每个词
hash_map<string, int> wordhash; // 用于读取词典后的哈希
hash_map<string, int >::iterator worditer; //
typedef pair<string, int> sipair;
};

//将词典文件读入并构造为一个哈希词典
CDictionary::CDictionary()
{
ifstream infile("wordlexicon"); // 打开词典
if (!infile.is_open()) // 打开词典失败则退出程序
{
cerr << "Unable to open input file: " << "wordlexicon" << " -- bailing out!" << endl; exit(-1); } while (getline(infile, strtmp, '\\n')) // 读入词典的每一行并将其添加入哈希中 { istringstream istr(strtmp); istr >> word; //读入每行第一个词
wordhash.insert(sipair(word, 1)); //插入到哈希中
}
}

CDictionary::~CDictionary()
{
}

//在哈希词典中查找词,若找到,则返回,否则返回
int CDictionary::FindWord(string w)
{
if (wordhash.find(w) != wordhash.end())
{
return 1;
}
else
{
return 0;
}
}

// 主程序main.cpp
#include "Dictionary.h"

# define MaxWordLength 10 // 最大词长为个字节(即个汉字)
# define Separator "/ " // 词界标记

CDictionary WordDic; //初始化一个词典

//对字符串用最大匹配法(正向或逆向)处理
string SegmentSentence(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 = WordDic.FindWord(w); // 在词典中查找相应的词
while(len > 2 && n == 0) // 如果不是词
{
len -= 2; // 从候选词右边减掉一个汉字,将剩下的部分作为候选词
//w = w.substr(0, len); //正向用
w = s1.substr(s1.length() - len, len); //逆向用
n = WordDic.FindWord(w);
}
//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 SegmentSentenceMM (string s1)
{
string s2 = ""; //用s2存放分词结果
int i;
int dd;
while(!s1.empty() )
{
unsigned char ch = (unsigned char)s1[0];
if (ch < 128) // 处理西文字符
{
i = 1;
dd = (int)s1.length();
while (i < dd && ((unsigned char)s1[i] < 128) && (s1[i] != 10) && (s1[i] != 13)) // s1[i]不能是换行符或回车符 { i++; } if ((ch != 32) && (ch != 10) && (ch != 13)) // 如果不是西文空格或换行或回车符 { s2 += s1.substr(0,i) + Separator; } else { //if (ch == 10 || ch == 13) // 如果是换行或回车符,将它拷贝给s2输出 if (ch == 10 || ch == 13 || ch == 32) //谢谢读者mces89的指正 { s2 += s1.substr(0, i); } } s1 = s1.substr(i,dd); continue; } else { if (ch < 176) // 中文标点等非汉字字符 { i = 0; dd = (int)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] == 172 || (unsigned char)s1[i+1] == 161) || (unsigned char)s1[i+1] == 168 || (unsigned char)s1[i+1] == 169 || (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; } if (!(ch == 161 && (unsigned char)s1[1] == 161)) // 不处理中文空格 { s2+=s1.substr(0, i) + Separator; // 其他的非汉字双字节字符可能连续输出 } s1 = s1.substr(i, dd); continue; } } // 以下处理汉字串 i = 2; dd = (int)s1.length(); while(i < dd && (unsigned char)s1[i] >= 176)
{
i += 2;
}
s2 += SegmentSentence(s1.substr(0, i));
s1 = s1.substr(i,dd);
}

return s2;
}

int main(int argc, char *argv[])
{
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("SegmentResult.txt"); //确定输出文件
if (!outfile1.is_open())
{
cerr << "Unable to open file:SegmentResult.txt" << "--bailing out!" << endl;
exit(-1);
}

while (getline(infile, strtmp, 'n')) //读入语料库中的每一行并用最大匹配法处理
{
line = strtmp;
line = SegmentSentenceMM(line); // 调用分词函数进行分词处理
outfile1 << line << endl; // 将分词结果写入目标文件
}

return 0;
}

注:原创文章,转载请注明出处“我爱自然语言处理”:www.52nlp.cn

本文链接地址:
https://www.52nlp.cn/maximum-matching-method-of-chinese-word-segmentation/

作者 52nlp

《中文分词入门之最大匹配法》有119条评论
  1. 学兄也太搞笑了吧?最大切词法是您这个流程?从句子的后面一个个地吃掉?哈哈为什么不用多线程两端折半去吃?或者从中间再加个precess不是吃的更好?哈哈,20年前就被证明没戏的方法您还在发文?!

    [回复]

    admin 回复:

    这里只是给了一个中文分词的入门示例,正向匹配的算法流程是詹卫东老师给的,不是我给的。我举得例子是逆向匹配,您大概看错了吧。另外,最大匹配法是中文分词的经典方法,现在看来虽然老,但也不是您所说的“20年前就被证明没戏”,如果有,请给个依据,不能空口说白话的。
      你给的方法不过是最大匹配法的一种改进,举得例子是逆向匹配也是是其中一种。现在真正流行的是基于字的中文分词方法,如CRF分词我也接触过。但这里仅仅是中文分词入门的介绍,发文只是希望给有需要的读者一定的启示,另外程序也只是我当初入门时的练习而已,可笑之处,请多多包涵。
      如果您对中文分词还有什么高见,欢迎在这里讨论。

    [回复]

    Mr.Z 回复:

    你在开玩笑?20年前你几岁?物质妄徒!这是引新手入门的,不是写给你看的。估计你学术水平也不咋地,与人品正相关。

    [回复]

    52nlp 回复:

    呵呵,好久的事了,谢谢!

    [回复]

  2. 我看了已有的算法实现后得出一个挺极端的结论。

    HMM,MMSEG,还有一个什么基于CRF。

    发觉只有MMSEG算法好理解,效率高,实用,且好扩展。

    其他的算法简直和看天书差不多。而且糟糕的地方是,花了那么大的代价,对中文分词的那些真正老大难的问题。依旧无法解决~~

    比如人名啊,地名啊,未登陆词啊。

    最靠谱的方法还是MMSEG+词典,加上一个离线的新词发现机制。

    不知楼主能否更深入谈谈分词,有没有真正觉得好用的分词方法。

    [回复]

    admin 回复:

    用MMSEG做中文分词的确简洁有效,但是只属于一个特定的方法,所以比较好理解。而一个好的数学模型,像HMM、CRF虽然开始时比较难理解,但是其用处不仅仅在中文分词,在自然语言处理甚至更多的领域都有用武之地,所以感觉多花一些时间还是值得的。
    关于中文分词,自己的掌握也有限,不过有计划在翻译完HMM这个系列之后尝试讲讲如何利用HMM来做词性标注、中文分词,主要从从动手练习的角度入手,欢迎继续关注。

    [回复]

  3. 学兄,能不能把你的全部源程序发给我
    哈希表的构造hash_map.h这部分
    谢谢!!

    [回复]

  4. hash_map是属于STL的一个容器,如果你安装了C++的编译器的话,应该自带了,具体可以参考:http://www.stlchina.org/twiki/bin/view.pl/Main/STLDetailHashMap

    [回复]

  5. stlport\hash_map(24) : fatal error C1083: Cannot open include file: 'stl/_prolog.h': No such file or directory
    我把hash_map加进去老是提示这个错误,我查了一下,路径是对的????
    还请大虾指点

    [回复]

  6. hash_map wordhash;
    上边是不是应该还有一句啊?链接是提示hash_map要带参数,我很想学习一下,还请大虾指点一下!!!!

    [回复]

    admin 回复:

    抱歉,白天在公司不方便管理博客,现在才给你回复。
    非常感谢你的提示,我才发现wordpress在发布代码时屏蔽掉了那个地方的几处尖括号,已经更改了,不好意思啊,是我发布后的粗心没有检查!

    [回复]

  7. 已将源程序发到你的邮箱了,非常感谢!

    [回复]

    kathy 回复:

    可以麻烦你发一份源程序代码给我吗?

    [回复]

    52nlp 回复:

    晚上有事刚回来,已经发到你邮箱了。

    [回复]

  8. 最大匹配法是一个基本的分词算法,还是很有意义的,如正向和逆向一起找到歧义等,有了新方法,老方法并不是一无是处,可以辅助新方法,像现在的机器翻译,统计方法占主流,但是目前商用的基本上都是规则的方法,而现在都是在统计方法中和规则结合。
    我硕士时做过基于字位的分词,用最大熵实现的,对于未登录词有很好的效果,如训练语料中没有人名,地名等,但对一些常用词有时会分错。

    [回复]

  9. 想请教一个基础问题,基于规则和基于统计的分词的定义是什么,基于字符串匹配的的分词等同于基于规则吗?

    [回复]

    52nlp 回复:

    基于字符串或者基于词典的方法是原始(机械)的分词方法,在此基础之上,如果你添加了一些规则来处理一些专有名词或者未登录词等等,如词法规则、语法规则甚至语义规则来提高其分词质量,那么可以称这种分词方法为基于规则的分词方法。

    基于统计的分词方法需要一个加工好的熟语语料库(如人民日报语料库)作为分词系统的原始统计数据的支撑,在该语料库的基础上,利用一些统计方法,如统计语言模型,最大熵等,进行分词模型的训练,最终利用训练好的模型进行分词。

    [回复]

    heaiyuan 回复:

    请问如果要写分词方面的论文,现在对哪个方面比较好挖掘点新的东西?最近看了些文章,基于统计的和基于词典匹配方面的文章都比较成熟。我自己也找不出新的研究点了。对基于规则。好像都是什么神经网络,专家系统方面的,不太懂,没怎么看。

    还有管理员大哥,现在给的这个最大匹配算法的程序是完整的吗?我编译后怎么Cannot open include file: 'hash_map': No such file or directory
    词典一般在哪里下载?能把程序和词典给我发一份吗?
    heiayaungo@126.com
    小弟拜谢!!!

    [回复]

    52nlp 回复:

    关于中文分词目前的关注点,我不太清楚,以前也没有专门研究这个,建议你多读读分词领域的经典和最新论文,读多了,自然会把握这个领域的现状,然后才可能找到某个切入点进行深入的研究。

    这个分词程序应该是完整的,但是有可能在发布时被Wordpress屏蔽了某些特殊符号。不过你遇到的问题可能有如下的原因:
    “某些STL类没有在标准库中实现,如hash map,STLPort中实现了。C++标准库包含一个STL的实现,但该实现是标准STL的子集。”
    建议编译一个STLPort试试。

    程序已经发到你的邮箱了,词典格式很简单,你可以利用“人民日报语料库”自动生成一个,或者参考“中文分词入门之资源”中的介绍:
    “在gold目录里包含了测试集标准切分及从训练集中抽取的词表(Contains the gold standard segmentation of the test data along with the training data word lists.)”

    luojia.tys 回复:

    谢谢解答。那基于字位的分词方法是属于哪一种呢?是不是应该属于统计与规则结合的啊?

    [回复]

    52nlp 回复:

    基于字标注的中文分词方法本质上属于基于统计的方法,因为它需要一个加工好的熟语语料库作为分词系统的支撑。

  10. 我的邮箱是906821902@qq.com
    我找你说的做了还是有错

    [回复]

    52nlp 回复:

    抱歉,较忙,刚刚发到你的邮箱中了。

    [回复]

  11. 非常感谢楼主的文章,不要跟那些狂妄自大的××一般见识。大家支持你。

    [回复]

    52nlp 回复:

    不客气,谢谢支持!

    [回复]

  12. 现在给的这个最大匹配算法的程序是完整的吗?我编译后怎么Cannot open include file: ‘hash_map’: No such file or directory
    词典一般在哪里下载?能把你编译好的cpp应用文件发给我吗?谢谢了,今天急用,明天就交作业了。

    [回复]

    maoli 回复:

    我的邮箱874582517@qq.com

    [回复]

    52nlp 回复:

    程序是完整的,关于hash_map的问题,前面已经有人遇到了:hash_map是属于STL的一个容器,如果你安装了C++的编译器的话,应该自带了,具体可以参考:http://www.stlchina.org /twiki/bin/view.pl/Main/STLDetailHashMap

    不过已经将程序发到你的邮箱了,关于词典,可以参考:中文分词入门之资源

    [回复]

  13. 刚接触这块` 以后有问题得请教您` 望多关照. 留名先. 呵呵

    [回复]

    52nlp 回复:

    多请教Google是最好的方式,呵呵

    [回复]

  14. 试了一下这个代码,好像西文处理是有问题的,以西文空格开头的那一段西文会本直接忽略掉。比如说 test计算语言学课程有意思,那么 test会被忽略掉。

    [回复]

    52nlp 回复:

    谢谢指正,这一行有问题:
    if (ch == 10 || ch == 13) // 如果是换行或回车符,将它拷贝给s2输出
    应该修改为:
    if (ch == 10 || ch == 13 || ch == 32)
    已经在文章里改了,非常感谢。

    [回复]

  15. 能否将源码,发我邮箱一份。xinxiyuanjt@126.com
    非常感谢!

    [回复]

    52nlp 回复:

    已发,请自行修改上面的这个bug!

    [回复]

  16. 刚学这个没多久,要实现下最大正向匹配或是最小逆向匹配的算法,但要用分词词典,书上也没写怎么用,也不知道哪里去下,网上下了几个txt,打开有很多符号看也看不懂,也不知道怎么加到自己的程序里面去。
    能麻烦介绍下词典用法吗?
    程序可以的话,发我一份。saber1013@live.cn

    [回复]

    52nlp 回复:

    程序已经发了,事实上关于词典的用法,文章里已经讲得够清楚了,不知道还需要怎么讲?

     准备文件:建立一个词表文件wordlexicon,格式如下
        计算语言学
        课程
        意思

    这里wordlexicon就是一个词典。

    [回复]

  17. 你好,我一开始也出现hashmap的问题,后来把这个问题解决后就出现了好多问题,我用的是VC6.0,请问能发源代码到我邮箱吗?我的邮箱是727576779@qq.com.谢谢了

    [回复]

    52nlp 回复:

    已经发了,和这里的代码是一样的。

    [回复]

  18. 能发我份代码吗, 邮箱344733148@qq.com
    还有上面代码编译后#include “Dictionary.h”这句有误,是不是我没写词典的关系

    [回复]

    52nlp 回复:

    已经发了。

    [回复]

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注