课程
4.2.3 案例中 `Bigram._forwardSplitSentence` 函数分析
4.2.3 案例中 Bigram._forwardSplitSentence 函数分析
源码位置:chapter04/4.2.3/4.2.3.py:13
1. 函数作用
_forwardSplitSentence(self, sentence, word_max_len=5) 的作用是:
- 对输入句子进行前向递归切分;
- 枚举出该句子的所有可能切分方案;
- 为后续
_maxP()函数做准备; - 最终由
_maxP()在这些候选方案中选出二元语法概率最大的那一种。
也就是说,这个函数本身不直接决定最终答案,它的任务是“先把所有可能的切法找出来”。
2. 带注释的函数代码
def _forwardSplitSentence(self, sentence, word_max_len=5):
split_groups = []
sentence = sentence.strip()
sentence_len = len(sentence)
if sentence_len < 2:
return [[sentence]]
range_len = [sentence_len, word_max_len][sentence_len > word_max_len]
current_groups = []
single_cut = True
for i in range(1, range_len)[::-1]:
if self.DICT.__contains__(sentence[:i]) and i != 1:
current_groups.append([sentence[:i], sentence[i:]])
single_cut = False
if single_cut or self.DICT.__contains__(sentence[1:3]):
current_groups.append([sentence[:1], sentence[1:]])
if sentence_len == 2:
current_groups.append([sentence])
for one_group in current_groups:
if len(one_group) == 1:
split_groups.append(one_group)
continue
for child_group in self._forwardSplitSentence(one_group[1]):
child_group.insert(0, one_group[0])
split_groups.append(child_group)
return split_groups
下面给出逐句注释:
-
split_groups = []- 保存当前
sentence的所有完整切分方案。
- 保存当前
-
sentence = sentence.strip()- 去掉首尾空白字符,避免影响分词。
-
sentence_len = len(sentence)- 记录当前待切分串的长度。
-
if sentence_len < 2: return [[sentence]]- 递归终止条件。
- 当只剩 0 或 1 个字时,已经无法继续切分,直接把它看成一种方案返回。
- 返回的是二维列表,例如
[["我"]],因为外层列表表示“方案集合”。
-
range_len = [sentence_len, word_max_len][sentence_len > word_max_len]- 取“句长”和“最大词长”中的较小者。
- 作用是限制前缀扫描的最大长度,避免枚举过长的前缀。
-
current_groups = []- 保存当前层的“二切分”结果。
- 例如把
喜欢观赏日出先切成["喜欢", "观赏日出"]。
-
single_cut = True- 标记当前是否还没有找到合法词典前缀。
- 如果最后仍然为
True,就说明只能退化成“第一个字单独切出”。
-
for i in range(1, range_len)[::-1]- 从长到短扫描前缀长度。
- 例如
range_len = 5时,依次检查前缀长度4、3、2、1。 - 这体现了“前向最大匹配”的思想:优先尝试较长前缀。
-
if self.DICT.__contains__(sentence[:i]) and i != 1- 如果前
i个字在词典里,且i != 1,就把它当作一个合法前缀词。 - 这里故意不在循环里处理
i == 1,因为“单字切分”统一由后面的回退逻辑控制。
- 如果前
-
current_groups.append([sentence[:i], sentence[i:]])- 形成一个二切分结果:前缀词 + 剩余串。
-
single_cut = False- 说明已经找到过词典中的合法前缀,不属于“完全匹配不到”的情况。
-
if single_cut or self.DICT.__contains__(sentence[1:3])- 两种情况下,从第 1 个字后切一刀:
- 当前没有任何合法前缀词;
sentence[1:3]在词典中,说明后面两个字可能构成词,保留“首字单独成词”的可能。
- 两种情况下,从第 1 个字后切一刀:
-
current_groups.append([sentence[:1], sentence[1:]])- 把句子切成“第一个字 + 剩余部分”。
- 这样能提高对未登录词、歧义情况的覆盖能力。
-
if sentence_len == 2: current_groups.append([sentence])- 当当前串长度正好为 2 时,额外保留“不切分”的可能。
- 例如
日出既可能切成日/出,也可能整体作为一个双字词。
-
for one_group in current_groups- 遍历当前层生成的所有二切分结果。
-
if len(one_group) == 1- 说明这是“长度为 2 时整体保留”的情形,例如
["日出"]。 - 已经是一条完整方案,直接加入结果。
- 说明这是“长度为 2 时整体保留”的情形,例如
-
for child_group in self._forwardSplitSentence(one_group[1])- 对后半部分递归切分。
- 当前函数只负责确定“第一个词”,其余部分交给递归继续处理。
-
child_group.insert(0, one_group[0])- 把当前层切出的前缀词插入到子方案的最前面。
-
split_groups.append(child_group)- 得到一条完整切分路径,加入总方案集合。
-
return split_groups- 返回当前句子的所有候选切分结果。
3. 算法详细过程
该函数本质上是一个“前向扫描 + 递归枚举”算法。
第一步:预处理和递归终止
- 先去空格;
- 再判断长度;
- 如果句子长度小于 2,就直接返回。
这是递归算法的基线条件。
第二步:在当前句子上做“第一刀”
函数会在当前句子的前部寻找可作为“第一个词”的前缀。
- 前缀长度从大到小枚举;
- 只要某个前缀在词典里,就记为一种二切分方案;
- 这样可以同时保留多个候选前缀,而不是只保留最长的那一个。
例如某串既可以前切为 研究生/命起源,又可以前切为 研究/生命起源,该函数会把这些可能性都保留下来。
第三步:必要时加入“单字切分”回退方案
如果完全找不到词典前缀,算法不能停止,否则句子就会丢失。
所以它会回退到:
- 把第一个字单独切出来;
- 剩余部分继续递归处理。
这一步保证了任何句子都能被切分。
第四步:长度为 2 时额外保留整体成词
双字串在中文里很常见,例如:
- 日出
- 学习
- 中文
因此当剩余串长度等于 2 时,程序会同时保留:
- 切开:
日/出 - 不切:
日出
为后续概率比较留下空间。
第五步:递归求解后半部分
对于当前得到的每个二切分 [前缀, 后缀]:
- 前缀已经确定;
- 后缀继续调用
_forwardSplitSentence(); - 递归返回后,再把前缀接回每个子方案前面。
这样就能把局部二切分逐层扩展成完整句子的切分方案。
第六步:返回所有候选方案
函数最终返回的是一个二维列表,例如:
[
['我', '喜欢', '观赏', '日', '出'],
['我', '喜欢', '观赏', '日出']
]
这些候选并不代表最终结果,真正的最优方案由 _maxP() 用二元语法概率再筛选一次。
4. 以“我喜欢观赏日出”为例说明执行过程
程序主函数中的测试句为:我喜欢观赏日出。
在当前词典下,_forwardSplitSentence() 共生成 2 种候选方案:
['我', '喜欢', '观赏', '日', '出']
['我', '喜欢', '观赏', '日出']
具体递归过程如下。
第 1 层:处理“我喜欢观赏日出”
- 句长大于 2,进入扫描;
- 由于前部没有更长合法前缀被保留,程序回退到单字切分;
- 得到:
['我', '喜欢观赏日出']。
第 2 层:处理“喜欢观赏日出”
- 扫描到合法前缀
喜欢; - 得到:
['喜欢', '观赏日出']。
第 3 层:处理“观赏日出”
- 扫描到合法前缀
观赏; - 得到:
['观赏', '日出']。
第 4 层:处理“日出”
因为长度等于 2,所以会保留两种情况:
['日', '出']['日出']
于是向上回溯后,得到完整方案:
我 / 喜欢 / 观赏 / 日 / 出我 / 喜欢 / 观赏 / 日出
接着 _maxP() 比较这两种方案的二元概率,最后输出:
['我', '喜欢', '观赏', '日出']
5. 算法特点总结
优点
- 能枚举多种候选切分,避免单纯最大匹配的局限;
- 通过递归把局部切分扩展为全局方案;
- 能处理未登录词,因为保留了单字回退策略;
- 给后续概率模型提供了充分候选空间。
局限
- 候选方案数可能随着句长增加而增多;
_forwardSplitSentence()只负责“列举方案”,不负责判断优劣;- 最终效果依赖词典覆盖率以及
_maxP()中的二元概率质量。
6. 一句话结论
_forwardSplitSentence() 的核心思想是:
先从左到右找出所有可能的前缀词,再递归切分剩余子串,最终生成整句的所有候选切分方案,供二元语法模型选择最优结果。













