心法利器
本栏目主要和大家一起讨论近期自己学习的心得和体会。具体介绍:仓颉专项:飞机大炮我都会,利器心法我还有。
2023年新的文章合集已经发布,获取方式看这里:又添十万字-CS的陋室2023年文章合集来袭,更有历史文章合集,欢迎下载。
往期回顾
bad case修复其实是一个非常考验内功的事情,我也在实践当中尽自己之力去理解和吸收,慢慢的形成一些自己的经验,累积5篇,给大家介绍一下如何修复bad case,具体的流程如下(这里有各自的传送门,当然也可以直接点进去):
当然,我的视角主要从NLP、对话、搜索的角度来看的,对于信息更为复杂的推荐,希望大家在方法论上也能有些收获吧。
认知篇
前言
插一段前言,其实原计划是写3篇就够,认知篇并不在计划之内,后期增加的原因是很多人可能并不理解bad case在整个问题分析中的重要性,因此我觉得有必要在开始讲方法之前先给大家树立基本的概念和观念。
什么是bad case
所谓bad case,是算法领域的一个术语,用于表示在推理阶段产生的与预期不同的结果,例如一个文本分类任务,应该让文本A分为正类,结果算法分成了负类,这就是一条bad case,就算是人类这种高级碳基生物进行预测,也很可能因为走神、认知差异、知识受限等因素出错,而现在并不强大的模型当然也就经常会出现bad case,我们甚至可以说bad case是普遍存在的。
bad case分析的意义
通过基本的指标,分析出我们现在的算法方案的现状可能并不困难,但是怎么“提升效果”,却是很多人很迷茫的问题,这是微观角度,更宏观地讲,如何提升自己解决问题的能力,如何让自己更好更快地解决更多的问题,这个同样让人非常迷茫,我们为了提升自己不断学习各种算法,各种论文,却发现除了模型逐步内卷之外,自己解决问题的能力并没有提升,一个问题试了成千上万的模型却发现提升可谓是杯水车薪,解决问题的天花板变得很低,效果提升也变得很不可控。
私以为,要突破,必定是要做出一个超出当前维度的工作。第一轮在美团的实习,让我知道了大道至简,简单的方案并不比复杂模型的效果差多少,而第二次在OPPO的实习,让我知道了分析bad case是破解问题,探索问题的关键钥匙,关键点分别是这几个:
如果模型效果不好是病,那bad case就是症状,是问题现场留下的线索,足够了解bad case能让你对问题背后的病因有更多的了解,方便对症下药。
bad case分析是我了解业务、了解场景的重要途径,知道当前研究、实践中容易出现的问题,往通俗的说,吸收经验的重要途径。
所以,我个人是非常建议大家多看bad case,多分析bad case,在模型理解和吸收出现瓶颈的时候,多看bad case可能可以拓展你的思路,发现的问题多了自然会产生一些解决方案,创新点、解决问题的能力也会随之增强。
bad case分析的心理关
很多人之所以不去看数据,除了没有这个分析case的意识,很大部分的原因是抵触,包括之前的我在内,主要是这几个原因:
数据多,分析起来很枯燥,时间还很长。
不清楚该怎么分析。
对于问题1,要突破的话我主要说这几个点:
其实不用分析很多,50-100条其实就很能说明问题。
数量看着很多,但我自己做过实验,自己去标结果的话,文本分类500条可能就10来分钟,分析100条bad case可能也是10多分钟,时间并不长对吧,这个时间总比找不到解决方法时痛苦没方向要好吧。
其实整个过程可以不枯燥,因为分析完你就可以触摸到问题的根本了,自己本身也有问题理解的提升,收获很大好吧。
而第二点,有关不知道该怎么分析,我是总结了一套思路方案,认知篇先到着,后续我会给大家介绍我的这套方案,敬请期待~
现状篇
bad case修复其实是一个非常考验内功的事情,我也在实践当中尽自己之力去理解和吸收,慢慢的形成一些自己的经验,最近一连4篇,给大家介绍一下如何修复bad case,具体的流程如下:
认知篇:介绍bad case分析的背景,给大家树立一个看bad case的意识和尝试。
现状篇:治疗bad case,首先要明确现状,就是现在的技术方案的真实效果如何。
分析篇:我们都知道要开始看bad case了,要怎么看,怎么分析出问题,在篇告诉你。
解决篇:知道病情,知道病因,就该开始治疗了。
当然,我的视角主要从NLP、对话、搜索的角度来看的,对于信息更为复杂的推荐,希望大家在方法论上也能有些收获吧。
本期第二篇,开始分析bad case之前要做的事情,概括的,我认为就是对宏观的现状有一个基本的了解,简单地说就是看指标,但是这点在我看来很多人其实做的并不好,我在这里给大家详细展开说一下吧,有则改之,无则加勉。
什么叫做现状
现在有一个基本baseline,baseline自然就有对应的评测集基线方案的效果,这个所谓的效果,就是现状,好or不好,是否需要持续优化,哪个方面需要持续优化,这就是所谓的现状,只有明确了宏观的现状,才能知道我们下一步的动作。
现状定位第一步——评测集
评测集可以理解为一份试卷,用来测试一个算法方案的效果,流程是这么个流程,但是这个流程的说法还是非常多的。
首先,要确认是一个完整意义的评测集,而不是一两条样本。这里背后有一个很关键点,就是不能用一两条case来断定一个方法的好或者坏,例如遇到了一个bad case,那就说明这个方法不好,这是草率的,要综合评定一个东西的好坏,不应该只看一次,而是应该看多次,所以需要的是一个具有一定数量的数据集。
第二,是一个具有一定统计意义的数据集。统计意义不仅仅是指的数量,简单地为了数量我们直接把同一条数据复制几十万次就完事了,我们都知道不应该这么做,那所谓的具有统计意义是什么意思,我的理解是这几个:
数量上能支撑特定指标的计算足够稳定。1条数据,误差就是100%,2条数据误差就是0.5了,到了100条,误差就只有1%,所以一般地数量足够多,抽样所带来的指标波动就会越小,结合数据集的实际使用需求,我们可以确定一个可靠数据集的数量。
抽样情况和实际使用的场景尽可能一致。最理想的情况,我们能用尽可能少的样本体现未来我们使用的时候可能出现的问题,这样能让我们在正式上线之前就把问题给暴露出来,其严重程度也可以从频次等方式快速得到观测,因此这个数据集我们希望的是能尽可能和正式的使用场景一致,最常用的方式就是用在线随机query来构造我们需要的评测集。
第三,质量可靠,足够支撑当前方案的数据集。但凡自己有看过些数据,其实都会发现哪怕是一些开源的数据集,也会出现标注错误的情况,更何况我们实际使用的更为复杂困难的数据集,因此我们需要尽可能保证的是,数据集质量足够高,出错的概率尽可能低,这样才能更好地用来测试我们的算法方案。最简单的检测方式就是自己拿100条出来自己标,看是否和预期相同,一般准确率在95%以上的数据集就可以说是非常靠谱了,90%只能说勉强可用,一般90%准确的数据顶多能支撑准确率在85%的方案的评测,再高你就会发现很多模型认为正确但是标注错误的case出现。
在科研的时候,我们可能会对着sota拼分数做实验,但是到了真正落地的场景,就会发现有一个可靠的评测集是多么困难,或者说我们可能很少会去找评测集中可能出现的问题,强如LCQMC的数据集依旧有大量错误,只是看我们当前的方案时候会因为这些错误而影响我们的评估和判断而已,有的时候LCQMC的准确率到了90%就上不去,很可能就是因为评测集中有些问题,所以不一定要为自己提了几个点而高兴,不妨先去看看数据。
但我们可以确定的是,评测集,这份试卷,必须得准备好,才能够用来评估算法效果,这步绝对不能轻易简化。
现状定位第二步——指标
算法最喜欢的就是结合指标评估效果,也就是我们所谓的现状,指标这个东西我们其实在初学算法的时候就已经了解很多了,这里为什么还要单独讲呢,原因其实和上面类似,表面上看着简单,其实里面的学问其实非常多。
首先,指标的设计必须要考虑观测的目标。指标是用来体现现状的,那要体现什么现状,这就是指标存在的根本。准确率是用来评估预测的正确性的,这个大家都知道,但是我们需要评估的是哪种方面的准确?这个就很关键了,例如我们在线的关键目标是为了避免误召回,要看误召回的情况,则准确率很自然地就会想到用“实际是该意图/算法预测该意图正类”来作为准确率指标,对于负类的预测情况,我们可能关心的并不多,所以就可以不设计在计算公式里了,这就是指标设计和背后的观测目标的关系。
其次,注重指标的口径。数据分析非常讲究一个点,就是口径,即我们取数据的条件,不同的条件取数据很可能会得到截然不同的结论,因此我们必须非常关注口径。举例,我们在分析和解决bad case的时候,针对多路召回,我们是要看每一路的效果情况来归因的,所以我们就要划分口径了,方案A召回的正类准确率是多少,方案B召回的准确率又是多少,所有出错的case中,哪一路召回的量最大,通过这样的口径切分和分析,我们能快速知道是哪个方案出现了问题,哪个方案的问题是关键问题。
第三,多个指标的组合观测。一个指标能让我们知道一个问题,但是有的时候单独看一个指标很可能会让我们忽略一切别的关键问题。例如,准确率虽然能让我们观测算法方案误召回的情况,一旦要优化准确率,召回的数量肯定也会随之降低,为了避免出现召回过低的问题,在看准确率的同时我们还要看召回率,两者平衡提升才是健康可靠的。
同样地,指标的合理设计、口径的关注以及指标的组合问题,都是我们需要综合考虑的。
现状定位第三步——结论
现状的确定,本身就是要求一个结论,现在算法效果好or不好,我们是否需要进行优化,经过一系列的现状分析,我们是能对当前算法有一个基本的认识,结论也就呼之欲出了。
首先,我们回顾一下预期的目标是什么。我们可能有一个比较明确的预期,例如准招是80%+,那我们只需要直接看好准招就够了,但是预期目标还是要有,毕竟得到一个结果80%,到底是高还是低,我们固然是很难得到的,我们要有一个预判,达到多少算是正常的,100%这事难度本身就大,别说现在的模型,让人来都很困难。
其次,这个结论本身是指导我们后续工作的,好or不好取决于我们是否还需要做进一步的优化,所以要下结论的时候,要顾及我们下一步的工作。
最后,如果我们还需要进一步优化我们当前的算法方案,在初步的结论下我们其实是可以总结出一些比较大的方向的,例如优化准确、扩大召回等等,具体怎么做,那是正式到了bad case分析的时候就会开始找,步步为营地,我们这一步要有一个模糊的方向,这个方向非常重要,毕竟我们要分析bad case,我们要知道哪些bad case是关键,这种宏观的信息是我们需要知道的。
小结
bad case之前的现状分析,可以说是让我们对现在的数据和效果有一个比较宏观的把握,好还是不好,好在哪不好在哪,未来的优化目标是什么样的。在这里,我们需要明确的是,每一个步骤都不能忽略,甚至是要努力做好,磨刀不误砍柴工,有这个思路和方法,找问题和分析问题的能力能大幅提升。
分析篇
本期应该是重头戏了,讨论bad case如何分析,从而找到病源的方法。有了前面的铺垫,我们知道了现在我们算法方案的现状,比较宏观地知道方案要在哪个方向提升,接着这个宏观的信息,我们得以着手开始进行更加微观细节的分析了。
什么样的bad case适合被分析
结合现状分析,我们知道要优化算法的方面,例如准确率、召回率等,那么在这个比较宏观的思路下,我们去看影响准确率、召回率上升的因素在哪里,这就要求我们看准确率分错的case,看漏召回的case,这就是核心分析的对象。
数量上,个人建议在50个以上,200个以下吧,潜心分析下去,除非是发现了严重而又明显的bug,不分析到预定的数量,最好不要停止,少量的分析可能会影响我们的判断,有了一定统计意义的case分析好以后,我们才能知道主干问题在哪里,针对性突破解决,从而提升效果。
抽样上,建议一定要随机,这点很容易被忽略,非随机的分析可能会导致有些主干问题可能不会出现在待分析的样本里,虽然这个点很明显但是在实际应用中,要注意检查这个点。
bad case分析思路
好了,要开始分析了,很多人会和我说,感觉分析不出来,这里和大家好好聊聊我的主要分析思路吧。
粗看法
并非一个一个深究,而是比较笼统地大体上去看,错误的样本都有什么特点,带有哪些特征,例如长度、句式等是否有什么特点,又或者是问句等等,这个只要通篇看下来,就会有比较明显的特点了,一下子能总结出比较突出的问题。
掌握方法,还是要了解其优缺点,粗看法的本质是快速看快速找到比较大的问题,优点就是快,但问题是你要能粗看的出来才行,所以缺点就是细节问题比较难发现,而且有些问题是比较深层次的,例如同样是问句,但是为什么问句就会出错,这个原因是很难探究的出来的,在baseline刚建立,效果还没有收敛的时候,这个方法非常有用,但是到了后期收官的时候,这个方法就不太好用了。
追溯法
追溯法是一个比较花时间的方法,就是对一个case,重现整个训练和预测过程的方法。给定一个bad case,准备好日志,分析预测的每个阶段的分析结果,查看是否符合预期,不符合预期的,大概率这个部分有bug、词典词汇等什么明显的问题了。
对于模型,本身效果我们则去训练样本里查找是否含有比较多的类似的bad case。例如我们在百科意图里发现很多问句会被误召回,这时候我们可以查查训练集中带有问句关键词的正负样本的比例是不是有问题,例如正样本远多于负样本,此时模型很可能就会认为只要带有疑问词的就是正类,我们需要加一些带疑问词的负样本进去,例如“为什么你的心情不好”这种属于闲聊类而非百科类的样本。
追溯法是比较常见的一种思考问题点的模式,从整个过程寻找问题的蛛丝马迹。要是想找到单个case问题的核心原因,这一步几乎是不可忽略的。
解决法
问题是用来解决的,我们的目标是发现问题,然后去解决,反过来,我们知道有哪些解决问题的方法,可以在没什么思路的时候,看看一些bad case是否可以用什么方式就可以解决(黑白名单可就没啥意思了啊),那我们就可以把问题归纳到这个方案上。
例如短句预测不正确,而且一般都比较模糊,所以我们考虑的是过滤所有短句,然后再通过白名单的方式补召回,那我们就可以把query归类为短句。
问题标记
case是需要我们逐个分析的,后续我们需要对重点问题进行归纳总结,所以case分析往往是需要我们做一些关键的记录,例如归因、解决方案的总结等等,所以,我们需要学会对case进行标记。来看个例子吧:
query | 原因分析 |
---|---|
关闭 | 短句 |
今天天气怎么样 | 疑问词,关键词拒绝 |
你为什么 | 疑问词,模糊 |
什么 | 短句,疑问词 |
这是一些文本分类中发现的bad case,经过我们的分析,可以标注出一些我们分析得到的bad case,标记的时候注意词汇和问题的统一,方便我们后续统计各个问题的占比,值得注意的是,一个问题是可能有多个解决方法的,我们记得要标记上,统计分析的时候都是要关注的。
问题统计和归纳
有了上面的标记,我们能很轻易地看拿到各个问题的分布情况,甚至可以总结出来,例如上面的例子,有3个问题和疑问词有关,2个和短句有关,关键词拒绝和模糊分别都有1个,可以知道疑问词问题是占比最高的问题,我们需要去解决,具体怎么解决再说,反正核心就是占比高,所以我们肯定要针对性解决了,这种解决是批量解决的。
重点问题,基本就是占比高的问题,我们要是缓解甚至是解决,那对效果的影响无疑就是巨大的,在值不太高的场景,例如准确率只有60%,要是有一个问题的占比有50%,你要是完全解决了,那这个准确率的提升就是,那就是20%,哪怕只能解决这个问题的50%,也能提升10%嘛,这个量非常大。
时间问题
很多人会担心分析的时间会很长,但按照我的经验,小则5分钟解决战斗,多也就半小时,在解决问题毫无头绪的情况下,花点时间定位问题然后针对性解决,这个思路更加清晰了,和我在认知篇说的一样,大胆去做去分析就好了。
一些自己的思考
其实自己还挺喜欢case分析这块的工作的,算法的增长点、创新点都是从这里产生的,我们很多时候所谓的在努力地刷sota,是否有真正看过现在所谓的前沿方案,里面的bad case是什么样的,我们能通过什么方式让他变得更好,思路就是从中一点一点找出来的,所谓的明察秋毫,就是需要我们观察,bad case就是我们的线索,要突破必须要把这块东西给分析清楚。
解决篇
治疗术最后一篇,也是和问题最终决战的最后一篇,有了前面对问题的精准定位和分析,我们其实在这一步已经胸有成竹了,具体要怎么做,开讲。
模型预测错误的解决方案
算法工程师的舒适区就是模型,我们可以用模型的快速处理一些问题,但是缺点是调教模型在现实中难度其实并不大,因为大多数出问题的却不在模型本身,很多问题不是换模型就能够解决的,换模型带来的收益可能很小,那我们应该如何解决模型预测错误的问题呢?
样本的误导
条件概率的角度表示一个机器学习模型,其实就是,这个对大家来说理解并不困难,这里的就是一个模型了,对于一些数据内很浅层直白的信息,模型也会预测的非常直白,例如正类里面有大量含有“怎么”的样本,而负类里面没有,则模型很有可能会把所有带有“怎么”的句子都预测为正类,这个和模型的好坏是没关系的,因为这是数据指导模型学到的东西,因为这个数据就告诉了模型“带有”怎么的句子100%是正类了。在这个情况下,我们要解决这个问题的核心点就在于要调整这种数据,让模型摒弃这种想法,最直接的方式就是添加带有“怎么”的句子到负类数据里,平衡这种关系,告诉模型不能根据“怎么”来判断正负类,而是有其他信息。
当然,这是一个极端情况,但是这个对于面模型的问题非常常见,一般bad case中从句子层面发现了很明显共性的,大概率就是这个问题。
阈值的确定和权衡
阈值本质是个准入条件,准入的本质其实就是准召的权衡,这个才是一个完整的逻辑链路,因为阈值只是约束准入条件的其中一种方式(其他的我稍后再说),但又是和模型一起出现的标配,毕竟模型大部分时候只提供意见,要不要采纳还是要通过阈值。
既然阈值能控制准招,直线思路,那我们就对标准招来做权衡即可,取舍是肯定要取舍的,准招本身是一对矛盾的概念。我们需要一批数据做一套这样的预测实验(假数据,自己编的例子):
阈值 | 准确率 | 召回率 |
---|---|---|
0.9 | 0.92 | 0.40 |
0.85 | 0.90 | 0.60 |
0.80 | 0.85 | 0.75 |
0.75 | 0.79 | 0.80 |
随着阈值的下降,准确率会下降,而召回率会上升,我们需要确定一个最佳阈值,对于搜索和对话场景,更多的是需要我们保准确,一般是0.85+,召回率另说,持续优化,因此这里我们就只能定阈值在0.8以上,最好的就是0.8了,于是阈值就定下来了。
没有标注数据的,请自行准备,必要时候只能自己标,这个在认知篇已经讲过了。
前后处理和预处理
模型可以处理很多问题,但不见得是所有问题,规则的存在,让我们能更快速解决一些问题,另外把一些内容进行了预处理做了一些改变后,预测起来会更加方便。
先聊预处理吧,预处理这是一个很模糊的概念,很多人也会问出“要怎么做预处理”的说法,我的回答一般只能是看场景和需求,预处理的本质是对数据进行处理使之更好地被用来预测,不同的场景和数据的预处理方式会很不同,哪怕是同样的NLP问题,到了细分场景也会有很多不同的操作,共同的可能是对标点的处理,而不同的例如客服场景要把“你好”之类的删除,语音助手要把“小爱同学”、“小布”之类的称呼删除等,重点不在于做了什么,而是要把一些对预测有影响干扰项、不利于预测的因素给去掉,至于哪些是干扰项,请回去看bad case,这些错误样本会告诉你。
前处理是指模型预测之前的处理,尤其在模型比较大,性能要求比较高的场景,我们需要把一些肯定确定能快速预测出来的东西给过滤掉,从而提升性能也降低模型的负担,最简单的例如黑白名单,复杂的可以有一些针对业务的规则,例如超短句或者超长句直接拒绝等等,能用规则的尽量用规则,能不用模型的尽量不用模型,模型的不可控性众所周知,我们知道修复bad case的困难性的情况下,没必要太往依赖模型了。
后处理是指在模型预测以后的一些调整,最直接能想到的就是阈值过滤,但不仅是这些,有的时候需要结合模型的预测打分进行调整,例如关键词抽取领域,需要对预测的重要性分数进行加减分,如句首词和句尾词要提权,对于百科的一些问题,语义相似度的定义可能会和实际应用不同,于是我们也要进行一些过滤,例如“五一节的由来”和“六一节的由来”从语义相似度上来说确实很接近,但是我们不能然你给他们相似,因为“五一节”和“六一节”很不同,所以我们通过“实体约束”来过滤掉他们,当然背后就需要一批实体词典了。其实后处理的本质就是一个对模型的修正,模型很难学习的东西,我们可以直接通过后处理的方式来修复这些问题。
当然,一些特定的规则,我们是可以通过挖掘得到一些新的样本,这些样本批量放入模型的时候,是可以让模型去支撑一定的任务的,这也提升了泛化能力,这个方式非常适合在规则逐渐臃肿的情况下用来做减法,但值得注意的是,词典敏感的规则最好还是保留,毕竟词典的可枚举型要放到模型里需要依赖的样本会很多,模型没见过固然就不认识。
解决问题的验证和迭代
在进行一通操作以后,我们就要开始重跑验证集了,重新看看我们修复之后的效果如何,这就回到了我们的第一步——现状确认,重新确定我们是否还要优化,如果需要,则继续进行bad case分析,再持续优化,这是个螺旋上升的过程,我们持续分析持续优化就能逐步逼近最终目标最终效果。
有几个值得展开说的点再聊一下。
问题的暴露和转移
随之已知问题的解决,并不一定就会带来非常直接的优化,根本原因在于你很难知道箱子打开后里面是否还是箱子,老的问题不解决新的问题暴露不出来,或者是随着问题的逐步解决,很多问题会逐步从主要问题变成不主要的问题,新的问题会暴露出来。
做过推荐的小伙伴应该有很明显的感觉,推荐的论文里很多模型,看着实验效果很好但用起来效果就不明显甚至负向,很大一部分原因就是新模型没有解决问题,或者是解决的问题不是当前的核心问题,所以效果提升不够,例如要整所谓的用户序列,如果用户点击量不够,这个意义就不大,这个对于各个场景都是这样,我们随着学习都能有各种各样的装备武器来解决问题,但如果工具不适用于现在的核心问题,那用了的效果就是负向,这也是我希望大家多去分析的原因之一。
虽然问题千千万,一山放过一山拦,但随着解决,我们总会朝着目标靠近,持续努力即可。
什么时候该换模型
我一直没讲什么时候该换模型,我放在这里讲,因为我不完全认为换模型的本质是在提升效果,他只是一个算法系统的预测能力上限的提升,换个模型并不肯定会使效果有提升,甚至有的时候换了模型会有新的问题暴露,我们又要持续调教,效果才有可能完成新的提升。
所以,我认为提升模型的时机应该是能明显看到当前方案的天花板的时候,所谓的天花板,应该就是你会看到很多零散的bad case,问题不太能总结出来,或者是总结出来会发现分布很均匀,每个都只错了一两条的那种,此时很可能是当前模型吃不下了,这个时候,我们要开始寻求新的模型结构了。
某种程度上说,sota其实就是在探索这个上限的,但请记住,sota是特定数据集的sota,是否适合你还要多想想,不过归根结底还是要做实验,靠修补提升不了效果的时候,也就只能靠模型来进一步探索。
结束语
4篇文章接近1w字,总结了整个bad case从定位分析到解决的全流程思路方案,这其实是我自己总结的一套完整的方法论吧,来源是我的前辈的指导,其实前辈帮我分析问题解决问题也是通过这么做的,有这套严格的方法论指导,能让我在分析问题的时候更加严谨全面,不遗漏细节和关键步骤,也不容易陷入迷茫,很多东西看似自然但是做起来就很容易忘记,例如现状的确认,好or不好,我们很容易忘记去确定这个,发现一个bad case就去修,很可能会导致新的问题会出现(谁能想到带有“怎么去”的不一定能是地点意图呢,怎么去天堂...)。
我自己很喜欢这么总结,因为这种方法论是能知道解决很多问题的武器,此乃“万人敌”之术,当然,“百人敌”的方案也要跟上,持续学习吧,敬请期待。
实践篇
今天想在这基础上补充一篇,就是实践,借助一个案例,让大家理解整个bad case的修复流程是什么样的,从而让大家更加理解这个系列的内容。
现在开始,我会按照我之前聊的流程,逐步和大家讲这个流程。
案例背景
这次的案例,我选择的是上周讲过的这篇里的方案和数据:心法利器[114] | 通用大模型文本分类实践(含代码)。
首先是数据,直接上连接:https://github.com/aceimnorstuvwxz/toutiao-text-classfication-dataset,这个链接里的样本是各个场景下的文章标题,类目则是文章标题所属的领域,里面大概有15个左右,数据集也不小,有38万,此处就是取了1万出来,测试集1000,训练集9000。
方案上,就用的上期提到的大模型文本分类方案,给定一个待分类的文本query,第一步是从训练集中查出与query相似的样本,取TOP N,第二步是交给大模型做最终决策,看属于这个query属于哪个类,思路上还是比较好理解的,具体的方案细节可以参考我的这篇文章(心法利器[115] | bad case治疗术:实践篇)以及对应的开源项目:https://github.com/ZBayes/poc_project/tree/main/llm_classification。
识别现状
按照bad case治疗术的流程,第一步是识别现状,先回忆一下,我定义的现状是什么样的:
现在有一个基本baseline,baseline自然就有对应的评测集基线方案的效果,这个所谓的效果,就是现状,好or不好,是否需要持续优化,哪个方面需要持续优化,这就是所谓的现状,只有明确了宏观的现状,才能知道我们下一步的动作。
这里有3个要素,评测集、指标、结论。
评测集是评估效果的原料,数量和质量上都有很高的要求,只有评测集正确了,那评估出来的指标才是可靠的。
指标,指标能体现有两个点,一个是好坏,一个是暴露问题,前者很好理解,后者是可以通过构造特地给的指标来观测是什么位置出了问题,这点会在后面展现。
结论,结合指标,给出具体效果好坏以及问题点的初步结论。
然后我们一步步来整。
第一步就是评测集。因为这只是个实验,所以没有生产数据集,只能简单从数据集中随机抽1000条数据作为测试集(请注意,如果是现实应用中,还是应该尽可能拿到和实际应用场景分布一致的测试集,如某个问题在实际应用中出现的概率是1%则测试集也尽量要在1%左右),随机从给定数据里抽是实验是里一个常见的模拟方法了。
第二步是指标,作为多分类问题,常见的指标就是精确率、召回率和F1,详细如下:
precision recall f1-score support
科技-科技 0.64 0.90 0.75 96
国际-国际 0.73 0.71 0.72 79
娱乐-娱乐 0.69 0.81 0.74 91
教育-教育 0.71 0.89 0.79 90
民生-故事 0.54 0.47 0.50 15
证券-股票 0.00 0.00 0.00 1
军事-军事 0.80 0.60 0.69 58
旅游-旅游 0.64 0.84 0.73 58
文化-文化 0.83 0.74 0.79 74
体育-体育 0.87 0.84 0.86 89
房产-房产 0.87 0.73 0.80 45
财经-财经 0.92 0.37 0.53 89
农业-三农 0.84 0.73 0.78 49
电竞-游戏 0.89 0.77 0.83 77
汽车-汽车 0.92 0.87 0.89 89
micro avg 0.76 0.76 0.76 1000
macro avg 0.73 0.69 0.69 1000
weighted avg 0.79 0.76 0.76 1000
可以看到,显然整体效果0.76(weighted avg f1-score)不尽人意,但并能不止于此,进一步分析,要看分类目的F1,可以看到,有一些support比较多的高频类目,效果也并不是很好,比较凸显的“财经-财经”、“军事-军事”都不高,当然低频的类似“证券-股票”、“民生-故事”效果也不是很好,这个跟测试集内的数据量有些关系了。
为了进一步分析,还有一个很好的工具可以使用,那就是混淆矩阵,可以快速定位到一些容易混淆的类目。
from sklearn.metrics import classification_report, confusion_matrix
# 。。。
logger.info("\n{}".format(confusion_matrix(test_list, pred_list, labels=labels)))
执行结果,具体的顺序和上面的classification_report类目顺序是一样:
[[86 1 0 1 0 0 0 2 2 1 0 1 0 1 1]
[ 1 56 6 4 2 0 4 2 1 1 0 0 1 0 1]
[ 0 0 74 2 2 0 0 3 3 1 0 0 0 2 1]
[ 2 0 4 80 0 0 0 3 0 0 0 0 0 1 0]
[ 2 0 1 0 7 0 0 0 1 2 0 0 1 1 0]
[ 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0]
[ 2 12 2 5 0 0 35 0 0 1 0 0 0 0 0]
[ 1 1 1 1 0 0 0 49 3 0 0 0 1 0 1]
[ 2 2 4 5 0 0 1 4 55 0 0 0 0 0 1]
[ 0 0 3 2 0 0 2 0 0 75 0 1 0 2 2]
[ 5 2 2 1 0 0 0 2 0 0 33 0 0 0 0]
[23 2 2 5 2 10 0 2 0 2 3 33 4 0 0]
[ 2 0 2 0 0 0 0 6 1 0 2 0 36 0 0]
[ 4 0 5 2 0 0 2 2 0 3 0 0 0 59 0]
[ 4 1 2 4 0 0 0 1 0 0 0 0 0 0 77]]
倒数第四行第四列是33,对应的就是“财经-财经”,这里横向看,第1列和第6列的数比较大,分别是23和10,说明很多“财经-财经”类的内容被这两个类召走,分别是“科技-科技”、“证券-股票”,说明很大程度和这两个类目之间存在混淆,需要重点优化。
相比总体的指标,细分的指标能帮助我们进一步探知关键问题,这就是我前面所说的“暴露问题”,有了细节指标的信息,我们能对问题点有相对清楚的认识,在后续进一步分析bad case的时候,我们充分参考这些信息,能较快找到问题的根源。
分析bad case
然后,我们就可以开始进行bad case的分析了。这次我们直接拿上面的数据来分析,建议是分成4列,分别是case、实际值、预测值以及是否正确,如下面的样例所示:
case | real | pred | is_right |
---|---|---|---|
5月9日15:30 G3017金武段金昌站与水源站之间进行养护维修施工,请注意! | 汽车-汽车 | 汽车-汽车 | TRUE |
瑜伽服务平台Wake宣布完成数千万元A+轮融资 | 科技-科技 | 科技-科技 | TRUE |
小米、努比亚、360齐上阵,游戏手机,还是手机的“游戏”? | 科技-科技 | 科技-科技 | TRUE |
深圳地铁“扫码过闸”正式上线:马化腾亲身示范 | 科技-科技 | 科技-科技 | TRUE |
中国迎来第三次留学生“归国潮”,为什么他们都选择回来? | 教育-教育 | 教育-教育 | TRUE |
美国在亚洲有多少军事基地,分别在哪里? | 国际-国际 | 军事-军事 | FALSE |
拿到这些数据后,我们可以结合我们上一步分析到的问题类目来进行分析,也可以直接抽is_right的数据来进行分析,我们先考虑后者,并做出记录。
此处,我以其中一个case为例,讲一下分析的过程:"美国在亚洲有多少军事基地,分别在哪里?",这个句子标注结果是“国际-国际”,但是模型预测为“军事-军事”。此处,因为整个分类经历了多个流程,因此把整个流程的日志都给打出来。
2024-07-28 16:11:53.825 | INFO | __main__:predict:27 - request: 美国在亚洲有多少军事基地,分别在哪里?
2024-07-28 16:11:54.494 | DEBUG | __main__:predict:30 - [[370, ['世界上被美国驻军的国家有哪些?', ['6552991347354108167', '110', 'news_military', '世界上被美国驻军的国家有哪些?', '', '军事-军事']], 219.74318, 0.7373642921447754], [3601, ['南美洲大军事强国', ['6553019751621198344', '110', 'news_military', '南美洲大军事强国', '南美洲,阿根廷,哥伦比亚,军事强国,智利', '军事-军事']], 254.86084, 0.692778468132019], [6285, ['南美洲 大军事强国', ['6553019696919085581', '110', 'news_military', '南美洲大军事强国', '军事强国,智利,南美洲,阿根廷,哥伦比亚', '军事-军事']], 254.86084, 0.692778468132019], [2935, ['欧洲人怎么看待出现在欧洲的美国军队?', ['6552130324262813956', '113', 'news_world', '欧洲人怎么看待出现在欧洲的美国军队?', '', '国际-国际']], 254.54695, 0.6925537586212158], [752, ['美国一反常态,帮助日本提高海军实力', ['6553052718921941517', '110', 'news_military', '美国一反常态,帮助日本提高海军实力', '旗风级,军事实力,日本,驱逐舰,军舰', '军事-军事']], 271.53687, 0.6715511083602905]]
2024-07-28 16:11:54.498 | INFO | __main__:predict:58 - 你是一个优秀的句子分类师,能把给定的用户query划分到正确的类目中。现在请你根据给定信息和要求,为给定用户query,从备选类目中选择最合适的类目。
下面是“参考案例”即被标注的正确结果,可供参考:
世界上被美国驻军的国家有哪些?——军事-军事
南美洲大军事强国——军事-军事
欧洲人怎么看待出现在欧洲的美国军队?——国际-国际
美国一反常态,帮助日本提高海军实力——军事-军事
备选类目:
军事-军事,国际-国际
类目概念:
【军事-军事】:权威军事资讯、追踪军事热点、反映军事动态、介绍国内外最新武器发展动态
【国际-国际】:世界新闻、国际博览、新闻人物、评论分析、媒体聚焦
用户query:
美国在亚洲有多少军事基地,分别在哪里?
请注意:
1. 用户query所选类目,仅能在【备选类目】中进行选择,用户query仅属于一个类目。
2. “参考案例”中的内容可供推理分析,可以仿照案例来分析用户query的所选类目。
3. 请仔细比对【备选类目】的概念和用户query的差异。
4. 如果用户quer也不属于【备选类目】中给定的类目,或者比较模糊,请选择“拒识”。
5. 请在“所选类目:”后回复结果,不需要说明理由。
所选类目:
2024-07-28 16:11:55.010 | INFO | __main__:predict:62 - llm response: 军事-军事
2024-07-28 16:11:55.011 | INFO | __main__:predict:70 - parse result: 军事-军事
2024-07-28 16:11:55.012 | INFO | __main__:predict:73 - response: 军事-军事
2024-07-28 16:11:55.013 | INFO | __main__:<module>:80 - 军事-军事
我一段段解释,首先是这段:
2024-07-28 16:11:54.494 | DEBUG | __main__:predict:30 - [[370, ['世界上被美国驻军的国家有哪些?', ['6552991347354108167', '110', 'news_military', '世界上被美国驻军的国家有哪些?', '', '军事-军事']], 219.74318, 0.7373642921447754], [3601, ['南美洲大军事强国', ['6553019751621198344', '110', 'news_military', '南美洲大军事强国', '南美洲,阿根廷,哥伦比亚,军事强国,智利', '军事-军事']], 254.86084, 0.692778468132019], [6285, ['南美洲 大军事强国', ['6553019696919085581', '110', 'news_military', '南美洲大军事强国', '军事强国,智利,南美洲,阿根廷,哥伦比亚', '军事-军事']], 254.86084, 0.692778468132019], [2935, ['欧洲人怎么看待出现在欧洲的美国军队?', ['6552130324262813956', '113', 'news_world', '欧洲人怎么看待出现在欧洲的美国军队?', '', '国际-国际']], 254.54695, 0.6925537586212158], [752, ['美国一反常态,帮助日本提高海军实力', ['6553052718921941517', '110', 'news_military', '美国一反常态,帮助日本提高海军实力', '旗风级,军事实力,日本,驱逐舰,军舰', '军事-军事']], 271.53687, 0.6715511083602905]]
这个是向量召回的TOP5,可以看到,他们和原始的句子"美国在亚洲有多少军事基地,分别在哪里?",多少都存在一些相似性,说明向量召回这块的问题好像并不是很大,然而无论是前面几个的“军事-军事”的,可见似乎类似的句子,标注上都会划分到这个类,而非'国际-国际'类,这里仅有一条样本是'国际-国际'类,显然就比较少,而且相似度也比不过前几个结果。
__main__:predict:58
下的是prompt,初步看和预期的结果一致,没什么大问题。
后续连续的3行分别就是大模型的回复、解析结果以及最终的的回复了,可以看见都没什么问题:
2024-07-28 16:11:55.010 | INFO | __main__:predict:62 - llm response: 军事-军事
2024-07-28 16:11:55.011 | INFO | __main__:predict:70 - parse result: 军事-军事
2024-07-28 16:11:55.012 | INFO | __main__:predict:73 - response: 军事-军事
从此可见,整体流程初步看下都没有什么问题,似乎是标注的标准可能存在一定的混淆,类似的问题需要考虑澄清或者是更新标注,但是因为这是公开数据集,肯定难易处理这种问题,其实这种问题在公开数据集内并不少见,但是要修复只能说是无能为力。
刚才有提到“财经-财经”类下的错误有很多,此时我们就筛选real为“财经-财经”,但是pred不是这个的样本来分析一下,此处以“金融炼金术——里根的“大循环””为例进行分析。
2024-07-28 16:43:19.892 | INFO | __main__:predict:27 - request: 金融炼金术——里根的“大循环”
2024-07-28 16:43:20.471 | DEBUG | __main__:predict:30 - [[4800, ['新手理财首选:陆金所、小宝金融、宜聚网!', ['6553462877376741901', '104', 'news_finance', '新手理财首选:陆金所、小宝金融、宜聚网!', '陆金所,工薪
族,金融,平安宝-金色人生,宜聚网', '财经-财经']], 311.99478, 0.6219339966773987], [8826, ['大田环球贵金属:现货黄金走势分析操作建议', ['6553455628277252616', '104', 'news_finance', '大田环球贵金属:现货黄金走势分析操作建议', '铀浓缩,止损3,鲍威尔,ma5,第一目标,FOMC,伊朗', '财经-财经']], 314.15875, 0.6188830137252808], [4861, ['安达比索微交易——四点技巧分分钟赚钱', ['6553173895879328269', '104', 'news_finance', '安达比索微交易——四点技巧分分钟赚钱', '微交易,投资者', '财经-财经']], 312.19086, 0.6164110898971558], [7637, ['直击全球5大战略金属:最后一种有钱也买不到', ['6552340743468352013', '110', 'news_military', '直击全球5大战略金属:最后一种有钱也买不到', '国防工业,燃烧弹,耐蚀性,铀235,航天器', '军事-军事']], 315.8933, 0.611515998840332], [3833, ['陶以平:以FICC为抓手助力黄金市场高质量发展', ['6553402235752022536', '104', 'news_finance', '陶以平:以FICC为抓手 助力黄金市场高质量发展', '黄金市场,兴业银行,陶以平,工商银行,FICC,上海黄金交易所', '财经-财经']], 330.36914, 0.5959779024124146]]
2024-07-28 16:43:20.474 | INFO | __main__:predict:58 - 你是一个优秀的句子分类师,能把给定的用户query划分到正确的类目中。现在请你根据给定信息和要求,为给定用户query,从备选类目中选择最合适的类目。
下面是“参考案例”即被标注的正确结果,可供参考:
新手理财首选:陆金所、小宝金融、宜聚网!——财经-财经
大田环球贵金属:现货黄金走势分析操作建议——财经-财经
安达比索微交易——四点技巧分分钟赚钱——财经-财经
直击全球5大战略金属:最后一种有钱也买不到——军事-军事
陶以平:以FICC为抓手助力黄金市场高质量发展——财经-财经
备选类目:
财经-财经,军事-军事
类目概念:
【财经-财经】:股票、债券、基金、期货、信托、理财、管理等服务新闻
【军事-军事】:权威军事资讯、追踪军事热点、反映军事动态、介绍国内外最新武器发展动态
用户query:
金融炼金术——里根的“大循环”
请注意:
1. 用户query所选类目,仅能在【备选类目】中进行选择,用户query仅属于一个类目。
2. “参考案例”中的内容可供推理分析,可以仿照案例来分析用户query的所选类目。
3. 请仔细比对【备选类目】的概念和用户query的差异。
4. 如果用户quer也不属于【备选类目】中给定的类目,或者比较模糊,请选择“拒识”。
5. 请在“所选类目:”后回复结果,不需要说明理由。
所选类目:
2024-07-28 16:43:20.906 | INFO | __main__:predict:62 - llm response: 金融-财经
2024-07-28 16:43:20.909 | INFO | __main__:predict:70 - parse result: 拒识
2024-07-28 16:43:20.909 | INFO | __main__:predict:73 - response: 拒识
2024-07-28 16:43:20.910 | INFO | __main__:<module>:80 - 拒识
这里能很明显的看到,大模型的返回并没有给到我们“备选类目”中的结果,给的是“金融-财经”,这导致最终预测的结果是“拒识”,这是出错的根源。
分析不能掰棒子,我们要把每一条分析结果,都记录下来,并且最好是规范化,方便统计的那种。下面给出一些范例:
case | real | pred | is_right | 分析 |
---|---|---|---|---|
区块链与科技一拍即合,三角形主机开启数字资产的人人时代 | 财经-财经 | 科技-科技 | FALSE | 类目标准问题 |
黑龙江省的基本养老金是什么标准? | 财经-财经 | 农业-三农 | FALSE | 多标签问题 |
金融炼金术——里根的“大循环” | 财经-财经 | 拒识 | FALSE | 模型输出内容不符合规范 |
年逾古稀,却要面临着失去房产,是啥逼迫这对老夫妇? | 财经-财经 | 民生-故事 | FALSE | 未召回正确类目 |
升级后的余额宝还值得我们投资吗? | 财经-财经 | 科技-科技 | FALSE | 类目标准问题 |
未来几年最有前途的行业是哪个? | 财经-财经 | 教育-教育 | FALSE | 类目标准问题 |
什么是庞氏骗局? | 财经-财经 | 科技-科技 | FALSE | 相似度阈值 |
这里给出的是实际值是‘财经-财经’但实际并不是‘财经-财经’这个类目的bad case的分析数据,这里比原始数据多了一列,即分析,分析内给出这个case的归因分析,这里有几个细节需要强调:
可以考虑尽可能用统一的说法,方便归类。
如果出现多个问题,可以都写上去,逗号隔开。
坚持下去,多分析一些,从而体现出各个问题的实际占比,少则50多则100,看得多对问题理解、解决思路都有很大好处。
这个过程毫无疑问是比较枯燥的,但坚持下来,无论是对解决问题,还是对个人成长,都是有巨大好处的,所谓的经验就是从中而来,对问题有了解,在遇到新的问题时能提前预知风险和难题,这都是前期看case积累沉淀下来的,所以这个投入绝对值得。
在逐个对bad case进行分析后,希望大家能够梳理出类似这样的一个文档:
分析X个bad case后,分析出问题按频率排序如下:
1. 类目标准问题,X个,占比30%。
2. 多标签问题,X个,占比20%。
3. 模型输出内容不规范,X个,占比10%。
问题已经定位好了,那我们距离解决就非常接近了。
解决问题
剩下的,就是针对问题提供解决方案了,这是一个非常考验经验和积累的活。上面的问题,其实很多都有一些trick的方式来解决,或者压根就是系统性问题不好解决。
以类目标准问题为例,类目的标准来自开源方,我们无法直接改变这个标准,以及内部的数据标注,但在实际应用中,我们是可以沟通更新解决的,边界明显化不仅是对模型效果收益,在业务层面,也能更加清晰。类似的还有多标签问题,只有把方案升级为多标签方案(包括标签体系),才有机会继续搞。
比较好处理的是分类方案内的细节问题,例如相似度模型错误,相似度阈值,模型输出内容不规范等,都是可以轻松解决的,相似度模型方面可以通过阈值、微调模型之类的来优化,模型输出内容不规范可以优先考虑prompt或者是别的兜底方案,例如参照向量召回的结果。在问题比较明确的情况下,解决问题的方法就逐步明确了。
这里真的非常考验积累,不得不再次强调两个很经典的拷打。
你不会只会微调这招吧。
说得好像微调后,效果就能提升似的。
会针对问题提出方案的能力还是非常重要的,模型外围增加规则或者别的模型进行辅助,根据bad case定向收集更多样本提升覆盖率,调整阈值,增加“垃圾桶”类目(例如拒识)等,都是些技巧,需要多打开思路,也多研究各种分享的案例。
小结
之前的bad case治疗术系列写的比较抽象,这次结合一个具体任务来给大家讲解这里的细节,该分析哪些数据,具体分析哪些方面,具体怎么做有效,怎么对最终的结果有好处,这种思维模式能让我们更聚焦于问题本身,更有针对性,更有利于解决当下问题。
当然,也有缺点,在这里也补充说一下,这个思路容易让我们陷入局部的问题,对更大的,类似模型结构的,大方向上的问题,很容易被忽略,这个还是要注意。