JieBa使用java
List<SegToken> process = segmenter.process("今天早上,出门的的时候,天气很好", JiebaSegmenter.SegMode.INDEX);
for (SegToken token:process){
//分词的结果
System.out.println( token.word);
}
复制代码
输出内容以下node
今天
早上
,
出门
的
的
时候
,
天气
很
好
复制代码
int N = chars.length; //获取整个句子的长度
int i = 0, j = 0; //i 表示词的开始 ;j 表示词的结束
while (i < N) {
Hit hit = trie.match(chars, i, j - i + 1); //从trie树中匹配
if (hit.isPrefix() || hit.isMatch()) {
if (hit.isMatch()) {
//彻底匹配
if (!dag.containsKey(i)) {
List<Integer> value = new ArrayList<Integer>();
dag.put(i, value);
value.add(j);
}
else
dag.get(i).add(j); //以当前字符开头的词有哪些,词结尾的坐标记下来
}
j += 1;
if (j >= N) {
//以当前字符开头的全部词已经匹配完成,再以当前字符的下一个字符开头寻找词
i += 1;
j = i;
}
}
else {
//以当前字符开头的词已经所有匹配完成,再以当前字符的下一个字符开头寻找词
i += 1;
j = i;
}
}
复制代码
好比输入的是 "今天早上"git
今/今天/早/早上/上
复制代码
JieBa内部存储了一个文件dict.txt,好比记录了 X光线 3 n
。在内部的存储trie树结构则为github
nodeState:当前DictSegment状态 ,默认 0 , 1表示从根节点到当前节点的路径表示一个词 ,好比 x光和 x光线算法
storeSize:当前节点存储的Segment数目数组
好比除了x光线以外,还有x射bash
storeSize <=ARRAY_LENGTH_LIMIT ,使用数组存储, storeSize >ARRAY_LENGTH_LIMIT,则使用Map存储 ,取值为3app
核心代码以下ui
for (int i = N - 1; i > -1; i--) {
//从右往左去查看句子,这是由于中文的重点通常在后面
//表示词的开始位置
Pair<Integer> candidate = null;
for (Integer x : dag.get(i)) {
//x表示词的结束位置
// wordDict.getFreq表示获取trie这个词的频率
//route.get(x+1)表示当前词的后一个词的几率
//因为频率自己存储的是数学上log计算后的值,这里的加法其实就是当前这个词为A而且后面紧跟着的词为B的几率,B已经由前面算出
double freq = wordDict.getFreq(sentence.substring(i, x + 1)) + route.get(x + 1).freq;
if (null == candidate) {
candidate = new Pair<Integer>(x, freq);
}
else if (candidate.freq < freq) {
//保存几率高的词
candidate.freq = freq;
candidate.key = x;
}
}
//可见route中存储的数据为key:词头下标 value:词尾下标,词的频率
route.put(i, candidate);
}
复制代码
高频词选取过程:spa
(3,<3,-5.45>) :第一个3是词头,第二个3是 '上' 的词尾下标;-5.45是它出现的几率;
(4,<0,0>):初始几率
此时 route保留了 (3,<3,-5.45>)、(4,<0,0>)和(2, <3,-10.81> )
依此类推,通过route以后的取词以下
取完了高频词以后,核心逻辑以下
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);
}
}
}
//保留多个字符组成的词
tokens.add(lWord);
}
x = y; //从当前词的词尾开始找下一个词
}
复制代码
词提取的过程
至此 '今天早上' 这句话分词结束。能够看到这都是创建在这个词已经存在于字典的基础上成立的。
若是出现了多个单个字成词的状况,好比 '出门的的时候' 中的 '的',一方面它成为了单个的词,另外一方面后面紧跟着的 '的'与它一块儿成为了两个字符组成的词,又在词典中不存在 '的的' ,于是识别为未知的词,调用 finalSeg.cut
使用的方法为Viterbi算法。首先预加载以下HMM模型的三组几率集合和隐藏状态集合
未知的词定义了4个隐藏状态。 B 表示词的开始 M 表示词的中间 E 表示词的结束 S 表示单字成词
初始化每一个隐藏状态的初始几率
start.put('B', -0.26268660809250016);
start.put('E', -3.14e+100);
start.put('M', -3.14e+100);
start.put('S', -1.4652633398537678);
复制代码
初始化状态转移矩阵
trans = new HashMap<Character, Map<Character, Double>>();
Map<Character, Double> transB = new HashMap<Character, Double>();
transB.put('E', -0.510825623765990);
transB.put('M', -0.916290731874155);
trans.put('B', transB);
Map<Character, Double> transE = new HashMap<Character, Double>();
transE.put('B', -0.5897149736854513);
transE.put('S', -0.8085250474669937);
trans.put('E', transE);
Map<Character, Double> transM = new HashMap<Character, Double>();
transM.put('E', -0.33344856811948514);
transM.put('M', -1.2603623820268226);
trans.put('M', transM);
Map<Character, Double> transS = new HashMap<Character, Double>();
transS.put('B', -0.7211965654669841);
transS.put('S', -0.6658631448798212);
trans.put('S', transS);
复制代码
好比trans.get('S').get('B')表示若是当前字符是 'S',那么下个是另外一个词(非单字成词)开始的几率为 -0.721
读取实现准备好的混淆矩阵,存入 emit中
另外它预约义了每一个隐藏状态以前只能是那些状态
prevStatus.put('B', new char[] { 'E', 'S' }); prevStatus.put('M', new char[] { 'M', 'B' }); prevStatus.put('S', new char[] { 'S', 'E' }); prevStatus.put('E', new char[] { 'B', 'M' }); 复制代码
好比 'M' 它的前面一定是 'M' 和 'B' 之间的一个
算法的流程以下:
for (char state : states) {
Double emP = emit.get(state).get(sentence.charAt(0));
if (null == emP)
emP = MIN_FLOAT;
//存储第一个字符 是 'B' 'E' 'M' 'S'的几率,即初始化转移几率
v.get(0).put(state, start.get(state) + emP);
path.put(state, new Node(state, null));
}
复制代码
for (int i = 1; i < sentence.length(); ++i) {
Map<Character, Double> vv = new HashMap<Character, Double>();
v.add(vv);
Map<Character, Node> newPath = new HashMap<Character, Node>();
for (char y : states) {
//y表示隐藏状态
//emp是获取混淆矩阵的几率,好比 在 'B'发生的状况下,观察到字符 '要' 的几率
Double emp = emit.get(y).get(sentence.charAt(i));
if (emp == null)
emp = MIN_FLOAT; //样本中没有,就设置为最小的几率
Pair<Character> candidate = null;
for (char y0 : prevStatus.get(y)) {
Double tranp = trans.get(y0).get(y);//获取状态转移几率,好比 E -> B
if (null == tranp)
tranp = MIN_FLOAT; //转移几率不存在,取最低的
//v中放的是当前字符的前一个字符的几率,即前一个状态的最优解
//tranp 是状态转移的几率
//三者相加即计算已知观察序列和HMM的条件下,求得最可能的隐藏序列的几率
tranp += (emp + v.get(i - 1).get(y0));
if (null == candidate)
candidate = new Pair<Character>(y0, tranp);
else if (candidate.freq <= tranp) {
//存储最优可能的隐藏几率
candidate.freq = tranp;
candidate.key = y0;
}
}
//存储是'B'仍是 'E'各自的几率
vv.put(y, candidate.freq);
//记下先后两个词最优的路径,以便还原原始的隐藏状态分隔点
newPath.put(y, new Node(y, path.get(candidate.key)));
}
//存储最终句子的最优路径
path = newPath;
}
复制代码
double probE = v.get(sentence.length() - 1).get('E');
double probS = v.get(sentence.length() - 1).get('S');
Vector<Character> posList = new Vector<Character>(sentence.length());
Node win;
if (probE < probS)
win = path.get('S');
else
win = path.get('E');
while (win != null) {
//沿着指针找到句子的每一个字符的个子位置
posList.add(win.value);
win = win.parent;
}
Collections.reverse(posList);
复制代码
int begin = 0, next = 0;
for (int i = 0; i < sentence.length(); ++i) {
char pos = posList.get(i);
if (pos == 'B')
begin = i;
else if (pos == 'E') {
//到词尾了,记下
tokens.add(sentence.substring(begin, i + 1));
next = i + 1;
}
else if (pos == 'S') {
//单个字成词,记下
tokens.add(sentence.substring(i, i + 1));
next = i + 1;
}
}
if (next < sentence.length())
tokens.add(sentence.substring(next));
复制代码
自此执行结束