全文检索、数据挖掘、推荐引擎系列---去除停止词添加同义词

转自:http://www.cnblogs.com/yantao7589/archive/2011/08/19/2145991.html

Lucene对文本解析是作为全文索引及全文检索的预处理形式出现的,因此在一般的Lucene文档中,这一部分都不是重点,往往一带而过,但是对于要建立基于文本的内容推荐引擎来说,却是相当关键的一步,因此有必要认真研究一下Lucene对文解析的过程。

Lucene对文本的解析对用户的接口是Analyzer的某个子类,Lucene内置了几个子类,但是对于英文来说StandardAnalyzer是最常用的一个子类,可以处理一般英文的文解析功能。但是对于汉字而言,Lucene提供了两个扩展包,一个是CJKAnalyzer和SmartChineseAnalyzer,其中SmartAnalyzer对处理中文分词非常适合,但是遗憾的是,该类将词典利用隐马可夫过程算法,集成在了算法里,这样的优点是减小了体积,并且安装方便,但是如果想向词库中添加单词就需要重新学习,不太方便。因此我们选择了MMSeg4j,这个开源的中文分词模块,这个开源软件的最大优点就可用户可扩展中文词库,非常方便,缺点是体积大加载慢。

首先通过一个简单的程序来看中文分词的使用:

Analyzeranalyzer=null;

//analyzer=newStandardAnalyzer(Version.LUCENE_33);

//analyzer=newSimpleAnalyzer(Version.LUCENE_33);

analyzer=newMMSegAnalyzer();

TokenStreamtokenStrm=analyzer.tokenStream("content",newStringReader(examples));

OffsetAttributeoffsetAttr=tokenStrm.getAttribute(OffsetAttribute.class);

CharTermAttributecharTermAttr=tokenStrm.getAttribute(CharTermAttribute.class);

PositionIncrementAttributeposIncrAttr=

tokenStrm.addAttribute(PositionIncrementAttribute.class);

TypeAttributetypeAttr=tokenStrm.addAttribute(TypeAttribute.class);

Stringterm=null;

inti=0;

intlen=0;

char[]charBuf=null;

inttermPos=0;

inttermIncr=0;

try{

while(tokenStrm.incrementToken()){

charBuf=charTermAttr.buffer();

termIncr=posIncrAttr.getPositionIncrement();

if(termIncr>0){

termPos+=termIncr;

}

for(i=(charBuf.length-1);i>=0;i--){

if(charBuf[i]>0){

len=i+1;

break;

}

}

//term=newString(charBuf,offsetAttr.startOffset(),offsetAttr.endOffset());

term=newString(charBuf,0,offsetAttr.endOffset()-offsetAttr.startOffset());

System.out.print("["+term+":"+termPos+"/"+termIncr+":"+

typeAttr.type()+";"+offsetAttr.startOffset()+"-"+offsetAttr.endOffset()+"]");

}

}catch(IOExceptione){

//TODOAuto-generatedcatchblock

e.printStackTrace();

}

这里需要注意的是:

TermAttribute已经在Lucene的新版本中被标为过期,所以程序中使用CharTermAttribute来提取每个中文分词的信息

MMSegAnalyzer的分词效果在英文的条件下基本与Lucene内置的StandardAnalyzer相同

可以进行初步的中文分词之后,我们还需处理停止词去除,例如的、地、得、了、呀等语气词,还有就是添加同义词:第一种是完全意义上的同义词,如手机和移动电话,第二种是缩写与全称,如中国和中华人民共和国,第三种是中文和英文,如计算机和PC,第四种是各种专业词汇同义词,如药品名和学名,最后可能还有一些网络词语如神马和什么等。

在Lucene架构下,有两种实现方式,第一种是编写TokenFilter类来实现转换和添加,还有一种就是直接集成在相应的Analyzer中实现这些功能。如果像Lucene这样的开源软件,讲求系统的可扩展性的话,选择开发独立的TokenFilter较好,但是对于我们自己的项目,选择集成在Analyzer中将是更好的选择,这样可以提高程序执行效率,因为TokenFilter需要重新逐个过一遍所有的单词,效率比较低,而集成在Analyzer中可以保证在分解出单词的过程中就完成了各种分词操作,效率当然会提高了。

Lucene在文本解析中,首先会在Analyzer中调用Tokenizer,将文本分拆能最基本的单位,英文是单词,中文是单字或词组,我们的去除停止词和添加同义词可以放入Tokenizer中,将每个新拆分的单词进行处理,具体到我们所选用的MMSeg4j中文分词模块来说,就是需要在MMSegTokenizer类的incrementToken方法中,添加去除停止词和添加同义词:

publicbooleanincrementToken()throwsIOException{

if(0==synonymCnt){

clearAttributes();

Wordword=mmSeg.next();

currWord=word;

if(word!=null){

//去除截止词如的、地、得、了等

StringwordStr=word.getString();

if(stopWords.contains(wordStr)){

returnincrementToken();

}

if(synonymKeyDict.get(wordStr)!=null){//如果具有同义词则需要先添加本身这个词,然后依次添加同义词

synonymCnt=synonymDict.get(synonymKeyDict.get(wordStr)).size();//求出同义词,作为结束条件控制

}

//termAtt.setTermBuffer(word.getSen(),word.getWordOffset(),word.getLength());

offsetAtt.setOffset(word.getStartOffset(),word.getEndOffset());

charTermAttr.copyBuffer(word.getSen(),word.getWordOffset(),word.getLength());

posIncrAttr.setPositionIncrement(1);

typeAtt.setType(word.getType());

returntrue;

}else{

end();

returnfalse;

}

}else{

char[]charArray=null;

StringorgWord=currWord.getString();

inti=0;

Vector<String>synonyms=(Vector<String>)synonymDict.get(synonymKeyDict.get(orgWord));

if(orgWord.equals(synonyms.elementAt(synonymCnt-1))){//如果是原文中出现的那个词则不作任何处理

synonymCnt--;

returnincrementToken();

}

// 添加同意词

charArray=synonyms.elementAt(synonymCnt-1).toCharArray();//termAtt.setTermBuffer(t1,0,t1.length);

offsetAtt.setOffset(currWord.getStartOffset(),currWord.getStartOffset()+charArray.length);//currWord.getEndOffset());

typeAtt.setType(currWord.getType());

charTermAttr.copyBuffer(charArray,0,charArray.length);

posIncrAttr.setPositionIncrement(0);

synonymCnt--;

returntrue;

}

}

停止词实现方式:

private static String[] stopWordsArray = {"的", "地", "得", "了", "呀", "吗", "啊", "a", "the", "in", "on"};

在构造函数中进行初始化:

if (null == stopWords) {

inti=0;

stopWords=newVector<String>();

for(i=0;i<stopWordsArray.length;i++){

stopWords.add(stopWordsArray[i]);

}

}

同义词的实现方式:

privatestaticCollection<String>stopWords=null;

privatestaticHashtable<String,String>synonymKeyDict=null;

private static Hashtable<String, Collection<String>> synonymDict = null;

同样在初始化函数中进行初始化:注意这里只是简单的初始化示例

// 先找出一个词的同义词词组key值,然后可以通过该key值从

//最终本部分内容将通过数据库驱动方式进行初始化

if(null==synonymDict){

synonymKeyDict=newHashtable<String,String>();

synonymDict=newHashtable<String,Collection<String>>();

synonymKeyDict.put("猎人","0");

synonymKeyDict.put("猎户","0");

synonymKeyDict.put("猎手","0");

synonymKeyDict.put("狩猎者","0");

Collection<String>syn1=newVector<String>();

syn1.add("猎人");

syn1.add("猎户");

syn1.add("猎手");

syn1.add("狩猎者");

synonymDict.put("0",syn1);

//添加狗和犬

synonymKeyDict.put("狗","1");

synonymKeyDict.put("犬","1");

Collection<String>syn2=newVector<String>();

syn2.add("狗");

syn2.add("犬");

synonymDict.put("1",syn2);

}

在经过上述程序后,再对如下中文进行解析:咬死猎人的狗

解析结果为:

[咬:1/1:word;0-1] [死:2/1:word;1-2] [猎人:3/1:word;2-4] [狩猎者:3/0:word;2-5] [猎手:3/0:word;2-4] [猎户:3/0:word;2-4] [狗:4/1:word;5-6] [犬:4/0:word;5-6]

由上面结果可以看出,已经成功将猎人和狗的同义词加入到分词的结果中,这个工具就可以作为下面全文内容推荐引擎的实现基础了。