第一次接触FP-Growth是在《数据挖掘概念与技术》,当时对它的理解只停留在概念层面。后来又在《机器学习实战》中接触到了它,结合着书中的讲解和代码,跑了点结果,理解加深了一点。最近,工作中需要使用到它,又重新捡起,开始精读和思考,发现收获很大。
FP-Growth(Frequent Pattern Growth, 频繁模式增长),它比Apriori算法效率更高,在整个算法执行过程中,只需要遍历数据集2次,就可完成频繁模式的发现。
dataset=[['啤酒','牛奶','可乐'],
['尿不湿','啤酒','牛奶','橙汁'],
['啤酒','尿不湿'],
['啤酒','可乐','尿不湿']]
对于输入的dataset,统计所有事项中各元素的出现频次,即每个1项集的频数,并将各元素按照频数降序排序,删除那些出现频数少于设定支持度sup的元素,形成列表L,留下来的元素就构成了频繁1项集。(这是对数据集的第一遍扫描)
对数据集中每个事务的元素按照列表L排序(按支持度降序排列),开始构造FP-tree。树的根节点为空,每个事务中的所有元素形成一条从根节点到叶子结点的路径。若几个事务的元素按列表L排序后,具有相同的前m个元素,则它们在FP-tree中共享前m个元素代表的节点。树中每个节点的计数为路径经过该节点的事务集的个数。(这是对数据集的第二遍扫描)
在创建FP-tree的同时,headTable也就创建好了,headTable可以理解为一个具有三列的表。第一列为元素(项ID),第二列为支持度计数,第三列为节点链。如下所示
项ID | 支持度计数 | 节点链 |
---|---|---|
啤酒 | 4 | nodelink1 |
尿不湿 | 3 | nodelink2 |
… | … | … |
牛奶 | 2 | nodelinkn |
headTable中的项也是按照支持度计数从大到小排列的。节点链则链接到FP-tree中这一项所在的各个叶子结点上,后面频繁项集的发现就是靠的这些节点链。
从headTable中的最后一行开始,依次往上取项,比如最开始我们取‘牛奶’。寻找‘牛奶’的所有前缀路径,这些前缀路径形成‘牛奶’的CPB(Condition Pattern Base,条件模式基),而这些CPB又形成新的事务数据库。将‘牛奶’这一项添加到我们的集合中,此时,‘牛奶’这个频繁1-项集的支持度就是headTable中的支持度计数。然后用‘牛奶’的CPB形成的事务数据构造FP-tree,如2.1所述,构造的过程中将不满足支持度的项删除,而满足支持度的项又会构成另外一个FP-tree和headTable,我们记为FP-tree1和headTable1。同样的从headTable1的最后一行开始,比如是可乐,那么把‘可乐’和之前的‘牛奶’就形成了频繁2-项集,此时,{‘牛奶’,‘可乐’}这个频繁2-项集的支持度就是headTable1中‘可乐’的支持度。同时,我们也要寻找‘可乐’的前缀路径形成的CPB构成的又一个事务数据集,仍旧是构造FP-tree,从headTable取元素项,将元素项加集合。。。
不知大家发现没,FP的发现过程就是一个循环里不断递归的操作。循环,循环的是headTable中的各个元素项;递归,递归的是元素项的CPB构成的事务数据集形成的FP-tree中发现FP。
起初,我用《机器学习实战》中的代码,稍加修改,得到了频繁项集和关联规则,但由于数据量大,程序跑起来有点慢,就往Spark上移植(Spark的MLlib库中有现成的FP-Growth接口)。就在我移植完,跑了一遍后,尴尬的事情发生了,两种实现跑出来的结果不一样,而且差别还挺大的!经过自己确认,两种实现输入都是一样的,Spark上直接调用的接口,输入格式也是它要求的格式。于是就把目光转到了FP-Growth算法原理和《机器学习实战》中的代码实现上。
在网上看了很多博主对FP-Growth算法的解读,最后看到这篇时,http://www.cnblogs.com/qwertWZ/p/4510857.html 发现了一些端倪。
这篇博文最后提到,《机器学习实战》中的createInitSet函数在统计事务时,简单的将频数都置为1,如原函数所示。对于没有重复事务的数据集,这种方法并没有什么问题,但是当数据集中存在相同事务时,这种频数置1的做法就会带来错误,正确的做法是应当对相同的事务频数累加起来,如修改后的函数所示。
原函数:
def createInitSet(dataSet):
retDict = {}
for trans in dataSet:
retDict[frozenset(trans)] = 1#如有相同事项,则计数出错
return retDict
修改后的函数:
def createInitSet(dataSet):
retDict = {}
for trans in dataSet:
retDict[frozenset(trans)] = retDict.get([frozenset(trans)], 0) + 1#若没有相同事项,则为1;若有相同事项,则加1
return retDict
将这一行代码修改后,再运行,两种实现的结果就一样了。
完整的fp-growth及关联规则代码在这里
由于我之前用sklearn比较多,但要实现频繁项集挖掘时,发现sklearn并没有包含apriori和fp-growth这类频繁项集发现算法,于是就直接借用《机器学习实战》里的代码了。后来在解决问题的过程中发现有一个名叫fp_growth包,安装后,发现调用非常简单。
import fp_growth as fpg
frequent_itemsets = fpg.find_frequent_itemsets(transactions, minimum_support, include_support=False)
其中,transactions是我们的输入,格式可以是list of list;minimum_support是算法的最小支持度,这个是小数表示的;include_support设置为True,则结果中包括频繁项集的支持度,否则不包括;frequent_itemsets就是我们要获得的频繁项集。