我们在搜索引擎上查询东西时,搜索引擎就会自动补全查询词项。为了给出这些推荐的词时,搜索引擎公司的研究人员就需要本章的算法来查找互联网上经常出现在一起的词队。这需要一种高效的发现频繁项集的方法。
该算法为FP-growth,比之前的apriori更快。它基于apriori构建,但在完成相同任务时采取了一些不同的技术。这里的任务时将数据集存储在一个特定称为FP树的结构之后发现频繁性及或则频繁项对,即常在一块出现的元素项的集合FP树。这种做法使得算法的执行速度要快于apriori,性能通常要好两个数量级以上。
本章继续关注发现频繁项集这一任务,并应用FP-growth算法进行处理,该算法能够更有效的挖掘数据。这种算法虽然能够更高效的挖掘数据,但是不能用于发现关联规则。
FP-growth算法只需要对数据库进行两次扫描,而apriori算法对于每个潜在的频繁项集都会扫描数据集给定模式是否频繁,因此FP-growth算法的速度要比apriori算法更快。它返现频繁项集的基本过程如下:
下面开始讨论FP树的数据结构。
1.1 FP树: 用于编码数据集的有效方式
FP-growth算法将数据存储在一种称为FP树的紧凑结构中。FP代表频繁模式(Frequent Patten)。一棵FP树看上与计算机科学的其他结构类似,但是它通过连接(link)来连接相似元素,被连起来的元素项可以看做一个链表。
同搜索树不同的是,一个元素项可以在一颗FP树中出现多次,FP树会存储项集的出现频率,而每个项集会以路径的方式存储在树中。存在相似元素的集合会共享树的一部分。只有当集合之间完全不同时,树才会分叉。树节点上给出集合中的单个元素以及其在序列中出现次数,路径会给出该序列的出现次数。
相似项之间的链接即 节点链接 (node link), 用于快速发现相似项的位置。表12-1中给出了用于生成上图FP树的数据。
在图12-1中,元素项z出现了5次,集合{r,z}出现了1次。于是可以得出结论:z一定是自己本身或则和其他符号一起出现了4次。我们再看z的其他可能性。集合{t,s,y,x,z}出现了2次,集合{t,r,y,x,z}出现了1次。刚才我们说z总计出现了5次,现在我们给出了四种可能性,所以他一定单独出现过1次。通过观察上表发现刚才的结论是正确的。前面我们说集合{t,r,y,x,z}只出现一次,在事物数据集中我们看到005记录上是{y,r,z,x,q,t,p}。那么q和p去哪了呢?
这里使用支持度定义,该指标对应一个最小阈值,低于最小阈值的元素项被认为是不频繁的。如果将支持度定义为3,然后应用频繁项分析算法,就会获得出现3次或3次以上的项集。上面在生成图12-1时,使用的最小支持度为3,因此p和q没有出现在最后的树中。
FP-growth算法的工作流程如下,首先构建FP树,然后挖掘频繁项集。为构建FP树,需要对原始数据集扫描两边。第一遍是对所有元素项的出现次数进行计数。之前说到的apriori原理,即如果某元素是不频繁的,那么该元素的超集也是不频繁的,所以就不需要考虑这些超集。数据库的第一遍用来扫描统计出现的频率,第二遍扫描中只考虑那些频繁元素。
1.2 构建FP树
第二次扫描时会构建一颗FP树,为了构建树,需要一个容器来保存树。
1.2.1 创建FP树的数据结构
这个FP树比较复杂,需要一个类来保存树的每一个节点。
# 用于存放节点名字的变量和1个计数值
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.2 创建FP树
除了图12-1给出的FP树之外,还需要一个头指针来指向给定类型的第一个实例。利用头指针表,可以快速访问FP树中一个给定类型的所有元素。
这里使用字典来保存,除了存放指针外,头指针表还可以用来保存FP树中每类元素的总数。
第一次遍历数据集会获得每个元素的出现频率。接下来,去掉不满足最小支持度的元素项。再下一步构建FP树。在构建中个,读入每个项集并将其添加到一条已经存在的路径中。如果该路径不存在,则创建一条新的路径。每个事务是一个无序集合。假设有集合{z, x, y}和{y, z, r},那么在FP树中,相同项会只表示一次。为了解决此问题,在将集合添加到树之前,需要对每个集合进行排序。排序基于元素项的绝对出现频率来进行。使用头指针节点值,对表12-1中数据进行过滤,重排序后的数据显示在表12-2中。
对事务记录过滤和排序后,就可以构建FP树了。从空集开始,向其中不断添加频繁项集。过滤排序后的事务依次添加到树中,如果树中已存在现有元素,则增加现有元素的值;如果现有元素不存在,则向树添加一个分枝。
构建FP树的前两步。
通过上面的叙述,大致了解了构建FP树的基本思想,现在通过代码实现:
# 第一次遍历统计每个元素项出现的频度,存储在头指针表中,删除小于最小支持度的项
# 对头指针表稍加扩展以便保存计数值及指向每种类型第一个元素项的指针
# 第二次只遍历频繁项集
def createTree(dataSet, minSup=1):
'''
decs: create fp-tree with dataset and min-Support
'''
headerTable = {}
# go over dataSet twice in the program
for trans in dataSet:# first pass counts frequency of every occurance
print('trans:',trans)
for item in trans:
print(item)
print('--- into for ----')
print('headerTable.get:',headerTable.get(item, 0))
print('dataSet[trans]:',dataSet[trans])
# store the info in the dict
headerTable[item] = headerTable.get(item, 0) + dataSet[trans]
print('headerTable:',headerTable)
# python3 在遍历字典时不能删除或修改里面的东西,故转化为list进行处理
for k in list(headerTable.keys()): # remove items not meeting minSup
if headerTable[k] < minSup:
del(headerTable[k])
freqItemSet = set(headerTable.keys())
print ('freqItemSet: ',freqItemSet)
if len(freqItemSet) == 0: return None, None # if no items meet min support -->get out
for k in headerTable:
headerTable[k] = [headerTable[k], None] # reformat headerTable to use Node link
print ('headerTable: ',headerTable)
# Root Node
retTree = treeNode('Null Set', 1, None) #create tree include None
for tranSet, count in dataSet.items(): #go through dataset 2nd time
localD = {}
for item in tranSet: #put transaction items in order
if item in freqItemSet: # go over the freq
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(orderedItems, retTree, headerTable, count)#populate tree with ordered freq itemset
return retTree, headerTable #return tree and header table
'''
测试事务中一个元素项是否作为子节点存在,若存在则更新元素项计数,若不存在,则创建一个新的treeNode并将其作为一个子节点加入树中
'''
def updateTree(items, inTree, headerTable, count):
if items[0] in inTree.children:# check if orderedItems[0] in retTree.children
inTree.children[items[0]].inc(count) # incrament count
else: # add items[0] to inTree.children
inTree.children[items[0]] = treeNode(items[0], count, inTree)
if headerTable[items[0]][1] == None: # update header table
headerTable[items[0]][1] = inTree.children[items[0]]
else:
updateHeader(headerTable[items[0]][1], inTree.children[items[0]])
if len(items) > 1:# call updateTree() with remaining ordered items
updateTree(items[1::], inTree.children[items[0]], headerTable, count)
# 节点链接指向树中该元素项的每一个实例
def updateHeader(nodeToTest, targetNode): #this version does not use recursion
while (nodeToTest.nodeLink != None): #Do not use recursion to traverse a linked list!
nodeToTest = nodeToTest.nodeLink
nodeToTest.nodeLink = targetNode
建立一个数据集。createTree()函数的输入数据类是字典,键为项集,值为频率。
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:
# set无序排序且不重复,是可变的,
# frozenset是冻结的集合,它是不可变的,存在哈希值,好处是它可以作为字典的key
retDict[frozenset(trans)] = 1
return retDict
数据集为:
对上面的数据进行格式化处理:
创建FP树:
现在我们已经构建了FP树,接下来就使用他进行频繁项挖掘。
1.3 从一颗FP树中挖掘频繁项集
有了fp树就可以抽取频繁项集了,首先从单项元素开始,然后在此基础上逐步构建更大的集合。这里使用FP树来实现上述的过程,不再需要原始数据集了。
1.3.1 抽取条件模式基
从发现的已经保存在头指针表中的单个频繁元素开始。对于每一个元素项,获得其对应的 条件模式基(conditional pattern base)。条件模式基是以所查找元素项为结尾的路径集合。每一条路径其实都是一条 前缀路径(prefix path)。简而言之,一条前缀路径是介于查找元素项与树根节点之间的所有内容。
回到图12-2,r的前缀路径是{x,s} {z.x.y} {z}。每一条前缀路径都与一个计数值关联。该计数值等于起始元素项的计数值,该计数值给了每条路径上r的数目。表12-3列出了每个频繁项的所有前缀路径。
发现以给定元素项结尾的所有路径函数:利用先前创建的头指针表来得到一种更有效的方法,头指针表包含相同类型元素链表的起始指针。一旦到达了每一个元素项,就可以上溯到这棵树直到根节点为止。
def ascendTree(leafNode, prefixPath): #ascends from leaf node to root
if leafNode.parent != None:
prefixPath.append(leafNode.name)
ascendTree(leafNode.parent, prefixPath)
'''
遍历链表直到结尾,每遇到一个元素都会调用ascendTree来上溯FP树,并收集遇到的所有元素项的名称
'''
def findPrefixPath(basePat, treeNode): #treeNode comes from header table
condPats = {}
while treeNode != None:
prefixPath = []
ascendTree(treeNode, prefixPath)
if len(prefixPath) > 1:
condPats[frozenset(prefixPath[1:])] = treeNode.count
treeNode = treeNode.nodeLink
return condPats
1.3.2 创建条件FP树
对于每个频繁项都要创建一个条件FP树。可以使用刚才的条件模式基作为输入数据,并通过相同的建树代码来构建这些树。递归发现频繁项,发现条件模式基,以及发现另外的条件树。
假定为频繁项t创建一个条件FP树,然后对{t,y} {t,x} .......重复该过程。创建过程如图所示:
上图中s r是 t 的条件基的一部分,但是它们并不属于条件FP树,因为单独讨论s r的话,它们频繁项,但是是在t的条件树中,它们却不是频繁的,也就是说,{t,r} {t,s} 是不频繁的。
接下里,对集合{t,z} {t,x} {t,y}来挖掘相对应的条件树,这会产生更复杂的频繁项集。该过程重读进行,直到条件树中没有元素为止。
def mineTree(inTree, headerTable, minSup, preFix, freqItemList):
# (sort header table)
bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p: p[1][0])]
for basePat in bigL: # start from bottom of header table
newFreqSet = preFix.copy()
newFreqSet.add(basePat)
#print 'finalFrequent Item: ',newFreqSet #append to set
freqItemList.append(newFreqSet)
condPattBases = findPrefixPath(basePat, headerTable[basePat][1])
#print 'condPattBases :',basePat, condPattBases
#2. construct cond FP-tree from cond. pattern base
myCondTree, myHead = createTree(condPattBases, minSup)
#print 'head from conditional tree: ', myHead
if myHead != None: #3. mine cond. FP-tree
print('conditional tree for: ',newFreqSet)
myCondTree.disp(1)
mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)
运行显示所有的条件树:
返回的项集:
2. 小结
FP-growth是一种用于发现数据集中频繁模式的有效方法。FP-growth算法利用apriori原则执行更快。apriori算法产生候选项集,然后扫描数据检查它们是否频繁。由于只对数据扫描两次,因此FP更快。在FP-growth算法中,数据集存储在一个名为fp树的结构中。fp树构建完成后,可以通过查看元素项的条件基及构建条件fp树来发现频繁项集。该过程不断以更多元素作为条件重复进行,直到FP树包含一个元素为止。