中文分词功能是一项常用的基础功能,有很多开源的工程实现,目前能应用于Android手机端的中文分词器没有很完善的版本。经过调研,我选择了结巴分词,该开源工程思路简单,易于理解,分词效果也还不错,目前有众多语言版本,PYTHON、C++、JAVA、IOS等,暂时还没有Android版本,所以我在Java版本的基础上进行了移植,开发了适用于Android手机的结巴分词Android版(Github)。
相比于Java版本的实现,Android版将字典文件存放在Asset目录下进行读取,同时对字典加载速度进行了大幅优化。原始的Java版本加载完整的字典文件在测试手机上需要28秒,时间太长,经过优化,成功将加载时间降到1.5秒,分词速度1秒以内,满足了Android手机的启动速度要求。
本文将结合代码通过以下三个方面展开介绍:结巴分词的基本原理,Android版的接入方式,以及启动速度优化的实现。
结巴分词的原理
结巴分词采用两种方式进行分词,基于字典的分词和基于HMM(隐马尔科夫模型)的分词。模型会首先加载词典文件生成一个字典树,并利用该字典树进行一段中文的分词,比如“我要去五道口吃肯德基”被分词成“我/要/去/五道口/吃/肯德基”,其中被分成单蹦个的连续中文字符,如“我/要/去”会继续经过HMM模型进行二次分词,看能不能合并成完整的单词,这种设计是为了对不在字典中的字符提供一种兜底的分词方案,可以尽可能的避免单蹦个的分词结果,优化分词的效果。
下面是进行分词的主函数:
private List<String> sentenceProcess(String sentence) {
List<String> tokens = new ArrayList<String>();
int N = sentence.length();
long start = System.currentTimeMillis();
// 将一段文字转换成有向无环图,该有向无环图包含了跟字典文件得出的所有可能的单词切分
Map<Integer, List<Integer>> dag = createDAG(sentence);
Map<Integer, Pair<Integer>> route = calc(sentence, dag);
int x = 0;
int y = 0;
String buf;
StringBuilder sb = new StringBuilder();
while (x < N) {
// 遍历一遍贪心算法生成的最小路径分词结果,对单蹦个的字符看看能不能粘合成一个词汇
y = route.get(x).key + 1;
String lWord = sentence.substring(x, y);
if (y - x == 1)
sb.append(lWord);
else {
if (sb.length() > 0) {
buf = sb.toString();
sb = new StringBuilder();
if (buf.length() == 1) {
// 如果两个单词之间只有一个单蹦个的字符,添加
tokens.add(buf);
} else {
if (wordDict.containsWord(buf)) {
// 如果连续单蹦个的字符粘合成的一个单词在字典树里,作为一个单词添加
tokens.add(buf);
} else {
finalSeg.cut(buf, tokens); // 如果连续单蹦个的字符粘合成的一个单词不在字典树里,使用维特比算法计算每个字符BMES如何选择使得概率最大
}
}
}
tokens.add(lWord);
}
x = y;
}
buf = sb.toString();
if (buf.length() > 0) {
// 处理余下的部分
if (buf.length() == 1) {
tokens.add(buf);
} else {
if (wordDict.containsWord(buf)) {
tokens.add(buf);
} else {
finalSeg.cut(buf, tokens);
}
}
}
return tokens;
}
该函数首先通过createDAG将输入的一段文字转换成有向无环图(DAG),该有向无环图包含了根据字典文件得出的所有可能的单词切分,以每个字为单位,比如“我去五道口吃肯德基”,经过createDAG处理后会生成每个字和后面字符可能的单词组合,比如“我/去/五道口/吃/肯德基”/“我去/五道口/吃/肯德基”/“我去/五/道口/吃/肯德基”/“我/去/五道口/吃/肯德/基”等等。
然后经过calc函数,对这个DAG从后向前依据贪婪算法选择一种分词方式。实现比较简单,从最后一个字开始,找出从该字符前面字符跳转到当前字符概率最大的切分方式,然后依次往前走,直到完成整句话的切分。概率的大小依据是字典中该单词的频率值。
/**
* 计算有向无环图的一条最大路径,从后向前,利用贪心算法,每一步只需要找出到达该字符的最大概率字符作为所选择的路径
*
* @param sentence
* @param dag
* @return
*/
private Map<Integer, Pair<Integer>> calc(String sentence, Map<Integer, List<Integer>> dag) {
int N = sentence.length();
HashMap<Integer