1. 程式人生 > >機器學習實戰(十一)FP-growth(頻繁項集)

機器學習實戰(十一)FP-growth(頻繁項集)

目錄

0. 前言

學習完機器學習實戰的FP-growth,簡單的做個筆記。文中部分描述屬於個人消化後的理解,僅供參考。

本篇綜合了先前的文章,如有不理解,可參考:

如果這篇文章對你有一點小小的幫助,請給個關注喔~我會非常開心的~

0. 前言

從大規模的資料集中,尋找不同特徵或者物品之間的隱含關係,稱為關聯分析(association analysis),或者關聯規則學習(association rule learning)。

Apriori 演算法中,尋找頻繁項集,需要對每一個可能的頻繁項掃描一遍資料集計算支援度,計算量龐大。

FP-growth 演算法

中,尋找頻繁項集,只需要掃描兩遍資料集,將資料儲存在FP樹的結構上,然後在FP樹上挖掘頻繁項集。

  • 優點:速度一般要快於 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樹挖掘頻繁項集的過程可描述為:

  1. 對於頭指標表中的每一個元素,獲取其條件模式基
  2. 根據條件模式基,構建條件FP樹(即,將條件模式基當作新的資料集,按照建樹的流程,重新構建一棵FP樹)
  3. 繼續對於條件FP樹的頭指標表中的每一個元素,獲取其條件模式基
  4. 繼續根據條件模式基,構建條件FP樹
  5. 如此遞迴過程,直到無法構建出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)

如果這篇文章對你有一點小小的幫助,請給個關注喔~我會非常開心的~