【中文分詞】簡單高效的MMSeg

最近碰到一個分詞匹配需求——給定一個關鍵詞表,做爲自定義分詞詞典,用戶query文本分詞後,是否有詞落入這個自定義詞典中?現有的大多數Java系的分詞方案基本都支持添加自定義詞典,可是卻不支持HDFS路徑的。所以,我須要尋找一種簡單高效的分詞方案,稍做包裝便可支持HDFS。MMSeg分詞算法正是完美地契合了這種需求。java

1. MMseg簡介

MMSeg是蔡志浩(Chih-Hao Tsai)提出的基於字符串匹配(亦稱基於詞典)的中文分詞算法。基於詞典的分詞方案沒法解決歧義問題,好比,「武漢市長江大橋」是應分詞「武漢/市長/江大橋」仍是「武漢市/長江/大橋」。基於此,有人提出了正向最大匹配策略,可是可能會出現分詞錯誤的狀況,好比:若詞典中有「武漢市長」,則原句被分詞成「武漢市長/江大橋」。單純的最大匹配仍是沒法完美地解決歧義,於是MMSeg在正向最大匹配的基礎上設計了四個啓發式規則。isnowfy大神的《淺談中文分詞》對於各類主流的分詞算法作了精闢的論述。git

MMSeg的字符串匹配算法分爲兩種:github

  • Simple,簡單的正向最大匹配,即按能匹配上的最長詞作切分;
  • Complex,在正向最大匹配的基礎上,考慮相鄰詞的詞長,設計了四個去歧義規則(Ambiguity Resolution Rules)指導分詞。

在complex分詞算法中,MMSeg將切分的相鄰三個詞做爲詞塊(chunk),應用以下四個消歧義規則:算法

  1. 備選詞塊的長度最大(Maximum matching),即三個詞的詞長之和最大;
  2. 備選詞塊的平均詞長最大(Largest average word length),即要求詞長分佈儘量均勻;
  3. 備選詞塊的詞長變化最小(Smallest variance of word lengths );
  4. 備選詞塊中(如有)單字的出現詞自由度最高(Largest sum of degree of morphemic freedom of one-character words)。

這篇文章《mmseg分詞算法及實現》對於這四個規則作了更爲細緻的介紹,本文無再贅言了。apache

2. 實戰

MMSeg的Java實現有mmseg4j,本地路徑添加自定義詞典分詞:併發

String txt = "在一塊兒併發生了中文分詞.";
// user-defined dictionary parent-path
Dictionary dic = Dictionary.getInstance("src\\resources\\dict");
Seg seg = new ComplexSeg(dic);
MMSeg mmSeg = new MMSeg(new StringReader(txt), seg);
Word word = null;
while ((word = mmSeg.next()) != null) {
  System.out.print(word + "|");
}

mmseg4j("com.chenlb.mmseg4j" % "mmseg4j-core" % "1.10.0")沒有wiki,經過分析源碼才知道getInstance方法的路徑參數應是父目錄,而且詞典文件的命名應符合規範:chars.dic(單字詞表)、wordsXXX.dic(詞長>1詞表)。mmseg4j所加載的分詞詞典爲類Dictionary數據成員Map<Character, CharNode> dict,其中Character爲詞的首字,CharNode是一棵trie樹,存儲擁有共同前綴(首字)的詞。loadWord方法爲加載自定義詞表。dom

基於上面的代碼分析,封裝添加HDFS路徑詞典的Scala代碼以下:oop

import com.chenlb.mmseg4j.CharNode
import org.apache.hadoop.conf.Configuration
import org.apache.hadoop.fs.{FSDataInputStream, FileSystem, Path}

import scala.io.Source

/**
  * @author rain
  */
object MMSegUtil {
  // str[1:-1], the last len(str)-1 characters
  def tail(str: String): Array[Char] = {
    str.toCharArray.takeRight(str.length - 1)
  }

  // load user-define word dictionary
  def loadWord(path: String, dic: java.util.Map[Character, CharNode]) = {
    val fs = FileSystem.get(new Configuration)
    val in: FSDataInputStream = fs.open(new Path(path))
    Source.fromInputStream(in).getLines()
      .filter(_.length > 1)
      .foreach { line =>
        val cn: CharNode = dic.get(line.charAt(0))
        cn match {
          case null => dic.put(line.charAt(0), cn)
          case _ => cn.addWordTail(tail(line))
        }
      }
  }
}

便可在Spark程序中調用分詞:ui

val dictionary = Dictionary.getInstance()
MMSegUtil.loadWord(dicPath, dictionary.getDict)
val seg = new ComplexSeg(dictionary)

值得指出,ComplexSeg類有List remove操做,於是不適於作成廣播變量,否則則報ConcurrentModificationException。推薦的作法,配合RDD的mapPartitions在for yield外層new ComplexSeg;至關於每一個Partition都有一個ComplexSeg。.net

mmseg4j存在分詞不許確的狀況,好比,『培養併發揮熱忱的特性』被分詞成『培養/併發/揮/熱忱/的/特性』。這是由於mmseg4j的chars詞典中,揮 20429的詞頻高於並 2789(針對於MMSeg的規則4)。基於詞典的分詞方案的準確性,嚴重依賴於詞典;必需要有好的詞典,纔會有好的分詞結果。

相關文章
相關標籤/搜索