机器学习实战(十一)FP-growth(频繁项集)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhq9695/article/details/83650774

目录

0. 前言

1. 构建FP树

2. 从FP树中挖掘频繁项集

3. 实战案例

3.1. FP-growth寻找频繁项集


学习完机器学习实战的FP-growth,简单的做个笔记。文中部分描述属于个人消化后的理解,仅供参考。

本篇综合了先前的文章,如有不理解,可参考:

机器学习实战(十)Apriori算法

所有代码和数据可以访问 我的 github

如果这篇文章对你有一点小小的帮助,请给个关注喔~我会非常开心的~

0. 前言

从大规模的数据集中,寻找不同特征或者物品之间的隐含关系,称为关联分析(association analysis),或者关联规则学习(association rule learning)。

Apriori 算法中,寻找频繁项集,需要对每一个可能的频繁项扫描一遍数据集计算支持度,计算量庞大。

FP-growth 算法中,寻找频繁项集,只需要扫描两遍数据集,将数据存储在FP树的结构上,然后在FP树上挖掘频繁项集。

  • 优点:速度一般要快于 Apriori。
  • 缺点:实现比较困难,在某些数据集上性能会下降。
  • 适用数据类型:标称型数据。

例如在下述例子中(图源:机器学习实战),右侧是一颗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

FP代表频繁模式(Frequent Pattern),一个元素项可以在一颗FP树上出现多次。

树节点上给出了当前节点的路径在数据集中的出现次数,例如{z:5}表示元素{z}在数据集中出现了5次;{y:3}表示路径{y, x, z}在数据集中出现了3次;{s:2}表示路径{s, y, x, z}在数据集中出现了2次。

左侧为头指针表,给出了每个元素在数据集中出现的次数,并由链表通过节点链接(node link)依次链接每个元素。部分元素因为不满足最小支持度的要求,所以不储存在FP树中。

在 FP-growth 算法中,同样采用了 Apriori 算法的思想,如果某个项是非频繁的,那么这个项的所有超集也是非频繁的。

1. 构建FP树

构建FP树的过程只需要扫描两遍数据集。

第一遍扫描,计算每个单个元素的频率,并根据最小支持度,滤除不满足的元素。

第二遍扫描,首先对数据集进行处理,每一条数据按照元素的绝对出现频率排序,并滤除不满足最小支持度的元素

例如根据上述的头指针表,元素排序为{z:5, x:4, y:3, s:3, r:3, t:3},所以处理后的数据为:

事务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树中,从根节点开始递归添加路径,存在则将数值增加,不存在则创建新的节点

例如下图所示(图源:机器学习实战),① 根节点不存在子节点{z},所以创建新的子节点{z},递归节点{z},因不存在子节点{r},所以创建新的子节点{r},② 根节点存在子节点{z},所以数值增加,递归节点{z},因不存在子节点{x},所以创建新的子节点{x},递归节点{x},......,如此递归。

2. 从FP树中挖掘频繁项集

一个元素的条件模式基(conditional pattern base),是这个元素所有前缀路径(prefix path)的集合。

前缀路径(prefix path),是当前元素到根节点之间的路径(不包括当前元素和根节点)。

例如下图所示(图源:机器学习实战),{r}的条件模式基是{{z}{z, x, y}{x, s}}:

从FP树挖掘频繁项集的过程可描述为:

  1. 对于头指针表中的每一个元素,获取其条件模式基
  2. 根据条件模式基,构建条件FP树(即,将条件模式基当作新的数据集,按照建树的流程,重新构建一棵FP树)
  3. 继续对于条件FP树的头指针表中的每一个元素,获取其条件模式基
  4. 继续根据条件模式基,构建条件FP树
  5. 如此递归过程,直到无法构建出FP树为止

记录频繁项集的过程在创建一棵新的FP树时记录,伪代码如下表示:

关于此处的理解:首先构建了一棵FP树,此时FP树中的单个元素均满足最小支持度(假设有{a}{b}{c}{d}{e}5个元素),遍历其中的每一个元素(假设此时遍历{a}),先将元素{a}加入总的频繁项集,再寻找元素{a}的条件模式基(假设有{c, b}{b, d}{b, c, e, d}),根据这些前缀路径递归构建一棵条件FP树(若这棵树能够构建的起来,说明树中的单个元素也是满足最小支持度的,假设条件FP树中有{b}{c}{d}3个元素,{e}不满足最小支持度),说明{b}{c}{d}这三个元素满足最小支持度,遍历其中的每一个元素(假设此时遍历{b}),复制上一层递归的频繁项集(即,{a}),将当前遍历元素{b}加入复制的频繁项集中(即,构成{a, b}),然后再将{a, b}加入总的频繁项集,再在条件FP树中寻找元素{b}的条件模式基,继续递归构建。因为上一层递归中的频繁项集{a}是一定满足最小支持度的,由这个元素{a}搜寻得到的条件模式基,一定是在数据集中跟{a}有组合的,若能据此构建一棵条件FP树,说明这棵树中的元素{b}{c}{d}也一定满足最小支持度,因这元素{b}与{a}在原始数据集中有组合,且这元素{b}与上一层递归频繁项集{a}均满足最小支持度,所以这元素{b}和{a}的组合{a, b}一定满足最小支持度,且存在在原始数据集中,所以加入总的频繁项集。

3. 实战案例

以下将展示书中案例的代码段,所有代码和数据可以在github中下载:

3.1. FP-growth寻找频繁项集

# coding:utf-8
from numpy import *

"""
FP-growth寻找频繁项集
"""


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


# 将数据集转换为set类型
def createInitSet(dataSet):
    retDict = {}
    for trans in dataSet:
        retDict[frozenset(trans)] = 1
    return retDict


# 树节点
class treeNode:
    # name: 节点名称
    # count: 出现次数
    # nodeLink: 节点链接
    # parent: 父节点
    # children: 子节点集
    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)


# 创建FP树
def createTree(dataSet, minSup=1):
    headerTable = {}
    # 第一次遍历数据集
    # 获取单个元素的频率
    for trans in dataSet:
        for item in trans:
            headerTable[item] = headerTable.get(item, 0) + dataSet[trans]
    # 去除不满足最小支持度的单个元素
    for k in list(headerTable.keys()):
        if headerTable[k] < minSup:
            del (headerTable[k])
    # 频繁项集
    # freqItemSet: {'p', 'v', 'u', 'q', ...}
    freqItemSet = set(headerTable.keys())
    # 无频繁项就返回
    if len(freqItemSet) == 0:
        return None, None
    # 扩展头指针表
    # 添加指向每种类型第一个元素的指针(节点链接)
    # headerTable: {'j': [1, None], 'p': [2, None], 'r': [3, None], ...}
    for k in headerTable:
        headerTable[k] = [headerTable[k], None]
    # 创建根节点
    retTree = treeNode('Null Set', 1, None)
    # 第二次遍历数据集
    # 构建FP树
    for tranSet, count in dataSet.items():
        # tranSet: frozenset({'h', 'p', 'z', 'j', 'r'})
        # count: 1
        localD = {}
        # 如果单个元素是频繁项,则加入localD列表
        for item in tranSet:
            if item in freqItemSet:
                localD[item] = headerTable[item][0]
        # localD: {'r': 3, 'j': 1, 'z': 5, 'h': 1, 'p': 2}
        if len(localD) > 0:
            # 排序
            orderedItems = [v[0] for v in sorted(localD.items(), key=lambda p: p[1], reverse=True)]
            # 更新FP树
            updateTree(orderedItems, retTree, headerTable, count)
    return retTree, headerTable


# 更新FP树函数
def updateTree(items, inTree, headerTable, count):
    # 判断排序后列表的第一个元素是否已经是根节点的子节点
    if items[0] in inTree.children:
        # 添加出现次数
        inTree.children[items[0]].inc(count)
    else:
        # 创建根节点的子节点
        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]])
    # 列表元素长度大于1
    # 递归调用更新FP树函数
    if len(items) > 1:
        updateTree(items[1::], inTree.children[items[0]], headerTable, count)


# 更新头指针表的节点链接的函数
def updateHeader(nodeToTest, targetNode):
    # 将元素放在指针链表的最后
    while (nodeToTest.nodeLink != None):
        nodeToTest = nodeToTest.nodeLink
    nodeToTest.nodeLink = targetNode


# 寻找节点basePat的所有前缀路径
# treeNode: 头节点表的basePat的指针指向元素
def findPrefixPath(basePat, treeNode):
    condPats = {}
    # 有指向的元素
    while treeNode != None:
        prefixPath = []
        # 回溯父节点,寻找前缀路径
        # prefixPath: ['r', 't', 'x', 'z']
        ascendTree(treeNode, prefixPath)
        # 路径长度大于1,不是单个元素
        if len(prefixPath) > 1:
            # 添加进condPats,记录路径的出现次数
            condPats[frozenset(prefixPath[1:])] = treeNode.count
        # 继续寻找basePat为结尾的前缀路径
        treeNode = treeNode.nodeLink
    # condPats: {frozenset({'z'}): 1, frozenset({'s', 'x'}): 1, frozenset({'x', 'z'}): 1}
    return condPats


# 单个节点回溯,寻找前缀路径
def ascendTree(leafNode, prefixPath):
    if leafNode.parent != None:
        prefixPath.append(leafNode.name)
        ascendTree(leafNode.parent, prefixPath)


# 根据FP树寻找频繁项集
def mineTree(inTree, headerTable, minSup, preFix, freqItemList):
    # 头指针表排序
    # bigL: ['h', 'j', 'u', 'v', 'w',...]
    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('finalFrequent Item: ', newFreqSet)
        freqItemList.append(newFreqSet)
        # 以basePat为节点的所有前缀路径
        # condPattBases: {frozenset({'z', 'r', 'p'}): 1, ...}
        condPattBases = findPrefixPath(basePat, headerTable[basePat][1])
        print('condPattBases :', basePat, condPattBases)
        # 以当前元素的所有前缀路径,创建条件FP树
        myCondTree, myHead = createTree(condPattBases, minSup)
        print('head from conditional tree: ', myHead)
        # 根据条件FP树和条件头指针表,递归创建下一个条件FP树
        if myHead != None:
            print('conditional tree for: ', newFreqSet)
            myCondTree.disp(1)
            mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)


if __name__ == '__main__':
    # simpDat = loadSimpDat()
    # initSet = createInitSet(simpDat)
    # retTree, headerTable = createTree(initSet)
    # freqItems = []
    # mineTree(retTree, headerTable, 3, set([]), freqItems)
    # print(freqItems)

    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(myFreqList)

如果这篇文章对你有一点小小的帮助,请给个关注喔~我会非常开心的~

猜你喜欢

转载自blog.csdn.net/zhq9695/article/details/83650774