个人博客:无奈何杨(wnhyang)
个人语雀:wnhyang
共享语雀:在线知识共享
Github:wnhyang -
Overview
想必大家都有听过或做过职业和性格测试吧,尤其是现在的毕业生,在投了简历之后经常会收到一个什么测评,那些测评真的是又臭又长,做的简直让人崩溃,很多时候都是边骂边做,都什么玩意!?
然而,本篇就由此出发,把整个测评作为一个策略的话,其中每一项都是一条规则,通常每一条规则(问答)需要我们输入一个类似1-9的分数,1和9分别代表两个极端,最终这个策略会结合所有的问答结果计算出我们的性格/职业。这是如何做的呢?其实就是一种分类算法,就拿二维平面直角坐标系举例吧!
如下二维平面直角坐标系下分出了4个区域,性格/职业测评的每条问答可以理解为其中一条经过原点的直线,1-9分别指示两个方向,你的答案最终会是一个由原点出发的n
条直线,这n
条直线可以绘成一个多边形,而这个多边形就构成了最终结果,长得有点类似雷达图。
当然这只是二维平面直角坐标系的例子,实际上现实往往比这个更复杂,高于三维的我也举不出例子啊🙂↔︎️
总之最后结果绝大多数情况下都会是一个不规则的东西(我实在不知道更高维的该怎么描述),这种测评会取出凸点作为我们的倾向性格/职业。
好吧,关于文章开篇就到这里了,下面就可以正式开始了。不过我还是想讲一个题外话,小时候接触的数学函数(方程)可以很轻易的表示在二维直角坐标系下,随着对于数学的深入探索,出现了越来越多的奇奇怪怪的字母和方程,有人也讲“数学的尽头是字母”🤔然而当我们换一个坐标系,这些是不是也会变个模样呢?所以说有时候换个角度看问题就会有不同收获,或者说换个角度问题就会迎刃而解。
策略
策略组件大致实现如下,编排时会使用p_cn.tag("code")
,运行FOR(p_fn).DO(r_cn).BREAK(p_bn);
或FOR(p_fn).parallel(true).DO(r_cn);
,前者适用于顺序模式,其他皆适用于后者。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| @LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = LFUtil.POLICY_COMMON_NODE, nodeType = NodeTypeEnum.COMMON, nodeName = "策略普通组件") public void policy(NodeComponent bindCmp) { PolicyContext policyContext = bindCmp.getContextBean(PolicyContext.class); PolicyContext.PolicyCtx policy = PolicyConvert.INSTANCE.convert2Ctx(policyMapper.selectByCode(bindCmp.getTag())); policyContext.addPolicy(policy.getCode(), policy);
log.info("当前策略(code:{}, name:{}, code:{})", policy.getCode(), policy.getName(), policy.getCode());
if (PolicyMode.ORDER.equals(policy.getMode())) { bindCmp.invoke2Resp(LFUtil.P_F, policy.getCode()); } else { bindCmp.invoke2Resp(LFUtil.P_FP, policy.getCode()); } }
|
循环次数组件p_fn
如下,查询策略下的所有规则(规则还没做版本控制,后面再改),返回规则列表大小。
1 2 3 4 5 6 7 8
| @LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_FOR, nodeId = LFUtil.POLICY_FOR_NODE, nodeType = NodeTypeEnum.FOR, nodeName = "策略for组件") public int policyFor(NodeComponent bindCmp) { PolicyContext policyContext = bindCmp.getContextBean(PolicyContext.class); String policyCode = bindCmp.getSubChainReqData(); List<PolicyContext.RuleCtx> ruleList = RuleConvert.INSTANCE.convert2Ctx(ruleMapper.selectByPolicyCode(policyCode)); policyContext.addRuleList(policyCode, ruleList); return ruleList.size(); }
|
循环中断组件p_bn
如下,当策略上下文中有命中风险规则时就可以停止循环了。
1 2 3 4 5 6
| @LiteflowMethod(value = LiteFlowMethodEnum.PROCESS_BOOLEAN, nodeId = LFUtil.POLICY_BREAK_NODE, nodeType = NodeTypeEnum.BOOLEAN, nodeName = "策略break组件") public boolean policyBreak(NodeComponent bindCmp) { PolicyContext policyContext = bindCmp.getContextBean(PolicyContext.class); String policyCode = bindCmp.getSubChainReqData(); return policyContext.isHitRisk(policyCode); }
|
另外在使用异步循环编排时需要注意并发操作问题,尤其是对上下文的操作。
剩下的规则组件r_cn
和策略上下文PolicyContext
请往下看。
顺序:按部就班、循序渐进
顺序模式是最好理解,就是顺序运行策略下的所有规则,默认在第一条设定的风险规则触发后结束,其实更准确的叫法应该是首次。如下表在顺序模式下执行,到规则2就结束了,因为默认pass
之外的才是风险规则。
1 |
true |
pass |
2 |
true |
reject |
3 |
false |
sms |
4 |
true |
review |
最坏:未雨绸缪,防患未然
与顺序模式不同,需要执行所有的规则,综合最坏的作为结果。如下表在最坏模式下,最终结果是reject
(因为reject
>review
>pass
,这个是配置的)。
1 |
true |
pass |
2 |
true |
reject |
3 |
false |
sms |
4 |
true |
review |
投票:集体智慧,共同决策
同上,需要执行完所有规则,以命中规则的结果最多的作为最终结果。如下表在投票模式下,结果是pass
。
1 |
true |
pass |
2 |
true |
reject |
3 |
false |
review |
4 |
true |
pass |
可以使用这样的计数器,但是考虑到策略集下有不一样的策略集,想必还要再包一层Map<String,ConcurrentHashMap<String, AtomicInteger>>
,以策略code
作为键。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| private final ConcurrentHashMap<String, AtomicInteger> counters = new ConcurrentHashMap<>();
public void increment(String key) { counters.computeIfAbsent(key, k -> new AtomicInteger(0)).incrementAndGet(); }
public int get(String key) { AtomicInteger counter = counters.get(key); return (counter != null) ? counter.get() : 0; }
|
本来考虑的是在规则True
组件中根据策略不同做不同的事情,但后来放弃了,还是统一放在上下文中吧,且往下看。
权重:量化评估,科学分配
一样,需要运行完所有规则,综合权重模式阈值配置得出最终结论。如下表在权重模式下,结果是23
+21
+20
=64
,注意!!!这里只是得到一个数字,在策略设置为权重模式后额外还需要配置一个阈值表,拿这个数字去匹配对应的阈值区间得出最终结论。当然这只是个最简单例子,下面将展开,讨论其丰富的应用场景和更灵活的使用方法。
1 |
true |
23 |
2 |
true |
21 |
3 |
false |
30 |
4 |
true |
20 |
阈值配置表
(-214,20] |
pass |
(20,45] |
review |
(45,70] |
sms |
(70,900) |
reject |
设计过程
阶段一:规则增加简单的分数属性,命中时累计就好,如:规则1命中时+10,规则2命中时-2,这样最为简单,也因此适用场景最少,最不灵活。
阶段二:固定公式计算,如下规则附加这些属性,在规则命中时计算一下,这种只适合简单的线性相关的规则计算。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58
|
@Data @NoArgsConstructor @AllArgsConstructor public class Weight {
private Double base; private Double al; private String aOpType; private String opType; private String value; private Double upperLimit; private Double lowerLimit;
public double compute(double trueValue) { if (al == null || trueValue == 0 && ("/".equals(aOpType))) { throw new IllegalArgumentException("Invalid parameters for computation."); }
double adjustment = 0; switch (aOpType) { case "+": adjustment = al + trueValue; break; case "-": adjustment = al - trueValue; break; case "*": adjustment = al * trueValue; break; case "/": adjustment = al / trueValue; break; default: throw new UnsupportedOperationException("Unsupported operation type: " + aOpType); }
double result = base + adjustment; return Math.min(upperLimit, Math.max(lowerLimit, result)); }
public static void main(String[] args) { Weight weight = new Weight(10.41, -2.154, "*", "zb", "count", 5000.545, -56.654); double compute = weight.compute(25.21); System.out.println("Computed weight: " + compute); } }
|
阶段三:灵活公式,使用QLExpress
实现。如下讲计算公式作为规则的一个属性,通过getOutVarNames
获取需要用到的外部变量名,在运行表达式之前通过LiteFlow
上下文取值塞到QLExpress
的上下文中。
当然还有可以优化的地方,1、设计上下文时实现QLExpress
的IExpressContext
接口,也就不用获取后在塞,直接拿LiteFlow
上下文作为QLExpress
上下文用就行;2、还有就是min(upperLimit, max({}, lowerLimit))
是否要放在表达式中,其实也是没必要,可以放在表达式计算完成之后嘛。3、是否要计算平均值,现在是权重之和,是否要做加权平均呢?4、等等
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Test public void test3() throws Exception { ExpressRunner runner = new ExpressRunner(); DefaultContext<String, Object> context = new DefaultContext<>(); String fun = "base + al * value"; String express = StrUtil.format("min(upperLimit, max({}, lowerLimit))", fun); log.info(express); String[] outVarNames = runner.getOutVarNames(express); log.info(Arrays.toString(outVarNames)); context.put("base", 45.434); context.put("al", 3.352); context.put("value", 24.3264); context.put("lowerLimit", -35.342); context.put("upperLimit", 3463.57); Object r = runner.execute(express, context, null, true, false); log.info("{}", r); }
|
注意点
对于像这种可输入的、脚本类的、在系统中运行的,一定要做好安全性校验,避免直接操作系统资源,稍微不注意控制就会有安全漏洞。在保证安全的前提下,再考虑如何优化用户使用体验,如用户需要使用一些系统字段时,在编辑器文本域输入特殊字符(像“@”或“#”),监听到输入后显示候选列表,可以关键词匹配并选择需要的字段,一旦选中,这个将作为一个整体,只能整体操作,就像我们在发邮件,或者聊天时输入“@”一样,另外再做一个内置运算符的提示符,这样编辑公式就更加便捷,且能降低出错率。再进一步就是做一个常用公式库,提示列表中有直接选中就行,剩下的就是填充需要的字段就行。
应用场景
比如在做密码登录时,设置了两条规则,一条正向规则y1=f(x1)
,x1
表示最近人脸登录成功次数,其与结果负相关,人脸登录成功次数越多得到的负数越大;一条反向规则y2=f(x2)
,x2
表示最近密码登录失败次数,其与结果正相关,密码登录失败次数越多得到的分数越大,而且保证其“增长率”大于f(x1)
。
可以大致表示为下面的曲线,最近人脸登录成功次数少时,风险高一些,多时也会存在上限,因为再多也没有意义了;最新密码登录错误次数少时风险低,但密码登录次数越多风险急剧增加,这样的话在整合y=y1+y2=f(x1)+f(x2)
后,风险受密码登录错误次数的影响更大。
当然将两个公式整合到一块做为一个规则也是可以的,差别就是是否需要独立的规则条件。
不合并 |
condition1 |
f(x1) |
condition2 |
f(x2) |
|
合并 |
condition1 |
f(x1)+f(x2) |
还有就是在信贷计算信用时,需要计算收入稳定性+信用历史+就业情况+债务水平+资产情况的场景时,当然这依赖多方数据,而且一般的信用评估不是简单的规则配置能解决的。
模型:智能学习,进化升级
最终都将到这一步的,虽然现在做的项目中还没有集成模型,但是我之后一定会做。先立flag
嘛,实现不是实现另说吧🧐可别连立flag
的勇气都没有了!
关于模型我也不是专业的,也是仅有一点点了解。我一直认为学习能力、看待、解决问题的思想是最重要的,特别是我看了几个关于机器学习的视频后,虽然其中很多的公式我都不懂,但是能理解到其看待、解决问题的思路方法,很受益,很有启发。
规则
以下分别是规则组件(包含isAccess
实现)、命中规则、未命中规则组件。在规则命中时额外计算规则配置的权重表达式。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| @LiteflowMethod(value = LiteFlowMethodEnum.IS_ACCESS, nodeId = LFUtil.RULE_COMMON_NODE, nodeType = NodeTypeEnum.COMMON) public boolean ruleAccess(NodeComponent bindCmp) { String policyCode = bindCmp.getSubChainReqData(); int index = bindCmp.getLoopIndex(); PolicyContext policyContext = bindCmp.getContextBean(PolicyContext.class); PolicyContext.RuleCtx rule = policyContext.getRule(policyCode, index); return !RuleStatus.OFF.equals(rule.getStatus()); }
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = LFUtil.RULE_COMMON_NODE, nodeType = NodeTypeEnum.COMMON, nodeName = "规则普通组件") public void rulProcess(NodeComponent bindCmp) { String policyCode = bindCmp.getSubChainReqData(); int index = bindCmp.getLoopIndex(); PolicyContext policyContext = bindCmp.getContextBean(PolicyContext.class); PolicyContext.RuleCtx rule = policyContext.getRule(policyCode, index); bindCmp.invoke2Resp(StrUtil.format(LFUtil.RULE_CHAIN, rule.getCode()), rule); }
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = LFUtil.RULE_TRUE, nodeType = NodeTypeEnum.COMMON, nodeName = "规则true组件") public void ruleTrue(NodeComponent bindCmp) { PolicyContext policyContext = bindCmp.getContextBean(PolicyContext.class); PolicyContext.RuleCtx rule = bindCmp.getSubChainReqData(); log.info("命中规则(name:{}, code:{})", rule.getName(), rule.getCode()); if (RuleStatus.MOCK.equals(rule.getStatus())) { policyContext.addHitMockRuleVO(rule.getPolicyCode(), rule); } else { if (PolicyMode.WEIGHT.equals(policyContext.getPolicy(rule.getPolicyCode()).getMode())) { try { Double value = (Double) QLExpressUtil.execute(rule.getExpress(), bindCmp.getContextBean(FieldContext.class)); rule.setExpressValue(value); } catch (Exception e) { log.error("规则表达式执行异常", e); } } policyContext.addHitRuleVO(rule.getPolicyCode(), rule); } }
@LiteflowMethod(value = LiteFlowMethodEnum.PROCESS, nodeId = LFUtil.RULE_FALSE, nodeType = NodeTypeEnum.COMMON, nodeName = "规则false组件") public void ruleFalse(NodeComponent bindCmp) { log.info("规则未命中"); }
|
策略上下文
直接上代码了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218
|
public class PolicyContext {
private final Map<String, DisposalCtx> disposalMap = new ConcurrentHashMap<>();
private PolicySetCtx policySet;
public void init(List<DisposalCtx> disposalCtxList, PolicySetCtx policySet) { for (DisposalCtx disposalCtx : disposalCtxList) { disposalMap.put(disposalCtx.getCode(), disposalCtx); } this.policySet = policySet; }
private final Map<String, PolicyCtx> policyMap = new ConcurrentHashMap<>();
public void addPolicy(String policyCode, PolicyCtx policy) { policyMap.put(policyCode, policy); }
public PolicyCtx getPolicy(String policyCode) { return policyMap.get(policyCode); }
private final Map<String, List<RuleCtx>> ruleListMap = new ConcurrentHashMap<>();
public void addRuleList(String policyCode, List<RuleCtx> ruleList) { ruleListMap.put(policyCode, ruleList); }
public RuleCtx getRule(String policyCode, int index) { return ruleListMap.get(policyCode).get(index); }
private final Map<String, List<RuleCtx>> hitRuleListMap = new ConcurrentHashMap<>();
public void addHitRuleVO(String policyCode, RuleCtx rule) { if (!hitRuleListMap.containsKey(policyCode)) { hitRuleListMap.put(policyCode, CollUtil.newArrayList()); } hitRuleListMap.get(policyCode).add(rule); }
public boolean isHitRisk(String policyCode) { if (CollUtil.isNotEmpty(hitRuleListMap.get(policyCode))) { for (RuleCtx ruleCtx : hitRuleListMap.get(policyCode)) { if (!DisposalConstant.PASS_CODE.equals(ruleCtx.getDisposalCode())) { return true; } } } return false; }
private final Map<String, List<RuleCtx>> hitMockRuleListMap = new ConcurrentHashMap<>();
public void addHitMockRuleVO(String policyCode, RuleCtx rule) { if (!hitMockRuleListMap.containsKey(policyCode)) { hitMockRuleListMap.put(policyCode, CollUtil.newArrayList()); } hitMockRuleListMap.get(policyCode).add(rule); }
public PolicySetResult convert() { PolicySetResult policySetResult = new PolicySetResult(policySet.getName(), policySet.getCode(), policySet.getChain(), policySet.getVersion());
for (Map.Entry<String, PolicyCtx> entry : policyMap.entrySet()) { PolicyCtx policy = entry.getValue(); PolicyResult policyResult = new PolicyResult(policy.getName(), policy.getCode(), policy.getMode());
String maxDisposalCode = DisposalConstant.PASS_CODE; int maxGrade = Integer.MIN_VALUE; Map<String, Integer> votes = new HashMap<>(); double weight = 0.0; List<RuleCtx> ruleList = hitRuleListMap.get(policy.getCode()); if (CollUtil.isNotEmpty(ruleList)) { for (RuleCtx rule : ruleList) { if (PolicyMode.VOTE.equals(policy.getMode())) { votes.put(rule.getDisposalCode(), votes.getOrDefault(rule.getDisposalCode(), 0) + 1); } else if (PolicyMode.WEIGHT.equals(policy.getMode())) { weight += rule.getExpressValue(); }
RuleResult ruleResult = new RuleResult(rule.getName(), rule.getCode(), rule.getExpress());
DisposalCtx disposal = disposalMap.get(rule.getDisposalCode()); if (null != disposal) { ruleResult.setDisposalName(disposal.getName()); ruleResult.setDisposalCode(disposal.getCode()); if (disposal.getGrade() > maxGrade) { maxGrade = disposal.getGrade(); maxDisposalCode = disposal.getCode(); } } if (RuleStatus.MOCK.equals(rule.getStatus())) { policyResult.addMockRuleResult(ruleResult); } else { policyResult.addRuleResult(ruleResult); } } } if (PolicyMode.VOTE.equals(policy.getMode())) { String maxVoteDisposalCode = DisposalConstant.PASS_CODE; int maxVoteCount = Integer.MIN_VALUE; for (Map.Entry<String, Integer> entry1 : votes.entrySet()) { if (entry1.getValue() > maxVoteCount) { maxVoteCount = entry1.getValue(); maxVoteDisposalCode = entry1.getKey(); } } policyResult.setDisposalName(disposalMap.get(maxVoteDisposalCode).getName()); policyResult.setDisposalCode(maxVoteDisposalCode); } else if (PolicyMode.WEIGHT.equals(policy.getMode())) { List<Th> thList = policy.getThList(); thList.sort(Comparator.comparing(Th::getScore)); for (Th th : thList) { if (weight <= th.getScore()) { policyResult.setDisposalName(disposalMap.get(th.getCode()).getName()); policyResult.setDisposalCode(th.getCode()); break; } } } else { policyResult.setDisposalName(disposalMap.get(maxDisposalCode).getName()); policyResult.setDisposalCode(maxDisposalCode); } policySetResult.addPolicyResult(policyResult); } policySetResult.setDisposalName(DisposalConstant.PASS_NAME); policySetResult.setDisposalCode(DisposalConstant.PASS_CODE);
return policySetResult; } }
|
策略集
策略集是用来编排策略的,即前面的策略组件p_cn.tag("code")
,从前面已知策略会有结果的,那么编排他们的策略集如何取这个结果呢?
这就要考虑如何设计策略集的编排了,两种情况,入度为1,入度大于1。
如下,图1并行运行策略1、2,并最终都返回到结束节点,这时就要考虑如何处理策略1、2的结果了,投票?加权平均?随机选一个?还是其他什么方法?图2通过分流结束节点只会接收到一个策略,那么此时就不会有冲突,分流到哪个就返回哪个。
当然这块还没想好怎么做,只是一些想法。
小结
本来还想分享一下项目进展的,但转眼一看好像写的已经有点多了,那就下次吧!
写在最后
拙作艰辛,字句心血,望诸君垂青,多予支持,不胜感激。
个人博客:无奈何杨(wnhyang)
个人语雀:wnhyang
共享语雀:在线知识共享
Github:wnhyang -
Overview