(一)综述:
概念介绍
频繁项集指的是频繁共同出现的 item组成的集合。如在购物场景下,用户常常同时购买 A 和 B 两种物品。A 和 B则构成一个频繁项集合。挖掘频繁项集能够帮助商家向用户推送商品,如在淘宝上购买完鼠标后往往会出现鼠标垫的推荐。
在实际挖掘的过程中,需要挖掘出的频繁项集满足一定的支持度。
支持度即为 A 和 B 项集在总体数据中出现的次数,设置支持度是为了过滤不值得注意的模式。假设电商场景中A 和 B 只共同购买过1次,那么在大量的电商数据中,这样的项集不值得引起注意。
下图中{A,B}的支持度为1,{A,C}的支持度为2.
订单 ID | item |
1 | {A , B , C} |
2 | {A, C} |
3 | {A} |
Apriori
Apriori为传统的频繁项集挖掘算法,从单项集开始,经过不断迭代和剪枝,得到所有频繁项集。
对于上表中的例子,Apriori 先遍历所有数据统计所有的单项集得到每个项集的支持度{A}:3,{B}:1,{C}:2。
然后进行剪枝,减去低于支持度阈值的项集,假设支持度阈值 sup_thred = 2
那么{B}项集被减去。
通过剩余子集构造包含两个元素的项集得到{A,C}。然后遍历所有数据得到其支持度为2。
依次类推。剪枝使用的原理是频繁项集的支持度一定小于等于其子集的支持度,即sup({A}) >= sup({A,B}),所以子集无法满足阈值,那本身必定无法满足阈值。每轮通过剪纸后的子集构造新的项集。下图为 Apriori 剪枝,图中从 {a,b}项集支持度低于阈值,意味着其产生的项集的支持度都将低于阈值,可以在早期通过剪纸去掉。
值得注意的是:Apriori算法中,每轮迭代需要遍历一次全体数据,一旦待挖掘的频繁项集较长,且数据量较大则效率通常低下。FPGrowth 通过构造 FP 树解决了这个问题,在 FPGrowth 算法中,仅需要在构造 FP 树过程中遍历两次全局数据即可。
文章结构
下文将 FPGrowth 算法分为 FP 树搭建和频繁项集挖掘两个部分。
(二)FP 树搭建
论文中算法步骤:
算法步骤:
step1:
遍历数据,统计所有单项集的支持度,去掉低于支持度阈值的单项集,并将剩余的项按照支持度逆序排列。
对于图中数据,统计结果为
{f}:4,{c}:4,{a}:3,{b}:3,{m}:3,{p}:3,{d}:1,{g}:1,{i}:1,{l}:2,{o}:2,{h}:1,{j}:1,{k}:1,{s}:1,{e}:1,{n}:1
由于支持度sup_thred 为2,剔除小于等于2的项集{d}:1,{g}:1,{i}:1,{l}:2,{o}:2,{h}:1,{j}:1,{k}:1,{s}:1,{e}:1,{n}:1。
剩余项集按逆序排列为 fcabmp。
step2:
先构造空的 FP 树T 以及表格 Header。表格 Header 保存了所有频繁项,可以通过链表访问到树 T 中同名的项,Header 表中按照项的降序排列。
然后遍历数据,对每条记录,将 step1中剔除的项剔除后,按照 step1给出的逆序顺序排列当前项集剩余的项。如上图中第二条记录 去除不频繁项{l,o}后逆序排列结果为 {f,c,a,b,m}。
对于每条记录,执行 insert([p|P],T)算法,我们将排序结果定义为(p|P),其中 p 代表的是排序结果头部项,P 则代表剩余项。对于排序{f,c,a,b,m},p为{f},而 P 为{c,a,m,p}。算法如下
insert([p|P],T)算法:
======================================
if(T 的子节点中存在与 p 名称相同的节点){
设置节点 N 为该节点,并使得 N 节点 count++;}
else{
创建新的节点N保存数据 p;
设置 N节点count=1;
访问表格 Header,找到指向 p 的链表尾部,将当前节点 N 插入该链表尾部;}
if(P 非空){
调用 insert(P,N);}
======================================
insert([p|P],T)算法为递归算法,最终构造出完整的 FP 树,通过上图中的例子可自行熟练算法过程。
算法中的细节:
1.对项集进行排序再执行插入:如此才能共用前缀节点。
2.使用降序排列的原因:减少根节点的子节点数目。
(三)频繁项集挖掘
论文中算法步骤:
算法步骤:
先解释算法中涉及到的构建 CPB 和构建条件 FP 树这两个操作。
构建 CPB:
对于 item i 来说,在FP 树中所有包含该元素的前缀路径即为此item 的 CPB。
举例来说,对于图中的 m ,其 CPB 即为包含 m 的两条前缀路径:fca 和 fcab。
由于构建 CPB 的过程是按照 header 表自底向上构建,所以元素 m 的后缀已经在之前的过程中考虑到了。构建 CPB 只考虑前缀路径。
构建条件 FP 树:
对于元素 m 来说,得到 CPB:{fca:2}和{fcab:1}之后,可以根据CPB 构建条件 FP树,即在包含元素 m 的前缀路径上构建新的 FP树,此即为条件 FP 树。
算法流程:
自底向上遍历 header 表,对于 header 表中的每个元素构建CPB 和条件 FP 树,然后将此条件 FP 树 T作为输入,执行 produce_FPGrowth(T,null)。
produce_FPGrowth(Tree T,pattern_base S):
======================================
if (T 包含一个单一的前缀路径){
(1)设置 P 为该单一前缀路径, Q 则为分叉之后的树,其中分叉节点替换成 null。分叉节点包含在 P 中。(根节点不为空也视为含有单一前缀路径)
举例:对于上图 m 的条件 FP 树来说, 将 P设置为{fca},Q 设置为由null 替代{a:3}后的树,包含{m:2},{b:1},{m:1}。
(2)对于 P 中所有的元素,生成所有可能的组合。对于每个组合,其支持度为组合中支持度最小的节点。对于支持度大于阈值的组合,合并进入待输出的pattern集合中。
举例:对于上图 m 的条件 FP 树,P为{fca},可以得到{f}:4,{c}:3,{a}:3,{fc}:3,{fa}:3,{ac}:3这六个在包含 m 的条件下的模式,将他们与 {m}组合得到六个频繁模式。
} else{
设置 Q 为树T。
}
for (树 Q中的每个元素ai){
(1)构造pattern belta,包含当前元素 {ai} 和条件{S},其支持度为元素 ai 的支持度。
解释:在条件 FP树中,节点 ai中的数字就代表了{ai, 条件集合S}的支持度。
(2)使用belta构建新的条件 FP 树,
if(新产生的条件 FP 树T2非空){
对于新创建的条件 FP 树,调用produce_FPGrowth(T2,belta)
}
举例:对于 m 的条件 FP 树,它的分叉树中包含
}
返回(1)Q 以及递归产生的频繁项集(2)P产生的频繁项集(3)Q 和 P 笛卡尔积组合产生的频繁项集,支持度为Q的支持度。
======================================
结论:
FPGrowth 的优点在于不需要多次遍历全局数据,而是通过两轮的遍历得到全局数据的FP 树后,在此基础上进行各种访问操作。
但对于数据量较小的场景来说,构建和处理 FP 树复杂度过高,并不适用。