apriori算法实现_Apriori算法的进化版,挖掘数据超快速的FPgrowth

本文深入探讨了FP-growth算法,这是一种基于FP-tree的数据挖掘技术,用于快速发现关联规则。首先,介绍了FP-growth的基本概念,接着详细阐述了头指针表的构建过程,最后讲解了如何建立FP-tree,以此提升apriori算法的效率。
摘要由CSDN通过智能技术生成
点击上方蓝字,和我一起学技术。 301162f887e7114fa24281e6a27b2e88.png 今天是机器学习专题的第20篇文章,我们来看看FP-growth算法。 这个算法挺冷门的,至少比Apriori算法冷门。很多数据挖掘的教材还会提一提Apriori,但是提到FP-growth的相对要少很多。原因也简单,因为从功能的角度上来说,FP-growth和Apriori基本一样,相当于Apriori的性能优化版本。 但不得不说有时候优化是一件很尴尬的事,因为优化意味着性能要求越高。但是反过来说,对于性能有着更高要求的应用场景,无论是企业也好,还是学术研究也罢,现在早就有了更好的选择,完全可以用更强大的算法和模型,没必要用这么个古老算法的优化版。对于那些性能要求不高的场景,简单的Apriori也就够了,优化的必要也不是很大。 但是不管这个算法命运如何,至少从原理和思路理念上来说的确有为人称道的部分。下面我们就来看看它的具体原理吧。

FP-growth与FP-tree

FP-growth的核心价值在于加速,在之前介绍的Apriori算法当中,我们每一次从候选集当中筛选出频繁项集的时候,都需要扫描一遍全量的数据来计算支持度,显然这个开销是很大的,尤其是我们数据量很大的时候。 FP-growth的精髓是构建一种叫做FP-tree的数据结构,它只会扫描数据集两次,因此整体运行的速度显然会比Apriori快得多。之所以能做到这么快,是因为FP-growth算法对于数据的挖掘并不是针对全量数据集的,而只针对FP-tree上的数据,因此这样可以省略掉很大一部分数据,从而节省下许多计算资源。 从这里我们可以看出,FP-tree是整个算法的精髓。在我们介绍整个树的构建方法以及一些细节之前,我们先来看下FP这两个字母的含义。相信很多同学从一开始的时候就开始迷惑了,其实FP这两个字母是frequent pattern的缩写,翻译过来是频繁模式,其实也可以理解成频繁项,说白了,FP-tree这棵树上只会存储频繁项的数据,我们每次挖掘频繁项集和关联规则都是基于FP-tree,这也就过滤了绝大多数不频繁的数据。 这是一棵生成好的FP-tree,我们先来看一下它的样子,再来详细解读其中的细节和原理。
52e02e765a973e8b58e535f672bdc379.png

头指针表

上图这个结构初看会觉得很混乱,完全不知道应该怎么理解。这是很正常的,但如果我们把上图拆开,可以分成两个部分,左边的部分是一个链表,右边是一棵树。只不过左边的链表指到了右边的树上,所以整体看起来有些复杂。 我们先忽略右侧的树的部分,以及指针表和树之间关联的指针,单纯地来看一下左侧的头指针表。仅仅看这一部分就简单多了,它其实是一个单频繁项的集合。 前面我们提到我们在使用FP-growth算法的过程当中,一共只需要遍历两次数据集。其中第一次遍历数据集就在这里,我们首先遍历了一遍数据集,求出了所有元素出现的次数。然后根据阈值过滤掉不频繁的元素,保留下来的结果就是单个频繁项的集合。 这里的逻辑非常简单,只有两件事,第一件事是统计每个单独的项出现的次数,第二件事是根据阈值将不频繁的项过滤掉。
def filter_unfreq_items(dataset, min_freq):
    data_dict = defaultdict(int)# 统计每一项出现的次数for trans in dataset:for item in trans:
            data_dict[item] += 1# 根据阈值过滤
    ret = {}for k, v in data_dict.items():if v >= min_freq:
            ret[k] = vreturn ret
通过这个方法,我们就生成了头指针表里的数据。之后我们要在建立FP-tree的过程当中将这份数据转化成链表,也就是左边的头指针表。虽然我们还不了解建树的原理,但至少我们可以把dict转化成指针表,这个逻辑非常简单,说白了我们只需要在dict的value当中增加一个引用即可。
def transform_to_header_table(data_dict):# 这里的None相当于链表的nextreturn {k: [v, None] for k, v in data_dict.items()}
这里的None要存的就是链表下一个位置的引用,只是目前我们只有链表头,所以全部设置为None。

建立FP-tree

我们有了头指针表的数据,也就是高频的单个元素的数据之后,显然要将它用起来。很明显,我们可以用它来过滤整个数据集,过滤掉其中低频的元素。 其实本质上来说FP-tree的构建过程,其实就是一个将过滤之后的结果插入到树上的过程。后面的事情后面再说,我们首先来看过滤。 单纯的过滤当然非常简单,我们只需要数据集中的元素是否在头指针表中出现即可,没出现的都是低频元素。但是不仅如此,由于我们之后想要将它插入到树中。这里利用了huffman树的思想,我们希望出现频次越高的元素存放的位置距离根节点越近。出现频次越低的离根节点越远,从而优化我们查询的效率。要做到这点,需要我们对数据进行排序。 我们来实现这部分内容,这部分内容分为两块,第一块是根据头指针表进行过滤,第二块是根据头指针表中出现的频次进行排序。
def rank_by_header(data, header_dict):
    rank_dict = {}for item in data:# 如果元素是高频的则保留,否则则丢弃if item in header_dict:
            rank_dict[item] = header_dict[item]# 对元素按照整体出现的频次排序
    item_list = sorted(rank_dict.items(), lambda x: x[1], reverse=True)return [i[0] for i in item_list]
有了这份数据之后,我们距离建树只有一步之遥。FP-tree的构建刚才也提到过,非常简单粗暴,就是将元素按照出现的频次进行排序之后从树根开始依次插入。在插入的过程当中,对路径上的节点进行更新,我这么说肯定很费解,但是看一张图就肯定明白了:
729b30f4701eb23bd436b221d77e895b.png
一开始的时候树为空,什么也没有,接着插入第一条数据{r, z}。由于z出现的次数大于r,所以先插入z再插入r,之后插入了一条{z,x,y,s,t},同样是按照整体出现的频次排序的。由于z已经插入了,所以我们将它出现的次数更新成2,之后发现没有重复的元素,那么就构建出一条新的分支。 本质上来说就是按照频次排序之后,由浅入深依次插入,如果相同的元素之前已经出现过了,那么就更新它出现的次数。 这个逻辑应该很好理解,在我们实现逻辑之前,我们先来创建树节点的类。
class Node:def __init__(self, item, freq, father):
        self.item = item
        self.freq = freq# 父节点指针
        self.father = father# 定义指针
        self.ptr = None# 孩子节点,用dict存储,方便根据item查找
        self.children = {}# 更新频次def update_freq(self):
        self.freq += 1# 新增孩子def add_child(self, node):
        self.children[node.item] = node
这个类我们只需要看代码就好了,应该完全没有难度,当然如果你愿意你也可以给它加上一个可视化方法用来debug。但老实说树结构单凭打印很难显示得很清楚,所以我就不加了。 树节点的定义写好了之后,接下来就到了最重要的实现更新FP-tree的环节了。其实如果对上面的逻辑都理解了,这部分代码也非常简单,我们只需要套用刚才的代码将生成的数据按照顺序插入进树上即可。 这种在树上依次插入元素的做法非常常见,在很多数据结构当中有类似的操作,最经典的就是Trie树了。如果你学过,会发现这个插入操作真的和Trie几乎一模一样,如果你没学过,也没有关系,这也不难理解。
def create_FP_tree(dataset, min_freq=3):
    header_dict = filter_unfreq_items(dataset, min_freq)
    root = Node('Null', 0, None)for data in dataset:# 根据整体出现次数进行排序
        item_list = rank_by_header(data, header_dict)
        print(item_list)
        head = root# 按照排序顺序依次往树上插入for item in item_list:if item in head.children:
                head = head.children[item]else:
                new_node = Node(item, 0, head)
                head.add_child(new_node)
                head = new_node
            head.update_freq()return root
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值