规则匹配
常见业务逻辑
if r := rule1(arg):
return r
if r := rule2(arg):
return r
...
代码逻辑调整为配置加载形式
for rule in rules:
if r := rule(arg):
return r
规则匹配性能优化:
if ... else
优先写最大 概率 * 计算用时 逻辑- 对于动态输入数据, 基于每个规则的命中率/用时定期调整遍历频率, 从而最小会平均匹配耗时
前提: 规则是互斥的, 即打乱规则顺序不影响输出结果. 实际上业务代码不能保障这一点, 都有潜在的依赖假设关系, 这也是潜在的BUG源. 如何找出? 配置形式, 随机化规则, 并测试保障.
进一步, 规则层级分组, 逻辑写成规则树模式, 减少不必要重复计算及遍历 (从不命中中找到有用的信息)
def match(arg, rules: dict):
for f, sub_rules in rules.items():
if r := f(arg):
return match(r, sub_rules)
return arg
例子
rules0 = {
"^https://.*byteoversea\.com/.*/([0-9a-z]{32})/toutiao.mp4$": {}
"^https://.*byteoversea\.com/.*/([0-9a-zA-Z]{38})/toutiao.mp4$": {},
...
}
# VS
rules1 = {
"^https://.*byteoversea\.com/(.*)": {
"([0-9a-z]{32})/toutiao.mp4$": {},
"([0-9a-zA-Z]{38})/toutiao.mp4$": {},
...
},
...
}
实际上, 任意业务逻辑, 都是这样一个匹配返回过程
文本规则匹配
文本分类问题, 加规则 (一般都可以表达成正则形式) 快速召回
优点: 简单有效, 快速定向提点 缺点: 规则加太多时的匹配性能问题; 规则冲突问题; 没考虑潜在分词问题
弱化形式: 词包模型, 关键词匹配
- PY代码
a in txt and b in txt and not (y not in txt and z in txt) or c in txt
- 简记为
a&b^(y&z)|c
- 正则规则可以先退化成关键词匹配规则预过滤后再判定. 例:
[ab]{.3}[^c]
->(a|b)^c
关键词匹配简化
黑名单去除
a&b^x|c&d^(y&z) VS (a&b|c&d)^(x|y&z)
- 等价翻译手段:
(a&b|c&d)^(a&b&x|c&d&y&z)
- 结论: 对于只有一阶黑名单的规则, 可以拆成独立的两阶段匹配
括号去除
a&(b|c) = a&b|a&c
因此关键是词包匹配
词包匹配
做关键词索引快速过滤需检查规则
- 规则集整理提取:
l1=a&b, l2=a
- 词出发的规则关联及关联词转换:
a={l1:[b], l2:[]}, b={l1:[a]}
- AC文本匹配找到所有潜在命中规则后进一步过滤
See Code
说明
- 只记录需额外检查的关联匹配词, 减少重复检查
- 由于等价性, 一个词包做一个代表类纳入即可, 其实并不用纳入匹配检索, 但为了b能纳入AC索引, 从而方便快速判定是否全关键词命中
- 在不考虑计算单个输入匹配多少条规则或者词的时候, 单规则只检查一次即可
这里的假设: 单个词包不能太长, 一个词不能出现于过多的词包中, 否则二次过滤是个耗时
本质问题: 给定一个集合, 从一批备选集合中快速找出被包含的
词包约化
由于规则都是人工加的, 会存在冲突或者歧义的场景
单分类下规则约化:
- 白名单不能有包含:
a&b|a -> a
- 白名单不能包含黑名单:
(a&b|c&d)^a
a&b|a&c
不能认为有问题
规则约化: 删除被包含规则, 删除不可能有匹配的规则
多分类下消岐:
- 包含:
a, a&b -> a^b, a&b
- 交集:
a&b, a&c -> a&b^c, a&c / a&b, a&c^b
- 无法消解例子: a&b, c&d, 输入abcd
- 要做到规则完全正交, 除非单分类匹配词是所有他分类的黑名单, 不现实
- 因此对于单分类任务, 仍需通过后处理规则, 如匹配文本长度, 匹配规则条目等, 做优先级消解
正交规则定义: 两规则不会同时命中同一序列 歧义规则定义: 两规则会命中相同文本, 且包含相同片段
消岐办法: 差集纳入黑名单, 一个避嫌即可
消岐问题翻译: 一组序列/集合的包含/相交判定
词包 = 序列的集合, 导致问题的额外复杂性, 这里集合元素(即词)本身也具有序列包含/相交关系
酸菜鱼&酸菜 = 酸菜
番茄炒蛋|番茄&炒蛋 = 番茄&炒蛋
- 完全不现实的问题简化手段: 不允许词典有交集
CSF & LCS
序列包含/相交问题: 判断是否单字共现即可, 是简单的, 更有价值的问题是找到最长公共子串 (longest common substring)
更有价值的问题: common substring (子串) finding (这里记为CSF问题先)
CSF解法: 用suffix tree, 记录公共枝以及对应共现频次即可
longest common substring for k strings: 找频次=序列大小的公共子串, 视作CSF的简单版
See Code
类似的问题 = common subsequence (子序列) finding
- 集合包含/相交: 集合按照元素编码后排序表达为序列形式即可, 但是把问题做难了 (WHY ???)
LCS (longest common subsequence) 问题 / 后提
关键词规则提取问题表述
samples = {
seq1: cls1,
seq2: cls1,
seq3: cls2,
...
}
# 目标规则
rules = {s1: cls1, s2: cls2, ...}
def classifier(txt, rules):
for seq, cls in rules.items():
if is_sub_sequence(seq, txt):
return cls
assert all(classifier(txt, rules) == cls for txt, cls in samples.items())
显然一个最无泛化性的规则
rules = {seq1: cls1, seq2: cls2, ...}
泛化性 ~ 整体检索关键词序列长度
问题定义: 寻找最短通过子序列匹配做判定的分类器
minimize sum(map(len, rules))
说明:
- 先要求对于样本数据全匹配, 不考虑泛化误差, 没当做一个数据挖掘问题处理
- 数据挖掘问题版本表述: 对于样本的任意划分, 在验证集上误差最小
- 简化问题不考虑黑名单提取 (可单独视作一个规则改写任务)
问题简化先认为只有两分类
对于两样本序列区分, 只需找出最短的唯一片段即可
若每个分类只有单词包规则, 则可以当作类LCS问题处理:
- 办法1: 每个类的最大子序列, 并判定不同类最大子序列是否相交; 如包含, 删除后再递归找; 如相交, 可在此基础上加黑名单从而保证规则正交.
- 办法2: 抽取每个类的所有公共子序列集合, 并找出其中无它类包含的最短子序列最为代表规则
- 办法1里面找最长, 因为希望规则泛化性最低, 最怂找法, 办法2里面找最短, 最大泛化性
实际上每个类样本肯定不存在LCS, 假设每个规则有最小样本支持度k, 则问题: 抽取每个类中所有最小频次k的子序列, 并分别做上述唯一性提取
核心问题翻译:
- 一堆序列中找出出现至少K频次的子串 (CSF问题), 注意只能提取单词匹配规则
- 这类规则相信是很少的, 例如某个类目的专属词
- 一堆序列中找出出现至少K频次, 且长度不超过Y的子序列 (难), 从而可以提取多词词包规则
- 自然要有长度Y限制, 否则我们有个显见解, 即完全匹配的结果
- 多个集合快速求差集, 找出每个集合的独有元素
- K理解为样本规则的支持度, K=1, 精准匹配, 越大则越怂
另外一种问题”曲解”: 忽略分类标号信息, 当作最小序列辨别器问题, 再映射到分类标号. 缺点: 泛化误差高, 因为倾向提取到每个样本数据的个性特征, 没有对于同标号数据充分压缩.
集合VS序列
- 序列还是集合不改变问题本质, 序列切割后, 当作词包, 即集合看待, 集合元素唯一编码排序后也可视作序列
- 区别是我们在讨论子序列还是子集
- 集合包含/相交问题 > 子序列包含/相交问题, 因为集合作为序列是排序且去重的
- 集合包含问题 不能 视作子串包含问题, 集合相交问题 可以 视作 找子串 (CSF) 问题, 但是是更容易的
- 字符串包含, 暴力: O(mn)
- 集合包含判定 (假设已排序列表形式): O(min(m, n))
序列最小辨别器和tokenization的联系及区别
序列最小辨别器问题: CSF+辨别顺序决策
- CSF提取所有片段, 按照片段长度及频数排序即可
- 注意这里是个顺序规则, 即规则检查不能乱序
tokenization: 给定一堆文本, 最少token数编码
简单的tokenization: CSF的片段先纳入词表, 然后剩余片段再纳入
和FPM问题的联系与区别
FPM: Frequent Pattern Mining / Frequent Itemset Mining / 关联规则提取, 规则学习, …
FPM对于无标号集合数据, 找共现概率最大共现元素. 需要文本先分词, 基于置信度的提取方案. 先单分类下FPM找出支持度较高的词包, 再跨分类判定该词包的唯一性, 或者识别质量提升.
- FPM提取的一定是多词的词包. 从分类目标来说, 单个词本身, 如专属词, 也具有很强的分类显著性
- FPM本身并不考虑分类信息, 我们需要找出的在该类目下相对它类支持度较高的词包
计算理论
知道点计算理论对于我们”凡人”的好处: 及时放弃幻想, 意识到问题的难度, 不要死磕问题, 认为会有精巧的最优解法.
NP问题: 可快速验证的问题, 是否一定存在快速的解法?
- P: 有多项式复杂度解的解的问题 (自然多项式复杂度验证)
- NP (nondeterministic polynomial time): 有多项式复杂度验证的问题
- P ⫋ NP ?
- NPH (NP Hard): 任意NP问题都可以多项式时间内翻译为NPH问题, NPH本身不要求是NP, 即不要求能快速验证
- NPC (NP-Completeness) = NP ∩ NPH
- 即NP问题里面最难的一类问题
- 由于全是等价的或可规约的问题, 解决其中一个等于解决了NP问题
NPC一般是决策问题表述:
- 原优化问题”找出最优解”, 对应决策问题”是否有大于N的解”.
- 显然决策问题, 给出一个答案之后, 是可快速验证真伪的
- 搞定了决策问题, 通过不断的提高N反复问问题, 最终定位到最优解的
以下一些NPC问题列举
SAT
satisfiability problem
第一个被证明是NPC的问题
判定任意一个逻辑运算表达式是否有可行解, 例子:
a^(a&b)
: YESa^(a|b)
: NO
给了一个解, 验证是否正确, 自然是平凡的; 暴力判定: 穷举2**n中可能性看是否有通过结果
回到我们业务:
- 判定一个任意规则是否一定有潜在匹配输入是很难的
- 但是简化下假设单规则里面不存在重复词, 则问题是简单的
LCS
决策问题表述: 是否有超过指定长度的子序列?
2输入时: DP, O(mn)时间空间复杂度, 注意对于长文本这也不是可接受的
暴力计算: 对于给定的一组序列, 枚举最短序列的所有子序列去判定即可, O(2**k)
对于确定的字典, 以及假定最大序列长度时, 则问题可以枚举解决 C**K, 理论上是P的, 但实际上是不可行的
对于不确定的输入文本时, 指数复杂度 (注意这里问题参数是输入序列个数, 且每个序列长度无上限)
Q: LCS和编辑距离关系? 最短公共序列(shortest common supersequence)问题的关系?
See Code
衍生问题:
- 判定一组序列是否有公共子序列?
- 按照有公共子序列标准, 对一组序列最小分组?
最大团问题
找出图里面最大的全联通子图
- 找到公司里面互相都认识的最多一组人
- 决策问题形式: 公司里面是否存在N个人互相都认识的情况?
背包问题
贪心算法不保证最优解
DP算法, 由于不能memorize子问题, 实际上是不可行的
Recall:
- 装最多物品 VS 装最大价值物品
- 参加最多会议问题 VS 参加最长会议问题
三色问题
一笔画问题
如何证明一个问题很难?
从一个已知NPC问题触发, 证明其可以约化成目标问题
注意不是反向, 随便一个问题当然可以泛化成一个NPC问题
如何”击败”NPC
- 问题较小时直接暴力计算没问题
- 上算力, 上并行 / GPU
- 近似算法, 概率计算方法
- BPP (Bounded-error probabilistic polynomial time)
- 难点在于论证和最优解的误差是有界的, 或者大概率是最优解的
- 启发式/贪心/…等经验算法
- 注意避免过分约化问题, 简单问题变成一个困难问题
- 观察业务上不显性的假设, 稍微变化能显著改变问题的表述
NPC之于数据挖掘问题
数据挖掘表述: 寻找算法, 使得, 对于样本数据的任意k参数训练/验证集划分, 基于训练集生成的规则, 在验证集上的评价指标期望最小
显然, 样本划分问题空间本身就是幂次的
Beat the impossible
对于数据特征的观察/假设, 经验性的算法, 达到一个还不错的结果
步步为营, 百尺竿头, 更近一步