机器学习实战---读书笔记:第12章 使用FP-growth算法来高效发现频繁项集

#!/usr/bin/env python
# encoding: utf-8

import os

'''
<<机器学习实战>> 读书笔记 第12章 使用FP-growth算法来高效发现频繁项集

关键:
1 FP-growth
含义: 比Apriori算法要快,基于Apriori构建,但完成相同任务时采用了不同技术。
      这里任务是指将数据集存储在一个特定的称做FP树的结构之后发现频繁项集或者频繁项集对。
      即常在一块出现的元素项的集合FP树。
优点: 高效发现频繁项集
缺点: 不能用于发现关联规则
原理: 只需要对数据库进行两次扫描,而Apriori算法对于每个潜在的频繁项集都会扫描数据集判定给定模式
      是否频繁。
      发现频繁项集的过程如下:
      1) 构建FP树
      2) 从FP树中挖掘频繁项集
应用: 从Twitter文本流中挖掘频常用词,从网民网页浏览行为中挖掘常见模式

2 FP树
作用: 用于编码数据集的有效方式
优点: 一般要快于Apriori
缺点: 实现比较困难,在某些数据集上性能会下降
适用数据类型: 标称型数据
原理: FP-growth算法将数据存储在称为FP树的数据结构中。
        FP表示频繁模式(Frequent Pattern)。
        与树结构相似,但是通过链接(link)来连接相似元素,被连接的元素可以看成一个链表。
与搜索树不同处:
一个元素项可出现多次。
FP树会存储项集的出现频率,每个项集会以路径的方式存储在树中。
存在相似元素的集合会共享树的一部分。
只有当集合之间完全不同时,树才会分叉。
树节点上给出集合中的单个元素及其在序列中的出现次数,路径会给出该序列的出现次数。

节点链接(node link): 相似项之间的链接,用于快速发现相似项的位置。

3 FP树构建例子
FP树样例:
                    空
            z:5         x:1
        r:1     x:3     s:1
                y:3     r:1
            s:2     r:1
            t:2     t:1

上述FP树对应的事务数据样例
事务ID              事务中的元素项
001                 r,z,h,j,p
002                 z,y,x,w,v,u,t,s
003                 z
004                 r,x,n,o,s
005                 y,r,x,z,q,t,p
006                 y,z,x,e,q,s,t,m

分析:
元素项z出现5次,集合{r,z}出现了1次。可以得出结论:
z一定是自己本身或者和其他符号一起出现了4次。
集合{t,s,x,y,z}出现了2次,集合{t,r,x,y,z}出现了1次。
元素项z的右边标的是5,表示z出现了5次,其中刚才已经给出了4次出现,
所以它一定单独出现过1次。
之所以没有p和q,这里定义了最小支持度为3,p和q被过滤了。

4 FP-growth算法
原理: 对原始数据集扫描两遍。
    第一遍: 对所有元素项的出现次数进行计数。
    Apriori原理:如果某元素不是频繁的,那么包含该元素的超集也是不频繁的。
    第二遍: 只考虑频繁元素

构建FP树:
第二次扫描数据集时会构建一颗FP树。需要一个容器来保存树。
需要创建一个类来保存树的每个节点。

FP树的数据结构:
整体是一颗多叉树,即当前节点有父节点和孩子节点列表,同时节点还有节点链接。
也就是说: FP树是一颗多叉树,同时还保留了节点到相似节点的链接。

需要一个头指针表,来快速访问FP树中一个给定类型的所有元素。
这里本质上有点类似于信息检索中的倒排索引了(也是多个链表)
头指针表作为一个起始指针来发现相似元素项。
这里使用一个字典作为数据结构,来保存头指针表,除了存放头指针,
头指针表还可以用来保存FP树中每类元素的总数。

算法步骤:
步骤1:第一次遍历数据集会获得每个元素项的出现频率
步骤2: 去掉不满足最小支持度的元素项
步骤3: 构建FP树。
        3.1 读入每个项集并将其添加到一条已经存在的路径中
            3.1.1 如果该路径不存在,则创建一条新路径

每个事务是一个无序集合,相同项只会表示一次。将集合添加到树之前,
需要对每个集合进行排序。排序基于元素项的绝对出现频率来进行。

过滤和重排序后的结果如下:
事务ID              事务中的元素项           过滤及重排序后的事务
001                 r,z,h,j,p               z,r
002                 z,y,x,w,v,u,t,s         z,x,y,s,t
003                 z                       z
004                 r,x,n,o,s               x,s,r
005                 y,r,x,z,q,t,p           z,x,y,r,t
006                 y,z,x,e,q,s,t,m         z,x,y,s,t

从空集开始,向其中不断添加频繁项集。过滤、排序后的事务一次添加到树中,如果树
中已经存在现有元素,则增加现有元素的值;
如果现有元素不存在,则向树添加一个分枝。

完整FP树构建算法:
步骤1: 令头指针表headerTable为空
步骤2: 遍历数据集中的每个事务,对事务中的每个元素统计其出现次数,得到<元素,元素出现次数>的字典
步骤3: 遍历<元素,元素出现次数>的字典,如果有某个元素不满足最小支持度,则在字典中删除该元素对应的键值对
步骤4: 如果<元素,元素出现次数>的字典为空,则直接返回,否则进入步骤5
步骤5: 新建一个根节点
步骤6: 遍历数据集中的事务以及事务出现次数的键值对
        6.1 根据全局频率对每个事务中的元素排序
        6.2 使用排序后的新的元素列表来构建FP树,具体构建FP树的过程如下:
            1): 测试事务中的第一个元素项是否作为子节点存在。
                    1.1) 如果存在的话,则更新该元素项的计数,否则进入1.2
                    1.2) 创建一个新的treeNode并将其作为一个子节点添加到树中;
                        头指针表也要更新以指向新的节点
            2): 如果事务中的元素个数大于1,则将事务中除去第一个元素的其他元素,并以事务的第一个元素对应的节点作为父节点,
                递归调用当前方法,继续构建FP树
步骤7: 返回构建好的FP树的根节点,以及头指针表


5 从一颗FP树中挖掘频繁项集
思路:
从单元素项集合开始,在此基础上逐步构建更大的集合,这里用FP树来实现上述过程,不再需要原始数据集。
步骤:
1) 从FP树中获得条件模式基
2) 利用条件模式基,构建一个条件FP树
3) 迭代重复步骤1)和步骤2),直到树包含一个元素项为止。
寻找条件模式基,为每个条件模式基创建对应的条件FP树。

5.1 抽取条件模式基
从已经保存在头指针表的单个频繁元素项开始,对每个元素项,获得其对应的条件模式基。
条件模式基: 是以所查找元素项为结尾的路径集合。
每一条路径实际是一条前缀路径。是所查找元素项和树根节点之间的所有内容。

举例:
符号r的前缀路径是{x, s}, {z, x,y}和{z}。每一条前缀路径都与一个计数值关联。
按计数值等于起始元素项的计数值,该计数值给了每条路径上r的数目。

下标列出了每个频繁项的所有前缀路径

频繁项           前缀路径
z               {}5
r               {x,s}1, {z,x,y}1, {z}1
x               {z}3, {}1
y               {z,x}3
s               {z,x,y}2, {x}1
t               {z,x,y,s}2, {z,x,y,r}1

头指针表包含相同类型元素链表的起始指针。一旦到达了每一个元素项,就可以上溯这棵树直到根节点为止。

抽取条件模式基的算法具体如下:
步骤1: 如果当前节点为空,则返回该元素的条件模式基字典,否则,进入步骤2
步骤2: 获取当前节点的前缀路径(从叶子节点上溯即可),如果当前节点的前缀路径长度大于1,则以该前缀路径为键,该节点的计数值为值,
        更新条件模式基字典
步骤3: 令当前节点为当前节点的nodeLink,并转步骤1


5.2 创建条件FP树
对于每个频繁项,都要创建一颗条件FP树。
可以使用刚才发现的条件模式基(实际就是一个字典,键是节点的前缀路径,值是节点在该前缀路径上的出现次数)
作为输入数据,通过相同的建树代码来构建这些树。
会递归发现频繁项,发现条件模式基,发现另外的条件树。
举例:
假定为频繁项t创建一个条件FP树,然后对{t,y},{t,x},...重复该过程。
元素项t的条件FP树的构建过程如下所示:
t的条件模式基: (y,x,s,z):2, (y,x,r,z):1
最小支持度 = 3
去掉: s & r


空 加入(y,x,z):2       空   加入(y,x,z):1     空
    ---------------->  y:2 ----------------> y:3
                       x:2                   x:3
                       z:2                   z:3

挖掘条件FP树算法:
步骤1: 对头指针表按照元素的频率从小到大排序,得到元素列表
步骤2: 遍历元素列表,对每个元素,
    2.1 先拷贝当前前缀路径,然后将当前元素加大到前缀路径中得到新的频繁项集
    2.2 根据当前元素和当前元素对应在头指针表中的节点,寻找该元素的条件模式基
        (条件模式基实际是一个字典,键: 元素的前缀路径,值:该元素的前缀路径出现的次数)
        具体过程如下:
        1): 如果当前节点为空,则返回该元素的条件模式基字典,否则,进入步骤2
        2): 获取当前节点的前缀路径(从叶子节点上溯即可),如果当前节点的前缀路径长度大于1,则以该前缀路径为键,该节点的计数值为值,
                更新条件模式基字典
        3): 令当前节点为当前节点的nodeLink,并转 1)
    2.3 根据元素的条件模式基,最小支持度构建当前元素的条件FP树
    2.4 如果条件FP树的头指针表不为空(即表示还存在频繁项集),则
        传入条件FP树的根节点,条件FP树的头指针表,最小支持度,当前前缀路径,频繁项集列表,
        来递归调用当前方法,继续挖掘条件FP树
总结:
挖掘条件FP树的过程主要就是:
先对头指针表排序,然后遍历元素列表,查找每个元素的条件模式基,然后根据条件模式基构建条件FP树,
如果条件FP树非空,则递归挖掘条件FP树

6 总结
FP-growth算法思想:
对原始数据集扫描两遍。
第一遍: 对所有元素项的出现次数进行计数。
Apriori原理:如果某元素不是频繁的,那么包含该元素的超集也是不频繁的。
第二遍: 只考虑频繁元素
第二次扫描数据集时会构建一颗FP树
最后通过挖掘FP树来获取频繁项集,本质利用了多叉树的前缀路径+头指针表来实现。

构建FP树算法:
步骤1: 令头指针表headerTable为空
步骤2: 遍历数据集中的每个事务,对事务中的每个元素统计其出现次数,得到<元素,元素出现次数>的字典
步骤3: 遍历<元素,元素出现次数>的字典,如果有某个元素不满足最小支持度,则在字典中删除该元素对应的键值对
步骤4: 如果<元素,元素出现次数>的字典为空,则直接返回,否则进入步骤5
步骤5: 新建一个根节点
步骤6: 遍历数据集中的事务以及事务出现次数的键值对
        6.1 根据全局频率对每个事务中的元素排序
        6.2 使用排序后的新的元素列表来构建FP树,具体构建FP树的过程如下:
            1): 测试事务中的第一个元素项是否作为子节点存在。
                    1.1) 如果存在的话,则更新该元素项的计数,否则进入1.2
                    1.2) 创建一个新的treeNode并将其作为一个子节点添加到树中;
                        头指针表也要更新以指向新的节点
            2): 如果事务中的元素个数大于1,则将事务中除去第一个元素的其他元素,并以事务的第一个元素对应的节点作为父节点,
                递归调用当前方法,继续构建FP树
步骤7: 返回构建好的FP树的根节点,以及头指针表

挖掘FP树算法:
步骤1: 对头指针表按照元素的频率从小到大排序,得到元素列表
步骤2: 遍历元素列表,对每个元素,
    2.1 先拷贝当前前缀路径,然后将当前元素加大到前缀路径中得到新的频繁项集
    2.2 根据当前元素和当前元素对应在头指针表中的节点,寻找该元素的条件模式基
        (条件模式基实际是一个字典,键: 元素的前缀路径,值:该元素的前缀路径出现的次数)
        具体过程如下:
        1): 如果当前节点为空,则返回该元素的条件模式基字典,否则,进入步骤2
        2): 获取当前节点的前缀路径(从叶子节点上溯即可),如果当前节点的前缀路径长度大于1,则以该前缀路径为键,该节点的计数值为值,
                更新条件模式基字典
        3): 令当前节点为当前节点的nodeLink,并转 1)
    2.3 根据元素的条件模式基,最小支持度构建当前元素的条件FP树
    2.4 如果条件FP树的头指针表不为空(即表示还存在频繁项集),则
        传入条件FP树的根节点,条件FP树的头指针表,最小支持度,当前前缀路径,频繁项集列表,
        来递归调用当前方法,继续挖掘条件FP树
总结:
挖掘条件FP树的过程主要就是:
先对头指针表排序,然后遍历元素列表,查找每个元素的条件模式基,然后根据条件模式基构建条件FP树,
如果条件FP树非空,则递归挖掘条件FP树
'''

class treeNode(object):

    def __init__(self, nameValue, numOccur, parentNode):
        self.name = nameValue
        self.count = numOccur
        self.nodeLink = None
        self.parent = parentNode
        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 loadSimpDat():
    data = [
        ['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 data


'''
作用: 由于构建FP树需要的输入参数是字典不是列表,这里将列表转换为字典,其中项集是键,项集的频率是指
输入参数: 数据集dataSet(数组类型)
返回结果: 字典,键是项集(frozenset类型),值是项集的频率
'''
def createInitSet(dataSet):
    result = {}
    for tran in dataSet:
        result[frozenset(tran)] = 1
    return result

'''
作用: 使用数据集以及最小支持度作为参数来构建FP树
输入参数:
    数据集dataSet,是字典,键:项集,值:项集出现的次数
    最小支持度minSup
返回结果:
    返回构建的FP树的根节点,
    以及头指针表,该表的元素是节点,节点包含两部分:第一部分是元素出现次数,第二部分是指向的下一个节点的指针
    头指针表的头结点包含两部分:
                第一部分是元素(可以理解为商品)的总的出现次数,
                第二部分是指向FP树中该元素的节点
思想:
构建FP树过程中会遍历数据集两次。
1 第一次遍历扫描数据集并统计每个元素项出现的频度。这些信息被存储在头指针表中。
2 接下来扫描头指针表,删除那些出现次数少于minSup的项。
3 接下来对头指针表稍加扩展以便可以保存计数值及指向每种类型第一个元素项的指针。
4 然后创建只包含空机和的根节点。
5 最后,再一次遍历数据集。这次只考虑频繁项。这些项已经进行了排序,
  最后调用updateTree方法。
  5.1 updateTree是为了让FP树生长,其中输入参数是一个项集
算法:
步骤1: 令头指针表headerTable为空
步骤2: 遍历数据集中的每个事务,对事务中的每个元素统计其出现次数,得到<元素,元素出现次数>的字典
步骤3: 遍历<元素,元素出现次数>的字典,如果有某个元素不满足最小支持度,则在字典中删除该元素对应的键值对
步骤4: 如果<元素,元素出现次数>的字典为空,则直接返回,否则进入步骤5
步骤5: 新建一个根节点
步骤6: 遍历数据集中的事务以及事务出现次数的键值对
        6.1 根据全局频率对每个事务中的元素排序
        6.2 使用排序后的新的元素列表来构建FP树,具体构建FP树的过程如下:
            1): 测试事务中的第一个元素项是否作为子节点存在。
                    1.1) 如果存在的话,则更新该元素项的计数,否则进入1.2
                    1.2) 创建一个新的treeNode并将其作为一个子节点添加到树中;
                        头指针表也要更新以指向新的节点
            2): 如果事务中的元素个数大于1,则将事务中除去第一个元素的其他元素,并以事务的第一个元素对应的节点作为父节点,
                递归调用当前方法,继续构建FP树
步骤7: 返回构建好的FP树的根节点,以及头指针表
'''
def createTree(dataSet, minSup=1):
    if not dataSet:
        return None, None
    headerTable = {}
    # 统计每个元素的出现次数
    for tran, count in dataSet.iteritems():
        for item in tran:
            headerTable[item] = headerTable.get(item, 0) + count
    # 过滤不满足最小支持度的元素
    realHeaderTable = {}
    for item, count in headerTable.iteritems():
        if count >= minSup:
            realHeaderTable[item] = headerTable[item]
    headerTable = realHeaderTable
    # 生成频繁项集集合
    freqItemSet = set(headerTable.keys())
    # 如果频繁项集个数为0,直接返回
    if 0 == len(freqItemSet):
        return None, None
    # 构建头指针表,键: 元素名称,
    # 值:是一个数组,数组的第一个元素是当前元素的出现次数,数组的第二个元素是指向的下一个节点
    for item, count in headerTable.iteritems():
        headerTable[item] = [headerTable[item], None]
    rootNode = treeNode("Null Set", 1, None)
    # 构建FP树
    for tran, count in dataSet.iteritems():
        # 先根据元素的全局频率来对每个事务中的所有元素按照频率从高到底排序
        localItems = {}
        for item in tran:
            if item in freqItemSet:
                localItems[item] = headerTable[item][0]
        # 然后用排好序的元素列表来构建FP树
        if localItems:
            sortedItems = [ v[0]  for v in sorted(localItems.items(), key=lambda p: p[1], reverse=True) ]
            updateTree(sortedItems, rootNode, headerTable, count)
    return rootNode, headerTable


'''
作用: 根据给定的当前一个事务(对应一组商品),以及父节点,构建FP树,将事务中的商品作为父节点的子节点,
    从而递归构建起FP树,同时更新节点中的计数值
输入参数:
项集items,实际是一个数组,数组中每个元素都是节点名称
父节点inTree, 实际是节点类型
头指针表headerTable,字典类型,键是项集中的具体元素(可以理解为商品名称),值是该具体元素的出现次数
该项集的出现次数count
返回结果: 无
算法:
步骤1: 测试事务中的第一个元素项是否作为子节点存在。
        1.1 如果存在的话,则更新该元素项的计数,否则进入1.2
        1.2 创建一个新的treeNode并将其作为一个子节点添加到树中;
            头指针表也要更新以指向新的节点
步骤2: 如果事务中的元素个数大于1,则将事务中除去第一个元素的其他元素,并以事务的第一个元素对应的节点作为父节点,
    递归调用当前方法,继续构建FP树
'''
def updateTree(items, inTree, headerTable, count):
    if not items or not inTree:
        return
    # 当前元素是当前节点的子节点,则更新计数值
    if items[0] in inTree.children:
        inTree.children[ items[0] ].inc(count)
    else:
        # 以当前元素新建节点,并作为父节点的子节点
        newNode = treeNode(items[0], count, inTree)
        inTree.children[ items[0] ] = newNode
        # 更新头指针表,让头指针指向FP树中对应该元素的节点
        if headerTable[ items[0] ][1] == None:
            headerTable[ items[0] ][1] = inTree.children[ items[0] ]
        else:
            # 遍历头指针表,让头指针表的最后一个节点指向当前元素对应在FP树中的节点
            updateHeader(headerTable[ items[0] ][1], inTree.children[ items[0] ])
    # 如果事务中元素数目大于1,则对剩下元素递归调用updateYree来继续构建FP树
    if len(items) > 1:
        updateTree(items[1:], inTree.children[ items[0] ], headerTable, count)

'''
作用: 确保节点链接指向树中该元素项的每一个实例
输入参数:
头指针nodeToTest,
FP树中对应元素的节点targetNode
返回结果: 无
算法:
从头指针表的nodeLink开始,一致沿着nodeLink到达链表末尾
'''
def updateHeader(nodeToTest, targetNode):
    while nodeToTest.nodeLink != None:
        nodeToTest = nodeToTest.nodeLink
    nodeToTest.nodeLink = targetNode


'''
作用: 获取某个叶子节点的前缀路径
输入参数: 叶子节点leafNode,
输出参数: 前缀路径prefixPath(是一个数组)
返回结果: 无
'''
def ascendTree(leafNode, prefixPath):
    if not leafNode:
        return
    if leafNode.parent != None:
        prefixPath.append(leafNode.name)
        ascendTree(leafNode.parent, prefixPath)


'''
作用: 用于为给定元素项生成一个条件模式基,通过访问树中所有包含给定元素项的节点来完成
输入参数:
    基本条件模式基basePattern,实际对应元素的名称
返回结果: 返回某个元素的所有前缀路径和该前缀路径出现次数的字典
算法:
步骤1: 如果当前节点为空,则返回该元素的条件模式基字典,否则,进入步骤2
步骤2: 获取当前节点的前缀路径(从叶子节点上溯即可),如果当前节点的前缀路径长度大于1,则以该前缀路径为键,该节点的计数值为值,
        更新条件模式基字典
步骤3: 令当前节点为当前节点的nodeLink,并转步骤1
'''
def findPrefixPath(basePattern, treeNode):
    conditionPattern = {}
    while treeNode != None:
        prefixPath = []
        ascendTree(treeNode, prefixPath)
        if len(prefixPath) > 1:
            conditionPattern[ frozenset(prefixPath[1:]) ] = treeNode.count
        treeNode = treeNode.nodeLink
    return conditionPattern


'''
作用: 递归查找FP条件树来发现频繁项集
输入参数:
    inTree:         FP树的根节点
    headerTable:    头指针表,实际是一个<元素,元素出现次数>的字典
    minSup:         最小支持度
    prefix:         前缀元素集合
输出参数:
    freqItemList:   频繁项集列表
返回结果: 无
挖掘条件FP树算法:
步骤1: 对头指针表按照元素的频率从小到大排序,得到元素列表
步骤2: 遍历元素列表,对每个元素,
    2.1 先拷贝当前前缀路径,然后将当前元素加大到前缀路径中得到新的频繁项集
    2.2 根据当前元素和当前元素对应在头指针表中的节点,寻找该元素的条件模式基
        (实际是一个字典,键: 元素的前缀路径,值:该元素的前缀路径出现的次数)
        具体过程如下:
        1): 如果当前节点为空,则返回该元素的条件模式基字典,否则,进入步骤2
        2): 获取当前节点的前缀路径(从叶子节点上溯即可),如果当前节点的前缀路径长度大于1,则以该前缀路径为键,该节点的计数值为值,
                更新条件模式基字典
        3): 令当前节点为当前节点的nodeLink,并转 1)
    2.3 根据元素的条件模式基,最小支持度构建当前元素的条件FP树
    2.4 如果条件FP树的头指针表不为空(即表示还存在频繁项集),则
        传入条件FP树的根节点,条件FP树的头指针表,最小支持度,当前前缀路径,频繁项集列表,
        来递归调用当前方法,继续挖掘条件FP树
总结:
挖掘条件FP树的过程主要就是:
先对头指针表排序,然后遍历元素列表,查找每个元素的条件模式基,然后根据条件模式基构建条件FP树,
如果条件FP树非空,则递归挖掘条件FP树
'''
def minTree(inTree, headerTable, minSup, prefix, freqItemList):
    # if not inTree or not headerTable:
    #     return
    ruleList = [ v[0] for v in sorted(headerTable.items(), key=lambda p: p[1]) ]
    for item in ruleList:
        newFreqSet = prefix.copy()
        newFreqSet.add(item)
        freqItemList.append(newFreqSet)
        conditionPatterns = findPrefixPath(item, headerTable[item][1])
        conditionFPTree, conditionHeaderTable = createTree(conditionPatterns, minSup)
        # note, conditionFPTree may be empty, so it needs to process
        if conditionFPTree:
            print "condition FP Tree for : {newFreqSet}, is: ".format(
                newFreqSet=newFreqSet,
            )
            conditionFPTree.disp(1)
        if conditionHeaderTable != None:
            minTree(conditionFPTree, conditionHeaderTable, minSup, newFreqSet, freqItemList)


def buildTree():
    data = loadSimpDat()
    dataSet = createInitSet(data)
    print dataSet
    minSup = 3
    fpTree, headerTable = createTree(dataSet, minSup)
    fpTree.disp()
    prefix = set([])
    freqItemList = list()
    minTree(fpTree, headerTable, minSup, prefix, freqItemList)
    print freqItemList

def process():
    buildTree()

if __name__ == "__main__":
    process()

猜你喜欢

转载自blog.csdn.net/qingyuanluofeng/article/details/87108023