機器學習實戰(十一)FP-growth(頻繁項集)
目錄
學習完機器學習實戰的FP-growth,簡單的做個筆記。文中部分描述屬於個人消化後的理解,僅供參考。
本篇綜合了先前的文章,如有不理解,可參考:
如果這篇文章對你有一點小小的幫助,請給個關注喔~我會非常開心的~
0. 前言
從大規模的資料集中,尋找不同特徵或者物品之間的隱含關係,稱為關聯分析(association analysis),或者關聯規則學習(association rule learning)。
在 Apriori 演算法中,尋找頻繁項集,需要對每一個可能的頻繁項掃描一遍資料集計算支援度,計算量龐大。
在 FP-growth 演算法
- 優點:速度一般要快於 Apriori。
- 缺點:實現比較困難,在某些資料集上效能會下降。
- 適用資料型別:標稱型資料。
例如在下述例子中(圖源:機器學習實戰),右側是一顆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代表頻繁模式(Frequent Pattern),一個元素項可以在一顆FP樹上出現多次。
樹節點上給出了當前節點的路徑在資料集中的出現次數,例如{z:5}表示元素{z}在資料集中出現了5次;{y:3}表示路徑{y, x, z}在資料集中出現了3次;{s:2}表示路徑{s, y, x, z}在資料集中出現了2次。
左側為頭指標表,給出了每個元素在資料集中出現的次數,並由連結串列通過節點連結(node link)依次連結每個元素。部分元素因為不滿足最小支援度的要求,所以不儲存在FP樹中。
在 FP-growth 演算法中,同樣採用了 Apriori 演算法的思想,如果某個項是非頻繁的,那麼這個項的所有超集也是非頻繁的。
1. 構建FP樹
構建FP樹的過程只需要掃描兩遍資料集。
第一遍掃描,計算每個單個元素的頻率,並根據最小支援度,濾除不滿足的元素。
第二遍掃描,首先對資料集進行處理,每一條資料按照元素的絕對出現頻率排序,並濾除不滿足最小支援度的元素。
例如根據上述的頭指標表,元素排序為{z:5, x:4, y:3, s:3, r:3, t:3},所以處理後的資料為:
事務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樹中,從根節點開始遞迴新增路徑,存在則將數值增加,不存在則建立新的節點。
例如下圖所示(圖源:機器學習實戰),① 根節點不存在子節點{z},所以建立新的子節點{z},遞迴節點{z},因不存在子節點{r},所以建立新的子節點{r},② 根節點存在子節點{z},所以數值增加,遞迴節點{z},因不存在子節點{x},所以建立新的子節點{x},遞迴節點{x},......,如此遞迴。
2. 從FP樹中挖掘頻繁項集
一個元素的條件模式基(conditional pattern base),是這個元素所有字首路徑(prefix path)的集合。
字首路徑(prefix path),是當前元素到根節點之間的路徑(不包括當前元素和根節點)。
例如下圖所示(圖源:機器學習實戰),{r}的條件模式基是{{z}{z, x, y}{x, s}}:
從FP樹挖掘頻繁項集的過程可描述為:
- 對於頭指標表中的每一個元素,獲取其條件模式基
- 根據條件模式基,構建條件FP樹(即,將條件模式基當作新的資料集,按照建樹的流程,重新構建一棵FP樹)
- 繼續對於條件FP樹的頭指標表中的每一個元素,獲取其條件模式基
- 繼續根據條件模式基,構建條件FP樹
- 如此遞迴過程,直到無法構建出FP樹為止
記錄頻繁項集的過程在建立一棵新的FP樹時記錄,虛擬碼如下表示:
關於此處的理解:首先構建了一棵FP樹,此時FP樹中的單個元素均滿足最小支援度(假設有{a}{b}{c}{d}{e}5個元素),遍歷其中的每一個元素(假設此時遍歷{a}),先將元素{a}加入總的頻繁項集,再尋找元素{a}的條件模式基(假設有{c, b}{b, d}{b, c, e, d}),根據這些字首路徑遞迴構建一棵條件FP樹(若這棵樹能夠構建的起來,說明樹中的單個元素也是滿足最小支援度的,假設條件FP樹中有{b}{c}{d}3個元素,{e}不滿足最小支援度),說明{b}{c}{d}這三個元素滿足最小支援度,遍歷其中的每一個元素(假設此時遍歷{b}),複製上一層遞迴的頻繁項集(即,{a}),將當前遍歷元素{b}加入複製的頻繁項集中(即,構成{a, b}),然後再將{a, b}加入總的頻繁項集,再在條件FP樹中尋找元素{b}的條件模式基,繼續遞迴構建。因為上一層遞迴中的頻繁項集{a}是一定滿足最小支援度的,由這個元素{a}搜尋得到的條件模式基,一定是在資料集中跟{a}有組合的,若能據此構建一棵條件FP樹,說明這棵樹中的元素{b}{c}{d}也一定滿足最小支援度,因這元素{b}與{a}在原始資料集中有組合,且這元素{b}與上一層遞迴頻繁項集{a}均滿足最小支援度,所以這元素{b}和{a}的組合{a, b}一定滿足最小支援度,且存在在原始資料集中,所以加入總的頻繁項集。
3. 實戰案例
以下將展示書中案例的程式碼段,所有程式碼和資料可以在github中下載:
3.1. FP-growth尋找頻繁項集
# coding:utf-8
from numpy import *
"""
FP-growth尋找頻繁項集
"""
# 載入資料集
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
# 將資料集轉換為set型別
def createInitSet(dataSet):
retDict = {}
for trans in dataSet:
retDict[frozenset(trans)] = 1
return retDict
# 樹節點
class treeNode:
# name: 節點名稱
# count: 出現次數
# nodeLink: 節點連結
# parent: 父節點
# children: 子節點集
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)
# 建立FP樹
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 list(headerTable.keys()):
if headerTable[k] < minSup:
del (headerTable[k])
# 頻繁項集
# freqItemSet: {'p', 'v', 'u', 'q', ...}
freqItemSet = set(headerTable.keys())
# 無頻繁項就返回
if len(freqItemSet) == 0:
return None, None
# 擴充套件頭指標表
# 新增指向每種型別第一個元素的指標(節點連結)
# headerTable: {'j': [1, None], 'p': [2, None], 'r': [3, None], ...}
for k in headerTable:
headerTable[k] = [headerTable[k], None]
# 建立根節點
retTree = treeNode('Null Set', 1, None)
# 第二次遍歷資料集
# 構建FP樹
for tranSet, count in dataSet.items():
# tranSet: frozenset({'h', 'p', 'z', 'j', 'r'})
# count: 1
localD = {}
# 如果單個元素是頻繁項,則加入localD列表
for item in tranSet:
if item in freqItemSet:
localD[item] = headerTable[item][0]
# localD: {'r': 3, 'j': 1, 'z': 5, 'h': 1, 'p': 2}
if len(localD) > 0:
# 排序
orderedItems = [v[0] for v in sorted(localD.items(), key=lambda p: p[1], reverse=True)]
# 更新FP樹
updateTree(orderedItems, retTree, headerTable, count)
return retTree, headerTable
# 更新FP樹函式
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]])
# 列表元素長度大於1
# 遞迴呼叫更新FP樹函式
if len(items) > 1:
updateTree(items[1::], inTree.children[items[0]], headerTable, count)
# 更新頭指標表的節點連結的函式
def updateHeader(nodeToTest, targetNode):
# 將元素放在指標連結串列的最後
while (nodeToTest.nodeLink != None):
nodeToTest = nodeToTest.nodeLink
nodeToTest.nodeLink = targetNode
# 尋找節點basePat的所有字首路徑
# treeNode: 頭節點表的basePat的指標指向元素
def findPrefixPath(basePat, treeNode):
condPats = {}
# 有指向的元素
while treeNode != None:
prefixPath = []
# 回溯父節點,尋找字首路徑
# prefixPath: ['r', 't', 'x', 'z']
ascendTree(treeNode, prefixPath)
# 路徑長度大於1,不是單個元素
if len(prefixPath) > 1:
# 新增進condPats,記錄路徑的出現次數
condPats[frozenset(prefixPath[1:])] = treeNode.count
# 繼續尋找basePat為結尾的字首路徑
treeNode = treeNode.nodeLink
# condPats: {frozenset({'z'}): 1, frozenset({'s', 'x'}): 1, frozenset({'x', 'z'}): 1}
return condPats
# 單個節點回溯,尋找字首路徑
def ascendTree(leafNode, prefixPath):
if leafNode.parent != None:
prefixPath.append(leafNode.name)
ascendTree(leafNode.parent, prefixPath)
# 根據FP樹尋找頻繁項集
def mineTree(inTree, headerTable, minSup, preFix, freqItemList):
# 頭指標表排序
# bigL: ['h', 'j', 'u', 'v', 'w',...]
bigL = [v[0] for v in sorted(headerTable.items(), key=lambda p: p[1][0])]
for basePat in bigL:
newFreqSet = preFix.copy()
newFreqSet.add(basePat)
print('finalFrequent Item: ', newFreqSet)
freqItemList.append(newFreqSet)
# 以basePat為節點的所有字首路徑
# condPattBases: {frozenset({'z', 'r', 'p'}): 1, ...}
condPattBases = findPrefixPath(basePat, headerTable[basePat][1])
print('condPattBases :', basePat, condPattBases)
# 以當前元素的所有字首路徑,建立條件FP樹
myCondTree, myHead = createTree(condPattBases, minSup)
print('head from conditional tree: ', myHead)
# 根據條件FP樹和條件頭指標表,遞迴建立下一個條件FP樹
if myHead != None:
print('conditional tree for: ', newFreqSet)
myCondTree.disp(1)
mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)
if __name__ == '__main__':
# simpDat = loadSimpDat()
# initSet = createInitSet(simpDat)
# retTree, headerTable = createTree(initSet)
# freqItems = []
# mineTree(retTree, headerTable, 3, set([]), freqItems)
# print(freqItems)
parsedDat = [line.split() for line in open('kosarak.dat').readlines()]
initSet = createInitSet(parsedDat)
myFPtree, myHeaderTab = createTree(initSet, 100000)
myFreqList = []
mineTree(myFPtree, myHeaderTab, 100000, set([]), myFreqList)
print(myFreqList)
如果這篇文章對你有一點小小的幫助,請給個關注喔~我會非常開心的~