本篇的数据和代码参见:https://github.com/stonycat/ML-in-Action
本章会在上一章讨论话题的基础上进行扩展,将给出一个非常好的频繁项集发现算法。该算法称作FP-growth,它比上一章讨论的Apriori算法要快。它基于Apriori构建,但在完成相同任务时采用了一些不同的技术。这里的任务是将数据集存储在一个特定的称作FP树的结构之后发现频繁项集或者频繁项对,即常在一块出现的元素项的集合FP树。
FP-growth算法只需要对数据库进行两次扫描,而Apriori算法对于每个潜在的频繁项集都会扫描数据集判定给定模式是否频繁,因此FP-growth算法的速度要比Apriori算法快。 它发现频繁项集的基本过程如下: (1) 构建FP树 (2) 从FP树中挖掘频繁项集 一、创建FP树
#FP树中节点的类定义 class treeNode: def __init__(self, nameValue, numOccur, parentNode): self.name = nameValue self.count = numOccur self.nodeLink = None #nodeLink 变量用于链接相似的元素项 self.parent = parentNode #指向当前节点的父节点 self.children = {} #空字典,存放节点的子节点 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)测试创建树,添加子节点:
二、构建FP树 FP-growth算法将数据存储在一种称为FP树的紧凑数据结构中。FP代表频繁模式(Frequent Pattern)。一棵FP树看上去与计算机科学中的其他树结构类似,但是它通过链接(link)来连接相似元素,被连起来的元素项可以看成一个链表。
在创建真正的频繁集FP树之前,需要对数据进行过滤(不符合频繁要求)和排序(按照频繁度排序)。利用头指针表,可以快速访问FP树中一个给定类型的所有元素。 同搜索树不同的是,一个元素项可以在一棵FP树中出现多次。FP树会存储项集的出现频率,而每个项集会以路径的方式存储在树中。存在相似元素的集合会共享树的一部分。只有当集合之间完全不同时,树才会分叉。 树节点上给出集合中的单个元素及其在序列中的出现次数,路径 会给出该序列的出现次数。
#构建FP-tree def createTree(dataSet, minSup=1): headerTable = {} for trans in dataSet: #第一次遍历:统计各个数据的频繁度 for item in trans: headerTable[item] = headerTable.get(item, 0) + dataSet[trans] #用头指针表统计各个类别的出现的次数,计算频繁量:头指针表[类别]=出现次数 for k in headerTable.keys(): #删除未达到最小频繁度的数据 if headerTable[k] < minSup: del (headerTable[k]) freqItemSet = set(headerTable.keys())#保存达到要求的数据 # print ('freqItemSet: ',freqItemSet) if len(freqItemSet) == 0: return None, None #若达到要求的数目为0 for k in headerTable: #遍历头指针表 headerTable[k] = [headerTable[k], None] #保存计数值及指向每种类型第一个元素项的指针 # print ('headerTable: ',headerTable) retTree = treeNode('Null Set', 1, None) #初始化tree for tranSet, count in dataSet.items(): # 第二次遍历: localD = {} for item in tranSet: # put transaction items in order 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(orderedItems, retTree, headerTable, count) # populate tree with ordered freq itemset return retTree, headerTable #返回树和头指针表 def updateTree(items, inTree, headerTable, 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]]) 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后面构建树时会 使用 createTree() 函数,而该函数的输入数据类型不是列表。其需要的是一部字典,其中项集为字典中的键,而频率为每个键对应的取值。 createInitSet() 用于实现上述从列表到字典的类型转换过程。
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 #createInitSet() 用于实现上述从列表到字典的类型转换过程 def createInitSet(dataSet): retDict = {} for trans in dataSet: retDict[frozenset(trans)] = 1 return retDict直接参考书中代码运行报错:RuntimeError: dictionary changed size during iteration 解决:修改createTree()函数的for k in headerTable.keys(): 为for k in list(headerTable): 测试: 上面给出的是元素项及其对应的频率计数值,其中每个缩进表示所处的树的深度。可以验证一下这棵树上图中所示的树是否等价。 三、从一棵 FP 树中挖掘频繁项集 从FP树中抽取频繁项集的三个基本步骤如下:
(1) 从FP树中获得条件模式基; (2) 利用条件模式基,构建一个条件FP树; (3) 迭代重复步骤(1)步骤(2),直到树包含一个元素项为止。接下来首先寻找条件模式基的过程。之后为每一个条件模式基创建对应的条件FP树。最后需要构造少许代码来封装上述两个函数,并从FP树中获得频繁项集。
条件模式基是以所查找元素项为结尾的路径集合。 每一条路径其实都是一条前缀路径(prefix path)。简而言之,一条前缀路径是介于所查找元素项与树根节点之间的所有内容。
#从FP树中发现频繁项集 def ascendTree(leafNode, prefixPath): #递归上溯整棵树 if leafNode.parent != None: prefixPath.append(leafNode.name) ascendTree(leafNode.parent, prefixPath) 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对于每一个频繁项都要创建一棵条件FP树,使用条件模式基作为输入数据,用相同的建树代码构建条件树,之后递归地发现频繁项、发现条件模式基,并且继续构造条件树,直到条件树中没有元素。
函数mineTree对参数inTree代表的FP树进行频繁项集挖掘。首先对headerTable中出现的单个元素按出现频率从小到大排序,之后将每个元素的条件模式基作为输入数据,建立针对当前元素的条件树,如果生成的这棵条件树仍有元素,就在这棵条件树里寻找频繁项集,因为prefix参数是在递归过程中不断向下传递的,因此由最初的headerTable中的某个元素x衍生出的所有频繁项集都带有x。
#递归查找频繁项集 def mineTree(inTree, headerTable, minSup, preFix, freqItemList): # 头指针表中的元素项按照频繁度排序,从小到大 bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p: str(p[1]))]#python3修改 for basePat in bigL: #从底层开始 #加入频繁项列表 newFreqSet = preFix.copy() newFreqSet.add(basePat) print ('finalFrequent Item: ',newFreqSet) freqItemList.append(newFreqSet) #递归调用函数来创建基 condPattBases = findPrefixPath(basePat, headerTable[basePat][1]) print ('condPattBases :',basePat, condPattBases) #2. 构建条件模式Tree myCondTree, myHead = createTree(condPattBases, minSup) #将创建的条件基作为新的数据集添加到fp-tree print ('head from conditional tree: ', myHead) if myHead != None: #3. 递归 print ('conditional tree for: ',newFreqSet) myCondTree.disp(1) mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)测试以上代码会报错:TypeError: unorderable types: treeNode() < treeNode()原因是python3:comparing integers and strings is not allowed,因此需要修改函数中的为bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p: str(p[1]))]添加str将类型转化为字符串类型。 修改参考下列链接说明:https://www.peterbe.com/plog/sorting-mixed-type-lists-in-python-3 测试结果:显示所有的条件树 检查一下返回的项集是否与条件树匹配:取消最后一个print注释
四、示例:从新闻网站点击流中挖掘 在上传的源码中,有一个kosarak.dat文件,它包含将近100万条记录 。该文件中的每一行包含某个用户浏览过的新闻报道。一些用户只看过一篇报道,而有些用户看过2498篇报道。 用户和报道被编码成整数,所以查看频繁项集很难得到更多的东西,但是该数据对于展示FP-growth算法的速度十分有效。
首先放python3测试结果:
#将数据集导入 >>> parsedDat=[line.split() for line in open('kosarak.dat').readlines()] #对初始集合格式化 >>> initSet=fpGrowth.createInitSet(parsedDat) #构建FP树,并从中寻找那些至少被10万人浏览过的新闻报道 >>>myFPtree,myHeaderTab=fpGrowth.createTree(initSet,100000) #需要创建一个空列表来保存这些频繁项集 >>> myFreqList=[] >>>fpGrowth.mineTree(myFPtree,myHeaderTab,100000,set([]),myFreqList)执行上述代码,构建树以及扫描100万行只需要几秒钟,这展示了FP-growth算法的强大威力。
#看下有多少新闻报道或报道集合曾经被10万或者更多的人浏览过: >>>len(myFreqList) #查询结果为9人 9 #看看具体为哪9项: >>> myFreqList [{'1'}, {'6', '1'}, {'3'}, {'11', '3'}, {'11', '6', '3'}, {'6', '3'}, {'11'}, {'11', '6'}, {'6'}]总结 Apriori算法产生候选项集,然后扫描数据集来检查它们是否频繁。由于只对数据集扫描两次,因此FP-growth算法执行更快。 在FP-growth算法中,数据集存储在一个称为FP树的结构中。FP树构建完成后,可以通过查找元素项的条件基及构建条件FP树来发现频繁项集。该过程不断以更多元素作为条件重复进行,直到FP树只包含一个元素为止。 可以使用FP-growth算法在多种文本文档中查找频繁单词。