update: 24/3/2015
最近接触NLP中文分词, 因为某些特殊环境要求,需要在客户端浏览器环境内实现分词,所以在lunr.js的基础上, 通过读取trie tree结构的inverted index实现了中文的最大正向匹配分词。
某些情况下, 我们在服务器端进行中文文本分词可以使用完整的基于mmseg算法的分词模块, 例如nodejieba, node-segment, 盘古分词等等, 但是在客户端环境下, 我们不能使用这些复杂的分词算法进行分词, 这个时候可以根据已经生成的索引进行简单的客户端分词, 就是所说的FMM (Forward Maximum Matching, 正向最大匹配), 有时候也可以使用正向匹配。
在做FMM的时候, 还需要进行交集歧义检测, 检测是否出现歧义, 如果出现歧义则进行其他的歧义处理, 歧义处理可以采用bi-gram或者对歧义片段进行一个完整的正向匹配, 划分出所有的可以切分的情况。
因为在作FMM(正向最大匹配)的时候,分词的结果是截取能够匹配的最大长度(一般情况下这个最大的长度要对应一个在服务器端获得分词,这里称为term node, 即这个最大长度的匹配的分词有对应的文档存在),但是不能保证这个最大长度的匹配是最合理的分词结果,有可能存在其他的分词结果。
例如,“乒乓球拍卖完了”, 如果FMM匹配到“乒乓球拍”,那么,就会出现歧义,因为有可能更加合理的分词是“乒乓球”,‘拍卖’,‘完了’。 所以,需要进行交集歧义检测。
交集歧义检测的另外一个例子, 例如“学院路途遥远”, 如果只做正向最大匹配, 那么首先切分出“学院路”, 然后对“途遥远”再次进行FMM切分, 这样的切分结果肯定是错误的。
本文是基于lunr.js的, 所以采用JavaScript代码进行实现(ps: 目前写了好多js代码了。。。 , 写多了js代码,发现也挺有意思的), 因为我只用了简单的10篇新闻构建索引, 所以这里的示例可能不是很理想, 但是可以把算法描述清楚。 关于构建索引的部分, 这里不再详述, 我们只是假定已经存在了索引, 并且索引是以Trie树的形式存在的。
ps: 使用node.js运行JS文件
var lunr = require("./lunr.js") var idxdata = require("./idx.json") var idx = lunr.Index.load(idxdata) var ii = idx.tokenStore //console.log(ii.root) var query1 = "中国人民银行指出我国最近经济不景气" var query2 = "习近平今日出席了中央气象台的联欢晚会" var query3 = "中国银行今日出台了最新的贷款政策" var query5 = "全部门" var query6 = "互联网金宝中央气象台" var query7 = "上下级别" var query8 = "确定期" var query9 = "引领土完整" query = query6 var result = tokenizer(ii.root, query) console.log(result) function tokenizer(root, str) { if (root == null || root == undefined) return []; if (str == null || str == undefined || str.length == 0) return []; var out = []; while (str.length > 0) { var word = matchLongest(root, str); out.push(word); str = str.slice(word.length); } return out; } function matchLongest(root, str) { if ( root == null || root == undefined ) return if ( str == null || str == undefined || str.length == 0 ) return var maxMatch = "" var currentNode = root for( var i = 0; i < str.length; i++ ) { if (str[i] in currentNode ) { maxMatch += str[i] currentNode = currentNode[str[i]] } else { if ( maxMatch.length == 0 ) maxMatch = str[i] break } } return maxMatch } function getAmbiguiousLength(root, str, word_length) { var i = 1 while ( i < word_length && i < str.length ) { var wid = tokenize(root, str.slice(i)) wid = wid[0] var length = wid.length if ( word_length < i + length ) word_length = i + length //console.log("i = " + i + ",length=" + wid.length + ", wid :" + wid + ", word_length : " + word_length) i += 1 } return word_length }
1.
query : "互联网金宝中央气象台"
切分结果: [ '互联网', '金', '宝', '中央气象台' ]
2.
query: "中国人民银行指出我国最近经济不景气"
切分结果: [ '中国人民银行', '指', '出', '我', '国', '最近', '经济', '不景气' ]
这里之所以切分效果不好, “我国”没有成词, 是因为我的索引太小了, 里面根本就没有这个路径。。。
3.
query : "中国银行今日出台了最新的贷款政策"
切分结果: [ '中国', '银行', '今日', '出台', '了', '最新', '的', '贷', '款', '政策' ]
注: 在query中, 所有的未登录词都被切分成了单字。