FP-growth (Frequent-Pattern Growth)是数据挖掘中用于挖掘频繁项集的经典算法之一。相较于 Apriori 算法,该算法消除了候选项集,并减少了对数据库扫描的次数,因而效率更高。具体算法思路可以参考数据挖掘教材 data mining concepts and techniques 第六章的内容。
本文主要介绍 FPTree 的实现,FPTree 结构是 Spark 中实现 FP-Growth 算法的主要数据结构。
FPTree 的实现
Spark 中 FPTree 的结构如图所示:
图中每个部分都可以和教材中给出的结构相对应。
Spark 中的实现结构如下,下面先概括性的知道一下每个变量和函数的用途,之后会详细说明,另外阅读时要注意变量的类型,以及它们和上图的对应关系,方便下面理解源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 class FPTree [T ] { val root: Node [T ] val summaries: mutable.Map [T , summary[T ]] def add (t: Iterator [T ], count: Long = 1 L): this .type def merge (other: FPTree [T ]): this .type def project (suffix: T ): FPTree [T ] def transactions : Iterator [(List [T ], Long )] def getTransactions (node: Node [T ]): Iterator [(List [T ], Long )] def extract (minCount: Long , validateSuffix: T => Boolean ): Iterator [(List [T ], Long )] } object FPTree { class Node [T ](val parent: Node [T ] ) { var item: T var count: Long val children: mutable.Map [T , Node [T ]] def isRoot : Boolean } class Summary [T ] { var count: Long val nodes: ListBuffer [Node [T ]] } }
下面主要说明对树的几种基本操作。
向树中增加事务
根据教材中的描述,将事务添加到树中时,需要更新 Summaries 的内容和结点树。
下面看一下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def add (t: Iterable [T ], count: Long = 1 L): this .type = { require(count > 0 ) var curr = root curr.count += count t.foreach { item => val summary = summaries.getOrElseUpdate(item, new Summary ) summary.count += count val child = curr.children.getOrElseUpdate(item, { val newNode = new Node (curr) newNode.item = item summary.nodes += newNode newNode }) child.count += count curr = child } this }
将FP树转换成事务集
FP树中包含了事务集的所有信息,所以也可以从结点树解析出事务集,Spark 中提供了getTransactions
方法,这个方法可以求得以某个结点为根结点的子树下的事务集(注意不包括这个结点),调用getTransactions(root)
即可得到整棵树的事务集。
getTransactions
方法是一个递归的方法,该方法能够返回参数node
结点下方 的所有事务(注意不包含 node 结点本身),如果要得到包含node
结点的所有事务只需在getTransactions
调用的返回值之中,给每一项前加入node
结点的item
即可。
如下图,如果以 I 2 I2 I 2 为根结点,得到的事务集为 [ ( I 2 , I 3 ) , ( I 2 ) ] [(I2, I3), (I2)] [ ( I 2 , I 3 ) , ( I 2 ) ] ,而调用getTransactions
时,将返回 [ ( I 3 ) , ( N i l ) ] [(I3), (Nil)] [ ( I 3 ) , ( N i l ) ] ,此时将调用结点 I 2 I2 I 2
加入返回值中的每一项 [ ( I 2 : : I 3 ) , ( I 2 : : N i l ) ] = [ ( I 2 , I 3 ) , ( I 2 ) ] [(I2::I3), (I2::Nil)] = [(I2, I3), (I2)] [ ( I 2 : : I 3 ) , ( I 2 : : N i l ) ] = [ ( I 2 , I 3 ) , ( I 2 ) ] ,即可得到完整的事务。
下面看下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 private def getTransactions (node: Node [T ]): Iterator [(List [T ], Long )] = { var count = node.count node.children.iterator.flatMap { case (item, child) => getTransactions(child).map { case (t, c) => count -= c (item :: t, c) } } ++ { if (count > 0 ) { Iterator .single((Nil , count)) } else { Iterator .empty } } }
生成相关后缀的子树
该过程输入后缀元素,生成并返回相关子树。这个过程遍历后缀元素对应的结点链,对于每个结点,逆序遍历得到一条事务,事务的频度就是后缀元素的 c o u n t count c o u n t 值。然后将这条事务加入到一棵新建的树中。重复直到结点链尾,最后返回这棵树即可。
注意生成的后缀树有如下性质:
该树不包含该后缀元素
后缀元素不一定是叶子结点
后缀元素指是该事务的最后一项(事务中的项已经有序)
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 private def project (suffix: T ): FPTree [T ] = { val tree = new FPTree [T ] if (summaries.contains(suffix)) { val summary = summaries(suffix) summary.nodes.foreach { node => var t = List .empty[T ] var curr = node.parent while (!curr.isRoot) { t = curr.item :: t curr = curr.parent } tree.add(t, node.count) } } tree }
根据验证的后缀和最小支持度计数生成模式
这个函数根据和最小支持度计数生成模式,验证后缀使得函数可以只生成指定后缀的模式,注意由于事务中的 i t e m item i t e m 已经排序,所以所有的模式组成的集合可以根据后缀元素不相互覆盖的划分 。
Spark 中这个过程由函数extract
实现,函数有两个参数,一个最小支持度计数,另一个是用于验证后缀的函数,这个函数根据输入的元素返回一个布尔值用以决定是否输出相关联的模式,该参数用于分布式生成模式,避免重复输出。
下面看下源代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 def extract ( minCount: Long , validateSuffix: T => Boolean = _ => true ): Iterator [(List [T ], Long )] = { summaries.iterator.flatMap { case (item, summary) => if (validateSuffix(item) && summary.count >= minCount) { Iterator .single((item :: Nil , summary.count)) ++ project(item).extract(minCount).map { case (t, c) => (item :: t, c) } } else { Iterator .empty } } }
小结
本文主要讲解了 Spark 中 FPTree 的实现,对该结构的重点操作的源代码(包括增加事务、树与事务集的转换以及频繁模式生成)进行了分析。