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

本章内容

  • 发现事物数据中的公共模式
  • FP-growth算法
  • 发现Twitter源*现词

搜索引擎自动补全查询此项,可以找出互联网上经常一块出现的词对。这需要一种高效发现频繁项集的方法。FP-growth比上一章讨论的Apriori算法要快,它基于Apriori构建,但在完成任务时采用不同的技术。这里的任务是将数据集存储在一个特定的称作FP树的结构之后发现频繁项集或者频繁项对,即常在一块出现的元素项的集合FP树。与Apriori相比,此算法执行速度要快两个数量级以上。但是,只能高效地发现频繁项集,不能用于发现关联规则。

FP-growth算法只需要对数据库进行两次扫描,而Apriori对于每个潜在的频繁项集都会扫描数据集判定给定模式是否频繁,因此FP-growth性能更好,它发现频繁项集的基本过程如下:

  1. 构建FP树
  2. 从FP树种挖掘频繁项集

一、FP树:用于编码数据集的有效方式

FP-growth算法将数据存储在FP树这种紧凑数据结构中。FP代表频繁模式(Frequent Pattern)。一颗FP树与计算机科学中其他树结构类似,但它通过链接(link)来连接相似元素。


机器学习实战-FP-growth算法
图1 一颗FP树,包含着连接相似点的链接

同搜索树不同的是,一个元素项可以在一棵FP树中出现多次。FP树会存储项集的出现频率,而每个项集会以路径的方式存储在树中。存在相似元素的集合会共享树的一部分。只有当集合之间完全不同时,树才会分叉。树节点上给出集合中单个元素及其在序列中的出现次数,路径会给出该序列的出现次数。

相似项之间的链接,即节点链接(node link),用于快速发现相似项的位置。上图FP树可由下表中的数据生成。

表1 用于生成上图中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


在上图中,元素项z出现了5次,集合{r, z}出现了1次。集合{t, s, y, x, z}出现了2次,集合{t, r, y, x, z}出现了1次。元素项z的右边是5,表示z出现了5次,刚才已经给出了四次出现,所以它一定单独出现过1次。005号记录是{y, r, x, z, q, t, p},那么q、p去哪儿了呢?

这里使用第11章给出的支持度定义,该指标对应一个最小阈值,低于最小阈值的元素项被认为是不频繁的。若将最小支持度设为3,然后应用频繁项分析算法,就会获得出现3次或3次以上的项集。图中FP树的最小支持度是3,因此p、q并没有出现在树中。

FP-growth算法的工作流程:首先构建FP树,然后利用它来挖掘频繁项集。为了构建FP树,需要对原始数据集扫描两遍。第一遍对所有元素项的出现次数进行计数。Apriori原理,即如果某元素是不频繁的,那么包含该元素的超集也是不频繁的,所以就不需要考虑超集。数据库的第一遍扫描用来统计出现的频率,第二遍扫描中只考虑哪些频繁元素。

二、构建FP树

使用一个容器来保存FP树。

2-1 创建FP树的数据结构

要创建一个类来保存树的每个节点。创建fpGrowth.py,加入下列代码。

# coding=utf-8

class treeNode :
    def __init__(self, nameValue, numOccur, parentNode) :
        # 节点名称
        self.name = nameValue
        self.count = numOccur
        # 用于链接相似的元素项
        self.nodeLink = None
        # 当前节点的父节点
        self.parent = parentNode
        # 用于存放节点的子节点
        self.children = {}

    # 对count变量增加给定值
    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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

运行程序结果:

>>> import ml.fpGrowth as fpGrowth
>>> rootNode = fpGrowth.treeNode('pyramid',9,None)
>>> rootNode.children['eye'] = fpGrowth.treeNode('eye',13,None)
>>> rootNode.disp()
  pyramid   9
   eye   13
>>> rootNode.children['phoenix']=fpGrowth.treeNode('phoenix',3,None)
>>> rootNode.disp()
  pyramid   9
   eye   13
   phoenix   3
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

2-2 构建FP树

除图1给出的FP树之外,还需要一个头指针表来指向给定类型的第一个实例。利用头指针表,可快速访问FP树中一个给定类型的所有元素。下图2给出了一个头指针表的示意图。


机器学习实战-FP-growth算法
图2 带头指针表的FP树,头指针表作为一个起始指针来发现相似元素项

这里使用一个字典来保存头指针表。除了存放指针外,头指针表还可以用来保存FP树中每类元素的总数。

第一次遍历数据集获得每个元素项的出现频率。接着,去掉不满足最小支持度的元素项,再构建FP树。在构建时,读入每个项集并将其添加到一条已经存在的路径中。如果该路径不存在,则创建一条新路径。每个事务就是一个无序集合。假设有集合{z, x, y}和{y, z, r},那么在FP树中,相同项会只表示一次。为了解决此问题,在将集合添加到树之前,需要对每个集合进行排序。排序基于元素项的绝对出现频率来进行。使用图2中头指针节点值,对表1中数据进行过滤,重排序后的数据显示在表2中。

表12-2 将非频繁项移除并且重排序后的事务数据集

事务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树了。从空集(符号为)开始,向其中不断添加频繁项集。过滤、排序后的事务依次添加到树中,如果树中已存在现有元素,则增加现有元素的值;如果现有元素不存在,则向树添加一个分枝。对表2前两条事务进行添加的过程显示在图3中。


机器学习实战-FP-growth算法
图3 FP树构建过程的一个示意图,图中给出了使用表2中数据构建FP树的前两步

下面代码用于实现上述过程。

# FP树构建函数
# 使用数据集以及最小支持度作为参数来构建FP树。树构建过程会遍历数据集两次。
def createTree(dataSet, minSup=1) :
    headerTable = {}
    # 第一次遍历扫描数据集并统计每个元素项出现的频度。这些信息被保存在头指针中。
    for trans in dataSet :
        for item in trans :
            headerTable[item] = headerTable.get(item, 0) + dataSet[trans]
    # 接着扫描头指针表删除那些出现次数小于minSup的项。
    for k in headerTable.keys() :
        if headerTable[k] < minSup :
            del(headerTable[k])
    freqItemSet = set(headerTable.keys())
    # 如果所有项都不频繁,无需下一步处理
    if len(freqItemSet) == 0 : return None, None
    # 对头指针表稍加扩展以便可以保存计数值及指向每种类型第一个元素项的指针
    for k in headerTable :
        headerTable[k] = [headerTable[k], None]
    # 创建只包含空集合的根节点
    retTree = treeNode('Null Set', 1, None)
    for tranSet, count in dataSet.items() :
        localD = {}
        # 根据全局频率对每个事务中的元素进行排序
        for item in tranSet :
            if item in freqItemSet :
                localD[item] = headerTable[item][0]
        if len(localD) > 0 :
            orderedItems = [v[0] for v in sorted(localD.items(), key=lambda p : p[1], reverse=True)]
            # 排序后,调用updateTree()方法
            updateTree(orderedItems, retTree, headerTable, count)
    return retTree, headerTable

# 为了让FP树生长,需调用updateTree函数。
def updateTree(items, inTree, headerTable, count) :
    # 该函数首先测试事务中的第一个元素项是否作为子节点存在。
    if items[0] in inTree.children :
        # 如果存在,则更新该元素项的计数
        inTree.children[items[0]].inc(count)
    else :
        # 如果不存在,则创建一个新的treeNode并将其作为一个子节点添加到树中,这时,头指针表也要更新以指向新的节点。
        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
            updateHeader(headerTable[items[0]][1], inTree.children[items[0]])
    # updateTree()完成的最后一件事是不断迭代调用自身,每次调用时会去掉列表中的第一个元素
    if len(items) > 1 :
        updateTree(items[1::], inTree.children[items[0]], headerTable, count)

# 确保节点链接指向树中该元素项的每一个实例,从头指针的nodeLink开始,一直沿着nodeLink直到到达链表末尾。
# 当处理树的时候,一种自然的反应就是迭代完整每一件事。当以相同方式处理链表时可能会遇到一些问题,
# 原因是如果链表很长可能会遇到迭代调用的次数限制
def updateHeader(nodeToTest, targetNode) :
    while (nodeToTest.nodeLink != None) :
        nodeToTest = nodeToTest.nodeLink
    nodeToTest.nodeLink = targetNode
  • 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

运行上例之前,需要一个真正的数据集,使用下面代码装入数据:

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

# 从列表向字典的类型转换
def createInitSet(dataSet) :
    retDict = {}
    for trans in dataSet :
        retDict[frozenset(trans)] = 1
    return retDict
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

上面代码运行结果:

>>> reload(fpGrowth)
<module 'ml.fpGrowth' from 'C:\Python27\ml\fpGrowth.pyc'>
>>> simpDat = fpGrowth.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']]
>>> initSet = fpGrowth.createInitSet(simpDat)
>>> initSet
{frozenset(['e', 'm', 'q', 's', 't', 'y', 'x', 'z']): 1, frozenset(['x', 's', 'r
', 'o', 'n']): 1, frozenset(['s', 'u', 't', 'w', 'v', 'y', 'x', 'z']): 1, frozen
set(['q', 'p', 'r', 't', 'y', 'x', 'z']): 1, frozenset(['h', 'r', 'z', 'p', 'j']
): 1, frozenset(['z']): 1}
>>> myFPtree, myHeaderTab = fpGrowth.createTree(initSet, 3)
>>> myFPtree.disp()
  Null Set   1
   x   1
    s   1
     r   1
   z   5
    x   3
     y   3
      s   2
       t   2
      r   1
       t   1
    r   1
>>>
  • 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

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

有了FP树后,就可以抽取频繁项集了,思路与Apriori算法大致类似,首先从单元素项集合开始,然后在此基础上逐步构建更大的集合。从FP树中抽取频繁项集的三个基本步骤如下:

  1. 从FP树中获得条件模式基;
  2. 利用条件模式基,构建一个条件FP树;
  3. 迭代重复步骤1、2,直到树包含一个元素项为止。

需重点关注第1步,即寻找条件模式基的过程。之后,为每一条件模式基创建对应的条件FP树。最后需构造少许代码来封装上述两个函数,并从FP树中获得频繁项集。

3-1 抽取条件模式基

首先从保存在头指针中的单个频繁元素项开始。对于每个元素项,获得其对应的条件模式基(conditional pattern base)。条件模式基是以所查找元素项为结尾的路径集合。每一条路径其实都是一条前缀路径(prefix path)。简而言之,一条前缀路径是介于所查找元素项与树根节点之间的所有内容。

在图2中,符号r的前缀路径是{x, s}、{z, x, y}和{z}。每条前缀路径都与一个计数值关联。该计数值等于起始元素项的计数值,该计数值给了每条路径上r的数目。表3列出了上例当中每一个频繁项的所有前缀路径。

表3 每个频繁项的前缀路径

频繁项 前缀路径
z {}5
r {x, s}1, {z, x, y}1, {z}1
x {z}3, {}1
y {z, x}3
s {z, x, y}2, {x}1
t {z, x, y, s}2, {z, x, y, r}1


前缀路径被用于构建条件FP树。为了获得这些前缀路径,可以对树进行穷举式搜索,直到获得想要的频繁项为止,或使用一个更有效的方法来加速搜索过程。可以利用先前创建的头指针来得到一种更有效的方法。头指针表包含相同类型元素链表的起始指针。一旦到达了每一个元素项,就可以上溯这棵树直到根节点为止。下面代码给出了如何发现前缀路径。

def ascendTree(leafNode, prefixPath) :
    # 迭代上溯整棵树
    if leafNode.parent != None :
        prefixPath.append(leafNode)
        ascendTree(leafNode.parent, prefixPath)

# 遍历链表直到到达结尾。每遇到一个元素项都会调用ascendTree()来上溯FP树,并收集所有遇到的元素项的名称。
# 该列表返回之后添加到条件模式基字典condPats中
def findPrefixPath(basePat, treeNode) :
    condPats = {}
    while treeNode != None :
        prefixPath = []
        ascendTree(treeNode, prefixPath)
        if len(prefixPath) > 1 :
            condPats[frozenset(prefixPath[1:])] = treeNode.count
        treeNode = treeNode.nodeLink
    return condPats
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

运行效果:

>>> reload(fpGrowth)
<module 'ml.fpGrowth' from 'C:\Python27\ml\fpGrowth.pyc'>
>>> fpGrowth.findPrefixPath('x', myHeaderTab['x'][1])
{frozenset(['z']): 3}
>>> fpGrowth.findPrefixPath('z', myHeaderTab['z'][1])
{}
>>> fpGrowth.findPrefixPath('r', myHeaderTab['r'][1])
{frozenset(['x', 's']): 1, frozenset(['z']): 1, frozenset(['y', 'x', 'z']): 1}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

3-2 创建条件FP树

对于每个频繁项,都创建一棵条件FP树。我们会为z、x以及其他频繁项构建条件树。可以使用刚才发现的条件模式基作为输入数据,并通过相同的建树代码来构建这些树。然后,我们会递归地发现频繁项、发现条件模式基,以及发现另外的条件树。举个例子,假定为频繁项 t 创建一个条件FP树,然后对{t, y}、{t, x}、……重复该过程。元素项t的条件FP树的构建过程如图4所示。


机器学习实战-FP-growth算法
图4 t的条件FP树的创建过程。最初树以空集作为根节点,接着,原始的集合{y, x, s, z}中的集合{y, x, z}被添加进来。因为不满足最小支持度要求,字符s并没有加入进来。类似地,{y, x, z}也从原始集合{y, x, r, z}中添加进来。

在图4中,元素项s、r是条件模式基的一部分,但它们并不属于条件FP树。单独来看它们都是频繁项,但是在t的条件树中,它们却不是频繁的,也就是说{t, r}、{t, s}是不频繁的。

接下来,对集合{t, z}、{t, x}、{t, y}来挖掘对应的条件树。这会产生更复杂的频繁项集。该过程重复进行,直到条件树中没有元素为止,然后就可以停止了。实现代码很直观,使用一些递归加上之前写的代码即可。具体如下:

def mineTree(inTree, headerTable, minSup, preFix, freqItemList) :
    # 对头指针表中元素项按照其出现频率进行排序,默认是从小到大
    bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p:p[1])]
    # 默认是从小到大,下面过程是从头指针的底端开始
    for basePat in bigL :
        newFreqSet = preFix.copy()
        newFreqSet.add(basePat)
        # 将每个频繁项添加到频繁项集列表freqItemList中
        freqItemList.append(newFreqSet)
        # 使用findPrefixPath()创建条件基
        condPattBases = findPrefixPath(basePat, headerTable[basePat][1])
        # 将条件基condPattBases作为新数据集传递给createTree()函数
        # 这里为函数createTree()添加足够的灵活性,确保它可以被重用于构建条件树
        myCondTree, myHead = createTree(condPattBases, minSup)
        # 如果树中有元素项的话,递归调用mineTree()函数
        if myHead != None :
            print 'conditional tree for: ', newFreqSet
            myCondTree.disp()
            mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

代码运行效果:

>>> reload(fpGrowth)
<module 'ml.fpGrowth' from 'C:\Python27\ml\fpGrowth.pyc'>
>>> freqItems = []
# 显示所有条件树
>>> fpGrowth.mineTree(myFPtree, myHeaderTab, 3, set([]), freqItems)
conditional tree for:  set(['y'])
  Null Set   1
   x   3
    z   3
conditional tree for:  set(['y', 'z'])
  Null Set   1
   x   3
conditional tree for:  set(['s'])
  Null Set   1
   x   3
conditional tree for:  set(['t'])
  Null Set   1
   y   3
    x   3
     z   3
conditional tree for:  set(['x', 't'])
  Null Set   1
   y   3
conditional tree for:  set(['z', 't'])
  Null Set   1
   y   3
    x   3
conditional tree for:  set(['x', 'z', 't'])
  Null Set   1
   y   3
conditional tree for:  set(['x'])
  Null Set   1
   z   3
# 检查返回的项集是否与条件树匹配
>>> freqItems
[set(['y']), set(['y', 'x']), set(['y', 'z']), set(['y', 'x', 'z']), set(['s']),
 set(['x', 's']), set(['t']), set(['y', 't']), set(['x', 't']), set(['y', 'x', '
t']), set(['z', 't']), set(['y', 'z', 't']), set(['x', 'z', 't']), set(['y', 'x'
, 'z', 't']), set(['r']), set(['x']), set(['x', 'z']), set(['z'])]
  • 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

5 示例:从新闻网站点击流中挖掘

>>> parsedDat = [line.split() for line in open('c:\python27\\kosarak.dat').readlines()]
>>> initSet = fpGrowth.createInitSet(parsedDat)
>>> myFPtree, myHeaderTab = fpGrowth.createTree(initSet, 100000)
>>> myFreqList = []
>>> fpGrowth.mineTree(myFPtree, myHeaderTab, 100000, set([]), myFreqList)
conditional tree for:  set(['1'])
  Null Set   1
   6   107404
conditional tree for:  set(['3'])
  Null Set   1
   11   9718
   6   186289
    11   117401
conditional tree for:  set(['11', '3'])
  Null Set   1
   6   117401
conditional tree for:  set(['11'])
  Null Set   1
   6   261773
>>> len(myFreqList)
9
>>> myFreqList
[set(['1']), set(['1', '6']), set(['3']), set(['11', '3']), set(['11', '3', '6']
), set(['3', '6']), set(['11']), set(['11', '6']), set(['6'])]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

6 小结

FP-growth算法是一种用于发现数据集中频繁模式的有效方法。FP-growth算法利用Apriori原则,执行更快。Apriori算法产生候选项集,然后扫描数据集来检查它们是否频繁。由于只对数据集扫描两次,因此FP-growth算法执行更快。在FP-growth算法中,数据集存储在FP树中。FP树构建完成后,可以通过查找元素项的条件基,及构建条件FP树来发现频繁项集。该过程不断以更多元素作为条件重复进行,直到FP树只包含一个元素为止。

可以使用FP-growth算法在多种文本文档中查找频繁单词。