segment
Version:
Chinese word segmentation 中文分词模块
361 lines (336 loc) • 10.6 kB
JavaScript
'use strict';
/**
* 字典识别模块
*
* @author 老雷<leizongmin@gmail.com>
*/
var FAMILY_NAME_1 = require('./CHS_NAMES').FAMILY_NAME_1;
var FAMILY_NAME_2 = require('./CHS_NAMES').FAMILY_NAME_2;
var SINGLE_NAME = require('./CHS_NAMES').SINGLE_NAME;
var DOUBLE_NAME_1 = require('./CHS_NAMES').DOUBLE_NAME_1;
var DOUBLE_NAME_2 = require('./CHS_NAMES').DOUBLE_NAME_2;
var debug = console.log;
/** 模块类型 */
exports.type = 'tokenizer';
/**
* 模块初始化
*
* @param {Segment} segment 分词接口
*/
exports.init = function (segment) {
exports.segment = segment;
};
/**
* 对未识别的单词进行分词
*
* @param {array} words 单词数组
* @return {array}
*/
exports.split = function (words) {
// debug(words);
var POSTAG = exports.segment.POSTAG;
var TABLE = exports.segment.getDict('TABLE');
var ret = [];
for (var i = 0, word; word = words[i]; i++) {
if (word.p > 0) {
ret.push(word);
continue;
}
// 仅对未识别的词进行匹配
var wordinfo = matchWord(word.w, 0, words[i - 1]);
if (wordinfo.length < 1) {
ret.push(word);
continue;
}
// 分离出已识别的单词
var lastc = 0;
for (var ui = 0, bw; bw = wordinfo[ui]; ui++) {
if (bw.c > lastc) {
ret.push({w: word.w.substr(lastc, bw.c - lastc)});
}
ret.push({w: bw.w, p: TABLE[bw.w].p});
lastc = bw.c + bw.w.length;
}
var lastword = wordinfo[wordinfo.length - 1];
if (lastword.c + lastword.w.length < word.w.length) {
ret.push({w: word.w.substr(lastword.c + lastword.w.length)});
}
}
return ret;
};
// =================================================================
// 日期时间常见组合
var _DATETIME = [
'世纪', '年', '年份', '年度', '月', '月份', '月度', '日', '号',
'时', '点', '点钟', '分', '分钟', '秒', '毫秒'
];
var DATETIME = {};
for (var i in _DATETIME) DATETIME[_DATETIME[i]] = _DATETIME[i].length;
//debug(DATETIME);
// =================================================================
/**
* 匹配单词,返回相关信息
*
* @param {string} text 文本
* @param {int} cur 开始位置
* @param {object} preword 上一个单词
* @return {array} 返回格式 {w: '单词', c: 开始位置}
*/
var matchWord = function (text, cur, preword) {
if (isNaN(cur)) cur = 0;
var ret = [];
var s = false;
var TABLE = exports.segment.getDict('TABLE2');
// 匹配可能出现的单词
while (cur < text.length) {
for (var i in TABLE) {
var w = text.substr(cur, i);
if (w in TABLE[i]) {
ret.push({w: w, c: cur, f: TABLE[i][w].f});
}
}
cur++;
}
return filterWord(ret, preword, text);
};
//debug(matchWord('长春市长春药店'));
/**
* 选择最有可能匹配的单词
*
* @param {array} words 单词信息数组
* @param {object} preword 上一个单词
* @param {string} text 本节要分词的文本
* @return {array}
*/
var filterWord = function (words, preword, text) {
var POSTAG = exports.segment.POSTAG;
var TABLE = exports.segment.getDict('TABLE');
var ret = [];
// 将单词按位置分组
var wordpos = getPosInfo(words, text);
//debug(wordpos);
// 使用类似于MMSG的分词算法
// 找出所有分词可能,主要根据一下几项来评价:
// x、词数量最少;
// a、词平均频率最大;
// b、每个词长度标准差最小;
// c、未识别词最少;
// d、符合语法结构项:如两个连续的动词减分,数词后面跟量词加分;
// 取以上几项综合排名最最好的
var chunks = getChunks(wordpos, 0, text);
//debug(chunks);
var assess = []; // 评价表
// 对各个分支就行评估
for (var i = 0, chunk; chunk = chunks[i]; i++) {
assess[i] = {x: chunk.length, a:0, b:0, c:0, d:0}
// 词平均长度
var sp = text.length / chunk.length;
// 句子经常包含的语法结构
var has_D_V = false; // 是否包含动词
// 遍历各个词
if (preword) {
var prew = {w: preword.w, p: preword.p, f: preword.f}
} else {
prew = false;
}
for (var j = 0, w; w = chunk[j]; j++) {
if (w.w in TABLE) {
w.p = TABLE[w.w].p;
assess[i].a += w.f; // 总词频
// ================ 检查语法结构 ===================
if (prew) {
// 如果上一个词是数词且当前词是量词(单位),则加分
if ((prew.p & POSTAG.A_M) > 0 &&
(((TABLE[w.w].p & POSTAG.A_Q) > 0) || w.w in DATETIME)) {
assess[i].d++;
}
// 如果当前词是动词
if ((w.p & POSTAG.D_V) > 0) {
has_D_V = true;
// 如果是连续的两个动词,则减分
//if ((prew.p & POSTAG.D_V) > 0)
//assess[i].d--;
// 如果是 形容词 + 动词,则加分
if ((prew.p & POSTAG.D_A) > 0) {
assess[i].d++;
}
}
// 如果是地区名、机构名或形容词,后面跟地区、机构、代词、名词等,则加分
if (((prew.p & POSTAG.A_NS) > 0 || (prew.p & POSTAG.A_NT) || (prew.p & POSTAG.D_A) > 0) &&
((w.p & POSTAG.D_N) > 0 || (w.p & POSTAG.A_NR) > 0 ||
(w.p & POSTAG.A_NS) > 0 || (w.p & POSTAG.A_NZ) > 0 ||
(w.p & POSTAG.A_NT) > 0
)) {
assess[i].d++;
}
// 如果是 方位词 + 数量词,则加分
if ((prew.p & POSTAG.D_F) > 0 &&
((w.p & POSTAG.A_M > 0) || w.p & POSTAG.D_MQ > 0)) {
//debug(prew, w);
assess[i].d++;
}
// 如果是 姓 + 名词,则加分
if ((prew.w in FAMILY_NAME_1 || prew.w in FAMILY_NAME_2) &&
((w.p & POSTAG.D_N) > 0 || (w.p & POSTAG.A_NZ) > 0)) {
//debug(prew, w);
assess[i].d++;
}
// 探测下一个词
var nextw = chunk[j + 1];
if (nextw) {
if (nextw.w in TABLE) {
nextw.p = TABLE[nextw.w].p;
}
// 如果是连词,前后两个词词性相同则加分
if ((w.p & POSTAG.D_C) > 0 && prew.p == nextw.p) {
assess[i].d++;
}
// 如果当前是“的”+ 名词,则加分
if ((w.w == '的' || w.w == '之') && (
(nextw.p & POSTAG.D_N) > 0 || (nextw.p & POSTAG.A_NR) > 0 ||
(nextw.p & POSTAG.A_NS) > 0 || (nextw.p & POSTAG.A_NZ) > 0 ||
(nextw.p & POSTAG.A_NT) > 0
)) {
assess[i].d += 1.5;
}
}
}
// ===========================================
} else {
assess[i].c++; // 未识别的词数量
}
// 标准差
assess[i].b += Math.pow(sp - w.w.length, 2);
prew = chunk[j];
}
// 如果句子中包含了至少一个动词
if (has_D_V === false) assess[i].d -= 0.5;
assess[i].a = assess[i].a / chunk.length;
assess[i].b = assess[i].b / chunk.length;
}
// 计算排名
var top = getTops(assess);
var currchunk = chunks[top];
// 剔除不能识别的词
for (var i = 0, word; word = currchunk[i]; i++) {
if (!(word.w in TABLE)) {
currchunk.splice(i--, 1);
}
}
ret = currchunk;
//debug(ret);
return ret;
};
/**
* 将单词按照位置排列
*
* @param {array} words
* @param {string} text
* @return {object}
*/
var getPosInfo = function (words, text) {
var wordpos = {};
// 将单词按位置分组
for (var i = 0, word; word = words[i]; i++) {
if (!wordpos[word.c]) {
wordpos[word.c] = [];
}
wordpos[word.c].push(word);
}
// 按单字分割文本,填补空缺的位置
for (var i = 0; i < text.length; i++) {
if (!wordpos[i]) {
wordpos[i] = [{w: text.charAt(i), c: i, f: 0}];
}
}
return wordpos;
};
/**
* 取所有分支
*
* @param {object} wordpos
* @param {int} pos 当前位置
* @param {string} text 本节要分词的文本
* @return {array}
*/
var getChunks = function (wordpos, pos, text) {
var words = wordpos[pos] || [];
// debug('getChunks: ');
// debug(words);
// throw new Error();
var ret = [];
for (var i = 0; i < words.length; i++) {
var word = words[i];
//debug(word);
var nextcur = word.c + word.w.length;
if (!wordpos[nextcur]) {
ret.push([word]);
} else {
var chunks = getChunks(wordpos, nextcur);
for (var j = 0; j < chunks.length; j++) {
ret.push([word].concat(chunks[j]));
}
}
}
return ret;
};
/**
* 评价排名
*
* @param {object} assess
* @return {object}
*/
var getTops = function (assess) {
//debug(assess);
// 取各项最大值
var top = {x: assess[0].x, a: assess[0].a, b: assess[0].b, c: assess[0].c, d: assess[0].d}
for (var i = 1, ass; ass = assess[i]; i++) {
if (ass.a > top.a) top.a = ass.a; // 取最大平均词频
if (ass.b < top.b) top.b = ass.b; // 取最小标准差
if (ass.c > top.c) top.c = ass.c; // 取最大未识别词
if (ass.d < top.d) top.d = ass.d; // 取最小语法分数
if (ass.x > top.x) top.x = ass.x; // 取最大单词数量
}
//debug(top);
// 评估排名
var tops = [];
for (var i = 0, ass; ass = assess[i]; i++) {
tops[i] = 0;
// 词数量,越小越好
tops[i] += (top.x - ass.x) * 1.5;
// 词总频率,越大越好
if (ass.a >= top.a) tops[i] += 1;
// 词标准差,越小越好
if (ass.b <= top.b) tops[i] += 1;
// 未识别词,越小越好
tops[i] += (top.c - ass.c);//debug(tops[i]);
// 符合语法结构程度,越大越好
tops[i] += (ass.d < 0 ? top.d + ass.d : ass.d - top.d) * 1;
//debug(tops[i]);debug('---');
}
//debug(tops.join(' '));
// 取分数最高的
var curri = 0;
var maxs = tops[0];
for (var i in tops) {
var s = tops[i];
if (s > maxs) {
curri = i;
maxs = s;
} else if (s == maxs) {
// 如果分数相同,则根据词长度、未识别词个数和平均频率来选择
var a = 0;
var b = 0;
if (assess[i].c < assess[curri].c) a++; else b++;
if (assess[i].a > assess[curri].a) a++; else b++;
if (assess[i].x < assess[curri].x) a++; else b++;
if (a > b) {
curri = i;
maxs = s;
}
}
// debug('i=' + i + ', s=' + s + ', maxs=' + maxs);
}
//debug('max: i=' + curri + ', s=' + tops[curri]);
return curri;
};