FP-growth算法是一種高效發現頻繁集的算法,比Apriori算法高效,但是不能用於發現關聯規則。FP-growth算法只需要對數據即信兩次掃描,而Apriori算法對於每個潛在的頻繁項集都會掃描數據集判定給定模式是否是頻繁,所以FP-growth更快。FP-growth算法主要分爲兩個過程:
- 構建FP樹;
- 從FP樹中挖掘頻繁項集。
1.FP樹介紹
FP代表頻繁模式(Frequent Pattern),它和數據結構中的其它樹特別相似,但是在FP樹中,一個元素項可以出現多次,如下圖所示:
圖1
如圖1所示,FP樹會存儲項集的出現頻率,而每個項集會以路徑的方式存儲在樹中。從最上面的空集合開始,每一條路徑就是一個項集,這裏要除過去帶箭頭的那些路徑鏈接,因爲帶箭頭的的鏈接是相似項之間的鏈接,叫節點鏈接,是用於快速發現相似項的位置(至於相似項是什麼,看後面就知道其含義了)。
這棵樹可以分爲縱向和橫向的,縱向的就是每個項集的集合,橫向的就是相似項,用於方便元素的查找。
爲了挖掘頻繁項集,我們首先要構建FP樹。我們需要對數據掃描兩遍。第一遍對所有元素項的出現次數進行統計,根據Apriori原理,如果某元素不是頻繁的,那麼包含該元素的超集也是不頻繁的,所以就不需要考慮這些超集,第二遍掃描值考慮哪些頻繁元素。
2.構建FP樹
首先給出FP樹的節點的結構:
class treeNode:
def __init__(self, nameValue, numOccur, parentNode):
self.name = nameValue
self.count = numOccur
self.nodeLink = None
self.parent = parentNode #needs to be updated
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)
我們看一下這個節點的結構,name就是節點元素,count是當前節點元素出現次數(經過這個節點的路徑的次數),nodeLink用來指向相似元素(維護橫向結構),parent父節點,從構造方法上來看,我們每次將傳入的parentNode節點作爲當前節點的父節點,children就是存放節點的子節點。另外也提供了增加自身數量的計數器,和子節點的遍歷函數。
下面先給出最終的FP樹的一個形式:
圖2
如圖2所示,頭指針表來放FP樹中每個元素出現的總數,用來表示橫向的關係,方便查找相似元素。
第一次遍歷的時候,去掉不滿足最小支持度的元素項。下一步構建的時候,讀入每個項集並將其添加到一條已經存在的路徑中。如果該路徑不存在,則創建一條新路徑。我們知道在FP樹中,每個項集都是一個無序的組合,但是又需要將某些有重複元素的集合放到同一條路徑上,爲了解決這個問題,我們 就需要對每個結合進行排序,排序是基於每個項集中元素項的絕對出現頻率來進行的。
圖3
如圖3所示,將非頻繁項移除後並且排序後的事物數據集。如001號事務,由於h,j,p的最小支持度沒有滿足最小支持度,所以把這個元素項去掉了,留下的z,r由於z的出現次數比r多,所以z在r前面。爲什麼這麼排,因爲在FP樹中,越是靠近根節點的元素是最靠前的,並且其出現次數也是最多的,也就是被共用次數最多的。
下面我們就可以構建FP樹了,從空集合開始,向其中不斷添加頻繁集。過濾、排序後的事物一次添加到樹中,如果樹中已存在現有的元素,則增加現有元素的值;如果現有元素不存在,則向樹添加一個分枝,如圖4所示:
圖4
下面我們結合代碼看一下具體的過程:
def createTree(dataSet, minSup=1): #create FP-tree from dataset but don't mine
headerTable = {}
#go over dataSet twice
for trans in dataSet:#first pass counts frequency of occurance
for item in trans:
headerTable[item] = headerTable.get(item, 0) + dataSet[trans]
for k in 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
retTree = treeNode('Null Set', 1, None) #create tree
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:
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
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函數看起。參數dataSet是數據集,minSup是集合中元素最少出現的次數,第一個for循環就是統計數據集中每個元素出現的次數,將其放到headerTable的數據字典中,然後第二個循環就是去掉次數小於minSup的單元素集合,然後將其放到freqItemSet(所有出現次數滿足minSup的單個元素的集合)中。
第3個for循環,就是初始化headerTable,這裏我們用來存儲FP樹中的橫向結構,包括該元素出現的次數,以及第一個對應的相似節點的位置(當然初始化這個位置都是空None),如:{'s': [3, None], 'r': [3, None], 't': [3, None], 'y': [3, None], 'x': [4, None], 'z': [5, None]}所示。下來就是聲明一個空節點作爲根節點。
從第4個for循環開始正式構建FP樹。次循環是遍歷傳入的dataSet中的值。正如循環中的所示,tranSet是頻繁項集,count是該集合對應的次數。然後我們看內循環就是將tranSet中的元素以及其出現的次數提取出來,並且以字典的形式將其放到localD中如({z,5},{r,3})的形式,主要是將傳入的集合中可能有一些已經在第一步去掉的單元素頻繁項,然後判斷他們是否在freqItemSet(所有出現次數滿足minSup的單個元素的集合)。如果len(locakD)的長度大於0,說明有滿足minSup的元素。orderedItems就是此次滿足minSup的元素按照出現的次數進行倒序排列,如形式:<type 'list'>: ['z', 'x', 'y', 's', 't'],然後調用updateTree函數。其實這一步就是從樣本數據的每條項目中找出滿足minSup的元素集合,下面就是將找出來的元素集合放到FPtree中。
updateTree函數。items就是需要往已有的FPtree中增加的list,如<type 'list'>: ['z', 'x', 'y', 's', 't'],inTree就是當前樹,headerTable就是橫向的存放每個元素及其出現次數和第一個相似元素位置的,count可以看作是items集合出現的次數。這個函數是自己調用自己的一個遞歸的過程,先看第一個if items[0] in inTree.children判斷items第一個元素是否是當前樹的子節點,如果是,說明items[0]已經有了,只需要將其計數增加1即可。先不看else,看最後面的if len(items)>1就是如果items不止一個元素的時候,就去掉當前已經判斷過的第一個元素,將後面的元素作爲items繼續調用自己,如第一次判斷了z,然後此時就是將xyst作爲items到updateTree中,此時x就作爲第一個元素判斷,一次遞歸。然後我們看else中的內容,就是沒有在當前樹根節點的子節點中出現,說明我們要開闢新的子節點分枝了。然後就簡單了,生成新節點,然後放到當前樹根節點的字典中。然後我們需要更新橫向的headerTable,中間調用了updateHeader函數,就是當headerTable中的相似節點已經有的時候,不斷向下找,直到最後,然後將當前新生成的點放到最後一個相似節點上。其實就是產生nodeLink的過程。
其實如果對遞歸熟悉的話,這個過程還是比較簡單的。至此我們已經構建好了這個FP樹了。
3.從一棵FP樹中挖掘頻繁項集
上面的過程是我們根據樣本數據構建好的一顆FP樹,下來就是我們需要從這棵樹中發現頻繁項集。
從FP樹種抽取頻繁項集的單個基本步驟如下:
- 從FP樹種獲得條件基;
- 利用條件模式基,構建一個條件FP樹;
- 迭代重複步驟(1)和(2),直到樹包含一個元素項爲止
接下來我們就需要抽取條件模式基。條件模式基是什麼,就是一系列的倒回到根節點的路徑。我們還記得headerTable中存的是每個元素出現次數,以及其相似元素出現的第一個位置(也就是其出現的第一個位置),然後參考圖2,這個元素可能在這個樹中出現了多次,然後從每次出現的位置開始,倒着向數的根節點,然後就得到了一系列的頻繁集,如r的前綴路線有{x,s},{z,x,y},{z},每條前綴路徑都有一個計數值關聯,然後我們給出圖5所示的結果:
圖5
前綴路徑將被用於構建條件FP樹。然後我們參考以下代碼:
def ascendTree(leafNode, prefixPath): #ascends from leaf node to root
if leafNode.parent != None:
prefixPath.append(leafNode.name)
ascendTree(leafNode.parent, prefixPath)
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
先看函數findPrefixPath,如名所示就是爲了找到對應的素有的前綴路徑。basePat是我們傳入的單個元素,如x,treeNode是我們傳入的一個節點,就是headerTable中第一個相似節點,也就是x第一次出現的位置,上述函數相對比較簡單,就不展開說了。找到所有的前綴路徑之後,我們就需要創建條件FP樹。
我們先看一個例子,看t的條件樹:
圖6
通過圖2所示,我們可以看出t的兩個條件模式基,然後用這個條件模式基重複了之前的建樹的過程,然後結果就是可以看作是所有包含t的頻繁集。但是我們可以看到對於t來說我們是去掉了s和r,爲什麼?因爲我們這裏的最小支持度爲3,而在t的條件模式基中,s和r是不頻繁,雖然在沒有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])]#(sort header table)
for basePat in bigL: #start from bottom of header table
newFreqSet = preFix.copy()
newFreqSet.add(basePat)
freqItemList.append(newFreqSet)
condPattBases = findPrefixPath(basePat, headerTable[basePat][1])
myCondTree, myHead = createTree(condPattBases, minSup)
if myHead != None: #3. mine cond. FP-tree
mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)
這個函數還是比較複雜的函數,是一個不斷遞歸的過程。preFix第一次傳入的就是t,bigL就是headerTable中所有的單元素key的集合。condPattBases就是t對應的條件模式基,然後再用這些條件模式基和minSup生成一顆樹,返回的結果是在這些條件基下的樹和對應的headerTable即myHead,這裏給出一個例子:{'x': [3, <fpGrowthBak.treeNode instance at 0x035747B0>], 'z': [3, <fpGrowthBak.treeNode instance at 0x03599C10>],'y': [3, <fpGrowthBak.treeNode instance at 0x03546A35>]}。如果myHead不爲空,說明樹還不爲空,然後再調用mineTree函數,此時bigL就變成了[‘z’,’x’,’y’],其中newFreqSet就變成了[‘t’,’z’],freqItemList就成爲了[[‘t’],[‘t’,’z’]],再遞歸就是[[‘t’],[‘t’,’z’],,[‘t’,’z’,’x’]],因爲有循環就這樣遞歸找到所有的可能。
FP-growth算法理解起來因爲用到了好多遞歸的過程,所以比較不太好了解,所以還是需要仔細閱讀代碼去理解其思想的。