FP-growth算法基于Apriori构建,但采用了高级的数据结构减少扫描次数,大大加快了算法速度。FP-growth算法只需要对数据库进行两次扫描,而Apriori算法对于每个潜在的频繁项集都会扫描数据集判定给定模式是否频繁,因此FP-growth算法的速度要比Apriori算法快。
FP-growth算法发现频繁项集的基本过程如下:
1、构建FP树
2、从FP树中挖掘频繁项集
FP-growth算法:
优点:一般要快于Apriori。
缺点:实现比较困难,在某些数据集上性能会下降。
适用数据类型:离散型数据。
一、FP树:用于编码数据集的有效方式
FP-growth算法将数据存储在一种称为FP树的紧凑数据结构中。FP代表频繁模式(Frequent Pattern)。一棵FP树看上去与计算机科学中的其他树结构类似,但是它通过链接(link)来连接相似元素,被连起来的元素项可以看成一个链表。图5给出了FP树的一个例子。
与搜索树不同的是,一个元素项可以在一棵FP树种出现多次。FP树辉存储项集的出现频率,而每个项集会以路径的方式存储在数中。存在相似元素的集合会共享树的一部分。只有当集合之间完全不同时,树才会分叉。 树节点上给出集合中的单个元素及其在序列中的出现次数,路径会给出该序列的出现次数。
相似项之间的链接称为节点链接(node link),用于快速发现相似项的位置。
图5中,元素项z出现了5次,集合{r, z}出现了1次。于是可以得出结论:z一定是自己本身或者和其他符号一起出现了4次。集合{t, s, y, x, z}出现了2次,集合{t, r, y, x, z}出现了1次,z本身单独出现1次。就像这样,FP树的解读方式是读取某个节点开始到根节点的路径。路径上的元素构成一个频繁项集,开始节点的值表示这个项集的支持度。根据图5,我们可以快速读出项集{z}的支持度为5、项集{t, s, y, x, z}的支持度为2、项集{r, y, x, z}的支持度为1、项集{r, s, x}的支持度为1。FP树中会多次出现相同的元素项,也是因为同一个元素项会存在于多条路径,构成多个频繁项集。但是频繁项集的共享路径是会合并的,如图中的{t, s, y, x, z}和{t, r, y, x, z}
和之前一样,我们取一个最小阈值,出现次数低于最小阈值的元素项将被直接忽略。图5中将最小支持度设为3,所以q和p没有在FP中出现。
FP-growth算法的工作流程如下。首先构建FP树,然后利用它来挖掘频繁项集。为构建FP树,需要对原始数据集扫描两遍。第一遍对所有元素项的出现次数进行计数。数据库的第一遍扫描用来统计出现的频率,而第二遍扫描中只考虑那些频繁元素。
二、构建FP树
1.创建FP树的数据结构
由于树节点的结构比较复杂,我们使用一个类表示。创建文件fpGrowth.py并加入下列代码:
#FP树的类定义,将树的节点定义为一个类
class treeNode:
def __init__(self,nameValue,numOccur,parentNode):
self.name = nameValue
self.count = numOccur
#nodeLink变量用于链接相似项
self.nodeLink = None
self.parent = parentNode
#子树的存储结构为字典
self.children = {}
#节点对应元素计数函数
def inc(self,numOccur):
self.count += numOccur
#对当前树结构以文本形式进行输出函数
def disp(self,ind=1):
#输出当前节点的名字即计数,print()函数自动换行
print(' '*ind,self.name,' ',self.count)
#子树递归输出
for child in self.children.values():
child.disp(ind+2)
每个树节点由五个数据项组成:
name:节点元素名称,在构造时初始化为给定值
count:出现次数,在构造时初始化为给定值
nodeLink:指向下一个相似节点的指针,默认为None
parent:指向父节点的指针,在构造时初始化为给定值
children:指向子节点的字典,以子节点的元素名称为键,指向子节点的指针为值,初始化为空字典
成员函数:
inc():增加节点的出现次数值
disp():输出节点和子节点的FP树结构
2.构建FP树
头指针表
FP-growth算法还需要一个称为头指针表的数据结构,其实很简单,就是用来记录各个元素项的总出现次数的数组,再附带一个指针指向FP树中该元素项的第一个节点。这样每个元素项都构成一条单链表。图示说明:
这里使用Python字典作为数据结构,来保存头指针表。以元素项名称为键,保存出现的总次数和一个指向第一个相似元素项的指针。
第一次遍历数据集会获得每个元素项的出现频率,去掉不满足最小支持度的元素项,生成这个头指针表。
元素项排序
上文提到过,FP树会合并相同的频繁项集(或相同的部分)。因此为判断两个项集的相似程度需要对项集中的元素进行排序(不过原因也不仅如此,还有其它好处)。排序基于元素项的绝对出现频率(总的出现次数)来进行。在第二次遍历数据集时,会读入每个项集(读取),去掉不满足最小支持度的元素项(过滤),然后对元素进行排序(重排序)。
构建FP树
在对事务记录过滤和排序之后,就可以构建FP树了。从空集开始,将过滤和重排序后的频繁项集一次添加到树中。如果树中已存在现有元素,则增加现有元素的值;如果现有元素不存在,则向树添加一个分支。对前两条事务进行添加的过程:
算法:构建FP树
输入:数据集、最小值尺度
输出:FP树、头指针表
1. 遍历数据集,统计各元素项出现次数,创建头指针表
2. 移除头指针表中不满足最小值尺度的元素项
3. 第二次遍历数据集,创建FP树。对每个数据集中的项集:
3.1 初始化空FP树
3.2 对每个项集进行过滤和重排序
3.3 使用这个项集更新FP树,从FP树的根节点开始:
3.3.1 如果当前项集的第一个元素项存在于FP树当前节点的子节点中,则更新这个子节点的计数值
3.3.2 否则,创建新的子节点,更新头指针表
3.3.2 对当前项集的其余元素项和当前元素项的对应子节点递归3.3的过程
#构建FP树还需要一个头指针表,该表中存储单元素及其出现的总次数,存储结构为字典
#单元素名称为键,其值是一个列表[该元素出现的次数,指向第一实例的指针]
#第一次遍历数据集,将元素和出现次数记录在头指针表中,然后去除不满足最小支持度的元素,以频繁的单元素头指针表为依据构建FP树
#构建时,先初始化根空节点,然后遍历事务集,将每个项集添加至树路径中,当前元素如果在树中存在,则更新出现次数,如果不存在,则创建节点,加入树中
#再将事物路径加入树中之前,先将每个元素按其出现频率降序排序,固定其元素位置
#FP树的构建
#更新头指针表中的链表
def updateHeader(nodeToTest,targetNode):
#遍历到链表尾节点
while(nodeToTest.nodeLink != None):
nodeToTest = nodeToTest.nodeLink
#将刚添加的树节点加入链表的尾部
nodeToTest.nodeLink = targetNode
#更新树节点,items表示一个事务集
def updateTree(items,inTree,headerTable,count):
#如果事物集中的第一个元素在树中存在,则更新树节点的count值
if(items[0] in inTree.children):
inTree.children[items[0]].inc(count)
#如果不存在,则创建节点,将节点加到树上,更新头指针表
else:
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(headerTable[items[0]][1],inTree.children[items[0]])
#如果事务集中元素个数大于1,则递归上述过程,使得树结构不断更新迭代
if (len(items)>1):
updateTree(items[1::],inTree.children[items[0]],headerTable,count)
#上述是树生长和跟新头指针表的函数,接下真正的构建树
def create(dataSet,minsup=1):
#输入参数为事务集(数据集,此处的数据集是字典形式{事务1:出现次数,事物2:出现次数,....})
#定义头指针表,字典形式({元素1:[出现次数,指针域],元素2:[出现次数,指针域],...})
headerTable = {}
#遍历事物集的每一个元素,开始构建FP树
for trans in dataSet:
for item in trans:
# 将headerTable中该元素的出现次数更新
#该元素出现次数=头指针表中的count+每个事物在事务集中出现的次数
headerTable[item] = headerTable.get(item,0) + dataSet[trans]
#遍历头指针表,去除不满足最小支持度的元素
#在python3.x中字典在遍历时不能修改
for k in list(headerTable.keys()):
if(headerTable[k]del(headerTable[k])
#此处的headerTable集合化之后便成为单元素的频繁项集
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]
#如果事务元素个数大于1,则排序,根据元素出现次数排序,排序后的元素仍然是字典,只需要包含key值的元素
if(len(localD)>0):
orderItems = [v[0] for v in sorted(localD.items(),key=lambda p:p[1],reverse=True)]
#更新树结构
updateTree(orderItems,retTree,headerTable,count)
return retTree,headerTable
注:需要注意的是,参数中的dataSet的格式比较奇特,不是直觉上得集合的list,而是一个集合的字典,以这个集合为键,值部分记录的是这个集合出现的次数。于是要生成这个dataSet还需要后面的createInitSet()函数辅助。因此代码中第7行中的dataSet[trans]实际获得了这个trans集合的出现次数(在本例中均为1),同样第21行的“for tranSet, count in dataSet.items():”获得了tranSet和count分别表示一个项集和该项集的出现次数。——这样做是为了适应后面在挖掘频繁项集时生成的条件FP树。
生成数据集:
#数据集是字典形式的,key值是事务集合,value值是该事务集出现的次数
def loadSimpleDat():
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):
retDit = {}
#计数事务在数据集中出现的次数
for trans in dataSet:
retDit[frozenset(trans)] = 1
return retDit
三、从一颗FP树中挖掘频繁项集
到现在为止大部分比较困难的工作已经处理完了。有了FP树之后,就可以抽取频繁项集了。这里的思路与Apriori算法大致类似,首先从单元素项集合开始,然后在此基础上逐步构建更大的集合。
从FP树中抽取频繁项集的三个基本步骤如下:
从FP树中获得条件模式基;
利用条件模式基,构建一个条件FP树;
迭代重复步骤1步骤2,直到树包含一个元素项为止。
1 抽取条件模式基
(这个翻译是什么鬼……英文是conditional pattern base)
首先从头指针表中的每个频繁元素项开始,对每个元素项,获得其对应的条件模式基(conditional pattern base)。条件模式基是以所查找元素项为结尾的路径集合。每一条路径其实都是一条前缀路径(prefix path)。简而言之,一条前缀路径是介于所查找元素项与树根节点之间的所有内容。
则每一个频繁元素项的所有前缀路径(条件模式基)为:
z存在于路径{z}中,因此前缀路径为空,另添加一项该路径中z节点的计数值5构成其条件模式基;r存在于路径{r, z}、{r, y, x, z}、{r, s, x}中,分别获得前缀路径{z}、{y, x, z}、{s, x},另添加对应路径中r节点的计数值(均为1)构成r的条件模式基;以此类推。
前缀路径将在下一步中用于构建条件FP树,暂时先不考虑。如何发现某个频繁元素项的所在的路径?利用先前创建的头指针表和FP树中的相似元素节点指针,我们已经有了每个元素对应的单链表,因而可以直接获取。
获取前缀路径函数:
#获得频繁项集需要构建条件模式基(前缀路径,此路径可有当前节点追溯至根节点,路径计数为当前节点在前缀路径中出现的次数)
#根据当前节点向前追溯至根节点,记录前缀路径
def ascendTree(treeNode,prefixPath):
#如果节点有父节点,则将当前节点添加至前缀路径中,之后再递归向上追溯
if(treeNode.parent!=None):
prefixPath.append(treeNode.name)
ascendTree(treeNode.parent,prefixPath)
#发现以给定元素项结尾的所有路径函数
def findPrefixPath(basePat,treeNode):
#定义记录所有条件模式基的字典
conPats = {}
#遍历该节点的整个链表节点,记录每个节点的前缀路径,并将其添加至条件模式基当中
while(treeNode!=None):
#定义每个节点对应的前缀路径
prefixPath = []
ascendTree(treeNode,prefixPath)
#如果有前缀路径,则将前缀路径加入条件模式基集合中,并且将该元素在该前缀路径中出现的次数也添加进去
if(len(prefixPath)>1):
conPats[frozenset(prefixPath[1:])] = treeNode.count
#当前节点的条件模式基查找完毕后,继续查找头指针链表中下一个节点的条件模式基
treeNode = treeNode.nodeLink
return conPats
2 创建条件FP树
对于每一个频繁项,都要创建一棵条件FP树。可以使用刚才发现的条件模式基作为输入数据,并通过相同的建树代码来构建这些树。例如,对于r,即以“{x, s}: 1, {z, x, y}: 1, {z}: 1”为输入,调用函数createTree()获得r的条件FP树;对于t,输入是对应的条件模式基“{z, x, y, s}: 2, {z, x, y, r}: 1”。
示例:t的条件FP树
在图8中,注意到元素项s以及r是条件模式基的一部分,但是它们并不属于条件FP树。因为在当前的输入中,s和r不满足最小支持度的条件。
3 递归查找频繁项集
有了FP树和条件FP树,我们就可以在前两步的基础上递归得查找频繁项集。
递归的过程是这样的:
输入:我们有当前数据集的FP树(inTree,headerTable)
1. 初始化一个空列表preFix表示前缀
2. 初始化一个空列表freqItemList接收生成的频繁项集(作为输出)
3. 对headerTable中的每个元素basePat(按计数值由小到大),递归:
3.1 记basePat + preFix为当前频繁项集newFreqSet
3.2 将newFreqSet添加到freqItemList中
3.3 计算t的条件FP树(myCondTree、myHead)
3.4 当条件FP树不为空时,继续下一步;否则退出递归
3.4 以myCondTree、myHead为新的输入,以newFreqSet为新的preFix,外加freqItemList,递归这个过程
#接下来真正的创建条件模式树
def mineTree(inTree,headerTable,minSup,preFix,freqItemList):
#对头节点指针排序
bigL = [v[0] for v in sorted(headerTable.items(),key=lambda p:p[0])]
#遍历单元素频繁集
for basePat in bigL:
#从条件模式基来构建条件模式树
newFreqSet = preFix.copy()
newFreqSet.add(basePat)
freqItemList.append(newFreqSet)
#获得该元素的所有条件模式基,相当于一个事务集合
condPattBases = findPrefixPath(basePat,headerTable[basePat][1])
#根据所有条件模式基集合来构建条件模式树
myconTree,myHead = create(condPattBases,minSup)
#如果条件模式树的头指针表不空(每次建树时对元素支持度有要求
# 如果小于支持度则该元素不参与建树过程,所以在建树时,条件模式基中的元素会越来越少,最后会是空树),则递归建树
if(myHead!=None):
print("conditional tree for :",newFreqSet)
myconTree.disp(2)
mineTree(myconTree,myHead,minSup,newFreqSet,freqItemList)
输入参数:
inTree和headerTable是由createTree()函数生成的数据集的FP树
minSup表示最小支持度
preFix请传入一个空集合(set([])),将在函数中用于保存当前前缀
freqItemList请传入一个空列表([]),将用来储存生成的频繁项集
举例说明,我们在这里分解输入myFPtree和myHeaderTab后,“for basePat in bigL:”一行当basePat为’t’时的过程
图中红色加粗的部分即实际添加到freqItemList中的频繁项集。
过程为:根据初始输入获得单元素频繁项集集合(headerTable用来存储每次建树时的频繁项集集合),从FP条件模式树中获得条件模式基,根据条件模式基构建条件模式树,再根据生成的条件模式树获得频繁项集,然后获得条件模式基…这是一个迭代过程,到最后,频繁项集会迭代为空则停止迭代。
4 封装
至此,完整的FP-growth算法已经可以运行。将其封装。
示例:从新闻网站点击流中挖掘新闻报道
在源数据集合保存在文件kosarak.dat中。该文件中的每一行包含某个用户浏览过的新闻报道。新闻报道被编码成整数,我们可以使用Apriori或FP-growth算法挖掘其中的频繁项集,查看那些新闻ID被用户大量观看到。
注:以上函数测试均在main函数中进行,main函数结构如下:
if __name__ == "__main__":
#测试节点类
# rootNode = treeNode('root',9,None)
# rootNode.children['leftNode'] = treeNode('leftNode',2,rootNode)
# rootNode.children['rightNode'] = treeNode('rightNode',3,rootNode)
# rootNode.disp()
# dataSet = loadSimpleDat()
# dataDict = createInitSet(dataSet)
# print("dataDict:",dataDict)
# retTree, headerTable = create(dataDict,3)
# # retTree.disp()
# # conPats_x = findPrefixPath('x',headerTable['x'][1])
# # print("conPats_x:",conPats_x)
# freqItems = []
# mineTree(retTree,headerTable,3,set([]),freqItems)
#示例:从新闻网站点击流中挖掘
parseDat = [line.split() for line in open('kosarak.dat').readlines()]
initSet = createInitSet(parseDat)
myFPtree,myHeaderTab = create(initSet,100000)
myFreqList = []
mineTree(myFPtree,myHeaderTab,100000,set([]),myFreqList)
print("len(myFreqList):",len(myFreqList))
print("myFreqList:",myFreqList)
总结:
FP-growth算法是一种用于发现数据集中频繁模式的有效方法。FP-growth算法利用Apriori原则,执行更快。Apriori算法产生候选项集,然后扫描数据集来检查它们是否频繁。由于只对数据集扫描两次,因此FP-growth算法执行更快。在FP-growth算法中,数据集存储在一个称为FP树的结构中。FP树构建完成后,可以通过查找元素项的条件基及构建条件FP树来发现频繁项集。该过程不断以更多元素作为条件重复进行,直到FP树只包含一个元素为止。
FP-growth算法过程可以参考下面这篇文章:
https://blog.csdn.net/javastart/article/details/50521453