分数没变,错的却换了一批
一句话总结
修改智能体之后,不能只凭一个分数来判断是否变好。分数相同,但内部有一个被修好、一个被弄坏,那就不是变好了,而是变了。
为什么需要它
在咨询分类规则中加入了“退货”。目的是让一直被归入其他的“我想退货”,改为归入退款。用黄金数据集一测,准确率没变。从 83% 到 83%。
“没变的话至少没变差”,于是发布了。两天后,“运费可以退吗”这类咨询全都跑到了配送组。
修改规则时也改了检查顺序,结果一个被修好,一个被弄坏。六条里对五条是一样的,但对的那五条不同。
分数把这个事实藏起来了。分数是个数,而我们需要知道的是名字。
工作原理
要想把评估做好,必须把三件事分开看。
- 质量——黄金数据集里对了几条。还有,哪一条错了。
- 成本——处理一条时节点运行了几次。
- 路径——同一个输入经过的路是否变了。
三者都可以在同一次运行中测量。只要给节点包一层就行。
def instrument(name, fn):
def wrapped(state):
started = time.perf_counter()
update = fn(state)
row = PROFILE.setdefault(name, {"calls": 0, "written": 0, "ms": 0.0})
row["calls"] += 1
row["written"] += len(update or {})
row["ms"] += (time.perf_counter() - started) * 1000.0
return update
return wrapped
这里有一个重要的选择。时间也测出来留着,但不用于判定。同样的代码、同样的输入,时间也会因机器和当时的负载而不同。用时间来判定“变慢了”,回归检查就会波动,而波动的检查很快就会被关掉。判定用次数和大小。时间在人去查看时作为线索。
黄金数据集可以小。但必须不变
黄金数据集是“手工标好正确答案的输入集合”。哪怕只有六条也有用——条件是每次测的都是同样的东西。
所以黄金数据集要固定写在代码或文件里,修改时要把“改过了”这件事本身记录下来。如果每次运行都重新抽样,昨天和今天就无法比较。
还有一点。黄金数据集里要留着会错的条目。全都答对的黄金数据集,只会说“现在运行得很好”,什么也抓不到。里面有真正会错的条目,修复之后才看得出修好了什么。
回归看集合,不看分数
比较两个版本时,要看的是这两项。
- 新坏掉的(newly broken)——旧版本里对的,在新版本里错了
- 新修好的(newly fixed)——旧版本里错的,在新版本里对了
两者数量相同,分数就不变。但这两者的分量通常并不相同。原本好好的东西坏掉,要痛得多——因为用户已经依赖那个行为了。所以先问“新坏掉的是不是 0”,再问“有没有新修好的”,这个顺序在实际工作中更安全。
成本和路径也会回归
即使质量不变,成本也可能增加。多加一个分支,节点就多运行一次,如果那个节点调用外部,钱就会相应地花出去。所以要把节点调用次数也作为回归指标留下。与时间不同,这个数字对同一个输入总是一样的。
路径也一样。即使输出同样的标签,如果是从另一条路出来的,那就是不同的行为。现在看起来答案相同,下次改动时会分道扬镳。比较足迹(trace),就能看出这一点。
在现场相遇的样子
第一,只记录分数。就是上面的事故。不留下哪一条错了,就无从回溯。
第二,黄金数据集每次运行都变。用随机抽样来测,数字每次都不同,分不清是回归还是样本的缘故。
第三,用时间判定性能回归。检查会波动,波动的检查被无视,最后被关掉。
第四,想以后再加观测。给节点包一层的位置,在创建图的时候成本最低。以后再加,就要逐个节点修改,漏掉一处,那个节点就不会留在账簿里。
实际工作中真正重要的事
- 连同分数一起留下错误条目的名字。这是唯一能回溯的记录。
- 回归按新坏掉的和新修好的来区分。先看坏掉的。
- 判定用次数和大小,时间只作线索。
- 成本和路径也是回归指标。答案相同但路变了,就是变了。
下一项实验要做什么
一步步扩展 /root/work/ageval/evalkit.py。先做出对咨询分类的图,给节点包一层,把调用次数和写入的键数量留在账簿里。挑出最忙的节点,用手工标好正确答案的黄金数据集得出准确率和错误条目的列表。接着做出修改规则后的第二个版本,比较两个版本,并用名字区分分数相同时哪些条目坏了、哪些修好了。最后连调用次数和路径的变化也测出来,留成记录。