频繁项集挖掘之apriori和fp-growth

Apriori和fp-growth是频繁项集(frequent itemset mining)挖掘中的两个经典算法,主要的区别在于一个是广度优先的方式,另一个是深度优先的方式,后一种是基于前一种效率较低的背景下提出来的,虽然都是十几年前的,但是理解这两个算法对数据挖掘和学习算法都有很大好处。在理解这两个算法之前,应该先了解频繁项集挖掘是做什么用的。

频繁项集挖掘是关联规则挖掘中的首要的子任务。关联规则挖掘是要找出一个数据集上,满足一定条件的项集。这些项的集合能构成 形如蕴含式"A=>B"这样的“规则”。这个"=>"符号是通过一些条件来定义的,如果没有条件那当然所有的项组合都能形成这样的关系。频繁程度就是一种要求,也就是AB共同次数出现了超过阈值。 A和B能否形成"=>"规则, 就要根据定义来算,那首先就得把A、B需要的条件挖出来,也就是频繁项集挖掘要做的。

关联规则定义比较容易能搜到,频繁项集挖掘简单的说就是:给定一个项列表list = {A,B,C,...},一个数据集D的每条记录都是list 的子集,要找出数据集中频繁共同出现次数超过阈值t,也就是支持度,的所有组合。

这个挖掘其实不好做,因为结果可能是list 中所有项的组合,有2^|list|个可能,搜索空间是个组合爆炸的空间。看下图,先别看红字红线:

                  

要弄好这件事不仅需要有效减小搜索空间,而且对每个可能的搜索都必须快速完成。所以频繁项集挖掘在算法实践和编码实现上就要有非常强的技巧。我们就来深入学习apriori和fp-growth中的搜索方式和技巧。这两个算法很容易找到完整的步骤,这里会更注重里面一些精彩之处,但是可能书写不会那么规范,建议和完整算法对照来读。


先来看看apriori。Apriori采用广度优先的搜索方式,缩小搜索空间用到了一个称为apriori的性质。Apriori性质是这么说的:频繁项集的所有非空子集必然也是频繁的。这是很显然的,比如 同时包含项AB的记录条数肯定比只包含A的记录少。这条性质反过来也可以这么说:如果一个项集是非频繁的,那么它的超集必然也是非频繁的。

算法过程如下:

 输入:数据集D,支持度minsup

 输出:满足支持度的所有项集的集合L

 L1 = 发现1-项集(D); 

 for (k=2;Lk-1 ≠Φ ;k++) {

      Ck = 连接剪枝生成(Lk-1, minsup)

      扫描D,为Ck中每个项集c 统计 c.count

      保留Lk ={c ∈ Ck|c.count≥min_sup}

      L = L ∪ Lk  

 }

 Return L

其中算法精华在于 连接剪枝生成(Lk-1, minsup) 这一步, 包含连接步和剪枝步两个动作:

1. 连接步:长度k-1的项连接成长度k的项;具体就是对两个k-1长的项L1和L2,必须满足前k-2个项都相同才能连接,最后把L1和L2剩下的最后一项加起来,形成k的长度的项。

2. 剪枝步:k项连接完成后,检查其所有k-1子集是否是频繁的,如果是,才保留作为候选项。


可以通过一张截图来演示一下apriori的过程:



对应第一张图,连接步是从第k层的项集,向下扩展一层的候选项集,剪枝步能够通过apriori性质过滤掉那些肯定非频繁的项集。


Apriori的框架其实很小,但是可以想象得到主要的两个步骤: 连接+剪枝(也就是候选集生成),以及,候选集统计是很耗费时间的。

剪枝步也需要对每个k-候选项集的k-1子集都进行一次检测,也很耗费时间;统计频繁次数是必须的,因此需要扫描数据库,经历I/O。那么有必要剪枝,直接统计会不会更好呢,虽然没有试验过,但我估计还是剪枝以后减少候选集的统计更划算。而这两个耗时的步骤在实现上如果能使用到技巧,对算法时间影响最直接。比如剪枝步中k-1候选项集需要逐一向已有的k-1频繁项集查询,这用什么数据结构最好?又如扫描数据库的时候是否能过进行一些压缩,相同的记录进行合并减少遍历次数,以及过滤掉对统计没用的记录?


面对apriori的问题,感觉Fp-growth突然间就冒出来了,它是一个挖掘方式和apriori完全不一样的算法,直接看可能不那么像apriori直观,因为算法一开始就介绍了它采用的数据结构和挖掘方式。所以我们先对比下apriori和fp-growth的差异在哪,再介绍它的算法。

简单的说apriori是先产生一批候选项集,再通过原数据集去过滤非频繁项集:先找A、B、C,检查一下通过了,再找AB、AC、AB,检查又通过了,再到ABC... 这样的广度优先的方式。而fp-growth是先从数据集中找频繁的项,再从包含这个频繁项的子数据集中找其他的频繁项,把它们俩连起来也肯定是频繁的:先找A,再在找包含A的子数据库里,找到B,就得到AB是频繁,再再包含AB的子数据库里,找到C,ABC就是频繁了。

在了解了fp-growth的大致思路以后,我们就能介绍它采用的数据结构和算法了。

首先fp-growth采用了一个叫fp-tree 的数据结构去压缩存储数据集,放到内存里,这样以后过问原数据集的事就不必经过IO了。

Fp-tree主要是一种前缀树,和字典树(trie)接近,并且节点把项的次数也记录下了。字符的顺序有所不同,字典树用的是字母表顺序,fp-tree (frequent pattern tree)用的是字母表的频率降序,这样的好处是出现次数多的项作为共享前缀的概率也大,fp-tree的压缩率就高(后面还会提到),根据apriori性质,非频繁的项没用,因此fp-tree上可以没有它们。

根据前面提到fp-growth步骤,需要找数据库上包含某个项的子数据库,不能从树根开始搜索,因此为了方便,需要把fp-tree中所有枝干、叶子上相同的项全串一起,这样项从一个起点开始,向树根遍历,就能得到包含这个项的子数据库了。这些起点和串起相同节点的链就是fp-tree的另一个部分:头表和兄弟链。头表包含树上所有的单项,并是兄弟链的起点,那么fp-tree不仅完整记录了数据库里所需的信息,还能找到对任一项找到包含了它的子数据库。



有了fp-tree,挖掘频繁项集就变得直观了。首先是压缩数据库,过滤非频繁项,得到一棵fp-tree 1号,对于一个项,比如A,通过兄弟链,遍历树找出 包含A的子树(库),又称A的条件模式树(库),英文原文叫condition pattern tree(base)。然后把这个子库当做一个新的数据库2号,过滤2号库非频繁项,建立一个小点的fp-tree 2号,那么那个A与这个2号树里的所有项,连起来肯定也是频繁的;比如有B,同理把B的条件树找出,也建立个fp-tree 3号,就能得到AB和3号树上的项连起来也肯定是频繁的。这个过程递归完成,建立不出条件子树递归就跳出去。


算法包含两个部分:

1. 是建立fp-tree:扫描一遍数据库,得到每个项的支持度,排序并过滤非频繁项;再扫描一次数据库,每条记录按顺序排序,添加到fp-tree上。

2. 调用算法FP_growth(FP-tree, null)。

   Function Fp_growth(FP-tree, a){

 if(fp-tree 是单条路径){

   对路径上的组合b, 都连接a,输出b∪ 

 }else{

   For each 项ai in 头表{

     输出一个模式b= ai∪ a,其支持度 support =ai.support 

     构造b的条件模式库,然后构造b的条件模式树 Treeb; 

     If (Treeb 不为空){

        调用算法FP_growth(Treeb,b )

  }

 }

 }

FP_growth是个递归算法,期间需要反复遍历树和构建fp-tree。fp-growth中判断单路径部分可以不要,最后实际结果其实是和下面部分是一样的,但是直接计算单路径产生所有组合会便捷很多。另外一点,fp-tree要按支持度降序的顺序的好处有几点?前面说了可以提高共享前缀的可能,提高压缩率,树小了,遍历的步数还能减小,寻找最优压缩的顺序是个NP难问题,因此选这个办法能有个比较好的压缩率足够了。

fp-growth虽然号称不产生候选集,但是实际上候选集产生已经在寻找条件子库的时候隐隐产生了,剪掉非频繁候选项的时候是通过建树步骤中的第一小步完成的。

fp-growth在实现上也可以有很多技巧,比如寻找条件子树的时候,同一条路径会被遍历很多次,如何有效避免(后来han自己提出,遇到扫把型的fp-tree,即上面是单路径、下面分叉的,可以把单路径所有的组合分别连接到下面的部分挖掘结果上输出,那就不用遍历上面的单路径了) 另外树上节点用什么数据结构保存指向子孙节点的指针,能同时兼顾查询时间和空间?


最后我们总结一下apriori和fp-growth之间的联系和差异。

初读fp-growth算法,估计都感觉不到它和apriori有什么关系,但我个人猜测fp-growth是从apriori的统计候选集太耗时的那里 改良开始的,希望实现候选项集的更快速的计算支持度,最后就彻底的改变的搜索频繁项集的方式。我觉得两个算法的最根本的差异在于apriori是以搜索项集组合的空间作为基础,通过数据库来对照。而fp-growth是以数据库为基础,在里面寻找项集是否频繁,表现为搜索方式一个是广度优先一个是深度优先。

apriori的那剪枝步和统计支持度在fp-growth上就是不断的建fp-tree和遍历。而前者的统计需要经过的IO,后者已经压缩到内存了;但fp-growth不是在所有数据集上都比apriori强,比如在稀疏的数据集上,fp-tree每个节点可能包含非常多子孙,因此保存子孙节点的指针也是很大开销,fp-tree本来就是通过压缩使得数据集能被内存容纳,结果导致最后fp-tree起不到压缩效果适得其反。优化实现的apriori在稀疏数据集上也往往比fp-growth要快。


这里fp-growth在大部分地方是完胜了apriori,后面很多改进都是基于深度优先的思想,并且更注重实现上的技巧。现在我们也没必要去费太多精力去改进这两个算法了,因为频繁项集挖掘是个组合爆炸的时间复杂度。在2003 2004年ICDM举办过两个workshop就是专门比谁实现的频繁项集挖掘最好(搜"FIMI 03",网站里有很多的源码)。在这里想多提一点,数据挖掘中,没有算法能在所有数据集上PK掉其他算法。因此我们应该了解一种任务的多种算法,看看它们为什么和如何在不同的数据集上体现出自己的优势,这样,通过比较我们不仅能更好的理解和掌握它们的精华,更能在当我们遇到新的数据集的时候,选取合适算法甚至做出针对性的优化措施。

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值