Spark MLlib中FPGrowth和FPTree详解之一

一、准备知识

1.1 Scala版本:2.10.4

1.2 Spark版本:1.5.0

Spark中实现关联规则算法的包是:org.apache.spark.mllib.fpm。包中的文件如下图所示:


这里面我重点讲解红色箭头指向的两个代码文件。讲解过程中如果有误解的地方,还请评论指正,谢谢!

1.3 频繁模式增长FP-Growth

要理解Spark MLlib中FPGrowth和FPTree中的源码,首先在理论上要有一定的基础。首先就是韩教授他们发明的基于频繁模式树的关联规则挖掘算法。这里就不细表了,只要记得如下图的频繁模式树的原理就行。友情提醒:结点链和叶子结点对理解源码很有帮助


注:摘自韩家炜《数据挖掘》

图中的数据样本也是我们等下理解源码进行测试的数据样本,样本如下表所示

测试样本

TID

商品ID的列表

T100

I1,I2,I5

T200

I2,I4

T300

I2,I3

T400

I1,I2,I4

T500

I1,I3

T600

I2,I3

T700

I1,I3

T800

I1,I2,I3,I5

T900

I1,I2,I3

1.4 并行频繁模式增长PFP-Growth

Spark MLlib中的FPGrowth一个并行化的版本实现,在其注释中也提到了实现算法的出处。摘录如下,红色字体为文献标题。

/**

 *

 * Aparallel FP-growth algorithm to mine frequent itemsets. The algorithm isdescribed in

 *[[http://dx.doi.org/10.1145/1454008.1454027 Li et al., PFP: Parallel FP-Growth for Query

 *  Recommendation]].PFP distributes computation in such a way that each worker executes an

 *independent group of mining tasks. The FP-Growth algorithm is described in

 *[[http://dx.doi.org/10.1145/335191.335372 Han et al., Mining frequent patternswithout candidate

 * generation]].

 *

 *@param minSupport the minimal support level of the frequent pattern, anypattern appears

 *                   more than (minSupport *size-of-the-dataset) times will be output

 *@param numPartitions number of partitions used by parallel FP-growth

 *

 *@see [[http://en.wikipedia.org/wiki/Association_rule_learning Association rulelearning

 *      (Wikipedia)]]

 *

 */

所以在开始阅读源码之前,最好还是先读几遍相关的文献。在下面的章节中,我会以刚才的样本为例,演示一下PFP的过程,如果读过该文献,理解起来应该还是容易的。因为文献是英文,这里大致讲解一下过程:

•步骤1:sharding(碎片化)
•步骤2:ParallelCounting(并行计数)
•步骤3:GroupingItems(项分组)
•步骤4:ParallelFP-Growth(并行的FP-Growth)
•步骤5:Aggregating(聚合)

以样本数据集为例,其运行过程如下所示:

TID

商品ID的列表

排序后的ID与对应组ID

基于Q对事务进行划分

T100

I1,I2,I5

I2, I1, I5 -> Q1, Q1, Q3

Q3->{I2, I1, I5 }, Q1->{I2, I1}

T200

I2,I4

I2, I4 -> Q1, Q2

Q2->{I2, I4}, Q1->{I2}

T300

I2,I3

I2, I3 -> Q1, Q2

Q2->{I2, I3}, Q1->{I2}

T400

I1,I2,I4

I2, I1, I4 -> Q1, Q1, Q2

Q2->{I2, I1, I4}, Q1->{I2, I1}

T500

I1,I3

I1, I3 -> Q1, Q2

Q2->{I1, I3}, Q1->{I1}

T600

I2,I3

I2, I3 -> Q1, Q2

Q2->{I2, I3}, Q1->{I2}

T700

I1,I3

I1, I3 -> Q1, Q2

Q2->{I1, I3}, Q1->{I1}

T800

I1,I2,I3,I5

I2, I1, I3, I5 -> Q1, Q1, Q2,Q3

Q3->{I2, I1,I3, I5 },

Q2->{I2,I1,I3},

Q1->{I2, I1}

T900

I1,I2,I3

I2, I1, I3 ->  Q1, Q1, Q2

Q2->{I2,I1,I3}, Q1->{I2, I1}

F-list

I2:7 I1:6 I3:6 I4:2 I5:2

Q-list

Q1:{I2, I1} Q2:{I3, I4} Q3:{I5}


其中,F-list为排序后的项;Q-list是对所有item进行分组的情况。表中第四列,是第三列中每一行从右往左遍历直到第一个组号出现时,将其前面的项和该项作为一种划分的情况。最终,程序先将组进行合并,然后在每一组内进行FPTree的创建,并通过FPGrowth得到频繁模式。

二、源码详解

2.1 首先给出这两个代码文件的概览,如下表所示,对源码进行的分解说明。


FPGrowth和FPTree源码概览

2.2 FPTree源码详解

FPTree.scala中定义了FPTree类和它的单例对象。

而在单例对象中又定义了两个类。一个是Node类,顾名思义,就是表示树上的结点。具体的说明见下面的注释

/**

   * Representing a node in an FP-Tree.

   * @paramparent该结点的父结点

   */

  class Node[T](valparent: Node[T])extends Serializable {

    var item: T = _         //结点中包含的项(商品)

    var count: Long = 0L    //对应的计数

   

    /**

     * children表示孩子结点,用一个item->Node[item]的映射表示

     * 根据FPTree的特点,一个结点的孩子结点中包含的项item肯定是互不相同的;

     * 或者一个item在树的每一层最多只能出现一次,在树的每个分枝也只可能出现一次

     */

    val children: mutable.Map[T, Node[T]] = mutable.Map.empty

 

    def isRoot: Boolean =parent ==null//判断是否到达根结点

  }

这里要注意的是不同的Node可以由相同的item构成,比如下图中框出来存在于不同分枝的I5结点。而连接这两个Node的虚线其实就是接下来要介绍的另一个类Summary。



包含相同item的不同Nodes

Summary是单例对象中另一个类,其注释如下:

/**

   * Summary of a item in an FP-Tree.

   * 就是同一个项item对应的结点链

   */

  private class Summary[T] extendsSerializable {

    var count: Long = 0L //item(商品)在数据样本(transactions)中出现的次数

    val nodes: ListBuffer[Node[T]] = ListBuffer.empty //FPTree中包该item的所有结点列表

  }

 

    理解了这两个类之后,我们再来看FPTree这个关键类。

    首先类成员变量包括一个root和summaries. Summaries定义如下,是所有item到它的结点链的映射。

private val summaries: mutable.Map[T, Summary[T]] = mutable.Map.empty

下图中的列表是比较形象的理解。



Summaries的形象化理解

 

    接下来的一些方法,这里只给出一些注释,具体的运行过程在测试样本(第二章)的时候再讲解。

/** Adds a transaction withcount. */

  def add(t: Iterable[T], count: Long =1L):this.type = {

    require(count > 0)

    var curr = root //从根结点开始,将事务中的项依次添加到当前结点的孩子结点中

    curr.count += count

    /**

     * 可以将一个transaction看作树的一个分枝

     * 接下来就是将这个分枝添加到树中,对应层上有包含相同itemNode时就只更新count

     * 没有就要生成新的结点。。。

     */

    t.foreach { item => //依次遍历事务中的每一项

      val summary = summaries.getOrElseUpdate(item,new Summary)//如果item在树中已经存在,获取其结点链,否则生成一个

      summary.count += count

      val child = curr.children.getOrElseUpdate(item,{//首先从当前结点的孩子结点中看item是否存在

        val newNode = new Node(curr//如果不存在,生成一个新的以curr为父结点的newNode

        newNode.item = item 

        summary.nodes +=newNode

        newNode

      })

      child.count += count

      curr = child //得到孩子结点之后,将孩子结点看作当前结点向下遍历

    }//foreach过程执行结束

    this

  }


/**

   * Gets a subtree with the suffix.

   *  以事务中某项item为后缀,向上遍历FPTree

   *  得到一颗以包含itemNode为叶子结点的子树

   *  但是该方法是从item的父结点开始的,所以子树中不包含item

   *  这是因为extract方法中递归提取的原因

   */

  private def project(suffix: T): FPTree[T] = {

    val tree = new FPTree[T]

    if (summaries.contains(suffix)) {//判断结点链中是否包含该后缀项(貌似没有必要)

      val summary = summaries(suffix) //获取该suffix对应的结点链

      summary.nodes.foreach { node =>

        var t = List.empty[T]

        var curr = node.parent //从父结点开始

        while (!curr.isRoot) {//一直向上遍历直到根结点

          t = curr.item :: t

          curr = curr.parent

        }

        //t中是一条以item为结点的分枝

        tree.add(t, node.count)

      }

    }

    tree

  }

 

举个例子,假设item=I5,I5对应的summary中包含两个结点。Project方法分别从这两个结点向上遍历,遍历过程如下图所示。


以I5为后缀的子树投影过程

遍历之后,就可以得到下图所示子树:


Project(I5)方法的输出

 

/**

   *  Extracts allpatterns with valid suffix and minimum count.

   *  分别以summaries一个item为后缀,递归输出所有的频繁项

   */

  def extract(

      minCount: Long,

      validateSuffix: T => Boolean = _ =>true): Iterator[(List[T], Long)] = {

    summaries.iterator.flatMap {case (item,summary) =>//每一项和它对应结点链

     

      /**

       * validateSuffix只在第一次进行判断,判断该item是否属于FPTree对应的分区part

       * FPGrowthgenFreqItemSets方法中,可以看到一行代码 tree.extract(minCount, x=> partitioner.getPartition(x) == part)

       * PFP算法中将所有item进行组划分,假设有3组。那么每个item都有一个组号,在spark中是分区号。

       * 另外,每组都对应着一颗FPTree,对每一组的FPTree提取频繁模式的时候,

       * 只能以属于该组的那些项items为后缀来提取,不属于该分区的则忽略

       *  但是提取过程中是不需要再判断的

       */

      if (validateSuffix(item) &&summary.count >= minCount) {//

        Iterator.single((item :: Nil,summary.count)) ++//item为频繁一项集中的一个

          project(item).extract(minCount).map{case (t,c) =>//递归的抽取item所在分枝上所有大于minCount的项集

            (item :: t, c)

          }

      } else {

        Iterator.empty

      }

    }

  }


未完待续……


  • 3
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值