什么是中文分词
什么是中文分词?
众所周知,英文是以词为单位的,词和词之间是靠空格隔开,而中文是以字为单位,句子中所有的字连起来才能描述一个意思。例如,英文句子I am a student,用中文则为:“我是一个学生”。计算机可以很简单通过空格知道student是一个单词,但是不能很容易明白“学”、“生”两个字合起来才表示一个词。把中文的汉字序列切分成有意义的词,就是中文分词,有些人也称为切词。我是一个学生,分词的结果是:我是 一个 学生。
目前主流的中文分词算法有:
1、 基于字符串匹配的分词方法
这种方法又叫做机械分词方法,它是按照一定的策略将待分析的汉字串与一个“充分大的”机器词典中的词条进行配,若在词典中找到某个字符串,则匹配成功(识别出一个词)。按照扫描方向的不同,串匹配分词方法可以分为正向匹配和逆向匹配;按照不同长度优先匹配的情况,可以分为最大(最长)匹配和最小(最短)匹配;按照是否与词性标注过程相结合,又可以分为单纯分词方法和分词与标注相结合的一体化方法。常用的几种机械分词方法如下:
1)正向最大匹配法(由左到右的方向);
2)逆向最大匹配法(由右到左的方向);
3)最少切分(使每一句中切出的词数最小)。
还可以将上述各种方法相互组合,例如,可以将正向最大匹配方法和逆向最大匹配方法结合起来构成双向匹配法。由于汉语单字成词的特点,正向最小匹配和逆向最小匹配一般很少使用。一般说来,逆向匹配的切分精度略高于正向匹配,遇到的歧义现象也较少。统计结果表明,单纯使用正向最大匹配的错误率为1/169,单纯使用逆向最大匹配的错误率为1/245。但这种精度还远远不能满足实际的需要。实际使用的分词系统,都是把机械分词作为一种初分手段,还需通过利用各种其它的语言信息来进一步提高切分的准确率。
一种方法是改进扫描方式,称为特征扫描或标志切分,优先在待分析字符串中识别和切分出一些带有明显特征的词,以这些词作为断点,可将原字符串分为较小的串再来进机械分词,从而减少匹配的错误率。另一种方法是将分词和词类标注结合起来,利用丰富的词类信息对分词决策提供帮助,并且在标注过程中又反过来对分词结果进行检验、调整,从而极大地提高切分的准确率。
对于机械分词方法,可以建立一个一般的模型,在这方面有专业的学术论文,这里不做详细论述。
2、 基于理解的分词方法
这种分词方法是通过让计算机模拟人对句子的理解,达到识别词的效果。其基本思想就是在分词的同时进行句法、语义分析,利用句法信息和语义信息来处理歧义现象。它通常包括三个部分:分词子系统、句法语义子系统、总控部分。在总控部分的协调下,分词子系统可以获得有关词、句子等的句法和语义信息来对分词歧义进行判断,即它模拟了人对句子的理解过程。这种分词方法需要使用大量的语言知识和信息。由于汉语语言知识的笼统、复杂性,难以将各种语言信息组织成机器可直接读取的形式,因此目前基于理解的分词系统还处在试验阶段。
3、 基于统计的分词方法
从形式上看,词是稳定的字的组合,因此在上下文中,相邻的字同时出现的次数越多,就越有可能构成一个词。因此字与字相邻共现的频率或概率能够较好的反映成词的可信度。可以对语料中相邻共现的各个字的组合的频度进行统计,计算它们的互现信息。定义两个字的互现信息,计算两个汉字X、Y的相邻共现概率。互现信息体现了汉字之间结合关系的紧密程度。当紧密程度高于某一个阈值时,便可认为此字组可能构成了一个词。这种方法只需对语料中的字组频度进行统计,不需要切分词典,因而又叫做无词典分词法或统计取词方法。但这种方法也有一定的局限性,会经常抽出一些共现频度高、但并不是词的常用字组,例如“这一”、“之一”、“有的”、“我的”、“许多的”等,并且对常用词的识别精度差,时空开销大。实际应用的统计分词系统都要使用一部基本的分词词典(常用词词典)进行串匹配分词,同时使用统计方法识别一些新的词,即将串频统计和串匹配结合起来,既发挥匹配分词切分速度快、效率高的特点,又利用了无词典分词结合上下文识别生词、自动消除歧义的优点。 {{{SharpICTCLAS分词系统简介(1)读取词典库(转) ICTCLAS分词的总体流程包括:1)初步分词;2)词性标注;3)人名、地名识别;4)重新分词;5)重新词性标注这五步。就第一步分词而言,又细分成:1)原子切分;2)找出原子之间所有可能的组词方案;3)N-最短路径中文词语粗分三步。
在所有内容中,词典库的读取是最基本的功能。ICTCLAS中词典存放在Data目录中,常用的词典包括coreDict.dct(词典库)、BigramDict.dct(词与词间的关联库)、nr.dct(人名库)、ns.dct(地名库)、tr.dct(翻译人名库),它们的文件格式是完全相同的,都使用CDictionary类进行解析。如果想深入了解ICTCLAS词典结构,可以参考sinboy的《ICTCLAS分词系统研究(二)--词典结构》一文,详细介绍了词典结构。我这里只给出SharpICTCLAS中的实现。
首先是对基本元素的定义。在SharpICTCLAS中,对原有命名进行了部分调整,使得更具有实际意义并适合C#的习惯。代码如下:
Copy CodeWordDictionaryElement.cs 程序
using System; using System.Collections.Generic; using System.Text;
namespace SharpICTCLAS {
//==================================================
// Original predefined in DynamicArray.h file //================================================== public class ArrayChainItem {
public int col, row;//row and column public double value;//The value of the array public int nPOS; public int nWordLen; public string sWord; //The possible POS of the word related to the segmentation graph
public ArrayChainItem next;
}
public class WordResult {
//The word public string sWord; //the POS of the word public int nPOS; //The -log(frequency/MAX) public double dValue;
}
//
--------------------------------------------------------------------------------
// data structure for word item
//
--------------------------------------------------------------------------------
public class WordItem {
public int nWordLen; //The word public string sWord; //the process or information handle of the word public int nPOS; //The count which it appear public int nFrequency;
}
//
--------------------------------------------------------------------------------
//data structure for dictionary index table item
//
--------------------------------------------------------------------------------
public class IndexTableItem {
//The count number of words which initial letter is sInit public int nCount; //The head of word items
public WordItem[] WordItems;
}
//
--------------------------------------------------------------------------------
//data structure for word item chain
//
--------------------------------------------------------------------------------
public class WordChain {
public WordItem data; public WordChain next;
}
//
--------------------------------------------------------------------------------
//data structure for dictionary index table item
//
--------------------------------------------------------------------------------
public class ModifyTableItem {
//The count number of words which initial letter is sInit public int nCount; //The number of deleted items in the index table public int nDelete; //The head of word items
public WordChain pWordItemHead;
}
}
其中ModifyTableItem用于组成ModifyTable,但在实际分词时,词库往往处于“只读”状态,因此用于修改词库的ModifyTable实际上起的作用并不大。因此在后面我将ModifyTable的代码暂时省略。
有了基本元素的定义后,就该定义“词典”类了。原有C++代码中所有类名均以大写的“C”打头,词典类名为CDictionary,在SharpICTCLAS中,我去掉了开头的“C”,并且为了防止和系统的Dictionary类重名,特起名为“WordDictionary”类。该类主要负责完成词典库的读、写以及检索操作。让我们看看如何读取词典库:
Copy Code词典库的读取:
public class WordDictionary {
public bool bReleased = true;
public IndexTableItem[] indexTable; public ModifyTableItem[] modifyTable; public bool Load(string sFilename) {
return Load(sFilename, false);
} public bool Load(string sFilename, bool bReset) {
int frequency, wordLength, pos; //频率、词长、读取词性 bool isSuccess = true;
FileStream fileStream = null; BinaryReader binReader = null; try {
fileStream = new FileStream(sFilename, FileMode.Open, FileAccess.Read); if (fileStream == null)
return false;
binReader = new BinaryReader(fileStream, Encoding.GetEncoding("gb2312"));
indexTable = new IndexTableItem[Predefine.CC_NUM]; bReleased = false;
for (int i = 0; i < Predefine.CC_NUM; i++) { //读取以该汉字打头的词有多少个 indexTable[i] = new IndexTableItem(); indexTable[i].nCount = binReader.ReadInt32(); if (indexTable[i].nCount <= 0) continue; indexTable[i].WordItems = new WordItem[indexTable[i].nCount]; for (int j = 0; j < indexTable[i].nCount; j++) { indexTable[i].WordItems[j] = new WordItem(); frequency = binReader.ReadInt32(); //读取频率 wordLength = binReader.ReadInt32(); //读取词长 pos = binReader.ReadInt32(); //读取词性 if (wordLength > 0)
indexTable[i].WordItems[j].sWord = Utility.ByteArray2String(binReader.ReadBytes(wordLength));
else
indexTable[i].WordItems[j].sWord = "";
//Reset the frequency if (bReset)
indexTable[i].WordItems[j].nFrequency = 0;
else
indexTable[i].WordItems[j].nFrequency = frequency;
indexTable[i].WordItems[j].nWordLen = wordLength; indexTable[i].WordItems[j].nPOS = pos;
}
}
} catch (Exception e) {
Console.WriteLine(e.Message); isSuccess = false;
} finally {
if (binReader != null)
binReader.Close();
if (fileStream != null)
fileStream.Close();
} return isSuccess;
} //......
}
下面内容节选自词库中CCID为2、3、4、5的单元, CCID的取值范围自1~6768,对应6768个汉字,所有与该汉字可以组成的词均记录在相应的单元内。词库中记录的词是没有首汉字的(我用带括号的字补上了),其首汉字就是该单元对应的汉字。词库中记录了词的词长、频率、词性以及词。
另外特别需要注意的是在一个单元内,词是按照CCID大小排序的!这对我们后面的分析至关重要。
Copy CodeICTCLAS词库部分内容
汉字:埃, ID :2
词长 频率 词性 词
0 128 h (埃) 0 0 j (埃) 2 4 n (埃)镑 2 28 ns (埃)镑 4 4 n (埃)菲尔 2 511 ns (埃)及 4 4 ns (埃)克森 6 2 ns (埃)拉特湾 4 4 nr (埃)里温 6 2 nz (埃)默鲁市 2 27 n (埃)塞 8 64 ns (埃)塞俄比亚
22 2 ns (埃)塞俄比亚联邦民主共和国
4 3 ns (埃)塞萨 4 4 ns (埃)舍德 6 2 nr (埃)斯特角 4 2 ns (埃)松省 4 3 nr (埃)特纳 6 2 nz (埃)因霍温
==================================== 汉字:挨, ID :3
词长 频率 词性 词
0 56 h (挨) 2 1 j (挨)次 2 19 n (挨)打 2 3 ns (挨)冻 2 1 n (挨)斗 2 9 ns (挨)饿 2 4 ns (挨)个 4 2 ns (挨)个儿 6 17 nr (挨)家挨户 2 1 nz (挨)近 2 0 n (挨)骂 6 1 ns (挨)门挨户 2 1 ns (挨)批 2 0 ns (挨)整 2 12 ns (挨)着 2 0 nr (挨)揍
==================================== 汉字:哎, ID :4
词长 频率 词性 词
0 10 h (哎) 2 3 j (哎)呀 2 2 n (哎)哟
==================================== 汉字:唉, ID :5
词长 频率 词性 词
0 9 h (唉) 6 4 j (唉)声叹气
在这里还应当注意的是,一个词可能有多个词性,因此一个词可能在词典中出现多次,但词性不同。若想从词典中唯一定位一个词的话,必须同时指明词与词性。
另外在WordDictionary类中用到得比较多的就是词的检索,这由FindInOriginalTable方法实现。原ICTCLAS代码中该方法的实现结构比较复杂,同时考虑了多种检索需求,因此代码也相对复杂一些。在SharpICTCLAS中,我对该方法进行了重载,针对不同检索目的设计了不同的FindInOriginalTable方法,简化了程序接口和代码复杂度。其中一个FindInOriginalTable方法代码如下,实现了判断某一词性的一词是否存在功能。
Copy CodeFindInOriginalTable方法的一个重载版本
private bool FindInOriginalTable(int nInnerCode, string sWord, int nPOS) {
WordItem[] pItems = indexTable[nInnerCode].WordItems; int nStart = 0, nEnd = indexTable[nInnerCode].nCount - 1; int nMid = (nStart + nEnd) / 2, nCmpValue; //Binary search
while (nStart <= nEnd) { nCmpValue = Utility.CCStringCompare(pItems[nMid].sWord, sWord); if (nCmpValue == 0 && (pItems[nMid].nPOS == nPOS || nPOS == -1)) return true;//find it else if (nCmpValue < 0 || (nCmpValue == 0 && pItems[nMid].nPOS < nPOS && nPOS != -1)) nStart = nMid + 1; else if (nCmpValue > 0 || (nCmpValue == 0 && pItems[nMid].nPOS > nPOS && nPOS != -1))
nEnd = nMid - 1;
nMid = (nStart + nEnd) / 2;
} return false;
}
其它功能在这里就不再介绍了。
小结 1、WordDictionary类实现了对字典的读取、写入、更改、检索等功能。
2、词典中记录了以6768个汉字打头的词、词性、出现频率的信息,具体结构需要了解。