机器学习实战-11-FP-growth算法

一、FP-growth介绍

从大规模的数据集中,寻找不同特征或者物品之间的隐含关系,称为关联分析(association analysis),或者关联规则学习(association rule learning)。
在 Apriori 算法中,寻找频繁项集,需要对每一个可能的频繁项扫描一遍数据集计算支持度,计算量庞大。
在 FP-growth 算法中,寻找频繁项集,只需要扫描两遍数据集,将数据存储在FP树的结构上,然后在FP树上挖掘频繁项集。

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

例如在下述例子中,下图是一颗FP树:

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 算法的思想,如果某个项是非频繁的,那么这个项的所有超集也是非频繁的。

二、构建FP树

构建FP树的过程只需要扫描两遍数据集。
第一遍扫描,计算每个单个元素的频率,并根据最小支持度,滤除不满足的元素。
第二遍扫描,首先对数据集进行处理,每一条数据按照元素的绝对出现频率排序,并滤除不满足最小支持度的元素。
例如根据上述的头指针表,元素排序为{z:5, x:4, y:3, s:3, r:3, t:3},所以处理后的数据为:

处理后,遍历数据集,将每一条数据插入FP树中,从根节点开始递归添加路径,存在则将数值增加,不存在则创建新的节点。

例如下图所示,① 根节点不存在子节点{z},所以创建新的子节点{z},递归节点{z},因不存在子节点{r},所以创建新的子节点{r},② 根节点存在子节点{z},所以数值增加,递归节点{z},因不存在子节点{x},所以创建新的子节点{x},递归节点{x},......,如此递归。

三、从FP树中挖掘频繁项集

全文代码:

from numpy import *
from time import *


def load_simple_data():
    """
    Function:
        创建加载简单数据集
    Parameters:
        无
    Returns:
         simple_data - 简单数据集
    Modify:
        2018-12-27
    """
    simple_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 simple_data


def create_init_set(data_set):
    """
    Function:
        计算项集在数据集的出现次数
    Parameters:
        data_set - 数据集
    Returns:
         ret_dict - 包含出现次数的项集字典
    Modify:
        2018-12-27
    """
    ret_dict = {}
    for trans in data_set:
        ret_dict[frozenset(trans)] = 1
    return ret_dict


def update_tree(items, in_tree, header_table, count):
    """
    Function:
        更新树节点,让FP树生长
    Parameters:
        items - 项集
        in_tree - 当前FP树
        header_table - 头指针表
        count - 次数
    Returns:
         无
    Modify:
        2018-12-27
    """
    # 判断排序后列表的第一个元素是否已经是根节点的子节点
    if items[0] in in_tree.children:
        # 添加出现次数
        # children = {}
        in_tree.children[items[0]].inc(count)
    else:
        # 创建根节点的子节点
        in_tree.children[items[0]] = TreeNode(items[0], count, in_tree)
        # 如果该元素的后继节点不存在,则直接加入。如果有后继节点,则遍历链表尾部将其加入
        if header_table[items[0]][1] == None:
            header_table[items[0]][1] = in_tree.children[items[0]]
        else:
            update_header(header_table[items[0]][1], in_tree.children[items[0]])
    # 列表元素长度大于1,递归调用更新FP树函数
    if len(items) > 1:
        update_tree(items[1::], in_tree.children[items[0]], header_table, count)


def update_header(node_to_test, target_node):
    """
    Function:
        更新头指针表的节点链接
    Parameters:
        node_to_test - 遍历节点
        target_node - 目标节点
    Returns:
         无
    Modify:
        2018-12-27
    """
    # 遍历到链表尾节点
    while (node_to_test.node_link != None):
        node_to_test = node_to_test.node_link
    # 将刚添加的树节点加入链表的尾部
    node_to_test.node_link = target_node


def create_tree(data_set, min_sup=1):
    """
    Function:
        遍历数据集两次构建FP树
    Parameters:
        data_set - 包含项集出现次数的数据集字典
        min_sup - 最小支持度
    Returns:
         ret_tree - FP树
         hearder_table - 头指针表
    Modify:
        2018-12-27
    """
    hearder_table = {}
    # 第一次遍历数据集,获取单个元素的次数
    for trans in data_set:
        for item in trans:
            hearder_table[item] = hearder_table.get(item, 0) + data_set[trans]
    # 去除不满足最小支持度的单个元素
    for k in list(hearder_table.keys()):
        if hearder_table[k] < min_sup:
            del (hearder_table[k])
    freq_item_set = set(hearder_table.keys())
    # 无频繁项就直接返回
    if len(freq_item_set) == 0:
        return None, None
    # 扩展头指针表,添加指向每种类型第一个元素的指针(节点链接)
    for k in hearder_table:
        hearder_table[k] = [hearder_table[k], None]
    # 创建根节点
    ret_tree = TreeNode('Null Set', 1, None)
    # 第二次遍历数据集,构建FP树
    for tran_set, count in data_set.items():
        # tran_set: frozenset({'h', 'p', 'z', 'j', 'r'})
        # count: 1
        local_d = {}
        # 如果单个元素是频繁项,则加入localD列表
        for item in tran_set:
            if item in freq_item_set:
                # hearder_table:{'b': [3, None]}
                local_d[item] = hearder_table[item][0]
        # localD: {'r': 3, 'j': 1, 'z': 5, 'h': 1, 'p': 2}
        if len(local_d) > 0:
            # 元素按出现次数排序
            ordered_items = [v[0] for v in sorted(local_d.items(), key=lambda p: p[1], reverse=True)]
            # 更新FP树,让FP树生长
            update_tree(ordered_items, ret_tree, hearder_table, count)
    return ret_tree, hearder_table


class TreeNode:
    # name:节点元素名称,在构造时初始化为给定值
    # count:出现次数,在构造时初始化为给定值
    # node_link:指向下一个相似节点的指针,默认为None
    # parent:指向父节点的指针,在构造时初始化为给定值
    # children:指向子节点的字典,以子节点的元素名称为键,指向子节点的指针为值,初始化为空字典
    def __init__(self, name_value, num_occur, parent_node):
        self.name = name_value
        self.count = num_occur
        self.node_link = None
        self.parent = parent_node
        self.children = {}

    def inc(self, num_occur):
        # 增加节点出现次数
        self.count += num_occur

    def disp(self, ind=1):
        # 用于将树以文本形式显示
        print('  ' * ind, self.name, ' ', self.count)
        for child in self.children.values():
            child.disp(ind + 1)


def ascend_tree(leaf_node, prefix_path):
    """
    Function:
        根据当前节点向前追溯至根节点,记录前缀路径
    Parameters:
        leaf_node - 给定元素项节点
        prefix_path - 前缀路径列表
    Returns:
         无
    Modify:
        2019-1-6
    """
    # 如果节点有父节点,则将当前节点添加至前缀路径中,之后再递归向上追溯
    if leaf_node.parent != None:
        prefix_path.append(leaf_node.name)
        ascend_tree(leaf_node.parent, prefix_path)


def find_prefix_path(base_pat, tree_node):
    """
    Function:
        发现以给定元素项结尾的所有前缀路径
    Parameters:
        base_pat - 元素项
        tree_node - 需遍历节点
    Returns:
         cond_pats - 所有条件模式基字典
    Modify:
        2019-1-6
    """
    # 所有条件模式基字典
    cond_pats = {}
    # 遍历该节点的整个链表节点,记录每个节点的前缀路径,并将其添加至条件模式基当中
    while tree_node != None:
        prefix_path = []
        ascend_tree(tree_node, prefix_path)
        # 因为该节点也被加进了路径当中,所以需要路径的长度大于1
        if len(prefix_path) > 1:
            # 如果有前缀路径,则将前缀路径加入条件模式基集合中,并且将该元素在该前缀路径中出现的次数也添加进去
            cond_pats[frozenset(prefix_path[1:])] = tree_node.count
        # 当前节点的条件模式基查找完毕后,继续查找头指针链表中下一个节点的条件模式基
        tree_node = tree_node.node_link
    return cond_pats


def mine_tree(in_tree, header_table, min_sup, pre_fix, freq_item_list):
    """
    Function:
        创建条件模式树
    Parameters:
        in_tree - FP树
        header_table - 头指针表
        min_sup - 最小支持度
        pre_fix - 上一次递归的频繁项集合的前缀
        freq_item_list - 频繁项集列表
    Returns:
        无
    Modify:
        2019-1-6
    """
    # 对头指针表中的元素项按照其出现频率从小到大进行排序
    big_l = [v[0] for v in sorted(header_table.items(), key=lambda p:p[1][0])]
    # 遍历单元素频繁集
    for base_pat in big_l:
        new_freq_set = pre_fix.copy()
        new_freq_set.add(base_pat)
        freq_item_list.append(new_freq_set)
        # 获得该元素的所有条件模式基,相当于一个事务集合
        cond_patt_bases = find_prefix_path(base_pat, header_table[base_pat][1])
        # 根据所有条件模式基集合来构建条件模式树
        my_cond_tree, my_head = create_tree(cond_patt_bases, min_sup)
        # 如果条件模式树的头指针表不空(每次建树时对元素支持度有要求
        # 如果小于支持度则该元素不参与建树过程,所以在建树时,条件模式基中的元素会越来越少,最后会是空树),则递归建树
        if my_head != None:
            print('conditional tree for:', new_freq_set)
            my_cond_tree.disp(1)
            mine_tree(my_cond_tree, my_head, min_sup, new_freq_set, freq_item_list)


if __name__ == '__main__':
    # simple_data = load_simple_data()
    # init_data = create_init_set(simple_data)
    # print(simple_data)
    # print(init_data)

    # simple_data = load_simple_data()
    # init_data = create_init_set(simple_data)
    # my_fp_tree, my_header_tab = create_tree(init_data, 3)
    # my_fp_tree.disp()

    # simple_data = load_simple_data()
    # init_data = create_init_set(simple_data)
    # my_fp_tree, my_header_tab = create_tree(init_data, 3)
    # cond_pats_1 = find_prefix_path('x', my_header_tab['x'][1])
    # print(cond_pats_1)
    # cond_pats_2 = find_prefix_path('z', my_header_tab['z'][1])
    # print(cond_pats_2)
    # cond_pats_3 = find_prefix_path('r', my_header_tab['r'][1])
    # print(cond_pats_3)

    # simple_data = load_simple_data()
    # init_data = create_init_set(simple_data)
    # my_fp_tree, my_header_tab = create_tree(init_data, 3)
    # freq_items = []
    # mine_tree(my_fp_tree, my_header_tab, 3, set([]), freq_items)
    # print(freq_items)

    start_time = time()
    parse_dat = [line.split() for line in open('./machinelearninginaction/Ch12/kosarak.dat').readlines()]
    init_set = create_init_set(parse_dat)
    my_fp_tree, my_header_tab = create_tree(init_set, 100000)
    my_freq_list = []
    mine_tree(my_fp_tree, my_header_tab, 100000, set([]), my_freq_list)
    end_time = time()
    print('被10万或者更多人浏览过的新闻报道或报道集合数:', len(my_freq_list))
    print('被10万或者更多人浏览过的新闻报道或报道集合:', my_freq_list)
    print('总共执行时间:', end_time - start_time)

一个元素的条件模式基(conditional pattern base),是这个元素所有前缀路径(prefix path)的集合。
前缀路径(prefix path),是当前元素到根节点之间的路径(不包括当前元素和根节点)。
例如下图,{r}的条件模式基是{{z}{z, x, y}{x, s}}:

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

  • 对于头指针表中的每一个元素,获取其条件模式基
  • 根据条件模式基,构建条件FP树(即,将条件模式基当作新的数据集,按照建树的流程,重新构建一棵FP树)
  • 继续对于条件FP树的头指针表中的每一个元素,获取其条件模式基
  • 继续根据条件模式基,构建条件FP树
  • 如此递归过程,直到无法构建出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}一定满足最小支持度,且存在在原始数据集中,所以加入总的频繁项集。

发现以给定元素项结尾的所有前缀路径
条件树
条件FP树与频繁项集

四、实例:从新闻网站点击流中挖掘

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

从上图可以看出,构建树以及扫描100万行找出频繁项集只要不到8秒钟,这展示了FP-growth算法的强大威力。

五、小结

FP-growth算法是一种用于发现数据集中频繁模式的有效方法。FP-growth算法利用了Apriori原则,并且只对数据集扫描两次,所以执行更快。Apriori算法产生候选项集,然后扫描数据集来检查它们是否频繁。在FP-growth算法中,数据集存储在一个称为FP树的结构中。

FP树构建完成后,可以通过查找元素项的条件基及构建条件FP树来发现频繁项集。该过程不断以更多元素作为条件重复进行,直到FP树只包含一个元素为止。

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

推荐阅读更多精彩内容