系列文章:《机器学习实战》学习笔记html
最近看了《机器学习实战》中的第11章(使用Apriori算法进行关联分析)和第12章(使用FP-growth算法来高效发现频繁项集)。正如章节标题所示,这两章讲了无监督机器学习方法中的关联分析问题。关联分析能够用于回答"哪些商品常常被同时购买?"之类的问题。书中举了一些关联分析的例子:node
从大规模数据集中寻找物品间的隐含关系被称做关联分析(association analysis)或者关联规则学习(association rule learning)。这里的主要问题在于,寻找物品的不一样组合是一项十分耗时的任务,所需的计算代价很高,蛮力搜索方法并不能解决这个问题,因此须要用更智能的方法在合理的时间范围内找到频繁项集。本文分别介绍如何使用Apriori算法和FP-growth算法来解决上述问题。git
关联分析是在大规模数据集中寻找有趣关系的任务。这些关系能够有两种形式:github
频繁项集(frequent item sets)是常常出如今一起的物品的集合,关联规则(association rules)暗示两种物品之间可能存在很强的关系。web
下面用一个例子来讲明这两种概念:图1给出了某个杂货店的交易清单。算法
交易号码数据库 |
商品数组 |
0数据结构 |
豆奶,莴苣app |
1 |
莴苣,尿布,葡萄酒,甜菜 |
2 |
豆奶,尿布,葡萄酒,橙汁 |
3 |
莴苣,豆奶,尿布,葡萄酒 |
4 |
莴苣,豆奶,尿布,橙汁 |
图1 某杂货店交易清单
频繁项集是指那些常常出如今一块儿的商品集合,图中的集合{葡萄酒,尿布,豆奶}就是频繁项集的一个例子。从这个数据集中也能够找到诸如尿布->葡萄酒的关联规则,即若是有人买了尿布,那么他极可能也会买葡萄酒。
咱们用支持度和可信度来度量这些有趣的关系。一个项集的支持度(support)被定义数据集中包含该项集的记录所占的比例。如上图中,{豆奶}的支持度为4/5,{豆奶,尿布}的支持度为3/5。支持度是针对项集来讲的,所以能够定义一个最小支持度,而只保留知足最小值尺度的项集。
可信度或置信度(confidence)是针对关联规则来定义的。规则{尿布}➞{啤酒}的可信度被定义为"支持度({尿布,啤酒})/支持度({尿布})",因为{尿布,啤酒}的支持度为3/5,尿布的支持度为4/5,因此"尿布➞啤酒"的可信度为3/4。这意味着对于包含"尿布"的全部记录,咱们的规则对其中75%的记录都适用。
假设咱们有一家经营着4种商品(商品0,商品1,商品2和商品3)的杂货店,2图显示了全部商品之间全部的可能组合:
图2 集合{0,1,2,3,4}中全部可能的项集组合
对于单个项集的支持度,咱们能够经过遍历每条记录并检查该记录是否包含该项集来计算。对于包含N中物品的数据集共有\( 2^N-1 \)种项集组合,重复上述计算过程是不现实的。
研究人员发现一种所谓的Apriori原理,能够帮助咱们减小计算量。Apriori原理是说若是某个项集是频繁的,那么它的全部子集也是频繁的。更经常使用的是它的逆否命题,即若是一个项集是非频繁的,那么它的全部超集也是非频繁的。
在图3中,已知阴影项集{2,3}是非频繁的。利用这个知识,咱们就知道项集{0,2,3},{1,2,3}以及{0,1,2,3}也是非频繁的。也就是说,一旦计算出了{2,3}的支持度,知道它是非频繁的后,就能够紧接着排除{0,2,3}、{1,2,3}和{0,1,2,3}。
图3 图中给出了全部可能的项集,其中非频繁项集用灰色表示。
前面提到,关联分析的目标包括两项:发现频繁项集和发现关联规则。首先须要找到频繁项集,而后才能得到关联规则(正如前文所讲,计算关联规则的可信度须要用到频繁项集的支持度)。
Apriori算法是发现频繁项集的一种方法。Apriori算法的两个输入参数分别是最小支持度和数据集。该算法首先会生成全部单个元素的项集列表。接着扫描数据集来查看哪些项集知足最小支持度要求,那些不知足最小支持度的集合会被去掉。而后,对剩下来的集合进行组合以生成包含两个元素的项集。接下来,再从新扫描交易记录,去掉不知足最小支持度的项集。该过程重复进行直到全部项集都被去掉。
数据集扫描的伪代码大体以下:
对数据集中的每条交易记录tran:
对每一个候选项集can:
检查can是不是tran的子集
若是是,则增长can的计数
对每一个候选项集:
若是其支持度不低于最小值,则保留该项集
返回全部频繁项集列表
下面看一下实际代码,创建一个apriori.py文件并加入一下代码:
# coding=utf-8 from numpy import * def loadDataSet(): return [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]
其中numpy为程序中须要用到的Python库。
def createC1(dataSet): C1 = [] for transaction in dataSet: for item in transaction: if not [item] in C1: C1.append([item]) C1.sort() return map(frozenset, C1)
其中C1即为元素个数为1的项集(非频繁项集,由于尚未同最小支持度比较)。map(frozenset, C1)的语义是将C1由Python列表转换为不变集合(frozenset,Python中的数据结构)。
def scanD(D, Ck, minSupport): ssCnt = {} for tid in D: for can in Ck: if can.issubset(tid): ssCnt[can] = ssCnt.get(can, 0) + 1 numItems = float(len(D)) retList = [] supportData = {} for key in ssCnt: support = ssCnt[key] / numItems if support >= minSupport: retList.insert(0, key) supportData[key] = support return retList, supportData
其中D为所有数据集,Ck为大小为k(包含k个元素)的候选项集,minSupport为设定的最小支持度。返回值中retList为在Ck中找出的频繁项集(支持度大于minSupport的),supportData记录各频繁项集的支持度。
retList.insert(0, key)一行将频繁项集插入返回列表的首部。
整个Apriori算法的伪代码以下:
当集合中项的个数大于0时:
构建一个由k个项组成的候选项集的列表(k从1开始)
计算候选项集的支持度,删除非频繁项集
构建由k+1项组成的候选项集的列表
程序代码以下:
def aprioriGen(Lk, k): retList = [] lenLk = len(Lk) for i in range(lenLk): for j in range(i + 1, lenLk): # 前k-2项相同时,将两个集合合并 L1 = list(Lk[i])[:k-2]; L2 = list(Lk[j])[:k-2] L1.sort(); L2.sort() if L1 == L2: retList.append(Lk[i] | Lk[j]) return retList
该函数经过频繁项集列表$ L_k $和项集个数k生成候选项集$ C_{k+1} $。
注意其生成的过程当中,首选对每一个项集按元素排序,而后每次比较两个项集,只有在前k-1项相同时才将这两项合并。这样作是由于函数并不是要两两合并各个集合,那样生成的集合并不是都是k+1项的。在限制项数为k+1的前提下,只有在前k-1项相同、最后一项不相同的状况下合并才为所须要的新候选项集。
因为Python中使用下标0表示第一个元素,所以代码中的[:k-2]的实际做用为取列表的前k-1个元素。
def apriori(dataSet, minSupport=0.5): C1 = createC1(dataSet) D = map(set, dataSet) L1, supportData = scanD(D, C1, minSupport) L = [L1] k = 2 while (len(L[k-2]) > 0): Ck = aprioriGen(L[k-2], k) Lk, supK = scanD(D, Ck, minSupport) supportData.update(supK) L.append(Lk) k += 1 return L, supportData
该函数为Apriori算法的主函数,按照前述伪代码的逻辑执行。Ck表示项数为k的候选项集,最初的C1经过createC1()函数生成。Lk表示项数为k的频繁项集,supK为其支持度,Lk和supK由scanD()函数经过Ck计算而来。
函数返回的L和supportData为全部的频繁项集及其支持度,所以在每次迭代中都要将所求得的Lk和supK添加到L和supportData中。
代码测试(在Python提示符下输入):
>>> import apriori >>> dataSet = apriori.loadDataSet() >>> dataSet >>> C1 = apriori.createC1(dataSet) >>> D = map(set, dataSet) >>> D >>> L1, suppDat = apriori.scanD(D, C1, 0.5) >>> L1 >>> L, suppData = apriori.apriori(dataSet) >>> L >>> L, suppData = apriori.apriori(dataSet, minSupport=0.7) >>> L
L返回的值为frozenset列表的形式:
[[frozenset([1]), frozenset([3]), frozenset([2]), frozenset([5])],
[frozenset([1, 3]), frozenset([2, 5]), frozenset([2, 3]), frozenset([3, 5])],
[frozenset([2, 3, 5])], []]
即L[0]为项数为1的频繁项集:
[frozenset([1]), frozenset([3]), frozenset([2]), frozenset([5])]
L[1]为项数为2的频繁项集:
[frozenset([1, 3]), frozenset([2, 5]), frozenset([2, 3]), frozenset([3, 5])]
依此类推。
suppData为一个字典,它包含项集的支持度。
解决了频繁项集问题,下一步就能够解决相关规则问题。
要找到关联规则,咱们首先从一个频繁项集开始。从杂货店的例子能够获得,若是有一个频繁项集{豆奶, 莴苣},那么就可能有一条关联规则“豆奶➞莴苣”。这意味着若是有人购买了豆奶,那么在统计上他会购买莴苣的几率较大。注意这一条反过来并不老是成立,也就是说,可信度(“豆奶➞莴苣”)并不等于可信度(“莴苣➞豆奶”)。
前文也提到过,一条规则P➞H的可信度定义为support(P | H)/support(P),其中“|”表示P和H的并集。可见可信度的计算是基于项集的支持度的。
图4给出了从项集{0,1,2,3}产生的全部关联规则,其中阴影区域给出的是低可信度的规则。能够发现若是{0,1,2}➞{3}是一条低可信度规则,那么全部其余以3做为后件(箭头右部包含3)的规则均为低可信度的。
图4 频繁项集{0,1,2,3}的关联规则网格示意图
能够观察到,若是某条规则并不知足最小可信度要求,那么该规则的全部子集也不会知足最小可信度要求。以图4为例,假设规则{0,1,2} ➞ {3}并不知足最小可信度要求,那么就知道任何左部为{0,1,2}子集的规则也不会知足最小可信度要求。能够利用关联规则的上述性质属性来减小须要测试的规则数目,相似于Apriori算法求解频繁项集。
1 关联规则生成函数:
def generateRules(L, supportData, minConf=0.7): bigRuleList = [] for i in range(1, len(L)): for freqSet in L[i]: H1 = [frozenset([item]) for item in freqSet] if (i > 1): # 三个及以上元素的集合 rulesFromConseq(freqSet, H1, supportData, bigRuleList, minConf) else: # 两个元素的集合 calcConf(freqSet, H1, supportData, bigRuleList, minConf) return bigRuleList
这个函数是主函数,调用其余两个函数。其余两个函数是rulesFromConseq()和calcConf(),分别用于生成候选规则集合以及对规则进行评估(计算支持度)。
函数generateRules()有3个参数:频繁项集列表L、包含那些频繁项集支持数据的字典supportData、最小可信度阈值minConf。函数最后要生成一个包含可信度的规则列表bigRuleList,后面能够基于可信度对它们进行排序。L和supportData正好为函数apriori()的输出。该函数遍历L中的每个频繁项集,并对每一个频繁项集构建只包含单个元素集合的列表H1。代码中的i指示当前遍历的频繁项集包含的元素个数,freqSet为当前遍历的频繁项集(回忆L的组织结构是先把具备相同元素个数的频繁项集组织成列表,再将各个列表组成一个大列表,因此为遍历L中的频繁项集,须要使用两层for循环)。
2 辅助函数——计算规则的可信度,并过滤出知足最小可信度要求的规则
def calcConf(freqSet, H, supportData, brl, minConf=0.7): ''' 对候选规则集进行评估 ''' prunedH = [] for conseq in H: conf = supportData[freqSet] / supportData[freqSet - conseq] if conf >= minConf: print freqSet - conseq, '-->', conseq, 'conf:', conf brl.append((freqSet - conseq, conseq, conf)) prunedH.append(conseq) return prunedH
计算规则的可信度以及找出知足最小可信度要求的规则。函数返回一个知足最小可信度要求的规则列表,并将这个规则列表添加到主函数的bigRuleList中(经过参数brl)。返回值prunedH保存规则列表的右部,这个值将在下一个函数rulesFromConseq()中用到。
3 辅助函数——根据当前候选规则集H生成下一层候选规则集
def rulesFromConseq(freqSet, H, supportData, brl, minConf=0.7): ''' 生成候选规则集 ''' m = len(H[0]) if (len(freqSet) > (m + 1)): Hmpl = aprioriGen(H, m + 1) Hmpl = calcConf(freqSet, Hmpl, supportData, brl, minConf) if (len(Hmpl) > 1): rulesFromConseq(freqSet, Hmpl, supportData, brl, minConf)
从最初的项集中生成更多的关联规则。该函数有两个参数:频繁项集freqSet,能够出如今规则右部的元素列表H。其他参数:supportData保存项集的支持度,brl保存生成的关联规则,minConf同主函数。函数先计算H中的频繁项集大小m。接下来查看该频繁项集是否大到能够移除大小为m的子集。若是能够的话,则将其移除。使用函数aprioriGen()来生成H中元素的无重复组合,结果保存在Hmp1中,这也是下一次迭代的H列表。
实际运行效果:
>>> import apriori >>> dataSet = apriori.loadDataSet() >>> L, suppData = apriori.apriori(dataSet, minSupport=0.5) >>> rules = apriori.generateRules(L, suppData, minConf=0.7) >>> rules
frozenset([1]) --> frozenset([3]) conf: 1.0
frozenset([5]) --> frozenset([2]) conf: 1.0
frozenset([2]) --> frozenset([5]) conf: 1.0
>>> rules = apriori.generateRules(L, suppData, minConf=0.5) >>> rules
frozenset([3]) --> frozenset([1]) conf: 0.666666666667
frozenset([1]) --> frozenset([3]) conf: 1.0
frozenset([5]) --> frozenset([2]) conf: 1.0
frozenset([2]) --> frozenset([5]) conf: 1.0
frozenset([3]) --> frozenset([2]) conf: 0.666666666667
frozenset([2]) --> frozenset([3]) conf: 0.666666666667
frozenset([5]) --> frozenset([3]) conf: 0.666666666667
frozenset([3]) --> frozenset([5]) conf: 0.666666666667
frozenset([5]) --> frozenset([2, 3]) conf: 0.666666666667
frozenset([3]) --> frozenset([2, 5]) conf: 0.666666666667
frozenset([2]) --> frozenset([3, 5]) conf: 0.666666666667
到目前为止,若是代码同书中同样的话,输出就是这样。在这里首先使用参数最小支持度minSupport = 0.5计算频繁项集L和支持度suppData,而后分别计算最小可信度minConf = 0.7和minConf = 0.5的关联规则。
若是仔细看下上述代码和输出,会发现这里面是一些问题的。
1 问题的提出
频繁项集L的值前面提到过。咱们在其中计算经过{2, 3, 5}生成的关联规则,能够发现关联规则{3, 5}➞{2}和{2, 3}➞{5}的可信度都应该为1.0的,于是也应该包括在当minConf = 0.7时的rules中——可是这在前面的运行结果中并无体现出来。minConf = 0.5时也是同样,{3, 5}➞{2}的可信度为1.0,{2, 5}➞{3}的可信度为2/3,{2, 3}➞{5}的可信度为1.0,也没有体如今rules中。
经过分析程序代码,咱们能够发现:
例如,对于频繁项集{a, b, c, …},H1的值为[a, b, c, …](代码中实际为frozenset类型)。若是将H1带入计算可信度的calcConf()函数,在函数中会依次计算关联规则{b, c, d, …}➞{a}、{a, c, d, …}➞{b}、{a, b, d, …}➞{c}……的支持度,并保存支持度大于最小支持度的关联规则,并保存这些规则的右部(prunedH,即对H的过滤,删除支持度太小的关联规则)。
当i > 1时没有直接调用calcConf()函数计算经过H1生成的规则集。在rulesFromConseq()函数中,首先得到当前H的元素数m = len(H[0])(记当前的H为$ H_m $)。当$ H_m $能够进一步合并为m+1元素数的集合$ H_{m+1} $时(判断条件:len(freqSet) > (m + 1)),依次:
因此这里的问题是,在i>1时,rulesFromConseq()函数中并无调用calcConf()函数计算H1的可信度,而是直接由H1生成H2,从H2开始计算关联规则——因而由元素数>3的频繁项集生成的{a, b, c, …}➞{x}形式的关联规则(图4中的第2层)均缺失了。因为代码示例数据中的对H1的剪枝prunedH没有删除任何元素,结果只是“巧合”地缺失了一层。正常状况下若是没有对H1进行过滤,直接生成H2,将给下一层带入错误的结果(如图4中的012➞3会被错误得留下来)。
2 对问题代码的修改
在i>1时,将对H1调用calcConf()的过程加上就能够了。好比能够这样:
def generateRules2(L, supportData, minConf=0.7): bigRuleList = [] for i in range(1, len(L)): for freqSet in L[i]: H1 = [frozenset([item]) for item in freqSet] if (i > 1): # 三个及以上元素的集合 H1 = calcConf(freqSet, H1, supportData, bigRuleList, minConf) rulesFromConseq(freqSet, H1, supportData, bigRuleList, minConf) else: # 两个元素的集合 calcConf(freqSet, H1, supportData, bigRuleList, minConf) return bigRuleList
这里就只须要修改generateRules()函数。这样实际运行效果中,刚才丢失的那几个关联规则就都出来了。
进一步修改:当i=1时的else部分并无独特的逻辑,这个if语句能够合并,而后再修改rulesFromConseq()函数,保证其会调用calcConf(freqSet, H1, …):
def generateRules3(L, supportData, minConf=0.7): bigRuleList = [] for i in range(1, len(L)): for freqSet in L[i]: H1 = [frozenset([item]) for item in freqSet] rulesFromConseq2(freqSet, H1, supportData, bigRuleList, minConf) return bigRuleList def rulesFromConseq2(freqSet, H, supportData, brl, minConf=0.7): m = len(H[0]) if (len(freqSet) > m): # 判断长度改成 > m,这时便可以求H的可信度 Hmpl = calcConf(freqSet, H, supportData, brl, minConf) if (len(Hmpl) > 1): # 判断求完可信度后是否还有可信度大于阈值的项用来生成下一层H Hmpl = aprioriGen(Hmpl, m + 1) rulesFromConseq2(freqSet, Hmpl, supportData, brl, minConf) # 递归计算,不变
运行结果和generateRules2相同。
进一步修改:消除rulesFromConseq2()函数中的递归项。这个递归纯粹是偷懒的结果,没有简化任何逻辑和增长任何可读性,能够直接用一个循环代替:
def rulesFromConseq3(freqSet, H, supportData, brl, minConf=0.7): m = len(H[0]) while (len(freqSet) > m): # 判断长度 > m,这时便可求H的可信度 H = calcConf(freqSet, H, supportData, brl, minConf) if (len(H) > 1): # 判断求完可信度后是否还有可信度大于阈值的项用来生成下一层H H = aprioriGen(H, m + 1) m += 1 else: # 不能继续生成下一层候选关联规则,提早退出循环 break
另外一个主要的区别是去掉了多余的Hmpl变量。运行的结果和generateRules2相同。
至此,一个完整的Apriori算法就完成了。
关联分析是用于发现大数据集中元素间有趣关系的一个工具集,能够采用两种方式来量化这些有趣的关系。第一种方式是使用频繁项集,它会给出常常在一块儿出现的元素项。第二种方式是关联规则,每条关联规则意味着元素项之间的“若是……那么”关系。
发现元素项间不一样的组合是个十分耗时的任务,不可避免须要大量昂贵的计算资源,这就须要一些更智能的方法在合理的时间范围内找到频繁项集。可以实现这一目标的一个方法是Apriori算法,它使用Apriori原理来减小在数据库上进行检查的集合的数目。Apriori原理是说若是一个元素项是不频繁的,那么那些包含该元素的超集也是不频繁的。Apriori算法从单元素项集开始,经过组合知足最小支持度要求的项集来造成更大的集合。支持度用来度量一个集合在原始数据中出现的频率。
关联分析能够用在许多不一样物品上。商店中的商品以及网站的访问页面是其中比较常见的例子。
每次增长频繁项集的大小,Apriori算法都会从新扫描整个数据集。当数据集很大时,这会显著下降频繁项集发现的速度。下面会介绍FP-growth算法,和Apriori算法相比,该算法只须要对数据库进行两次遍历,可以显著加快发现频繁项集的速度。
FP-growth算法基于Apriori构建,但采用了高级的数据结构减小扫描次数,大大加快了算法速度。FP-growth算法只须要对数据库进行两次扫描,而Apriori算法对于每一个潜在的频繁项集都会扫描数据集断定给定模式是否频繁,所以FP-growth算法的速度要比Apriori算法快。
FP-growth算法发现频繁项集的基本过程以下:
FP-growth算法
- 优势:通常要快于Apriori。
- 缺点:实现比较困难,在某些数据集上性能会降低。
- 适用数据类型:离散型数据。
FP-growth算法将数据存储在一种称为FP树的紧凑数据结构中。FP表明频繁模式(Frequent Pattern)。一棵FP树看上去与计算机科学中的其余树结构相似,可是它经过连接(link)来链接类似元素,被连起来的元素项能够当作一个链表。图5给出了FP树的一个例子。
图5 一棵FP树,和通常的树结构相似,包含着链接类似节点(值相同的节点)的链接
与搜索树不一样的是,一个元素项能够在一棵FP树种出现屡次。FP树辉存储项集的出现频率,而每一个项集会以路径的方式存储在数中。存在类似元素的集合会共享树的一部分。只有当集合之间彻底不一样时,树才会分叉。 树节点上给出集合中的单个元素及其在序列中的出现次数,路径会给出该序列的出现次数。
类似项之间的连接称为节点连接(node link),用于快速发现类似项的位置。
举例说明,下表用来产生图5的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 |
对FP树的解读:
图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}的支持度为五、项集{t, s, y, x, z}的支持度为二、项集{r, y, x, z}的支持度为一、项集{r, s, x}的支持度为1。FP树中会屡次出现相同的元素项,也是由于同一个元素项会存在于多条路径,构成多个频繁项集。可是频繁项集的共享路径是会合并的,如图中的{t, s, y, x, z}和{t, r, y, x, z}
和以前同样,咱们取一个最小阈值,出现次数低于最小阈值的元素项将被直接忽略。图5中将最小支持度设为3,因此q和p没有在FP中出现。
FP-growth算法的工做流程以下。首先构建FP树,而后利用它来挖掘频繁项集。为构建FP树,须要对原始数据集扫描两遍。第一遍对全部元素项的出现次数进行计数。数据库的第一遍扫描用来统计出现的频率,而第二遍扫描中只考虑那些频繁元素。
因为树节点的结构比较复杂,咱们使用一个类表示。建立文件fpGrowth.py并加入下列代码:
class treeNode: def __init__(self, nameValue, numOccur, parentNode): self.name = nameValue self.count = numOccur self.nodeLink = None 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)
每一个树节点由五个数据项组成:
成员函数:
测试代码:
>>> import fpGrowth >>> rootNode = fpGrowth.treeNode('pyramid', 9, None) >>> rootNode.children['eye'] = fpGrowth.treeNode('eye', 13, None) >>> rootNode.children['phoenix'] = fpGrowth.treeNode('phoenix', 3, None) >>> rootNode.disp()
头指针表
FP-growth算法还须要一个称为头指针表的数据结构,其实很简单,就是用来记录各个元素项的总出现次数的数组,再附带一个指针指向FP树中该元素项的第一个节点。这样每一个元素项都构成一条单链表。图示说明:
图6 带头指针表的FP树,头指针表做为一个起始指针来发现类似元素项
这里使用Python字典做为数据结构,来保存头指针表。以元素项名称为键,保存出现的总次数和一个指向第一个类似元素项的指针。
第一次遍历数据集会得到每一个元素项的出现频率,去掉不知足最小支持度的元素项,生成这个头指针表。
元素项排序
上文提到过,FP树会合并相同的频繁项集(或相同的部分)。所以为判断两个项集的类似程度须要对项集中的元素进行排序(不过缘由也不只如此,还有其它好处)。排序基于元素项的绝对出现频率(总的出现次数)来进行。在第二次遍历数据集时,会读入每一个项集(读取),去掉不知足最小支持度的元素项(过滤),而后对元素进行排序(重排序)。
对示例数据集进行过滤和重排序的结果以下:
事务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树
在对事务记录过滤和排序以后,就能够构建FP树了。从空集开始,将过滤和重排序后的频繁项集一次添加到树中。若是树中已存在现有元素,则增长现有元素的值;若是现有元素不存在,则向树添加一个分支。对前两条事务进行添加的过程:
图7 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.3 对当前项集的其他元素项和当前元素项的对应子节点递归3.3的过程
代码(在fpGrowth.py中加入下面的代码):
1 总函数:createTree
def createTree(dataSet, minSup=1): ''' 建立FP树 ''' # 第一次遍历数据集,建立头指针表 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()) if len(freqItemSet) == 0: return None, None # 增长一个数据项,用于存放指向类似元素项指针 for k in headerTable: headerTable[k] = [headerTable[k], None] retTree = treeNode('Null Set', 1, None) # 根节点 # 第二次遍历数据集,建立FP树 for tranSet, count in dataSet.items(): localD = {} # 对一个项集tranSet,记录其中每一个元素项的全局频率,用于排序 for item in tranSet: if item in freqItemSet: localD[item] = headerTable[item][0] # 注意这个[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) # 更新FP树 return retTree, headerTable
(代码比较宽,你们的显示器都那么大,应该不要紧吧……)
须要注意的是,参数中的dataSet的格式比较奇特,不是直觉上得集合的list,而是一个集合的字典,以这个集合为键,值部分记录的是这个集合出现的次数。因而要生成这个dataSet还须要后面的createInitSet()函数辅助。所以代码中第7行中的dataSet[trans]实际得到了这个trans集合的出现次数(在本例中均为1),一样第21行的“for tranSet, count in dataSet.items():”得到了tranSet和count分别表示一个项集和该项集的出现次数。——这样作是为了适应后面在挖掘频繁项集时生成的条件FP树。
2 辅助函数:updateTree
def updateTree(items, inTree, headerTable, count): if items[0] in inTree.children: # 有该元素项时计数值+1 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函数 updateTree(items[1::], inTree.children[items[0]], headerTable, count)
3 辅助函数:updateHeader
def updateHeader(nodeToTest, targetNode): while (nodeToTest.nodeLink != None): nodeToTest = nodeToTest.nodeLink nodeToTest.nodeLink = targetNode
这个函数其实只作了一件事,就是获取头指针表中该元素项对应的单链表的尾节点,而后将其指向新节点targetNode。
生成数据集
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
生成的样例数据同文中用得同样。这个诡异的输入格式就是createInitSet()函数中这样来得。
测试代码
>>> import fpGrowth >>> simpDat = fpGrowth.loadSimpDat() >>> initSet = fpGrowth.createInitSet(simpDat) >>> myFPtree, myHeaderTab = fpGrowth.createTree(initSet, 3) >>> myFPtree.disp()
结果是这样的(连字都懒得打了,直接截图……):
获得的FP树也和图5中的同样。
到如今为止大部分比较困难的工做已经处理完了。有了FP树以后,就能够抽取频繁项集了。这里的思路与Apriori算法大体相似,首先从单元素项集合开始,而后在此基础上逐步构建更大的集合。
从FP树中抽取频繁项集的三个基本步骤以下:
(这个翻译是什么鬼……英文是conditional pattern base)
首先从头指针表中的每一个频繁元素项开始,对每一个元素项,得到其对应的条件模式基(conditional pattern base)。条件模式基是以所查找元素项为结尾的路径集合。每一条路径其实都是一条前缀路径(prefix path)。简而言之,一条前缀路径是介于所查找元素项与树根节点之间的全部内容。
将图5从新贴在这里:
则每个频繁元素项的全部前缀路径(条件模式基)为:
频繁项 | 前缀路径 |
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 |
发现规律了吗,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树中的类似元素节点指针,咱们已经有了每一个元素对应的单链表,于是能够直接获取。
下面的程序给出了建立前缀路径的代码:
1 主函数:findPrefixPath
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
该函数代码用于为给定元素项生成一个条件模式基(前缀路径),这经过访问树中全部包含给定元素项的节点来完成。参数basePet表示输入的频繁项,treeNode为当前FP树种对应的第一个节点(可在函数外部经过headerTable[basePat][1]获取)。函数返回值即为条件模式基condPats,用一个字典表示,键为前缀路径,值为计数值。
2 辅助函数:ascendTree
def ascendTree(leafNode, prefixPath): if leafNode.parent != None: prefixPath.append(leafNode.name) ascendTree(leafNode.parent, prefixPath)
这个函数直接修改prefixPath的值,将当前节点leafNode添加到prefixPath的末尾,而后递归添加其父节点。最终结果,prefixPath就是一条从treeNode(包括treeNode)到根节点(不包括根节点)的路径。在主函数findPrefixPath()中再取prefixPath[1:],即为treeNode的前缀路径。
测试代码:
>>> fpGrowth.findPrefixPath('x', myHeaderTab['x'][1]) >>> fpGrowth.findPrefixPath('z', myHeaderTab['z'][1]) >>> fpGrowth.findPrefixPath('r', myHeaderTab['r'][1])
对于每个频繁项,都要建立一棵条件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”。
代码(直接调用createTree()函数):
condPattBases = findPrefixPath(basePat, headerTable[basePat][1]) myCondTree, myHead = createTree(condPattBases, minSup)
示例:t的条件FP树
图8 t的条件FP树的建立过程
在图8中,注意到元素项s以及r是条件模式基的一部分,可是它们并不属于条件FP树。由于在当前的输入中,s和r不知足最小支持度的条件。
有了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[1])] for basePat in bigL: newFreqSet = preFix.copy() newFreqSet.add(basePat) freqItemList.append(newFreqSet) condPattBases = findPrefixPath(basePat, headerTable[basePat][1]) myCondTree, myHead = createTree(condPattBases, minSup) if myHead != None: # 用于测试 print 'conditional tree for:', newFreqSet myCondTree.disp() mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)
输入参数:
测试代码:
>>> freqItems = [] >>> fpGrowth.mineTree(myFPtree, myHeaderTab, 3, set([]), freqItems) >>> freqItems
[set(['y']), set(['y', 'x']), set(['y', 'z']), set(['y', 'x', 'z']), set(['s']), set(['x', 's']), set(['t']), set(['z', 't']), set(['x', 'z', 't']), set(['y', 'x', 'z', 't']), set(['y', 'z', 't']), set(['x', 't']), set(['y', 'x', 't']), set(['y', 't']), set(['r']), set(['x']), set(['x', 'z']), set(['z'])]
想这一段代码解释清楚比较难,由于中间涉及到不少递归。直接举例说明,咱们在这里分解输入myFPtree和myHeaderTab后,“for basePat in bigL:”一行当basePat为’t’时的过程:
图9 mineTree函数解构图(basePat = ‘t’)
图中红色加粗的部分即实际添加到freqItemList中的频繁项集。
至此,完整的FP-growth算法已经能够运行。封装整个过程以下:
def fpGrowth(dataSet, minSup=3): initSet = createInitSet(dataSet) myFPtree, myHeaderTab = createTree(initSet, minSup) freqItems = [] mineTree(myFPtree, myHeaderTab, minSup, set([]), freqItems) return freqItems
注意,这里直接使用了上节(4.2)中的createInitSet()函数,这里有个问题:上节中的loadSimpDat()函数返回了一组简单的样例数据,没有相同的事务,因此createInitSet()函数中直接赋值“retDict[frozenset(trans)] = 1”没有问题。可是若是要封装成一个通用的FP-growth算法,就还须要处理输入数据有相同事务的情形,createInitSet()函数中须要累加retDict[frozenset(trans)]。(谢谢@xanxuslam的回复)
测试代码:
>>> import fpGrowth >>> dataSet = fpGrowth.loadSimpDat() >>> freqItems = fpGrowth.fpGrowth(dataSet) >>> freqItems
和以前的输出相同。
FP-growth算法是一种用于发现数据集中频繁模式的有效方法。FP-growth算法利用Apriori原则,执行更快。Apriori算法产生候选项集,而后扫描数据集来检查它们是否频繁。因为只对数据集扫描两次,所以FP-growth算法执行更快。在FP-growth算法中,数据集存储在一个称为FP树的结构中。FP树构建完成后,能够经过查找元素项的条件基及构建条件FP树来发现频繁项集。该过程不断以更多元素做为条件重复进行,直到FP树只包含一个元素为止。
FP-growth算法还有一个map-reduce版本的实现,它也很不错,能够扩展到多台机器上运行。Google使用该算法经过遍历大量文原本发现频繁共现词,其作法和咱们刚才介绍的例子很是相似(参见扩展阅读:FP-growth算法)。
书中的这两章有很多精彩的示例,这里只选取比较有表明性的一个——重新闻网站点击流中挖掘热门新闻报道。这是一个很大的数据集,有将近100万条记录(参见扩展阅读:kosarak)。在源数据集合保存在文件kosarak.dat中。该文件中的每一行包含某个用户浏览过的新闻报道。新闻报道被编码成整数,咱们可使用Apriori或FP-growth算法挖掘其中的频繁项集,查看那些新闻ID被用户大量观看到。
首先,将数据集导入到列表:
>>> parsedDat = [line.split() for line in open('kosarak.dat').readlines()]
接下来须要对初始集合格式化:
>>> import fpGrowth >>> initSet = fpGrowth.createInitSet(parsedDat)
而后构建FP树,并从中寻找那些至少被10万人浏览过的新闻报道。
>>> myFPtree, myHeaderTab = fpGrowth.createTree(initSet, 100000)
下面建立一个空列表来保存这些频繁项集:
>>> myFreqList = [] >>> fpGrowth.mineTree(myFPtree, myHeaderTab, 100000, set([]), myFreqList)
接下来看下有多少新闻报道或报道集合曾经被10万或者更多的人浏览过:
>>> len(myFreqList)
9
总共有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'])]
在看这两章的过程当中和以后又看到的一些相关的东西:
注: