热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

SmartChineseAnalyzer源码分析

smartcn是lucene自带的一个中文分词工具,它源自中科院的ICTCLAS中文分词系统。关于ICTCLAS的算法研究,可以参考这里。SmartChineseAnalyzer里的行为分析,可以从reusableTokenStream或tokenStream方法开始入手。其中前者可以重复使用以提高性能(简单看一下,像

smartcnlucene自带的一个中文分词工具,它源自中科院的ICTCLAS中文分词系统。关于ICTCLAS的算法研究,可以参考这里。SmartChineseAnalyzer里的行为分析,可以从reusableTokenStream或tokenStream方法开始入手。其中前者可以重复使用以提高性能(简单看一下,像是有些实例放到了ThreadLocal里,下次再调用时避免了重新构建)。以下是我以reusableTokenStream为切入点做的分析笔记。

reusableTokenStream方法判断是否已有SavedStreams,如果没有则创建,否则重置一些状态后,直接返回。我只关注新建过程。新建streams其实并未进行实际操作。
streams = new SavedStreams();
setPreviousTokenStream(streams);
streams.tokenStream = new SentenceTokenizer(reader);
streams.filteredTokenStream = new WordTokenFilter(streams.tokenStream);
streams.filteredTokenStream = new PorterStemFilter(streams.filteredTokenStream);
if (!stopWords.isEmpty()) {
streams.filteredTokenStream = new StopFilter(StopFilter.getEnablePositionIncrementsVersionDefault(matchVersion),
streams.filteredTokenStream, stopWords, false);
}

此方法返回TokenStream实例。调用TokenStream的incrementToken()方法可以逐个获取分词。跟踪incrementToken,来到PorterStemFilter.incrementToken(),而此方法又调用了WordTokenFilter.incrementToken()。

WordTokenFilter.incrementToken()里首先调用SentenceTokenizer.incrementToken()进行断句。句子结束的标志为:碰到句号及其它表明句字结束的标点符号(定义在Utility.PUNCTION中),或者碰到文本结束。其中,空字符(Utility.SPACES)会被忽略。

断完句子之后,调用segmentSentence()对句子进行切分。segmentSentence()方法里最重要的一步就是调HHMMSegmenter.process()进行分词处理。此process()方法是算法核心所在。而其类名则表明其基于隐马尔科夫算法(Hidden Markov Model, HMM)。来看看此方法:
/**
* Return a list of {@link SegToken} representing the best segmentation of a sentence
* @param sentence input sentence
* @return best segmentation as a {@link List}
*/
public List process(String sentence) {
SegGraph segGraph = createSegGraph(sentence);
BiSegGraph biSegGraph = new BiSegGraph(segGraph);
List shortPath = biSegGraph.getShortPath();
return shortPath;
}

这里有三个步骤,第一步是生成SegGraph,第二步则是创建BiSegGraph,第三步寻找最短路径作为结果。关于这里的两个图,可参考ICTCLAS的算法介绍中的相关部分。大致上,第一个图是所有可能的词,第二个图则是词与词之间可能的结合(有向图的路径)。这两个图中结点都有权重属性。后一个图的权重是根据两个词组合的频率经过平滑算法得出的。寻找最短路径其实就是在寻找最佳词语组合。以下对相关代码进行注释。

第一步:org.apache.lucene.analysis.cn.smart.hhmm.HHMMSegmenter.createSegGraph(String)。SegGraph的主要属性是一个键为整数,值为SegToken列表的Map:

private Map> tokenListTable = new HashMap>();

其键其实是切分出来的词语在源字符串中的起始位置,其另外一个属性maxStart = -1,用来记录最大起始位置。
/**
* Create the {@link SegGraph} for a sentence.
*
* @param sentence
* input sentence, without start and end markers
* @return {@link SegGraph} corresponding to the input sentence.
*/
private SegGraph createSegGraph(String sentence) {
int i = 0, j;
int length = sentence.length();
int foundIndex;
int[] charTypeArray = getCharTypes(sentence);
StringBuilder wordBuf = new StringBuilder();
SegToken token;
int frequency = 0; // the number of times word appears.
boolean hasFullWidth;
int wordType;
char[] charArray;

SegGraph segGraph = new SegGraph();
/* 从头至尾处理 */
while (i hasFullWidth = false;
switch (charTypeArray[i]) {
case CharType.SPACE_LIKE:
i++;
/* 略去空格 */
break;
case CharType.HANZI:
/* 处理汉字 */
j = i + 1;
wordBuf.delete(0, wordBuf.length());
// It doesn't matter if a single Chinese character (Hanzi) can
// form a phrase or not,
// it will store that single Chinese character (Hanzi) in the
// SegGraph. Otherwise, it will
// cause word division.
wordBuf.append(sentence.charAt(i));
charArray = new char[] { sentence.charAt(i) };
frequency = wordDict.getFrequency(charArray);
token = new SegToken(charArray, i, j, WordType.CHINESE_WORD,
frequency);
/* 加入单个汉字 */
segGraph.addToken(token);

foundIndex = wordDict.getPrefixMatch(charArray);
/* 寻找向后组合所能构成的所有词语并添加到图中 foundIndex != –1 表示有可能构成词 */
while (j <= length && foundIndex != -1) {
/* 如果已经构成词语(上面说有可能,是因为这也可能只是一个词语的前缀) */
if (wordDict.isEqual(charArray, foundIndex)
&& charArray.length > 1) {
// It is the phrase we are looking for; In other words,
// we have found a phrase SegToken
// from i to j. It is not a monosyllabic word (single
// word).
frequency = wordDict.getFrequency(charArray);
token = new SegToken(charArray, i, j,
WordType.CHINESE_WORD, frequency);
segGraph.addToken(token);
}

/* 向后组词时略去空格(空格格开的词smartcn一样可以正确切分出来) */
while (j && charTypeArray[j] == CharType.SPACE_LIKE)
j++;

/* 如果是汉字,继续拼加并测试是否成词 */
if (j wordBuf.append(sentence.charAt(j));
charArray = new char[wordBuf.length()];
wordBuf.getChars(0, charArray.length, charArray, 0);
// idArray has been found (foundWordIndex!=-1) as a
// prefix before.
// Therefore, idArray after it has been lengthened can
// only appear after foundWordIndex.
// So start searching after foundWordIndex.
foundIndex = wordDict.getPrefixMatch(charArray,
foundIndex);
j++;
} else {
break; /* 结束或不是汉字 */
}
}
i++;
break;
/* 以下是处理其它字符类型,这里就不作分析了 */
case CharType.FULLWIDTH_LETTER:
hasFullWidth = true;
case CharType.LETTER:
j = i + 1;
while (j && (charTypeArray[j] == CharType.LETTER || charTypeArray[j] == CharType.FULLWIDTH_LETTER)) {
if (charTypeArray[j] == CharType.FULLWIDTH_LETTER)
hasFullWidth = true;
j++;
}
// Found a Token from i to j. Type is LETTER char string.
charArray = Utility.STRING_CHAR_ARRAY;
frequency = wordDict.getFrequency(charArray);
wordType = hasFullWidth ? WordType.FULLWIDTH_STRING
: WordType.STRING;
token = new SegToken(charArray, i, j, wordType, frequency);
segGraph.addToken(token);
i = j;
break;
case CharType.FULLWIDTH_DIGIT:
hasFullWidth = true;
case CharType.DIGIT:
j = i + 1;
while (j && (charTypeArray[j] == CharType.DIGIT || charTypeArray[j] == CharType.FULLWIDTH_DIGIT)) {
if (charTypeArray[j] == CharType.FULLWIDTH_DIGIT)
hasFullWidth = true;
j++;
}
// Found a Token from i to j. Type is NUMBER char string.
charArray = Utility.NUMBER_CHAR_ARRAY;
frequency = wordDict.getFrequency(charArray);
wordType = hasFullWidth ? WordType.FULLWIDTH_NUMBER
: WordType.NUMBER;
token = new SegToken(charArray, i, j, wordType, frequency);
segGraph.addToken(token);
i = j;
break;
case CharType.DELIMITER:
j = i + 1;
// No need to search the weight for the punctuation. Picking the
// highest frequency will work.
frequency = Utility.MAX_FREQUENCE;
charArray = new char[] { sentence.charAt(i) };
token = new SegToken(charArray, i, j, WordType.DELIMITER,
frequency);
segGraph.addToken(token);
i = j;
break;
default:
j = i + 1;
// Treat the unrecognized char symbol as unknown string.
// For example, any symbol not in GB2312 is treated as one of
// these.
charArray = Utility.STRING_CHAR_ARRAY;
frequency = wordDict.getFrequency(charArray);
token = new SegToken(charArray, i, j, WordType.STRING,
frequency);
segGraph.addToken(token);
i = j;
break;
}
}

// Add two more Tokens: "beginning xx beginning"
/* 在最前面添加起始标志以方便后续处理(请参考ICTCLAS的算法) */
charArray = Utility.START_CHAR_ARRAY;
frequency = wordDict.getFrequency(charArray);
token = new SegToken(charArray, -1, 0, WordType.SENTENCE_BEGIN,
frequency);
segGraph.addToken(token);

// "end xx end"
/* 同上,添加结束标志 */
charArray = Utility.END_CHAR_ARRAY;
frequency = wordDict.getFrequency(charArray);
token = new SegToken(charArray, length, length + 1,
WordType.SENTENCE_END, frequency);
segGraph.addToken(token);

return segGraph;
}

第二步:org.apache.lucene.analysis.cn.smart.hhmm.BiSegGraph.generateBiSegGraph(SegGraph),创建BiSegGraph。

BiSegGraph在构造方法里先对segGraph进行了索引的生成,也就是给所有词一个索引号,后面会用到这个。然后就是调用generateBiSegGraph()来生成路径图。
/*
* Generate a BiSegGraph based upon a SegGraph
*/
private void generateBiSegGraph(SegGraph segGraph) {
double smooth = 0.1;
int wordPairFreq = 0;
int maxStart = segGraph.getMaxStart();
double oneWordFreq, weight, tinyDouble = 1.0 / Utility.MAX_FREQUENCE;

int next;
char[] idBuffer;
// get the list of tokens ordered and indexed
segTokenList = segGraph.makeIndex(); /* 这个是不是重复了呢?前面构造方法里已经做过了 */
// Because the beginning position of startToken is -1, therefore
// startToken can be obtained when key = -1
int key = -1;
List nextTokens = null;
/* 从头到尾处理 */
while (key /* 判断进入点是否存在。(可对照参考前面的源码) */
if (segGraph.isStartExist(key)) {

List tokenList = segGraph.getStartList(key);

/**
* 处理给定key(也即起始位置)的所有token,对这些token分别去遍历它们下面可以邻接的token,并将
* 它们的邻接关系(token pair)存入图中。这个token对儿其实就是token之前的路径。
*/
// Calculate all tokens for a given key.
for (SegToken t1 : tokenList) {
OneWordFreq= t1.weight;
next = t1.endOffset;
nextTokens = null;
// Find the next corresponding Token.
// For example: "Sunny seashore", the present Token is
// "sunny", next one should be "sea" or "seashore".
// If we cannot find the next Token, then go to the end and
// repeat the same cycle.
/* 寻找可邻接的token的起始位置 */
while (next <= maxStart) {
// Because the beginning position of endToken is
// sentenceLen, so equal to sentenceLen can find
// endToken.
if (segGraph.isStartExist(next)) {
nextTokens = segGraph.getStartList(next);
break;
}
next++;
}
if (nextTokens == null) {
break;
}
/* 遍历可邻接的tokens */
for (SegToken t2 : nextTokens) {
idBuffer = new char[t1.charArray.length
+ t2.charArray.length + 1];
System.arraycopy(t1.charArray, 0, idBuffer, 0,
t1.charArray.length);
idBuffer[t1.charArray.length] = BigramDictionary.WORD_SEGMENT_CHAR;
System.arraycopy(t2.charArray, 0, idBuffer,
t1.charArray.length + 1, t2.charArray.length);

// Two linked Words frequency
wordPairFreq = bigramDict.getFrequency(idBuffer);

/* 计算权重的平滑算法还没研究,现在数学忘光了,汗一个!抽空再分析这里 */

// Smoothing

// -log{a*P(Ci-1)+(1-a)P(Ci|Ci-1)} Note 0 weight = -Math.log(smooth
* (1.0 + oneWordFreq)
/ (Utility.MAX_FREQUENCE + 0.0)
+ (1.0 - smooth)
* ((1.0 - tinyDouble) * wordPairFreq
/ (1.0 + oneWordFreq) + tinyDouble));

SegTokenPair tokenPair = new SegTokenPair(idBuffer,
t1.index, t2.index, weight);
this.addSegTokenPair(tokenPair);
}
}
}
key++;
}
}

第三步:org.apache.lucene.analysis.cn.smart.hhmm.BiSegGraph.getShortPath(),寻找最佳路径。这部分其实就是一个动态规划算法,可以参考数据结构和算法方面的书。
/**
* Find the shortest path with the Viterbi algorithm.
*
* @return {@link List}
*/
public List getShortPath() {
int current;
int nodeCount = getToCount();
List path = new ArrayList();
PathNode zeroPath = new PathNode();
zeroPath.weight = 0;
zeroPath.preNode = 0;
path.add(zeroPath); /* 插了个头,当做起始“原点”吧 */
/**
* 从头到尾,计算每步的最佳路径并保存相应信息。后面的计算会采用前面的数据,
* 所以此循环处理完成后,可以直接从最后一个节点顺着向前找出最佳路径。
*/
for (current = 1; current <= nodeCount; current++) {
double weight;
List edges = getToList(current);

double minWeight = Double.MAX_VALUE;
SegTokenPair minEdge = null;
for (SegTokenPair edge : edges) {
weight = edge.weight;
PathNode preNode = path.get(edge.from);
if (preNode.weight + weight minWeight = preNode.weight + weight;
minEdge = edge;
}
}
PathNode newNode = new PathNode();
newNode.weight = minWeight;
newNode.preNode = minEdge.from;
path.add(newNode);
}

// Calculate PathNodes
int preNode, lastNode;
lastNode = path.size() - 1;
current = lastNode;
List rpath = new ArrayList(); /* 觉得这里有点多余 */
List resultPath = new ArrayList();

rpath.add(current);
while (current != 0) {
PathNode currentPathNode = path.get(current);
preNode = currentPathNode.preNode;
rpath.add(Integer.valueOf(preNode));
current = preNode;
}
/* 为什么上一步不直接生成resultPath呢?是不是有点多余? */
for (int j = rpath.size() - 1; j >= 0; j--) {
Integer idInteger = (Integer) rpath.get(j);
int id = idInteger.intValue();
SegToken t = segTokenList.get(id);
resultPath.add(t);
}
return resultPath;
}

smartcn的主要算法过程就是这样子。传说HMM算法比较学院派,比较复杂。今天看来,主要流程还是挺好理解。当然得感谢ICTCLAS的原理讲解,不然只看代码还是会很费劲儿的。下一步要学学它的Smooth算法,分析一下它的词典(org.apache.lucene.analysis.cn.smart.hhmm.WordDictionary)和词语组合的二元字典(org.apache.lucene.analysis.cn.smart.hhmm.BigramDictionary)。


推荐阅读
  • 一、Hadoop来历Hadoop的思想来源于Google在做搜索引擎的时候出现一个很大的问题就是这么多网页我如何才能以最快的速度来搜索到,由于这个问题Google发明 ... [详细]
  • 云原生边缘计算之KubeEdge简介及功能特点
    本文介绍了云原生边缘计算中的KubeEdge系统,该系统是一个开源系统,用于将容器化应用程序编排功能扩展到Edge的主机。它基于Kubernetes构建,并为网络应用程序提供基础架构支持。同时,KubeEdge具有离线模式、基于Kubernetes的节点、群集、应用程序和设备管理、资源优化等特点。此外,KubeEdge还支持跨平台工作,在私有、公共和混合云中都可以运行。同时,KubeEdge还提供数据管理和数据分析管道引擎的支持。最后,本文还介绍了KubeEdge系统生成证书的方法。 ... [详细]
  • [译]技术公司十年经验的职场生涯回顾
    本文是一位在技术公司工作十年的职场人士对自己职业生涯的总结回顾。她的职业规划与众不同,令人深思又有趣。其中涉及到的内容有机器学习、创新创业以及引用了女性主义者在TED演讲中的部分讲义。文章表达了对职业生涯的愿望和希望,认为人类有能力不断改善自己。 ... [详细]
  • 解决Cydia数据库错误:could not open file /var/lib/dpkg/status 的方法
    本文介绍了解决iOS系统中Cydia数据库错误的方法。通过使用苹果电脑上的Impactor工具和NewTerm软件,以及ifunbox工具和终端命令,可以解决该问题。具体步骤包括下载所需工具、连接手机到电脑、安装NewTerm、下载ifunbox并注册Dropbox账号、下载并解压lib.zip文件、将lib文件夹拖入Books文件夹中,并将lib文件夹拷贝到/var/目录下。以上方法适用于已经越狱且出现Cydia数据库错误的iPhone手机。 ... [详细]
  • sklearn数据集库中的常用数据集类型介绍
    本文介绍了sklearn数据集库中常用的数据集类型,包括玩具数据集和样本生成器。其中详细介绍了波士顿房价数据集,包含了波士顿506处房屋的13种不同特征以及房屋价格,适用于回归任务。 ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • 生成式对抗网络模型综述摘要生成式对抗网络模型(GAN)是基于深度学习的一种强大的生成模型,可以应用于计算机视觉、自然语言处理、半监督学习等重要领域。生成式对抗网络 ... [详细]
  • 本文介绍了设计师伊振华受邀参与沈阳市智慧城市运行管理中心项目的整体设计,并以数字赋能和创新驱动高质量发展的理念,建设了集成、智慧、高效的一体化城市综合管理平台,促进了城市的数字化转型。该中心被称为当代城市的智能心脏,为沈阳市的智慧城市建设做出了重要贡献。 ... [详细]
  • 本文介绍了在Win10上安装WinPythonHadoop的详细步骤,包括安装Python环境、安装JDK8、安装pyspark、安装Hadoop和Spark、设置环境变量、下载winutils.exe等。同时提醒注意Hadoop版本与pyspark版本的一致性,并建议重启电脑以确保安装成功。 ... [详细]
  • 自动轮播,反转播放的ViewPagerAdapter的使用方法和效果展示
    本文介绍了如何使用自动轮播、反转播放的ViewPagerAdapter,并展示了其效果。该ViewPagerAdapter支持无限循环、触摸暂停、切换缩放等功能。同时提供了使用GIF.gif的示例和github地址。通过LoopFragmentPagerAdapter类的getActualCount、getActualItem和getActualPagerTitle方法可以实现自定义的循环效果和标题展示。 ... [详细]
  • 本文介绍了Linux系统中正则表达式的基础知识,包括正则表达式的简介、字符分类、普通字符和元字符的区别,以及在学习过程中需要注意的事项。同时提醒读者要注意正则表达式与通配符的区别,并给出了使用正则表达式时的一些建议。本文适合初学者了解Linux系统中的正则表达式,并提供了学习的参考资料。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • Android系统移植与调试之如何修改Android设备状态条上音量加减键在横竖屏切换的时候的显示于隐藏
    本文介绍了如何修改Android设备状态条上音量加减键在横竖屏切换时的显示与隐藏。通过修改系统文件system_bar.xml实现了该功能,并分享了解决思路和经验。 ... [详细]
  • ch3中可视化软件pangolin的安装步骤及注意事项
    本文介绍了在ch3中安装可视化软件pangolin的步骤及注意事项。首先提供了pangolin的下载地址,并说明了下载后需要放到与虚拟机交互的文件夹地址。然后详细介绍了安装pangolin所需的依赖项,并提供了在终端进行安装的命令。最后给出了解压pangolin的步骤。 ... [详细]
  • FeatureRequestIsyourfeaturerequestrelatedtoaproblem?Please ... [详细]
author-avatar
捡到宝开封
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有