Apriori算法(频繁集发现以及关联分析)

mac2024-06-29  58

        我们在网上购物的时候都会收到一些相关产品的推荐,这些被推荐的东西是怎么来的呢?如果我们买了一个鱼竿,那么推荐鱼线,鱼饵什么的是很正常的,毕竟这些产品都是相关性比较大的,收到推荐也不足为奇;但是仅限于此吗?之前不是有个很出名的例子,啤酒和尿布的例子,在没被发现这个规律之前,谁能想到他们两个有一定的联系?所以除过去那些关联性特别明显的东西,还有许多隐藏的有相关性的关系被隐藏在大量的数据之下。所以,从大规模数据集中寻找物品间的隐含关系被称作为关联分析或者关联规则学习。

        关联分析也可以用于特征的发现,发现某些事情的共性规则,即同时有哪些特征出现,例如发现毒蘑菇的共同特征。

1.关联分析

        关联分析是一种在在大规模数据集中寻找有趣关系的任务。这些关系可以分为两种形式:频繁项集合关联规则。       

        频繁项集:是经常出现在一块儿的物品的集合;

        关联规则:暗示两种物品之间可能存在很强的关系。

        表1是一个购物清单

                                                                    表1

交易号码

商品

0

豆奶,莴苣

1

莴苣,尿布,葡萄酒,甜菜

2

豆奶,尿布,葡萄酒,橙汁

3

莴苣,豆奶,尿布,葡萄酒

4

莴苣,豆奶,尿布,橙汁

        频繁项集是指那些经常出现在一起的物品集合,如表1中的{尿布,葡萄酒}就是一个很好的例子,就是说人们经常会把尿布和葡萄酒一起购买。

        我们用支持度来衡量这个集合出现的频繁度,它被定义为数据集中宝行该项集的记录所占的比例。从表1中可以得到,{豆奶}的支持度为4/5,有3条包含{豆奶,尿布}的记录,一次{豆奶,尿布}的支持度为3/5,因此我们可以指定一个支持度,从而过滤掉那些支持度小的集合。

        可信度或者说是置信度是针对一条诸如{尿布}->{葡萄酒}的关联规则定义的,有很强的方向性,箭头反过来就不一定成立。上面箭头关联规则可定义为:支持度({尿布,葡萄酒})/支持度({尿布}),{尿布,葡萄酒}的支持度为3/5,尿布的支持度为4/5,所以{尿布}->{葡萄酒}的可信度为3/4=0.75。可以这么理解:尿布和葡萄酒同时出现的概率比上尿布单独出现的概率,即在尿布出现的情况下,葡萄酒出现的概率,从而衡量尿布出现的情况下,葡萄酒出现可能性的大小。

2.Apriori原理

        在进行频繁集发现的时候,我们需要从小的集合开始,为什么呢?我们看图2:

 

                                                              图2

        假如我们有4样商品,那么进行不同大小的集合的组合,一共有15种组合方式,那我们就需要进行15次的判断。那么有N种商品,我们就需要2的N次方-1种的组合,那么这种指数级的增长必定会加大运算量,所以我们需要想办法减少判断的组合。

        假设我们从小集合开始判断,判断出{3}的支持度比较低,也就是在样本数据中出现3的次数比较少,那么12,13,23…等组合都是包括3的,那么他们的支持度是不会比3大的,也就是说如果小集合的支持度比较小,那么包含小集合的大集合也就不需要判断了,那么这就大大减少了判断的成本,也就降低了计算成本。所以我们就可以运用这个原理来发现频繁集。

3.使用Apriori算法来发现频繁集

        Apriori算法使用Apriori原理来发现频繁集。我们需要输入最小支持度和数据集,通过最小支持度对数据集进行过滤,得到频繁集。

        我们首先需要找出最小的元素集合,也就是单元素组成的集合:

def createC1(dataSet):     C1 = []     for transaction in dataSet:         for item in transaction:             if not [item] in C1:                 C1.append([item])     C1.sort()     return map(frozenset, C1)

        dataSet是我们原始的数据集。下面是计算频次、支持度,然后将支持度小于最小支持度的集合去掉:

def scanD(D, Ck, minSupport):     ssCnt = {}     for tid in D:         for can in Ck:             if can.issubset(tid):                 if not ssCnt.has_key(can): ssCnt[can]=1                 else: ssCnt[can] += 1     numItems = float(len(D))     retList = []     supportData = {}     for key in ssCnt:         support = ssCnt[key]/numItems         if support >= minSupport:             retList.insert(0,key)         supportData[key] = support     return retList, supportData

        函数scanD中,D是我们的最原始的数据集,Ck是需要进行判断是否符合最小支持度的集合列表,minSupport就如字面意思就是我们设定的最小的支持度。SsCnt是一个字典用来存放数据集以及其出现的频次,如{{2,3},3}表示集合{2,3}在所有的样本数据中一共出现过3次。第一个循环就是将要被判断去留的集合和原始的数据集比较计算出各个数据集出现的频次,numItems是样本数据的集合个数,第二个循环就是计算所有集合的支持度,然后和最小支持度比较,然后去除掉支持度小的集合。返回的是频繁集及其对应的支持度。

        总结一下scanD函数的目的:输入样本数据、待测试的数据集以及最小支持度,返回支持度不小于最小支持度的数据集。样本数据和最小支持度都不需要计算,最重要的就是要计算出待测数据,下面给出完成的Apriori算法:

def aprioriGen(Lk, k):       retList = []     lenLk = len(Lk)     for i in range(lenLk):         for j in range(i+1, lenLk):             L1 = list(Lk[i])[:k-2]; L2 = list(Lk[j])[:k-2]             L1.sort(); L2.sort()             if L1==L2:              retList.append(Lk[i] | Lk[j])     return retList def apriori(dataSet, minSupport = 0.5):     C1 = createC1(dataSet)     D = map(set, dataSet)     L1, supportData = scanD(D, C1, minSupport)     L = [L1]     k = 2     while (len(L[k-2]) > 0):         Ck = aprioriGen(L[k-2], k)         Lk, supK = scanD(D, Ck, minSupport)         supportData.update(supK)         L.append(Lk)         k += 1     return L, supportData

        我们先从apriori函数看起,while循环之前的不难理解,就是将原始数据生成一个单元素组成的数据集,并将这些单元素组成的数据集中支持度小于0.5的单元素集合去掉,将其作为一个整体放倒L中:结合图1中的例子,我们是去掉了集合{3},保留了{0},{1},{2}三个集合用于接下来的判断。接下来的循环就是判断集合中元素为k的集合的支持度,我们从k=2开始判断,k=1的我们已经判断过了。

        aprioriGen函数看似一个特别不好理解的函数,其实他的功能就是在单个集合中元素个数为k-1的基础上,生成单个集合元素个数为k的集合,例如我们输入[{0},{1},{2}],然后返回的就是[{0,1},{0,2},{1,2}],这样一个功能然后再对返回的元素集合进行最小支持度的过滤,比如我们此时就又把{1,2}集合过滤掉了,保留[{0,1},{0,2}],此时L就是[[{0},{1},{2}],[{0,1},{0,2}]],然后我们在把[{0,1},{0,2}]放入aprioriGen函数,此时k=3,生成{0,1,2}。此处一定要注意:根据Apriori原理不是说如果小集合被去掉,那么包含大集合的小集合不是也要去掉吗?我们上一步去掉了{1,2},那么{0,1,2}不是不应该出现吗?其实感觉不能这么理解,Apriori原理生成的结果只是从最终得到的结果来说的,{0,1,2}是应该去掉,只是不是在组合的时候去掉,是在下一步进行最小支持度判断的时候{0,1,2}是肯定要去掉的。那么再想既然不能去掉包含小集合的大集合,那效率也没提高多少,其实也不是这样,如果我们把{1,2}换成{1,3},提前去掉{1,3},那么以后的组合就不会出现有3的集合,对于这种两两组合的情况,去掉一个元素运算复杂度可是指数级下降的,所以还是可取的。

        所以对于上述问题也可以改进,就是增加记录被去掉的集合记录,然后每次也遍历这个记录集合,如果大集合包含这些集合,就去掉这个大集合。

        代码是《机器学习实战》书上的代码,对于aprioriGen函数我感觉好像有点问题,书中的原话如下:

 

                              

        如果我传入的Lk是[{0,1},{1,2}]呢,此时k3,那么前k-2个项不可能相同了,也就是这两个不会合并,这明显是不对的。也不知道是我没有理解正确,希望哪位看明白了指教一下。我觉得这里的目的是,找出只有一个不同的元素的两个集合合并,然后元素个数就是k了。

2020/03/13更新


上面如果传入{0,1},{1,2}那么根据每个组合都是从小的集合组合过来的,那么肯定会有{0,2}一起被组合过来,那么在此按照k-2个相同元素组合的时候{0,1}和{0,2}就可以组合为{0,1,2}所以就不需要{0,1}和{1,2}的组合了。


 

4.从频繁集中挖掘关联规则

        要找到一个关联规则,首先需要从一个频繁集开始,看从中是否可以通过某个元素或者某个元素集合推导出另一个元素。如果有个频繁集{豆奶,莴苣},那么就有可能有一条关联规则“豆奶->莴苣”。这就意味着如果有人购买了豆奶,那么在统计上他购买莴苣的概率较大,但是反过来不一定成立,即“豆奶->莴苣”成立,但是“莴苣->豆奶”不一定成立。

        所以对于关联规则,我们也有对应的量化方法,这种量化指标就是称为可信度。定义为P->H(箭头左边的集合称作为前件,箭头右边的集合称作为后件)的可信度为(支持度(P|H)/支持度(P))。这里的符号|我觉得应该是同时出现P和H的集合,至于《机器学习实战》书上介绍的感觉不太明白。所以可信度的计算需要用到的是支持度,而我们上面就已经计算出了支持度,只需要取出对应的支持度然后做一下除法运算即可。

 

                  

                                                          图2

        图2是对于频繁集{0,1,2,3}的关联规则网格示意图。阴影区域给出的是底可信度的规则。如果发现0,1,2->3是一条低可信度规则,那么多有其他以后以3作为后件的规则可信度也会较低。以3作为单元素集合的出现较低,那么就是说样本数据中没有更多的多元素集合包含3了,所以多有包含3的作为后件的关联规则可信度都比较低。

        我们可以利用关联规则的上述属性来减少需要测试的规则的数据。类似于Apriori算法中的思想,可以首先从一个频繁项集开始,接着创建一个规则列表,其中规则右部只包含一个元素,然后对这些规则进行测试。接下来合并所有剩余规则来创建一个新的规则列表,其中规则右部包含两个元素。这种方法也被称为分级法。下面给出代码:

def generateRules(L, supportData, minConf=0.7): 

    bigRuleList = []     for i in range(1, len(L)):        for freqSet in L[i]:             H1 = [frozenset([item]) for item in freqSet]             if (i > 1):                 rulesFromConseq(freqSet, H1, supportData, bigRuleList, minConf)             else:                 calcConf(freqSet, H1, supportData, bigRuleList, minConf)     return bigRuleList         def calcConf(freqSet, H, supportData, brl, minConf=0.7):     prunedH = [] #create new list to return     for conseq in H:         conf = supportData[freqSet]/supportData[freqSet-conseq]         if conf >= minConf:             print freqSet-conseq,'-->',conseq,'conf:',conf             brl.append((freqSet-conseq, conseq, conf))             prunedH.append(conseq)     return prunedHdef rulesFromConseq(freqSet, H, supportData, brl, minConf=0.7):     m = len(H[0])     if (len(freqSet) > (m + 1)): #try further merging         Hmp1 = aprioriGen(H, m+1)#create Hm+1 new candidates         Hmp1 = calcConf(freqSet, Hmp1, supportData, brl, minConf)         if (len(Hmp1) > 1):    #need at least two sets to merge             rulesFromConseq(freqSet, Hmp1, supportData, brl, minConf)

        我们从函数generateRules开始说起。参数L是我们之前得到的频繁集列表,supportData是频繁集列表及其对应的频繁度,是一个字典数据,minConf是可信度参数,默认为0.7。

        多元素集合才有可能出现关联关系,单元素集合是不可能的,所以函数generateRules中的外循环需要从1开始,index为0的全部是单元素集合。内循环首先将集合中的每个元素拿出来,放到H1中,H1是一个list,每个元素是一个frozenset不变类型的单元素集合(如<type 'list'>: [frozenset([1]), frozenset([3])])。下来我们先看i=1情况,即要运行的是calcConf函数。

        CalcConf函数:首先参数freqSet如命所示为频繁集,H是我们将频繁集中的元素拆开,成了generateRules函数中的H1,supportData为所有频繁集及其对应的支持度,brl是为了用来存储我们的符合可信度的数据形式,minConf为最小的可信度。很明显conf = supportData[freqSet]/supportData[freqSet-conseq]是计算可信度代码,可信度关系:(freqSet-conseq)->conseq,其中conseq为H中的每个单独的元素。后面的if条件就很简单了,如果大于最小可信度,就将其放到列表brl中。下面看下rulesFromConseq函数。

        rulesFromConseq函数:参数和CalcConf函数的参数完全一样。其对应的是i>1的情况,也就是频繁集中的元素个数大于2的情况。然后我们跟着代码走一下,此时的i=2,freqSet是长度为3的频繁集。到rulesFromConseq函数中,m=1,len(freqSet)=3>2,所以我们进入到if语句中,然后根据我们前面介绍的aprioriGen函数的作用,Hmpl此时应该是三个长度为2的频繁集。假设此时的freqSet是{1,2,3},那么此时的Hmpl就是[{1,2},{2,3},{1,3}],那么将其再带入到CalcConf函数中,如果有可信度合适的,那就和可能是{1}->{2,3},{2}->{1,3},{3}->{1,2}的中的某个情况,如果上述三种都是符合要求的,那么返回的就是[[{1,2},{2,3},{1,3}],然后在将其作为参数带入到rulesFromConseq函数中,此时m是2,那么(len(freqSet)=3)=(m+1=3)那么就不继续向下了。其实这个一个递归调用的问题。

        上面代码及思路是《机器学习实战》上的代码,跟一下代码,我们就会发现,上面的递归调用的情况不会出现{2,3}->{1},也不知道是书上的问题,还是就不需要判断。我觉得是需要判断的,毕竟这个方向性很强,所以反过来的情况也是需要判断的。

5.总结

        Apriori算法发现频繁集需要不停的循环判断集合的包含情况,所以对于数据量大的情况就不太合适,当然为了解决这个问题,FP-growth算法就产生了,请参考FP-growth算法高效发现频繁集。

          通过分析上面的代码,发现原书上的代码有些地方可能是有些问题的,最起码我觉得是有问题的,我这里只是把问题指出来了,有兴趣的也可以研究下。

 

最新回复(0)