问题定义
给定数组arr,长度T,其中某些arr[i]到arr[j]存在转移概率,将arr分成k组,使得arr的整体分组和最大。 对于arr的每一个子数组arr[i..j],其分组值定义为 arr[i]到arr[j]的转移概率 arr的分组和是其每一个子数组的分组值之和
问题求解
正向定义
定义数据g[i]为arr[0..i]的最大分组和 在g[j], 0 <= j < i已知的前提下,将 arr[j .. i] 分成一组,则g[i] = g[j] + trans[j, i] ,所以 $$g[i] = max(g[j] + trans(j, i)), 0 \le j < i$$ g[T - 1]即为所求。
反向定义
定义数据g[i]为arr[i..T-1]的最大分组和,其余定义与正向定义类似
说明
jieba/init.py中的Tokenizer.calc与wordseg/ViterbiCWS.py中的ViterbiSearch的实现是等价的,均可实现序列的最大分组,不同点在于
- DAG的定义不同 如对于句子"学生会主动完成作业",
{0: [0, 1, 2], 1: [1], 2: [2, 3], 3: [3, 4], 4: [4], 5: [5, 6], 6: [6], 7: [7, 8], 8: [8]} # jieba
{0: [0], 1: [0, 1], 2: [0, 2], 3: [3], 4: [3, 4], 5: [5], 6: [5, 6], 7: [7], 8: [7, 8]} # ViterbiSearch
- 用于实现DP的状态定义不同 jieba使用的正向定义,ViterbiSearch使用的是反向定义
代码实现
使用反向定义,实现逻辑与ViterbiSearch一致。 可以尝试提升“学生会”的词频,使得cut能且分出“学生会”。 或者提升 长江大桥 的词频,使得cut能切分出 “长江大桥”
freq = {
"学生": 400,
"学生会": 20,
"完成": 500,
"作业": 300,
"主动": 400,
"学": 10,
"生": 10,
"会": 20,
"主": 50,
"动": 10,
"完": 100,
"成": 55,
"作": 40,
"业": 5,
"大桥": 20,
"南京": 60,
"南京市": 30,
"市长": 40,
"江大桥": 888,
"长江大桥": 666,
"南": 10,
"京": 40,
"市": 6,
"长": 7,
"江": 50,
"大": 5,
"桥": 40,
}
total = 3000
def build_dag(sentence: str) -> dict:
"""
构建以每个token结尾的有向词图
如 "学生会主动完成作业" => {0: [0], 1: [0, 1], 2: [0, 2], 3: [3], 4: [3, 4], 5: [5], 6: [5, 6], 7: [7], 8: [7, 8]}
:param sentence:
:return:
"""
n = len(sentence)
dag = {
i: [j for j in range(i + 1) if freq.get(sentence[j:i + 1], 0)]
for i in range(n)
}
return dag
def route(sentence: str, dag: dict):
"""
g[i] = (prob, j),prob为sentence[0..i]的最大分组和,j为sentence[i]所属的分组起始位置
g[i].prob = max(g[j] + trans(j, i)), 0 <= j < i
g[i].j = argmax max(g[j] + trans(j, i)), 0 <= j < i
:param sentence:
:param dag:
:return:
"""
n = len(sentence)
g = {-1: (0, 0)}
logtotal = math.log(total)
for idx in range(n):
g[idx] = max(
(
g[idx - 1][0] + math.log(freq.get(sentence[x: idx + 1]) or 1) - logtotal,
x
)
for x in dag[idx]
)
return g
def cut(sentence: str):
dag = build_dag(sentence)
g = route(sentence, dag)
n = len(sentence)
segments = deque()
idx = n - 1
while idx >= 0:
prev = g[idx][1]
segments.appendleft(sentence[prev: idx + 1])
idx = prev - 1
return segments
if __name__ == '__main__':
sents = [
"学生会主动完成作业",
"南京市长江大桥"
]
for sent in sents:
print("%s => %s" % (sent, "/".join(cut(sent))))
# 学生会主动完成作业 => 学生/会/主动/完成/作业
# 南京市长江大桥 => 南京/市长/江大桥