2012年5月17日 星期四

全文檢索、資料採擷、推薦引擎系列4---去除停止詞添加同義詞


Lucene對文本解析是作為全文索引及全文檢索的預處理形式出現的,因此在一般的Lucene文檔中,這一部分都不是重點,往往一帶而過,但是對於要建立基於文本的內容推薦引擎來說,卻是相當關鍵的一步,因此有必要認真研究一下Lucene對文解析的過程。
Lucene
對文本的解析對使用者的介面是Analyzer的某個子類,Lucene內置了幾個子類,但是對於英文來說StandardAnalyzer是最常用的一個子類,可以處理一般英文的文解析功能。但是對於漢字而言,Lucene提供了兩個擴展包,一個是CJKAnalyzerSmartChineseAnalyzer,其中SmartAnalyzer對處理中文分詞非常適合,但是遺憾的是,該類將詞典利用隱馬可夫過程演算法,集成在了演算法裡,這樣的優點是減小了體積,並且安裝方便,但是如果想向詞庫中添加單詞就需要重新學習,不太方便。因此我們選擇了MMSeg4j,這個開源的中文分詞模組,這個開源軟體的最大優點就可用戶可擴展中文詞庫,非常方便,缺點是體積大載入慢。
首先通過一個簡單的程式來看中文分詞的使用:
Analyzer analyzer = null;
  //analyzer = new StandardAnalyzer(Version.LUCENE_33);
  //analyzer = new SimpleAnalyzer(Version.LUCENE_33);
  analyzer = new MMSegAnalyzer();
  TokenStream tokenStrm = analyzer.tokenStream("content", new StringReader(examples));
  OffsetAttribute offsetAttr = tokenStrm.getAttribute(OffsetAttribute.class);
  CharTermAttribute charTermAttr = tokenStrm.getAttribute(CharTermAttribute.class);
  PositionIncrementAttribute posIncrAttr =
   tokenStrm.addAttribute(PositionIncrementAttribute.class);
  TypeAttribute typeAttr = tokenStrm.addAttribute(TypeAttribute.class);
  String term = null;
  int i = 0;
  int len = 0;
  char[] charBuf = null;
  int termPos = 0;
  int termIncr = 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 = new String(charBuf, offsetAttr.startOffset(), offsetAttr.endOffset());
    term = new String(charBuf, 0, offsetAttr.endOffset() - offsetAttr.startOffset());
    System.out.print("[" + term + ":" + termPos + "/" + termIncr + ":" +
      typeAttr.type() + ";" + offsetAttr.startOffset() + "-" + offsetAttr.endOffset() + "]  ");
   }
  } catch (IOException e) {
   // TODO Auto-generated catch block
   e.printStackTrace();
  }
這裡需要注意的是:
TermAttribute
已經在Lucene的新版本中被標為過期,所以程式中使用CharTermAttribute來提取每個中文分詞的資訊 
MMSegAnalyzer
的分詞效果在英文的條件下基本與Lucene內置的StandardAnalyzer相同 
可以進行初步的中文分詞之後,我們還需處理停止詞去除,例如的、地、得、了、呀等語氣詞,還有就是添加同義詞:第一種是完全意義上的同義詞,如手機和行動電話,第二種是縮寫與全稱,如中國和中華人民共和國,第三種是中文和英文,如電腦和PC,第四種是各種專業詞彙同義詞,如藥品名和學名,最後可能還有一些網路詞語如神馬和什麼等。
Lucene架構下,有兩種實現方式,第一種是編寫TokenFilter類來實現轉換和添加,還有一種就是直接集成在相應的Analyzer中實現這些功能。如果像Lucene這樣的開源軟體,講求系統的可擴展性的話,選擇開發獨立的TokenFilter較好,但是對於我們自己的項目,選擇集成在Analyzer中將是更好的選擇,這樣可以提高程式執行效率,因為TokenFilter需要重新逐個過一遍所有的單詞,效率比較低,而集成在Analyzer中可以保證在分解出單詞的過程中就完成了各種分詞操作,效率當然會提高了。
Lucene
在文本解析中,首先會在Analyzer中調用Tokenizer,將文本分拆能最基本的單位,英文是單詞,中文是單字或片語,我們的去除停止詞和添加同義詞可以放入Tokenizer中,將每個新拆分的單詞進行處理,具體到我們所選用的MMSeg4j中文分詞模組來說,就是需要在MMSegTokenizer類的incrementToken方法中,添加去除停止詞和添加同義詞:
public boolean incrementToken() throws IOException {
  if (0 == synonymCnt) {
   clearAttributes();
   Word word = mmSeg.next();
   currWord = word;
   if(word != null) {
    //
去除截止詞如的、地、得、了等
    String wordStr = word.getString();
    if (stopWords.contains(wordStr)) {
     return incrementToken();
    }
    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());
    return true;
   } else {
    end();
    return false;
   }
  } else {
   char[] charArray = null;
   String orgWord = currWord.getString();
   int i = 0;
   Vector<String> synonyms = (Vector<String>)synonymDict.get(synonymKeyDict.get(orgWord));
   if (orgWord.equals(synonyms.elementAt(synonymCnt - 1))) {
 // 如果是原文中出現的那個詞則不作任何處理
    synonymCnt--;
    return incrementToken();
   }
   // 添加同意詞
   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--;
  
   return true;
  }
 }
停止詞實現方式:
private static String[] stopWordsArray = {"", "", "", "", "", "", "", 
  "a", "the", "in", "on"};
在構造函數中進行初始化:
if (null == stopWords) {
   int i = 0;
   stopWords = new Vector<String>();
   for (i=0; i<stopWordsArray.length; i++) {
    stopWords.add(stopWordsArray[i]);
   }
  }
同義詞的實現方式:
 private static Collection<String> stopWords = null;
 private static Hashtable<String, String> synonymKeyDict = null;
 private static Hashtable<String, Collection<String>> synonymDict = null;
同樣在初始化函數中進行初始化:注意這裡只是簡單的初始化示例
// 先找出一個詞的同義詞片語key值,然後可以通過該key值從
  //
最終本部分內容將通過資料庫驅動方式進行初始化
  if (null == synonymDict) {
   synonymKeyDict = new Hashtable<String, String>();
   synonymDict = new Hashtable<String, Collection<String>>();
   synonymKeyDict.put("
獵人", "0");
   synonymKeyDict.put("
獵戶", "0");
   synonymKeyDict.put("
獵手", "0");
   synonymKeyDict.put("
狩獵者", "0");
   Collection<String> syn1 = new Vector<String>();
   syn1.add("
獵人");
   syn1.add("
獵戶");
   syn1.add("
獵手");
   syn1.add("
狩獵者");
   synonymDict.put("0", syn1);
   //
添加狗和犬
   synonymKeyDict.put("
", "1");
   synonymKeyDict.put("
", "1");
   Collection<String> syn2 = new Vector<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]
由上面結果可以看出,已經成功將獵人和狗的同義詞加入到分詞的結果中,這個工具就可以作為下面全文內容推薦引擎的實現基礎了。


沒有留言: