机器学习|FP-Growth

在上篇文章频繁项集挖掘实战和关联规则产生中我们实现了Apriori的购物篮实战和由频繁项集产生关联规则, 本文沿《数据挖掘概念与技术》的主线继续学习FP-growth。因《数据挖掘概念与技术》中FP-growth内容过于琐碎且不易理解,我们的内容主要参考了《机器学习实战》第12章的内容。本文是对书中内容的通俗理解和代码实现,更详细的理论知识请参考书中内容, 本文涉及的完整jupyter代码和《机器学习实战》全书配套代码包, 可以在我们的 "数据臭皮匠" 中输入"第六章3" 拿到

Apriori算法是经典算法, 但它也有比较明显的缺点,主要有两个:

  • 它可能需要产生大量候选集, 如果有10^4个频繁1项集, Apriori需要产生多达10^7个候选2项集

  • 每一步从候选k项集, 到频繁k项集都需要完整的扫描数据集, 反复扫描完整数据集开销很大

有没有某种方法可以不产生大量候选项集且不需要反复扫描完整数据集就可以产生频繁项集呢, 有! 他就是FP-growth(Frequent-Parttern Growth) 频繁模式增长。

FP-growth先将数据集压缩到一颗FP树(频繁模式数),再遍历满足最小支持度的频繁一项集,逐个从FP数中找到其条件模式基,进而产生条件FP树,并产生频繁项集。

一、基础概念

1、FP树

FP 树将每个集合以路径的方式存储在树中, 从根节点开始, 每个条路径上的节点按其出现频数递减. 存在相似元素的集合会共享树的一部分, 只有当集合之间出现不同时, 树才会分叉. 图示如下(左图是数据, 右图是最小支持度为 3 的 FP树)

image

2、Header表

链表, 每个节点存放的是某一元素项, 及其在所有集合中出现的总次数, 以及指向FP树各个分支上同样元素的指针. 从Header表, 可以快速定位到各个分枝上的元素. 头指针表图示如下:

image

3、条件模式基

从header表中的单个频繁元素开始, 对每个路径中的此元素, 向根节点回溯, 每一条回溯路径上的节点都组成一个集合. 即前缀路径.

4、条件FP树

使用 3 中的得到的各个前缀路径, 作为新的数据集合, 并按 1 中的方法构造出来的 FP 树, 即为 条件FP树.在构造条件 FP 树时, 同样要通过最小支持度来淘汰非频繁项

二、算法描述

1. 构造FP树

1.1) 遍历完整数据集, 对每个元素计数,每个元素在数据集中出现的次数组成header表(仅包含大于最小支持度的项)

image

1.2)遍历每个集合, 对此集合中的元素, 按其在总数据集中出现的次数排序, 并去除掉未达到最小支持度的元素.

1.3)对每个集合, 从树的根节点依次往下插入, 如果节点已存在, 则递增节点的计数值, 否则创建一个分支,如下图:

image

1.4)新加入节点的同时, 需要将新结点与header表建立连接, 如果header表的该元素已经有节点与之连接, 新结点就连接到该元素对应连接链的末端

1.5)循环 (3) -> (4), 直至所有集合操作完毕


image.png
# 树节点
class treeNode:
    def __init__(self, nameValue, numOccur, parentNode):
        self.name = nameValue
        self.count = numOccur         # 计数值
        self.nodeLink = None          # 连接相似变量
        self.parent = parentNode      #needs to be updated
        self.children = {} 
    
    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)


            
def fix_order(localD):
    """按照书中例子,给出字母r,s,t,y的固定排序"""
    ls = sorted(localD.items(),key=lambda p: p[1], reverse=True) 
    order_d = {'y':0,'s':1,'r':2,'t':3}
    i=0 
    while i  < len(ls)-1:      
        if ls[i][1] == ls[i+1][1]:
            if order_d.get(ls[i][0])>order_d.get(ls[i+1][0]):
                ls[i],ls[i+1] = ls[i+1],ls[i]
                i=0
                continue 
        
        i+=1         
    return [i[0] for i in ls]            
# 根据 dataSet 创建 FP-growth 树
# 参数 minSup 指的是最小支持度
def createTree(dataSet, minSup=1,fix=False): 
    headerTable = {}
    
    # 遍历所有元素项, 记录其出现次数,并新建headerTable
    # *****对应以上1.1) *******************
    # 内容类似为 {'t': 3, 'w': 1, 'v': 1, 'y': 3, 'x': 4, 'z': 5}
    for trans in dataSet:
        for item in trans:
            headerTable[item] = headerTable.get(item, 0) + dataSet[trans]

    # 移除未达到最小支持度的元素项
    ls_k = list(headerTable.keys())
    for k in ls_k:   
        if headerTable[k] < minSup: 
            del(headerTable[k])

    freqItemSet = set(headerTable.keys())
    if len(freqItemSet) == 0: return None, None  # 没有达到最小支持度的元素项, 返回

    # 格式化后格式为: 
    # {'t': [3, None], 'y': [3, None], 'x': [4, None], 'z': [5, None]}
    for k in headerTable:
        headerTable[k] = [headerTable[k], None] 

    retTree = treeNode('Null Set', 1, None)     # 只包含空值的根节点
    
    # **************对应1.5********************
    for tranSet, count in dataSet.items():      # count 都初始化为 1
        localD = {}
        for item in tranSet:                    # 取满足最小支持度的元素项
            if item in freqItemSet:
                localD[item] = headerTable[item][0]
         
        # 对每个项集中的元素项, 按支持度从大到小排序
        if len(localD) > 0:
            # ***************对应1.2***************
            orderedItems = [v[0] for v in sorted(localD.items(), 
                            key=lambda p: p[1], reverse=True)]
            # 书中没有该逻辑,但如果不加该逻辑, 会出现value相同时,key的排序随机出的问题
            if fix :
                orderedItems = fix_order(localD)
            updateTree(orderedItems, retTree, headerTable, count)   # 填充树
    return retTree, headerTable                 # 返回树与各个元素的头指针


# 使用 items 使树生长, FP-growth 的来历
# inTree 为树的根节点
def updateTree(items, inTree, headerTable, count):
    # 取出items 的第一个元素
    item_name = items[0]
     # 如果 items 的第一个元素是 树的第一层子节点, 则将此子节点对应的计算 加1
    # ******* 对应1.3 ************
    if item_name in inTree.children:
        inTree.children[item_name].inc(count) 
    else:   
        # 如果此节点不存在, 则新建子节点
        new_node = treeNode(item_name, count, inTree)
        # 将子节点添加到树中 (new_node添加到字典inTree.children中,key为item_name)
        inTree.children[item_name] = new_node 
        
        # 新建子节点后,需要与headerTable连接 
        # 获取 item_name 在headerTable中的对应的节点链
        # *******************对应1.4**************
        nodeLinks = headerTable[item_name][1]
        # 更新头指针表
        # 如果 节点链 为None,将新建的子节点作为初始化的 节点链的第一个元素 
        if nodeLinks == None: 
            headerTable[item_name][1] = new_node
        else:
            # 如果 节点链 不为空, 将新建的子节点连接到节点链的末尾
            updateHeader(headerTable[item_name][1], new_node)

    # 对除去第一个元素之外的 items 递归建树
    if len(items) > 1:
        updateTree(items[1::], inTree.children[items[0]], headerTable, count)
        

# 更新头指针链表, targetNode 接到 nodeToTest 所指链表的最后
# 这里不用递归, 因为递归在链表长度过长时, 可能会栈溢出
def updateHeader(nodeLinks, targetNode): 
    """将新建的子节点连接到节点链的末端"""
    # nodeLinks.nodeLink = None 时说明到末端了
    while (nodeLinks.nodeLink != None):    
        nodeLinks = nodeLinks.nodeLink
    # 将新建的子节点连接到节点链的末端
    nodeLinks.nodeLink = targetNode

以上FP树的实现代码中, class treeNode 用以新建新的节点, createTree 函数包含了为新建FPtree做的所有准备工作(构建header表, 遍历每一个几个等), 是updateTree 真正描述了FP树的构建逻辑(在原node加1,新建node,新node与header表建立链接等), 其中fix_order 是笔者新增的函数, 因为原书中代码,对于r,s,t,y 的value都等于3, 每次排序可能会给出不同的结果,加上fix_order 逻辑,能解决排序不确定的问题

2.从 FP 树挖掘频繁项集

2.1) 取header表的第一个频繁元素, 遍历其所在的各条树路径
2.2) 在每个树路径中, 都向根节点回溯, 得到条件模式基
2.3) 以 (2) 中得到的条件模式基创建新的 FP 树
2.4) 对 FP 树按 (1) ->(3) 递归挖掘
2.5) 每次循环时, (1) 中所取的频繁项元素, 与递归传进来的前缀合成频繁项集. 即每层递归的每个header表中的每个频繁元素都会生成一个频繁项集.

image.png
# 迭代上溯整棵树
# 即将叶节点到根节点的所有元素都存到 prefixPath 中
def ascendTree(leafNode, prefixPath): 
    if leafNode.parent != None:
        prefixPath.append(leafNode.name)
        ascendTree(leafNode.parent, prefixPath)


# 按给定元素项生成一个条件模式基
# 遍历头指针链表, 对每一个元素都向根节点上溯, 记录找到的前缀式及元素出现次数
def findPrefixPath(basePat, treeNode): #treeNode comes from header table
    condPats = {}
    # 遍历以 treeNode 为头节点的头指针链表
    while treeNode != None:
        prefixPath = []
        ascendTree(treeNode, prefixPath)
        # 保存前缀及其次数
        if len(prefixPath) > 1: 
            condPats[frozenset(prefixPath[1:])] = treeNode.count
        treeNode = treeNode.nodeLink
    return condPats


# 递归查找频繁项集
# 参 freqItemList 返回频繁项集列表
def mineTree(inTree, headerTable, minSup, preFix, freqItemList):
    # 头指针按其元素项出现的频率从小到大进行排序
    bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p: p[1][0])]
    # 对应*****************2.1 *****************
    for item_name in bigL:  #start from bottom of header table
        newFreqSet = preFix.copy()   # 生成一个频繁项集
        # ************对应2.5*****************
        newFreqSet.add(item_name)

        # 创造条件基
        freqItemList.append(newFreqSet)  # 繁频项集添加到参数中, 以待最后返回
        # 获取 item_name 在headerTable中的对应的节点链
        nodeLinks = headerTable[item_name][1] 
        # ************对应2.2************
        condPattBases = findPrefixPath(item_name, nodeLinks)

        # 创建条件 FP 树 对应***********2.3**************
        myCondTree, myHead = createTree(condPattBases, minSup)

        # 递归挖掘条件 FP 树 对应***********2.4***********
        if myHead != None:
            mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)

三、案例

1.《机器学习实战》 案例

# 载入数据
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


# 初始化每个项集的次数为 1
def createInitSet(dataSet):
    retDict = {}
    for trans in dataSet:
        retDict[frozenset(trans)] = 1
    return retDict


if __name__ == "__main__":
    # 建树
    simpDat = loadSimpDat()
    initSet = createInitSet(simpDat)
    myFPtree, myHeaderTab = createTree(initSet, 3,fix=True)          # 3 为最小支持度
#     myFPtree.disp()  # 显示树
 
#     findPrefixPath('t',myHeaderTab['t'][1])

#     根据树挖掘 频繁项集
    freqItems = []
    mineTree(myFPtree, myHeaderTab, 3, set([]), freqItems)  # 3 为最小支持度
freqItems
image.png

可以看到与《机器学习实战》书中P234结果一致

2.《数据挖掘概念与技术》案例

data_ls =[['I1','I2','I5'],
    ['I2','I4'],
    ['I2','I3'],
    ['I1','I2','I4'],
    ['I1','I3'],
    ['I2','I3'],
    ['I1','I3'],
    ['I1','I2','I3','I5'],
    ['I1','I2','I3']]

simpDat = data_ls.copy() 
initSet = createInitSet(simpDat)
myFPtree, myHeaderTab = createTree(initSet, 2,fix=True)          # 3 为最小支持度

freqItems = []
mineTree(myFPtree, myHeaderTab, 2, set([]), freqItems)  # 3 为最小支持度
freqItems
image.png

可以看到与《数据挖掘概念与技术》书中P168页结果一致

四、调包实现-pyfpgrowth

import pandas as pd
import pyfpgrowth

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']]
patterns = pyfpgrowth.find_frequent_patterns(simpDat, 3)
patterns
image.png
data_ls =[['I1','I2','I5'],
    ['I2','I4'],
    ['I2','I3'],
    ['I1','I2','I4'],
    ['I1','I3'],
    ['I2','I3'],
    ['I1','I3'],
    ['I1','I2','I3','I5'],
    ['I1','I2','I3']]

patterns = pyfpgrowth.find_frequent_patterns(data_ls, 2)
patterns

image.png

将两个例子使用pyfpgrowth包的实现, 发现结果与手工写的代码完全一致, 忙活了这么久,别人一行代码就解决了, 猛一下还有点失落。但笔者认为,如果能自己实现一遍FP-growth,是非常有意义的,它让我们更加了解技术细节,同时更能总结出算法的优缺点,擅长及局限。但同时, 由于本篇文章涉及较多的class(类)和递归函数的知识, 如果读者觉得看代码比较吃力,可以先调包跑一跑找找感觉, 也不必拘泥于非要手工实现一遍。
更多精彩内容,请关注:数据臭皮匠 共中号
作者:范小匠
审核:灰灰匠
编辑:森匠

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

推荐阅读更多精彩内容