聚类算法在传统测试数据抽样中的应用
众所周知,机器学习在传统行业落地困难,在测试领域更是如此。原因主要有:第一,数据量问题。传统行业数据量相对少,小样本数据想通过机器学习找出规则来,数据量不够;第二,适用落地场景难以选择,包括今天要讲的测试领域机器学习的应用也难见踪影。
传统测试领域里难以运用还有一个重要原因是,人们一般认为机器学习是基于统计的,其输出会因数据分布的不同而不同,而传统测试要求输出明确的对与错。但随着现在测试数据增多,再根据传统的测试理论选择测试数据,要么成本高周期长,要么容易出现重大的漏测现象。
所以今天本文从以下几个方面介绍机器学习的落地实践:
一. 背景
二. 解决方案
三. 具体实现
四. 怎么评估模型的好坏
五. 总结
一. 背景
我们常常发现,测试人员自认为测的很完美的产品经常被下游或客户发现bug。这一般是因为测试人员通常对这些测试数据进行了抽样,而使得某些类型的数据没有覆盖到。而抽样则实际上是测试的本质属性之一(抽样的概念来源于统计,是从想研究的全部样品中抽取一部分样品单位。其基本要求是要保证所抽取的样品单位对全部样品具有充分的代表性。抽样的目的是从被抽取样品单位的分析、研究结果来估计和推断全部样品特性。因此从海量数据里选择数据用抽样更合理)。
而且因为现在的开发迭代周期越来越快,压缩了测试的时间,测试往往是时间短任务重。如果把所有的case全跑完,时间不够;如果只跑smoke test,很难保证测试质量。尤其是数据量越大,测试越困难。
准备测试数据也是痛中之痛:第一,模拟数据难。造数据是功能测试常用的手段,但不是所有的场景都适合造数据,尤其对于复杂系统的集成测试、系统测试或用户验收测试,造数据不切实际或成本比较高。第二,从真实数据里面选数据难,当数据格式很复杂时,测试人员很难挑选哪条数据作为测试数据,比如从百万级数据里挑选下面这种格式的数据:
与此以外,测试中,抽样只是辅助测试去抽取数据,而不是判断程序的对与错,对与错的问题还是由测试人员去判断。同时,数据量越大,手工抽样数据越困难。
所以我们尝试使用机器学习来解决以上问题。而本文选择的聚类算法属于无监督学习,通常是用来寻找数据间的相似关系。而实践证明,聚类确实大大降低了测试成本,提高了测试效率。
接下来,我们将介绍此次的详细解决方案。
二. 解决方案
解决方案架构
如上所示,整个解决方案由一下几个模块组成。
输入:需要抽样的数据集,数据集应该是数据表或者csv格式
输出:抽样结果,直接提供给QA进行测试
专家系统模块:主要是根据测试人员或者专家经验用代码编写规则。比如正则表达式,或者测试理论里的等价类、边界值、异常值等。由于这些规则明确,抽取的数据也都有明确意义,所以要尽量多的编写规则。专家系统模块与机器学习没有关系。
聚类模块:对于不能用规则表示的数据,采用聚类算法抽取数据,然后从抽样的结果里寻找规则,如果能发现规则,就将新的规则加入到专家系统模块。这是我们下面要重点介绍的内容。
聚类模块流程
1.把需要抽样的数据集,经过专家系统模块过滤掉有明确规则的数据,剩下的数据为不能用规则表示的数据。
2.对于不能用规则表示的数据,根据需求做特征工程。
3.利用聚类算法抽样数据。如果测试时间很短,可以直接用抽样数据作为抽样结果直接进行测试。
4.如果时间充裕,手工检查这些抽样数据,试着提取新的规则,编写规则并加入到专家系统模块。
5.然后从专家系统模块过滤掉有明确规则的数据,对于不能用规则表示的数据,重复2~4。
直到发现不了新的规则,然后从专家系统模块根据规则抽取数据作为最终的抽样结果提供给测试人员去检查。
三. 具体实现
1.从数据库中获取数据,离线训练。这样做的好处是:
利用定时器在系统闲时取数据,这样取数据比较快,而且训练时不会影响其他人测试。
本地训练速度比较快,调试方便。
根据动态数据持续优化模型。
2.利用专家系统过滤掉能用规则表示的数据。比如对于枚举型,按枚举值分组,每组取一条数据;对于长整型数据,取最大值和最小值。
3.将不能用规则表示的数据根据测试点分层做特征工程。这一步是最关键的,也是花时间最多和最难的。在下面,我们详细展开讲讲。
分层做特征工程的意思是:对于新系统,只对少量但关键测试点做特征工程,以便第一时间给出反馈;而对于需求经常变动的系统,在关键特征的基础上根据需求调整特征工程;但对于比较稳定的系统,要尽量多的配置特征,以便能保证抽取数据的覆盖度。
下面举两个例子,怎么做特征工程:
1) 对于字符串类型数据,假设“列名”这列是需要抽样的数据,分析结果如下:
列名 |
能编写规则吗? |
能编写规则的原因 |
能否用规则表示? |
Y |
空值 |
能 |
|
A |
Y |
字符串的最小长度 |
能 |
Db |
否 |
||
abc |
Y |
在测试领域, a到z 是等价的,所以abc和aef是等价的 |
能 |
aef |
否 |
||
ab! |
否 |
||
23a! |
否 |
||
123 |
否 |
||
aaa23 |
否 |
||
AAAA@ |
否 |
||
#$$%% |
否 |
||
eeeeeeeeeeeeeeeeeeeeeeeeeeee |
Y |
字符串最大长度 |
能 |
eeeeeeeeeee |
否 |
将以上四条能用规则表示的数据编写规则,对于不能用规则表示的数据,分析过程如下:
· 根据测试理论,[A~Z,a~z]是等价的, 而特殊字符、数字和[A~Z, a~z]需要分别考虑。
· 直接使用[A~Z, a~z]的ASCII码来求得字符串的值
· 对于纯数字的字符串,在ASCII码求和的基础上加1000
· 对于含有特殊字符的字符串,在ASCII码求和的基础上加2000
例如:
"aef" = (97 + 101 + 102) = 300
"23a!" = 50 + 51 + 97 + 33 + 2000= 2231
"123" = 49 + 50 + 51+ 1000= 1150
"#$$%%" = 35 + 36 +36 + 37 + 37+ 2000 = 2181
经过实验,发现聚类效果并不好,修改特征工程,将求和改成求平均值:
"aef"= (97 + 101 + 102)/3 = 100
"23a!" = (50 + 51 + 97 +33)/4 + 2000 = 2058
"123" = (49 + 50 + 51)/3 + 1000 = 1050
"#$$%%" = (35 + 36 +36 + 37 +37)/5 + 2000 = 2036.2
当然如果认为字符串的长度也是需要考虑的因素,长度也可以作为一个特征。所以对于不能用规则表示的数据,最终特征工程的结果如下:
列名 |
ASCII value |
Length |
aef |
100 |
3 |
db |
99 |
2 |
ab! |
2076 |
3 |
23a! |
2057.75 |
4 |
123 |
1050 |
3 |
aaa23 |
78.4 |
5 |
AAAA@ |
2064.8 |
5 |
#$$%% |
2036.2 |
5 |
eeeeeeeeeee |
101 |
11 |
2) 对于复杂类型的数据,即使手工抽样也是相当困难的,尤其是在海量数据里怎么抽取数据来保证抽取结果是可靠的呢?
假设有下面三条数据,第一条和第二条是同一条记录的不同生命周期,第三条属于另一条记录。
键值 |
列名 |
起始时间 |
结束时间 |
K1 |
[{"restrictionAreaCode":112012,"restrictionAreaCode":100007,"restrictionBeginDate":"2020-02-26"}], |
2020-02-18 05:51:50.065 |
2020-02-18 05:59:27.421 |
K1 |
[{"restrictionAreaCode":100319,"restrictionBeginDate":"2020-02-12","zone":"Europe/Zurich"},{"restrictionBeginDate": 2020-04-12}] |
2020-02-18 05:59:27.421 |
9999-12-31 00:00:00 |
K2 |
[{"restrictionBeginDate":"2020-05-12"},{"restrictionAreaCode":100317}] |
2020-02-18 05:59:27.421 |
9999-12-31 00:00:00 |
在项目的不同阶段,采用不同的处理方式。
项目初期,测试点只考虑数据是否为空值:
将JSON Array解析成字符串列表
列名 |
字符串列表 |
[{"restrictionAreaCode":112012,"restrictionAreaCode":100007,"restrictionBeginDate":"2020-02-26"}] |
[restrictionAreaCode_0:112012,restrictionAreaCode_1:100007,restrictionBeginDate_0:"2020-02-26"] |
[{"restrictionAreaCode":100319,"restrictionBeginDate":"2020-02-12","zone":"Europe/Zurich"},{"restrictionBeginDate": 2020-04-12}] |
[restrictionAreaCode_0:100319,restrictionBeginDate_0:"2020-02-12",zone_0:"Europe/Zurich", restrictionBeginDate _1: "2020-04-12"] |
[{"restrictionBeginDate":"2020-05-12"},{"restrictionAreaCode":100317}] |
[restrictionBeginDate_0:"2020-05-12",restrictionAreaCode_0:100317] |
将字符串列表分割成空值和非空,特征工程结果如下:
项目发展期:如果不同的生命周期对数据的处理结果可能不一样也是一个测试点,应该将数据的生命周期条数当成另一个特征。
特征工程的结果是:
如果需求发生变化,restrictionBeginDate需要重点测试,则增大restrictionBeginDate的权重,比如乘以5,特征工程结果变为:
项目稳定期
当系统稳定时,将更多的测试点做成特征工程,比如restrictionBeginDate不仅要考虑空值,还需要考虑日期格式、最大值、最小值等。
4. 用聚类算法进行抽样
采用K-Means作为聚类算法,因为该算法简单,只需要一个超参数。
这里需要强调两点:
第一,特征工程和聚类算法是相辅相成的,要将聚类结果和特征工程综合考虑来进行反复尝试、并根据结果不断优化的过程,才能训练出好的模型。
第二,平衡测试时间、测试成本和测试范围灵活的选取测试数据的数量。如果测试时间很短,在每组聚类结果中抽取距离质心最近和最远两条数据作为抽样结果,来反应每组数据的基本特征和最不相似的特征,这样能在第一时间反应出被测产品的质量;如果时间在正常范围内,那么可以将每组聚类结果距质心的距离进行切分,然后根据测试时间,选择合适数量的结果进行测试;如果测试时间充裕,可以根据聚类结果多挑选一些数据,从中提取出较多新的规则来优化框架。
四. 怎么评估模型的好坏
因为非监督学习不像监督学习有明确的评估指标,所以在结果评估上会比较难。下面给出我们的评估过程:
1.根据测试点和重要程度配置特征工程和权重。
正常的测试过程是:列出检查点,然后根据检查点去选择数据或者造数据;利用聚类的做法是:在做特征工程时,先列出检查点和重要程度,然后根据检查点找出可能影响抽样结果的因子,将这些因子配置到特征工程中,然后根据重要程度配置权重。
2.根据抽样结果进行评估。
下面列出几个实践过程中发现的几个有趣的地方:
对于Boolean类型的数据,在pre-production环境里,除了y和n,还抽样出了a这种脏数据。
对于下面的数据,红色框中的数据是对于某个属性根据K-Means算法在预期分成5到10组的情况下的抽样结果。通过数据发现这些数据没有区别,那么QA就可以很自信的说:用一条数据就能确保需要测试的100万条数据(实验数据总数是100万条)没问题,这是多么高效的方式!同理对于绿色框中的属性,用2条数据就可以代表100万条数据。
对于图1中的数据,结果如下:
五. 总结
在实际项目的测试过程中,提高了5倍以上的测试覆盖度,时间下降到小时级,而且不受条件复杂度的影响。同时在海量数据的情况下,可以大大降低测试数据的数量级,而且覆盖度没有明显的降低。并且随着框架的不断优化,降低了测试人员对金融背景知识的要求,从而大大降低了进入门槛。
但也有一些需要继续改进的地方:目前只支持通过修改代码配置特征和特征的权重,后续我们希望使用UI进行灵活的配置和调试,帮助没有机器学习经验的人在页面上即可抽样数据。而且聚类算法是基于距离的,在训练的过程中要不断调整特征和权重,避免特征工程的结果呈均匀分布。通过PCA降维后画图的可视化方式是我们后续优化的一种方式。