《机器学习实战》之十二——使用FP-growth算法来高效发现频繁项集

本文深入探讨了FP-growth算法,一种高效的频繁项集发现方法,对比Apriori算法,仅需两次数据扫描,适用于购物篮分析等场景,介绍了FP树的构建与频繁项集挖掘过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、前言

  关联规则挖掘最典型的例子是购物篮分析,通过分析可以知道哪些商品经常被一起购买,从而可以改进商品货架的布局。

1、基本概念

首先回顾一下上一章中的一些基本概念。

(1) 关联规则:用于表示数据内隐含的关联性,一般用X表示先决条件,Y表示关联结果。

(2) 支持度(Support):所有项集中{X,Y}出现的可能性。

(3) 置信度(Confidence):先决条件X发生的条件下,关联结果Y发生的概率。

2、 Apriori算法

Apriori算法是常用的关联规则挖掘算法,基本思想是:

(1) .先搜索出1项集及其对应的支持度,删除低于支持度的项集,得到频繁1项集L1;
(2). 对L1中的项集进行连接,得到一个候选集,删除其中低于支持度的项集,得到频繁1项集L2;

迭代下去,一直到无法找到L(k+1)为止,对应的频繁k项集集合就是最后的结果。

  Apriori算法的缺点是对于候选项集里面的每一项都要扫描一次数据,从而需要多次扫描数据,效率低。为了提高效率,本章会在上一章讨论话题的基础上进行扩展,将给出一个非常好的频繁项集发现算法。该算法称为FP-growth算法。它是基于Apriori的算法构建,但在完成相同任务时采用了一些不同的技术。

3、 FP-growth算法

  FP-growth算法将数据集存储在一个特定的称作FP树的结构中去发现频繁项集或者频繁项对,即常在一块出现的元素项的集合FP树。FP树这种数据结构存储数据,主要包括项头表、FP-Tree和节点链表。

  FP-growth算法只需要对数据库进行两次扫描,而Apriori算法对于每个潜在的频繁项集都会扫描数据集判定给定模式是否频繁,因此FP-growth算法的速度要比Apriori算法快。

FP-growth算法只会扫描数据集两次,它发现频繁项集的基本过程如下:
(1) 构建FP数据
(2) 从FP树中挖掘频繁项集

接下来,我们先讨论FP树的数据结构,然后再看一下如何用该结构对数据集编码。

二、FP树:用于编码数据集的有效方式

  FP代表频繁模式(Frequent Pattern)。一棵FP树看上去与计算机科学中的其他树结构类似,但是它通过链接(link)来连接相似元素,被连接起来的元素项可以看成一个链表。另外,相似项之间的链接即节点链接(node link).

  接下来我们通过一个实际的例子来了解FP树。
在这里插入图片描述
  上图是用于生成FP树的数据。下图是FP-growth算法的步骤
在这里插入图片描述
1、第一遍扫描数据,找出频繁1项集L,按降序排序。得到的表叫项头表(Header Table)
在这里插入图片描述
上面的例子使用的最小支持度是3,上图中绿色填充的部分是非频繁1项,要去除掉的。因此,得到的项头表应该包含6项。

2、第二遍扫描数据:

  • 对每个transaction,过滤不频繁集合,剩下的频繁项集按L顺序排序

对上面的例子,进行该步骤之后,得到如下结果
在这里插入图片描述

  • 把每个transaction的频繁1项集插入到FP-tree中,相同前缀的路径可以共用

上面例子经过该步骤得到的结果如下:
在这里插入图片描述
在对事务记录过滤和排序之后,就可以从空集开始,向其中不断添加频繁项集。如果树中已经存在现有元素,则增加现有元素的值;如果现有元素不存在,则向树中添加一个分支。

  • 同时增加一个header table,把FP-tree中相同item连接起来,也是降序排序

上面的例子,经过该步骤,得到如下结果:
插入事务001:{z, r}
在这里插入图片描述
插入事务002:{z, x, y , s, t}
在这里插入图片描述
插入事务003:{z}
在这里插入图片描述
插入事务004:{ x, s, r}
在这里插入图片描述
插入事务005:{z, x, y , r, t}
在这里插入图片描述
插入事务006:{z, x, y , s, t}
在这里插入图片描述
到目前为止,我们已经很清楚FP-growth算法的过程了,那么接下来,就应该用python代码来实现该算法了。

三、构建FP树

  在第二次扫描数据集时会构建一棵FP树。为了构建一棵树,需要一个容器来保存树。

1、构建FP树的数据结构

  在这里我们要创建一个类来保存树的每一个节点。新建一个fpGrowth.py文件,并在其中加入如下代码:

# -*- coding: utf-8 -*-

class treeNode:
    def __init__(self, nameValue, numOccur, parentNode):
        self.name = nameValue
        self.count = numOccur
        self.nodeLink = None
        self.parent = parentNode
        self.children = {}
     
    #对count变量增加给定值numOccur
    def inc(self, numOccur):
        self.count += numOccur
        
    #用于将树以文本形式显示
    def disp(self, ind=1):
        print(" "*ind, self.name, " ", self.count)
        for child in self.children.values():
            child.disp(ind+1)
            
            
if __name__ == '__main__':
    rootNode = treeNode('pyramid', 9, None)
    rootNode.children['eye'] = treeNode('eye', 13, None)
    rootNode.children['phoenix'] = treeNode('phoenix', 3, None)
    rootNode.disp()

  运行上述代码得到如下图所示结果:
在这里插入图片描述
  现在FP树的数据结构已经建好了,下面就可以构建FP树了。

2、构建FP树

  通过上面的介绍,我们已经了解了从事务数据集转换为FP树的基本思想了,接下来我们通过代码来实现上述过程。打开fpGrowth.py文件,加入如下代码:

"""
函数说明:构建FP树
Parameters:
    dataSet:数据集
    minSup:最小支持度
Returns:
    retTree:FP树
    headerTable: 头指针表
"""           
def createTree(dataSet, minSup=1):
    headerTable = {}
    #扫描数据集,统计每个元素出现的频度
    for trans in dataSet:
        for item in trans:
            headerTable[item] = headerTable.get(item, 0)+dataSet[trans]
    #扫描头指针表删除掉那些出现次数少于minSup的项
    headerTableCopy = headerTable.copy()
    for k in headerTableCopy.keys():
        if headerTable[k] < minSup:
            del(headerTable[k])
    freqItemSet = set(headerTable.keys())
    
    if len(freqItemSet) == 0:  #如果没有满足最小支持度的项,则返回None
        return None, None
    #reformat the headerTable to use Node link,然后创建只包含空集合的根节点
    for k in headerTable:
        headerTable[k] = [headerTable[k], None]
    
    
    retTree = treeNode('Null Set', 1, None) #初始化树节点
    #扫描数据集,
    for tranSet, count in dataSet.items():
        localD = {}
        for item in tranSet:
            if item in freqItemSet:  #如果在频繁项集中,就存入localD字典中
                localD[item] = headerTable[item][0]
        if len(localD)> 0:
            #对localD中的项,按照出现频度进行降序排列
            orderedItems = [v[0] for v in sorted(localD.items(), key=lambda p: p[1], reverse=True)]
            updateTree(orderedItems, retTree, headerTable, count) #使用排序后的频繁项集对树进行填充
    return retTree, headerTable


"""
函数说明:更新FP树,让树生长
Parameters:
    items:排序后的频繁项集
    inTree:树
    headerTable:头指针表
    count:计数
Returns:
    无
"""   
def updateTree(items, inTree, headerTable, count):
    if items[0] in inTree.children: #判定第一个元素项是否作为子节点存在
        inTree.children[items[0]].inc(count)  #更新该元素项的计数
    else:
        #创建一个新的treeNode并将其作为一个子节点添加到树中
        inTree.children[items[0]] = treeNode(items[0], count, inTree) 
        if headerTable[items[0]][1] == None: #判断头指针表的该元素项对应的是否有指向
            headerTable[items[0]][1] = inTree.children[items[0]]  #如果没有指向,则指向该子节点
        else:
            #更新头指针表以指向新的节点
            updateHeader(headerTable[items[0]][1], inTree.children[items[0]])
    if len(items)>1:  #仍有未分配完的树,迭代
        updateTree(items[1::], inTree.children[items[0]], headerTable, count)

"""
函数说明:更新头指针表
Parameters:
    nodeToTest:头指针表中的某一元素项
    targetNode:目标节点
Returns:
    无
"""        
def updateHeader(nodeToTest, targetNode):
    #从头指针表的 nodeLink 开始,一直沿着nodeLink直到到达链表末尾
    while(nodeToTest.nodeLink != None):  
        nodeToTest = nodeToTest.nodeLink
    nodeToTest.nodeLink = targetNode


def loadSimpDat():
    simpDat = [['r','z','h','j','p'],
               ['z','y','x','w','v','u','t','s'],
               ['z'],
               ['r','x','n','o','s'],
               ['y','r','x','z','q','t','p'],
               ['y','z','x','e','q','s','t','m']
            ] 
    return simpDat

def createInitSet(dataSet):
    retDict = {}
    for trans in dataSet:
        retDict[frozenset(trans)] = 1
    return retDict


           
if __name__ == '__main__':
#    rootNode = treeNode('pyramid', 9, None)
#    rootNode.children['eye'] = treeNode('eye', 13, None)
#    rootNode.disp()
#    rootNode.children['phoenix'] = treeNode('phoenix', 3, None)
#    rootNode.disp()
    
    simpDat = loadSimpDat()
    print("simpDat:\n",simpDat)
    initSet = createInitSet(simpDat)
    print("initSet:\n",initSet)
    myFPtree, myHeaderTab = createTree(initSet,3)
    myFPtree.disp()
    

运行结果如下:
在这里插入图片描述  大家从运行结果来看,发现与上述我们推导构建出的树是不一样的,这是为什么呢?这其实在我们上面手动推导过程中,也会发现或者遇到一个问题,就是项头表中的频繁项的排序问题。因为有好几个频度是3的,那么排序的结果就会有多种,这样对事务过滤和重排序的结果也是不一样的,所以就导致构建出来的树出现了差异。
  我们可以将一些中间变量进行打印输出,看一下,将上述代码createTree()函数中添加上x下图这么一句,
在这里插入图片描述
  就会输出过滤和重排序后的事务,如下图所示:
在这里插入图片描述
  上述结果中最明显的就是标记出来的那条事务,所以根据这样的排序事务,我们构建出来的树应该是下面的结果,也就是上面程序输出的结果。
在这里插入图片描述

四、从一棵FP树中挖掘频繁项集

  实际上,到现在为止大部分比较困难的工作已经处理完了。有了FP树之后,就可以抽取频繁项集了。这里的思路与Apriori算法大致类似,首先从单元素项集合开始,然后在此基础上逐步构建更大的集合。当然这里将利用FP树来实现上述过程,不再需要原始数据集了。

从FP树中抽取频繁项集的三个基本步骤如下:
1. 从FP树中获得条件模式基;
2. 利用条件模式基,构建一个条件FP树;
3. 迭代重复步骤(1)步骤(2),直到树包含一个元素项为止。

  接下来重点关注第(1)步,即寻找条件模式基的过程。之后,为每个条件模式基创建对应的条件FP树。最后在从FP树中获得频繁项集。

1、抽取条件模式基

条件模式基(conditional pattern base)是以所查找元素项为结尾的路径集合。每一条路径其实都是一条前缀路径(prefix path)。简而言之,一条前缀路径是介于所查找元素项与树根节点之间的所有内容。
在这里插入图片描述
  如上图,符号 r r r 的前缀路径有:{z}、{x, s}、{z, x, y}。每一条前缀路径都与一个计数值关联。该计数值等于起始元素项的计数值,该计数值给了每条路径上 r r r 的数目。

  通过上面的介绍,我们就可以很容易的得到每一个频繁项的所有前缀路径。如下图所示:
在这里插入图片描述
  前缀路径将被用于构建条件FP树,为了获得这些前缀树,可以对树进行穷举式搜索,直到获得想要的频繁项为止,或者使用一个更有效的方法来加速搜索过程。可以利用先前创建的头指针表来得到一种更有效的方法。头指针表包含相同类型元素链表的起始指针。一旦到达了每一个元素项,就可以上溯这棵树直到根节点为止。
  打开fpGrowth.py文件中,加入下面前缀路径发现的代码:

"""
函数说明:递归上溯FP树
Parameters:
    leafNode:FP树的一个叶节点,它的数据类型仍然是treeNode
    prefixPath:前缀路径列表
Returns:
    无
"""
def ascendTree(leafNode, prefixPath):
    if leafNode.parent != None: #若父节点不为空
        prefixPath.append(leafNode.name)  #前缀路径列表中加入该叶节点
        ascendTree(leafNode.parent, prefixPath) #递归上溯该叶节点的父节点……
"""
函数说明:为给定元素项生成一个条件模式基
Parameters:
    basePat:给定元素项
    treeNode:FP树的头指针表
Returns:
    condPats:条件模式基字典
"""       
def findPrefixPath(basePat, treeNode):
    condPats = {}  #初始化条件模式基为空字典
    while treeNode != None: #若FP树不为空
        prefixPath = []  #初始化前缀路径列表
        ascendTree(treeNode, prefixPath)  #上溯FP树,来得到给定元素项的前缀路径列表
        if len(prefixPath) > 1:  #只要前缀路径列表不为空,则添加到条件模式基字典中
            condPats[frozenset(prefixPath[1:])] = treeNode.count
        treeNode = treeNode.nodeLink  #通过节点链接进入下一个树节点
    return condPats

           
if __name__ == '__main__':
#    rootNode = treeNode('pyramid', 9, None)
#    rootNode.children['eye'] = treeNode('eye', 13, None)
#    rootNode.disp()
#    rootNode.children['phoenix'] = treeNode('phoenix', 3, None)
#    rootNode.disp()
    
    simpDat = loadSimpDat()
    print("simpDat:\n",simpDat)
    initSet = createInitSet(simpDat)
    print("initSet:\n",initSet)
    myFPtree, myHeaderTab = createTree(initSet,3)
    myFPtree.disp()
    
    condPats_x = findPrefixPath('x',myHeaderTab['x'][1])
    print("x 的条件模式基:", condPats_x)
    condPats_r = findPrefixPath('r',myHeaderTab['r'][1])
    print("r 的条件模式基:", condPats_r)
    condPats_t = findPrefixPath('t',myHeaderTab['t'][1])
    print("t 的条件模式基:", condPats_t)

运行结果如下:
在这里插入图片描述

2、创建条件FP树

  对于每一个频繁项,都要创建一棵条件FP树。我们会为z、x以及其他频繁项构建条件树。可以使用刚才发现的条件模式基作为输入数据,并通过相同的构建树代码来构建这些树。然后,我们会递归地发现频繁项、发现条件模式基,以及发现另外的条件树。举个例子,假定为频繁项t创建一个条件FP树,然后对{t, y}、{t, x}……重复该过程。元素t的条件FP树的构建如下所示:
在这里插入图片描述
  在上图中提到了, t t t 的条件模式基为两项,但是里边去掉了 s s s r r r。实际上单独来看s和r他们都是频繁项,但是在 t t t 的条件树中,他们却是不频繁的。也就是说, { t , r } \left \{ t, r \right \} {t,r} { t , s } \left \{ t, s \right \} {t,s} 是不频繁的。

  接下来,对集合来挖掘对应的条件树。这会产生更复杂的频繁项集。该过程重复进行,直到条件树中没有元素为止,然后就可以停止了。

  打开fpGrowth.py文件,在其中加入如下代码:

"""
函数说明:构建条件树
Parameters:
    inTree:FP树
    headerTable:头指针列表
    minSup:最小支持度
    preFix:存放条件模式基
    freqItemList:频繁项列表
Returns:
    无
"""
def mineTree(inTree, headerTable, minSup, preFix, freqItemList):
    #对头指针表中的元素按照其出现频率进行升序排列,并将排序后的元素项保存到bigL列表中
    bigL = [v[0] for v in sorted(headerTable.items(),key=lambda p:p[0])]
    #print(bigL)
    for basePat in bigL:
        newFreqSet = preFix.copy()
        newFreqSet.add(basePat)
#        print("new frequent set:",newFreqSet)
        freqItemList.append(basePat)  #将每个头指针列表中的元素添加到频繁项列表freqItemList中
        #找出每个元素项的条件模式基
        condPattBases = findPrefixPath(basePat, headerTable[basePat][1])
        #根据条件模式基构建条件树
        myCondTree, myHead = createTree(condPattBases, minSup)
        
        if myHead != None: #如果头指针表中还有元素项,则递归调用
            print("conditinal tree for: ", newFreqSet)
            myCondTree.disp(1)
            mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)
           
if __name__ == '__main__':
#    rootNode = treeNode('pyramid', 9, None)
#    rootNode.children['eye'] = treeNode('eye', 13, None)
#    rootNode.disp()
#    rootNode.children['phoenix'] = treeNode('phoenix', 3, None)
#    rootNode.disp()
    
    simpDat = loadSimpDat()
    print("simpDat:\n",simpDat)
    initSet = createInitSet(simpDat)
    print("initSet:\n",initSet)
    myFPtree, myHeaderTab = createTree(initSet,3)
    myFPtree.disp()
    
    
    freqItems = []
    mineTree(myFPtree, myHeaderTab, 3, set([]), freqItems)
#    print(freqItems)

运行结果如下图所示:
在这里插入图片描述
  上述结果与书上给出的结果也不一样,主要还是上面提到的排序不同导致构建的树不同,所以前缀路径就不同,那么构建得到的条件树也就不同。
到现在为止,完整的FP-growth算法已经完成,接下来在一个真实的例子上看一下运行效果。

五、示例:在Twitter源中发现一些共现词

  twitter API stop working,因此,我们这个示例就没法进行了。下面我们看下一个示例。

六、示例:从新闻网站点击流中挖掘

  在数据集kosarak.dat文件中,它包含将近100万条记录。该文件中的每一行包含某个用户浏览过的新闻报道。一些用户只看过一篇报道,而有些用户看过2498篇报道。用户和报道被编码成整数,所以查看频繁项集很难得到更多的东西,但是该数据对于展示FP-growht算法的速度十分有效。

  首先,我们打开数据集,看下它的结构。
在这里插入图片描述
  现在我们的任务是要从中寻找出那些至少被10万人浏览过的新闻报道。

  在fpGrowth.py文件中加入如下代码:

if __name__ == '__main__':
    parsedDat = [line.split() for line in open('kosarak.dat').readlines()]++++
    initSet = createInitSet(parsedDat)
    myFPtree, myHeaderTab = createTree(initSet, 100000)
    myFreqList = []
    mineTree(myFPtree, myHeaderTab, 100000, set([]), myFreqList)
    print("至少被10万人浏览过的新闻报道有 %d 条"%len(myFreqList))
    print(myFreqList)
    

运行结果如图所示:
在这里插入图片描述
得到上述的运行结果只用了几秒钟的时间。可见FP-growth算法的效率是很高的。

七、总结

  • FP-growth算法是一种用于发现数据集中的频繁模式的有效方法。
  • FP-growth算法利用的是Apriori原理,但是执行效率更快,只需要扫描两次数据集。
  • FP-growth算法中,数据集存储在一个称为FP树的结构中。
  • 可以使用FP-growth算法在多种文本文档中查找频繁单词。
  • 频繁项集的生成还有其他的一些应用,比如购物交易、医学诊断及天气研究等。

参考文献

【1】Spark机器学习(9):FPGrowth算法:http://www.cnblogs.com/mstk/archive/2017/07/16/7190179.html
【2】python中的深拷贝和浅拷贝的区别:https://blog.csdn.net/u014745194/article/details/70271868
【3】频繁项集挖掘算法之FPGrowth: https://blog.csdn.net/huagong_adu/article/details/17739247
【4】《机器学习实战》第12章 利用FP-growth算法来高效发现频繁项集

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值