FP-Growth|高效挖掘频繁项集

01 搜索引擎如何联想的?

在使用搜索引擎时,你应该会发现一个现象:当我们在搜索框输入一个字符时,它就会帮你联想补全后面的搜索内容。

比如,你想搜索“西瓜”,当你输入“西”时,搜索引擎会帮你联想出“西部、西瓜、西南地区”等等。这大大提高了我们的搜索效率,你有没有疑惑过,这是如何实现的呢?

今天我们要学习的这个算法(FP-Growth)可以解答这个疑惑。FP-Growth算法是一种比Apriori算法更加高效的频繁项集挖掘算法,这两种算法都能够挖掘频繁项集,但它们的区别如下:

  1. Apriori:算法过程直观,除了挖掘频繁项集之外,还能够挖掘关联规则,但由于每次更新频繁项集都需要扫描一次整个数据集,效率不高
  2. FP-Growth:高效,只需要扫描两次数据集,第一次扫描计算各元素出现次数、第二次扫描构建FP树;频繁项集只需要从FP树挖掘即可

两种算法的最大区别是,FP-Growth通过构建FP树存储数据集,使得在面对大数据量的频繁项集挖掘时更加高效,因此对于搜索引擎这种体量的数据系统,一般采用FP-Growth算法为基底挖掘搜索词的频繁项集。

下面我们一起来学习这个应用广泛的算法吧~


02 FP-Growth算法原理

  • FP树

FP树是一种存储数据的树结构,如下图所示,每一路分支表示数据集的一个项集,数字表示该元素在某分支中出现的次数

  • 算法过程
  1. 构建FP树
    1. 遍历数据集获得每个元素项的出现次数,去掉不满足最小支持度的元素项
    2. 构建FP树:读入每个项集并将其添加到一条已存在的路径中,若该路径不存在,则创建一条新路径(每条路径是一个无序集合)
  2. 从FP树中挖掘频繁项集
    1. 从FP树中获得条件模式基
    2. 利用条件模式基构建相应元素的条件FP树,迭代直到树包含一个元素项为止

算法过程写得比较简略,具体过程我们在下节的实操中进一步理解。

03 算法实现

3.1 构建FP树

class treeNode:
    def __init__(self,nameValue,numOccur,parentNode):
        self.name=nameValue #节点名
        self.count=numOccur #节点元素出现次数
        self.nodeLink=None #存放节点链表中,与该节点相连的下一个元素
        self.parent=parentNode
        self.children={} #用于存放节点的子节点,value为子节点名
    
    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) #对于子节点,深度+1

# 构造FP树
# dataSet为字典类型,表示探索频繁项集的数据集,keys为各项集,values为各项集在数据集中出现的次数
# minSup为最小支持度,构造FP树的第一步是计算数据集各元素的支持度,选择满足最小支持度的元素进入下一步
def createTree(dataSet,minSup=1):
    headerTable={}

    #遍历各项集,统计数据集中各元素的出现次数
    for key in dataSet.keys():
        for item in key:
            headerTable[item]=headerTable.get(item,0)+dataSet[key] 
            
    #遍历各元素,删除不满足最小支持度的元素
    for key in list(headerTable.keys()):
        if headerTable[key]<minSup:
            del headerTable[key]
    freqItemSet=set(headerTable.keys())
    
    #若没有元素满足最小支持度要求,返回None,结束函数
    if len(freqItemSet)==0:
        return None,None
    for key in headerTable.keys():
        headerTable[key]=[headerTable[key],None] #[元素出现次数,**指向每种项集第一个元素项的指针**]
    retTree=treeNode("Null Set",1,None) #初始化FP树的顶端节点
    
    for tranSet,count in dataSet.items():
        localD={} #存放每次循环中的频繁元素及其出现次数,便于利用全局出现次数对各项集元素进行项集内排序
        for item in tranSet:
            if item in freqItemSet:
                localD[item]=headerTable[item][0]
        if len(localD)>0:
            orderedItems=[v[0] for v in sorted(localD.items(),key=operator.itemgetter(1),reverse=True)] #根据元素全局出现次数对每个项集(tranSet)中的元素进行排序
            updateTree(orderedItems,retTree,headerTable,count) #使用排序后的项集对树进行填充
    return retTree,headerTable


#树的更新函数
#items为按出现次数排序后的项集,是待更新到树中的项集;count为items项集在数据集中的出现次数
#inTree为待被更新的树;headTable为头指针表,存放满足最小支持度要求的所有元素
def updateTree(items,inTree,headerTable,count):
    #若项集items当前最频繁的元素在已有树的子节点中,则直接增加树子节点的计数值,增加值为items[0]的出现次数
    if items[0] in inTree.children: 
        inTree.children[items[0]].inc(count)
    else:#若项集items当前最频繁的元素不在已有树的子节点中(即,树分支不存在),则通过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]])
    #对于项集items元素个数多于1的情况,对剩下的元素迭代updateTree
    if len(items)>1:
        updateTree(items[1::],inTree.children[items[0]],headerTable,count)


#元素链表更新函数
#nodeToTest为待被更新的元素链表的头部
#targetNode为待加入到元素链表的元素节点
def updateHeader(nodeToTest,targetNode):
    #若待被更新的元素链表当前元素的下一个元素不为空,则一直迭代寻找该元素链表的末位元素
    while nodeToTest.nodeLink!=None: 
        nodeToTest=nodeToTest.nodeLink #类似撸绳子,从首位一个一个逐渐撸到末位
    #找到该元素链表的末尾元素后,在此元素后追加targetNode为该元素链表的新末尾元素
    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

simpDat=loadSimpDat()
dataSet=createInitSet(simpDat)
myFPtree1,myHeaderTab1=createTree(dataSet,minSup=3)
myFPtree1.disp(),myHeaderTab1

输入的数据集长这样:

由此数据集构建的FP树长这样,看看是不是满足上一节介绍的FP树结构

3.2 从FP树中挖掘频繁项集

具体过程如下:

  1. 从FP树中获得条件模式基
    • 条件模式基:以所查找元素项为结尾的路径集合,每条路径都是一条前缀路径,路径集合包括前缀路径和路径计数值。
      • 例如,元素"r"的条件模式基为 {x,s}2,{z,x,y}1,{z}1
    • 前缀路径:介于所查找元素和树根节点之间的所有内容
    • 路径计数值:等于该条前缀路径的起始元素项(即所查找的元素)的计数值
  2. 利用条件模式基构建相应元素的条件FP树
    • 对每个频繁项,都要创建一棵条件FP树。
    • 例如对元素t创建条件FP树:使用获得的t元素的条件模式基作为输入,利用构建FP树相同的逻辑构建元素t的条件FP树
  3. 迭代步骤(1)(2),直到树包含一个元素项为止
    • 接下来继续构建{t,x}{t,y}{t,z}对应的条件FP树(tx,ty,tz为t条件FP树的频繁项集),直到条件FP树中没有元素为止
    • 至此可以得到与元素t相关的频繁项集,包括2元素项集、3元素项集。。。
#由叶节点回溯该叶节点所在的整条路径
#leafNode为叶节点,treeNode格式;prefixPath为该叶节点的前缀路径集合,列表格式,在调用该函数前注意prefixPath的已有内容
def ascendTree(leafNode,prefixPath):
    if leafNode.parent!=None:
        prefixPath.append(leafNode.name)
        ascendTree(leafNode.parent,prefixPath)
        
#获得指定元素的条件模式基
#basePat为指定元素;treeNode为指定元素链表的第一个元素节点,如指定"r"元素,则treeNode为r元素链表的第一个r节点
def findPrefixPath(basePat,treeNode):
    condPats={} #存放指定元素的条件模式基
    while treeNode!=None: #当元素链表指向的节点不为空时(即,尚未遍历完指定元素的链表时)
        prefixPath=[]
        ascendTree(treeNode,prefixPath) #回溯该元素当前节点的前缀路径
        if len(prefixPath)>1:
            condPats[frozenset(prefixPath[1:])]=treeNode.count #构造该元素当前节点的条件模式基
        treeNode=treeNode.nodeLink #指向该元素链表的下一个元素
    return condPats

#有FP树挖掘频繁项集
#inTree: 构建好的整个数据集的FP树
#headerTable: FP树的头指针表
#minSup: 最小支持度,用于构建条件FP树
#preFix: 新增频繁项集的缓存表,set([])格式
#freqItemList: 频繁项集集合,list格式

def mineTree(inTree,headerTable,minSup,preFix,freqItemList):
    #按头指针表中元素出现次数升序排序,即,从头指针表底端开始寻找频繁项集
    bigL=[v[0] for v in sorted(headerTable.items(),key=lambda p:p[1][0])] 
    for basePat in bigL:
        #将当前深度的频繁项追加到已有频繁项集中,然后将此频繁项集追加到频繁项集列表中
        newFreqSet=preFix.copy()
        newFreqSet.add(basePat)
        print("freqItemList add newFreqSet",newFreqSet)
        freqItemList.append(newFreqSet)
        #获取当前频繁项的条件模式基
        condPatBases=findPrefixPath(basePat,headerTable[basePat][1])
        #利用当前频繁项的条件模式基构建条件FP树
        myCondTree,myHead=createTree(condPatBases,minSup)
        #迭代,直到当前频繁项的条件FP树为空
        if myHead!=None:
            mineTree(myCondTree,myHead,minSup,newFreqSet,freqItemList)

接着刚才构建的FP树,测试一下,

freqItems=[]
mineTree(myFPtree1,myHeaderTab1,3,set([]),freqItems)
freqItems

我们从FP树中挖掘到的频繁项集如下,这里设置的最小支持度为3:

上图表示数据集中,支持度大于3(出现3次以上)的元素项集,即,频繁项集。

回到文章开头说的搜索关键词“西瓜”的例子,以上结果相当于从大量的搜索词汇中,挖掘出常常在一起出现的字词,如“西”+“瓜”,“西”+“部”等等。

04 总结

本文介绍了一种高效挖掘频繁项集的算法FP-Growth算法,并对此进行了实践。

注意,在处理大数据量时,相比于Apriori算法,FP-Growth算法可以高效挖掘频繁项集,但不能挖掘关联规则。

05 参考

《机器学习实战》 Peter Harrington Chapter12

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容