關聯分析Apriori演算法和FP-growth演算法初探

Andrew.Hann發表於2018-08-04

1. 關聯分析是什麼?

Apriori和FP-growth演算法是一種關聯演算法,屬於無監督演算法的一種,它們可以自動從資料中挖掘出潛在的關聯關係。例如經典的啤酒與尿布的故事。下面我們用一個例子來切入本文對關聯關係以及關聯分析的討論。

0x1:一個購物籃交易的例子

許多商業企業在日復一日的運營中積聚了大量的交易資料。例如,超市的收銀臺每天都收集大量的顧客購物資料。

例如,下表給出了一個這種資料集的例子,我們通常稱其為購物籃交易(market basket transaction)。表中每一行對應一個交易,包含一個唯一標識TID和特定顧客購買的商品集合。

零售商對分析這些資料很感興趣,以便了解其顧客的購買行為。可以使用這種有價值的資訊來支援各種商業中的實際應用,如市場促銷,庫存管理和顧客關係管理等等。

交易號碼

商品

0

豆奶,萵苣

1

萵苣,尿布,葡萄酒,甜菜

2

豆奶,尿布,葡萄酒,橙汁

3

萵苣,豆奶,尿布,葡萄酒

4

萵苣,豆奶,尿布,橙汁

是購物籃資料中所有項的集合,而是所有交易的集合。包含0個或多個項的集合被稱為項集(itemset)。

如果一個項集包含 k 個項,則稱它為 k-項集。顯然,每個交易包含的項集都是 I 的子集。

接下來基於這個例子,我們來討論都有哪些關聯關係,以及如何發掘這些關聯關係。

0x2:事物之間關聯關係的兩種抽象形式

關聯分析是在大規模資料集中尋找關聯關係的任務。這些關係可以有兩種形式:

  • 頻繁項集:頻繁項集(frequent item sets)是經常出現在一塊兒的物品的集合,它暗示了某些事物之間總是結伴或成對出現。
  • 關聯規則:關聯規則(association rules)暗示兩種物品之間可能存在很強的關係,它更關注的是事物之間的互相依賴條件先驗關係

下面用一個例子來說明這兩種概念:下圖給出了某個雜貨店的交易清單。

交易號碼

商品

0

豆奶,萵苣

1

萵苣,尿布,葡萄酒,甜菜

2

豆奶,尿布,葡萄酒,橙汁

3

萵苣,豆奶,尿布,葡萄酒

4

萵苣,豆奶,尿布,橙汁

頻繁項集是指那些經常出現在一起的商品集合,圖中的集合{葡萄酒,尿布,豆奶}就是頻繁項集的一個例子;

從這個資料集中也可以找到諸如“尿布->葡萄酒”的關聯規則,即如果有人買了尿布,那麼他很可能也會買葡萄酒。

這裡我們注意,為什麼是說尿布->葡萄酒的關聯規則,而不是葡萄酒->尿布的關聯規則呢?因為我們注意到,在第4行,出現了尿布,但是沒有出現葡萄酒,所以這個關聯推導是不成立的,反之卻成立(至少在這個樣本資料集裡是成立的)。

0x3:如何度量事物之間的關聯關係

我們用支援度和可信度來度量事物間的關聯關係,雖然事物間的關聯關係十分複雜,但是我們基於統計規律以及貝葉斯條件概率理論的基礎進行抽象,得到一種數值化的度量描述。

1. 項與項集

設itemset={item1, item_2, …, item_m}是所有項的集合。

其中,item_k(k=1,2,…,m)成為項。項的集合稱為項集(itemset),包含k個項的項集稱為k項集(k-itemset)

k-項集對應到物理世界可能就是我們的規則集合,每個頻繁項集都是一個k-項集。

2. 支援度(support)- 用來尋找頻繁項集(k-項集)的,即尋找頻繁共現項

關聯規則的支援度定義如下:

其中表示事務包含集合A和B的並(即包含A和B中的每個項)的概率。這裡的支援度也可以理解為項集A和項集B的共現概率。

通俗的說,一個項集的支援度(support)被定義資料集中包含該項集(多個項的組合集合)的記錄所佔的比例。

如上圖中,{豆奶}的支援度為4/5,{豆奶,尿布}的支援度為3/5。

在實際的業務場景中,支援度可以幫助我們發現潛在的規則集合

例如在異常程式檢測中,當同時出現{ java->bash、bash->bash }這種事件序列集合會經常在發生了反彈shell惡性入侵的機器日誌中出現(即這種組合的支援度會較高),這種頻繁項集暗示了我們這是一個有代表性的序列標誌,很可能是exploited IOC標誌。

3. 置信度(confidence)- 評價一個關聯規則的置信度

關聯規則是形如 X→Y 的蘊涵表示式,其中 X 和 Y 是不相交的項集,即 X∩Y=∅。

關聯規則的置信度定義如下:

這個公式暗示一個非常質樸的道理,如果一個事件A出現概率很高,那麼這個事件對其他事件是否出現的推測可信度就會降低,很簡單的道理,例如夏天今天氣溫大於20°,這是一個非常常見的事件,可能大於0.9的可能性,事件B是今天你會中彩票一等獎。confidence(A => B)的置信度就不會很高,因為事件A的出現概率很高,這種常見事件對事件B的推導關聯幾乎沒有實際意義。

通俗地說,可信度置信度(confidence)是針對關聯規則來定義的。例如我們定義一個規則:{尿布}➞{葡萄酒},即購買尿布的顧客也會購買啤酒,這是一個關聯規則,這個關聯規則的可信度被定義為"支援度({尿布,葡萄酒}) / 支援度({尿布})"。

由於{尿布,葡萄酒}的支援度為3/5,尿布的支援度為4/5,所以"尿布➞葡萄酒"的可信度為3/4。

從訓練資料的統計角度來看,這意味著對於包含"尿布"的所有記錄,我們的規則對其中75%的記錄都適用。

從關聯規則的可信程度角度來看,“購買尿布的顧客會購買葡萄酒”這個商業推測,有75%的可能性是成立的,也可以理解為做這種商業決策,可以獲得75%的回報率期望。

可以發現,置信度本質就是,貝葉斯條件概率的基本形式:P(A | B)= P(A,B)/ P(B)

4. 強關聯規則與頻繁項集

支援度是針對項集來說的,因此可以定義一個最小支援度,而只保留滿足最小值尺度的項集。

置信度是針對關聯規則來說的,因此可以定義一個最小置信度,而只保留滿足最小值置信度的關聯規則

  • support ≥ minsup threshold
  • confidence ≥ minconf threshold

滿足最小支援度和最小置信度的關聯規則,即待挖掘的最終關聯規則。也是我們期望模型產出的業務結果。

這實際上是在工程化專案中需要關心的,因為我們在一個龐大的資料集中,頻繁項集合關聯規則是非常多的,我們不可能採納所有的這些關係,特別是在入侵檢測中,我們往往需要提取TOP N的關聯,並將其轉化為規則,這個過程也可以自動化完成。

0x4:關聯規則挖掘演算法的主要任務

這個小節其實和0x2小節是一樣的,關聯規則挖掘演算法就是在基於一種抽象評價函式,對事物間的關係進行抽象數值化,並進行計算。通過概率統計的基本定理,從中挖掘出有價值的“關係”。

因此,大多數關聯規則挖掘演算法通常採用的一種策略是,將關聯規則挖掘任務分解為如下兩個主要的子任務。

  • 頻繁項集產生:其目標是發現滿足最小支援度閾值的所有項集,這些項集稱作頻繁項集(frequent itemset)。
  • 規則的產生:其目標是從上一步發現的頻繁項集挖掘它們之間存在的依賴和推倒關係,並從所有關係中提取所有高置信度的規則,這些規則稱作強規則(strong rule)。

通常,頻繁項集產生所需的計算開銷遠大於產生規則所需的計算開銷。

0x5:怎麼去挖掘資料集中潛在的關係呢?暴力搜尋可以嗎?

一種最直接的進行關聯關係挖掘的方法或許就是暴力搜尋(Brute-force)的方法,實際上,如果算力足夠,理論上所有機器學習演算法都可以暴力搜尋,也就不需要承擔啟發式搜尋帶來的區域性優化損失問題。

1. List all possible association rules
2. Compute the support and confidence for each rule
3. Prune rules that fail the minsup and minconf thresholds

然而,由於Brute-force的計算量過大,所以取樣這種方法並不現實!

格結構(Lattice structure)常被用來列舉所有可能的項集。如下圖所示為 I={a,b,c,d,e} 的項集格。

一般來說,排除空集後,一個包含k個項的資料集最大可能產生個頻繁項集。由於在實際應用中k的值可能非常大,需要探查的項集搜尋空集可能是指數規模的。

Relevant Link:

https://blog.csdn.net/baimafujinji/article/details/53456931
https://www.cnblogs.com/qwertWZ/p/4510857.html
https://www.cnblogs.com/llhthinker/p/6719779.html

 

2. Apriori演算法

0x1:Apriori演算法中對頻繁項集的層級迭代搜尋思想

在上一小節的末尾,我們已經討論說明了Brute-force在實際中並不可取。我們必須設法降低產生頻繁項集的計算複雜度。

此時我們可以利用支援度對候選項集進行剪枝,它的核心思想是在上一輪中已經明確不能成功頻繁項集的項集就不要進入下一輪浪費時間了,只保留上一輪中的頻繁項集,在本輪繼續進行統計。

Apriori定律1:如果一個集合是頻繁項集,則它的所有子集都是頻繁項集

假設一個集合{A,B}是頻繁項集,即A、B同時出現在一條記錄的次數大於等於最小支援度min_support,則它的子集{A},{B}出現次數必定大於等於min_support,即它的子集都是頻繁項集。

Apriori定律2:如果一個集合不是頻繁項集,則它的所有超集都不是頻繁項集

假設集合{A}不是頻繁項集,即A出現的次數小於 min_support,則它的任何超集如{A,B}出現的次數必定小於min_support,因此其超集必定也不是頻繁項集

下圖表示當我們發現{A,B}是非頻繁集時,就代表所有包含它的超集也是非頻繁的,即可以將它們都剪除(剪紙)

一般而言,關聯規則的挖掘是一個兩步的過程:

1. 找出所有的頻繁項集
2. 由頻繁項集產生強關聯規則

0x2:挖掘頻繁項集

1. 偽碼描述

    • Let k=1:最開始,每個項都是候選1-項集的集合C1的成員
      • Generate frequent itemsets of length k, and Prune candidate itemsets that are infrequent:計算C1每個1-項集的頻率,在第一步就要根據支援度閾值對不滿足閾值的項集進行剪枝,得到第一層的頻繁項
    • Repeat until no new frequent itemsets are identified:迭代過程
      • Generate length (k+1) candidate itemsets from length k frequent itemsets:在上一步k-項集的基礎上,演算法掃描所有的記錄,獲得項集的並集組合,生成所有(k+1)-項集。
      • Prune candidate itemsets containing subsets of length k+1 that are infrequent:(k+1)-項集每個項進行計數(根據該項在全量資料集中的頻數進行統計)。然後根據最小支援度從(k+1)-項集中刪除不滿足的項,從而獲得頻繁(k+1)-項集,Lk+1
    • the finnal k-items:因為Apriori每一步都在通過項集之間的並集操作,以此來獲得新的候選項集,如果在某一輪迭代中,候選項集沒有新增,則可以停止迭代。因為這說明了在這輪迭代中,通過支援度閾值的剪枝,非頻繁項集已經全部被剪枝完畢了,則根據Apriori先驗定理2,迭代沒有必要再進行下去了。

下面是一個具體的例子,最開始資料庫裡有4條交易,{A、C、D},{B、C、E},{A、B、C、E},{B、E},使用min_support=2作為支援度閾值,最後我們篩選出來的頻繁集為{B、C、E}。

2. 一個頻繁項集生成的python程式碼示例

# coding=utf-8
from numpy import *


def loadDataSet():
    return [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]

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)


# 其中D為全部資料集,
# # Ck為大小為k(包含k個元素)的候選項集,
# # minSupport為設定的最小支援度。
# # 返回值中retList為在Ck中找出的頻繁項集(支援度大於minSupport的),
# # supportData記錄各頻繁項集的支援度
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


# 生成 k+1 項集的候選項集
# 注意其生成的過程中,首選對每個項集按元素排序,然後每次比較兩個項集,只有在前k-1項相同時才將這兩項合併。
# # 這樣做是因為函式並非要兩兩合併各個集合,那樣生成的集合並非都是k+1項的。在限制項數為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


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


dataSet = loadDataSet()
D = map(set, dataSet)
print dataSet
print D

C1 = createC1(dataSet)
print C1    # 其中C1即為元素個數為1的項集(非頻繁項集,因為還沒有同最小支援度比較)

L1, suppDat = scanD(D, C1, 0.5)
print "L1: ", L1
print "suppDat: ", suppDat


# 完整的頻繁項集生成全過程
L, suppData = apriori(dataSet)
print "L: ",L
print "suppData:", suppData

最後生成的頻繁項集為:

suppData: 
frozenset([5]): 0.75, 
frozenset([3]): 0.75, 
frozenset([2, 3, 5]): 0.5,
frozenset([1, 2]): 0.25,
frozenset([1, 5]): 0.25,
frozenset([3, 5]): 0.5,
frozenset([4]): 0.25, 
frozenset([2, 3]): 0.5, 
frozenset([2, 5]): 0.75, 
frozenset([1]): 0.5, 
frozenset([1, 3]): 0.5, 
frozenset([2]): 0.75 

需要注意的是,閾值設定的越小,整體演算法的執行時間就越短,因為閾值設定的越小,剪紙會更早介入。

0x3:從頻繁集中挖掘關聯規則

解決了頻繁項集問題,下一步就可以解決相關規則問題。

1. 關聯規則來源自所有頻繁項集

從前面對置信度的形式化描述我們知道,關聯規則來源於每一輪迭代中產生的頻繁項集(從C1開始,因為空集對單項集的支援推導是沒有意義的)

從公式中可以看到,計算關聯規則置信度的分子和分母我們都有了,就是上一步計算得到的頻繁項集。所以,關聯規則的搜尋就是圍繞頻繁項集展開的。

一條規則 S➞H 的可信度定義為 P(H | S)= support(P 並 S) / support(S)。可見,可信度的計算是基於項集的支援度的

2. 關聯規則的搜尋過程

既然關聯規則來源於所有頻繁項集 ,那要怎麼搜尋呢?所有的組合都暴力窮舉嘗試一遍嗎?

顯然不是的,關聯規則的搜尋一樣可以遵循頻繁項集的層次迭代搜尋方法,即按照頻繁項集的層次結構,進行逐層搜尋

3. 關聯規則搜尋中的剪枝策略

下圖給出了從項集{0,1,2,3}產生的所有關聯規則,其中陰影區域給出的是低可信度的規則。可以發現:

如果{0,1,2}➞{3}是一條低可信度規則,那麼所有其他以3作為後件(箭頭右部包含3)的規則均為低可信度的。即如果某條規則並不滿足最小可信度要求,那麼該規則的所有子集也不會滿足最小可信度要求。

反之,如果{0,1,3}->{2},則說明{2}這個頻繁項作為後件,可以進入到下一輪的迭代層次搜尋中,繼續和本輪得到的規則列表的右部進行組合。直到搜尋一停止為止

可以利用關聯規則的上述性質屬性來減少需要測試的規則數目,類似於Apriori演算法求解頻繁項集的剪紙策略。

4. 從頻繁項集中尋找關聯規則的python示例程式碼

# coding=utf-8
from numpy import *

def loadDataSet():
    return [[1, 3, 4], [2, 3, 5], [1, 2, 3, 5], [2, 5]]

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)


# 其中D為全部資料集,
# # Ck為大小為k(包含k個元素)的候選項集,
# # minSupport為設定的最小支援度。
# # 返回值中retList為在Ck中找出的頻繁項集(支援度大於minSupport的),
# # supportData記錄各頻繁項集的支援度
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


# 生成 k+1 項集的候選項集
# 注意其生成的過程中,首選對每個項集按元素排序,然後每次比較兩個項集,只有在前k-1項相同時才將這兩項合併。
# # 這樣做是因為函式並非要兩兩合併各個集合,那樣生成的集合並非都是k+1項的。在限制項數為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


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


# 頻繁項集列表L
# 包含那些頻繁項集支援資料的字典supportData
# 最小可信度閾值minConf
def generateRules(L, supportData, minConf=0.7):
    bigRuleList = []
    # 頻繁項集是按照層次搜尋得到的, 每一層都是把具有相同元素個數的頻繁項集組織成列表,再將各個列表組成一個大列表,所以需要遍歷Len(L)次, 即逐層搜尋
    for i in range(1, len(L)):
        for freqSet in L[i]:
            H1 = [frozenset([item]) for item in freqSet]    # 對每個頻繁項集構建只包含單個元素集合的列表H1
            print "\nfreqSet: ", freqSet
            print "H1: ", H1
            rulesFromConseq(freqSet, H1, supportData, bigRuleList, minConf)     # 根據當前候選規則集H生成下一層候選規則集
    return bigRuleList


# 根據當前候選規則集H生成下一層候選規則集
def rulesFromConseq(freqSet, H, supportData, brl, minConf=0.7):
    m = len(H[0])
    while (len(freqSet) > m):  # 判斷長度 > m,這時即可求H的可信度
        H = calcConf(freqSet, H, supportData, brl, minConf)     # 返回值prunedH儲存規則列表的右部,這部分頻繁項將進入下一輪搜尋
        if (len(H) > 1):  # 判斷求完可信度後是否還有可信度大於閾值的項用來生成下一層H
            H = aprioriGen(H, m + 1)
            print "H = aprioriGen(H, m + 1): ", H
            m += 1
        else:  # 不能繼續生成下一層候選關聯規則,提前退出迴圈
            break

# 計算規則的可信度,並過濾出滿足最小可信度要求的規則
def calcConf(freqSet, H, supportData, brl, minConf=0.7):
    ''' 對候選規則集進行評估 '''
    prunedH = []
    for conseq in H:
        print "conseq: ", conseq
        print "supportData[freqSet]: ", supportData[freqSet]
        print "supportData[freqSet - conseq]: ", supportData[freqSet - conseq]
        conf = supportData[freqSet] / supportData[freqSet - conseq]
        if conf >= minConf:
            print freqSet - conseq, '-->', conseq, 'conf:', conf
            brl.append((freqSet - conseq, conseq, conf))
            prunedH.append(conseq)
            print "prunedH: ", prunedH
    return prunedH





dataSet = loadDataSet()
L, suppData = apriori(dataSet, minSupport=0.5)      # 得到頻繁項集列表L,以及每個頻繁項的支援度
print "頻繁項集L: "
for i in L:
    print i
print "頻繁項集L的支援度列表suppData: "
for key in suppData:
    print key, suppData[key]

# 基於頻繁項集生成滿足置信度閾值的關聯規則
rules = generateRules(L, suppData, minConf=0.7)
print "rules = generateRules(L, suppData, minConf=0.7)"
print "rules: ", rules


rules = generateRules(L, suppData, minConf=0.5)
#print
#print "rules = generateRules(L, suppData, minConf=0.5)"
#print "rules: ", rules

Relevant Link:

https://blog.csdn.net/baimafujinji/article/details/53456931 
https://www.cnblogs.com/llhthinker/p/6719779.html
https://www.cnblogs.com/qwertWZ/p/4510857.html

 

3. FP-growth演算法

FP-growth演算法基於Apriori構建,但採用了高階的資料結構減少掃描次數,大大加快了演算法速度。FP-growth演算法只需要對資料庫進行兩次掃描,而Apriori演算法對於每個潛在的頻繁項集都會掃描資料集判定給定模式是否頻繁,因此FP-growth演算法的速度要比Apriori演算法快。

FP-growth演算法發現頻繁項集的基本過程如下:

1. 構建FP樹
2. 從FP樹中挖掘頻繁項集

0x1:FP樹資料結構 - 用於編碼資料集的有效方式

在討論FP-growth演算法之前,我們先來討論FP樹的資料結構,可以這麼說,FP-growth演算法的高效很大程度來源組FP樹的功勞。

FP-growth演算法將資料儲存在一種稱為FP樹的緊湊資料結構中。FP代表頻繁模式(Frequent Pattern)。FP樹通過連結(link)來連線相似元素,被連起來的元素項可以看成一個連結串列。下圖給出了FP樹的一個例子。

與搜尋樹不同的是,一個元素項可以在一棵FP樹種出現多次。FP樹輝儲存項集的出現頻率,而每個項集會以路徑的方式儲存在樹中。

存在相似元素的集合會共享樹的一部分。只有當集合之間完全不同時,樹才會分叉。

樹節點上給出集合中的單個元素及其在序列中的出現次數,路徑會給出該序列的出現次數。

相似項之間的連結稱為節點連結(node link),用於快速發現相似項的位置。 

為了更好說明,我們來看用於生成上圖的原始事務資料集:

事務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

上圖中:

元素項z出現了5次,集合{r, z}出現了1次。於是可以得出結論:z一定是自己本身或者和其他符號一起出現了4次。

集合{t, s, y, x, z}出現了2次,集合{t, r, y, x, z}出現了1次,z本身單獨出現1次。

就像這樣,FP樹的解讀方式是:讀取某個節點開始到根節點的路徑。路徑上的元素構成一個頻繁項集,開始節點的值表示這個項集的支援度

根據上圖,我們可以快速讀出:

項集{z}的支援度為5;

項集{t, s, y, x, z}的支援度為2;

項集{r, y, x, z}的支援度為1;

項集{r, s, x}的支援度為1。

FP樹中會多次出現相同的元素項,也是因為同一個元素項會存在於多條路徑,構成多個頻繁項集。但是頻繁項集的共享路徑是會合並的,如圖中的{t, s, y, x, z}和{t, r, y, x, z}

和Apriori一樣,我們需要設定一個最小閾值,出現次數低於最小閾值的元素項將被直接忽略(提前剪枝)。上圖中將最小支援度設為3,所以q和p沒有在FP中出現。 

0x2:構建FP樹過程

1. 建立FP樹的資料結構

我們使用一個類表示樹結構

# coding=utf-8

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)


rootNode = treeNode('pyramid', 9, None)
rootNode.children['eye'] = treeNode('eye', 13, None)
rootNode.children['phoenix'] = treeNode('phoenix', 3, None)
rootNode.disp()

2. 構建FP樹

1)頭指標表

FP-growth演算法需要一個稱為頭指標表的資料結構,就是用來記錄各個元素項的總出現次數的陣列,再附帶一個指標指向FP樹中該元素項的第一個節點。這樣每個元素項都構成一條單連結串列。圖示說明:

這裡使用Python字典作為資料結構,來儲存頭指標表。以元素項名稱為鍵,儲存出現的總次數和一個指向第一個相似元素項的指標。

第一次遍歷資料集會獲得每個元素項的出現頻率,去掉不滿足最小支援度的元素項,生成這個頭指標表。這個過程相當於Apriori裡的1-頻繁項集的生成過程。

2)元素項排序

上文提到過,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

3)構建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的過程

實現以上邏輯的py程式碼邏輯如下:

# coding=utf-8

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)


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



''' 建立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 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) # 根節點

    print dataSet.items()
    for tranSet, count in dataSet.items():  # 第二次遍歷資料集,建立FP樹
        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


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)


def updateHeader(nodeToTest, targetNode):
    while (nodeToTest.nodeLink != None):
        nodeToTest = nodeToTest.nodeLink
    nodeToTest.nodeLink = targetNode



simpDat = loadSimpDat()
initSet = createInitSet(simpDat)
myFPtree, myHeaderTab = createTree(initSet, 3)
myFPtree.disp()

0x3:從一棵FP樹種挖掘頻繁項集

有了FP樹之後,接下來可以抽取頻繁項集了。這裡的思路與Apriori演算法大致類似,首先從單元素項集合開始,然後在此基礎上逐步構建更大的集合。

從FP樹中抽取頻繁項集的三個基本步驟如下:

1. 從FP樹中獲得條件模式基;
2. 利用條件模式基,構建一個條件FP樹;
3. 迭代重複步驟1步驟2,直到樹包含一個元素項為止。

1. 抽取條件模式基

首先從頭指標表中的每個頻繁元素項開始,對每個元素項,獲得其對應的條件模式基(conditional pattern base)。

條件模式基是以所查詢元素項為結尾的路徑集合。每一條路徑其實都是一條字首路徑(prefix path)。簡而言之,一條字首路徑是介於所查詢元素項與樹根節點之間的所有內容。

則每一個頻繁元素項的所有字首路徑(條件模式基)為:

頻繁項 字首路徑
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的條件模式基;

以此類推。

2. 建立條件FP樹

對於每一個頻繁項,都要建立一棵條件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”。

3. 遞迴查詢頻繁項集

有了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,遞迴這個過程

4. 完整FP頻繁項集挖掘過程py程式碼

# coding=utf-8

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)


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



''' 建立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 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) # 根節點

    print dataSet.items()
    for tranSet, count in dataSet.items():  # 第二次遍歷資料集,建立FP樹
        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


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)


def updateHeader(nodeToTest, targetNode):
    while (nodeToTest.nodeLink != None):
        nodeToTest = nodeToTest.nodeLink
    nodeToTest.nodeLink = targetNode


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


def ascendTree(leafNode, prefixPath):
    if leafNode.parent != None:
        prefixPath.append(leafNode.name)
        ascendTree(leafNode.parent, prefixPath)


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)


def fpGrowth(dataSet, minSup=3):
    initSet = createInitSet(dataSet)
    myFPtree, myHeaderTab = createTree(initSet, minSup)
    freqItems = []
    mineTree(myFPtree, myHeaderTab, minSup, set([]), freqItems)
    return freqItems


dataSet = loadSimpDat()
freqItems = fpGrowth(dataSet)
print freqItems

FP-growth演算法是一種用於發現資料集中頻繁模式的有效方法。FP-growth演算法利用Apriori原則,執行更快。Apriori演算法產生候選項集,然後掃描資料集來檢查它們是否頻繁。由於只對資料集掃描兩次,因此FP-growth演算法執行更快。在FP-growth演算法中,資料集儲存在一個稱為FP樹的結構中。FP樹構建完成後,可以通過查詢元素項的條件基及構建條件FP樹來發現頻繁項集。該過程不斷以更多元素作為條件重複進行,直到FP樹只包含一個元素為止。

Relevant Link: 

https://www.cnblogs.com/qwertWZ/p/4510857.html

 

4. 支援度-置信度框架的瓶頸 - 哪些模式是有趣的?強規則不一定是有趣的?

0x1:支援度-置信度框架的瓶頸

關聯規則挖掘演算法基本都使用支援度-置信度框架。但是在實際工程專案中,我們可能會期望從資料集中挖掘潛在的未知模式(0day),但是低支援度閾值挖掘或挖掘長模式時,會產生很多無趣的規則,這是關聯規則挖掘應用的瓶頸之一。

基於支援度-置信度框架識別出的強關聯規則,不足以過濾掉無趣的關聯規則,它可能僅僅是資料集中包含的一個顯而易見的統計規律,或者僅僅是我們傳入的資料集中包含了髒資料。統計有時候就是魔鬼。

0x2:相關性度量 - 提升度(lift)

為識別規則的有趣性,需使用相關性度量來擴充關聯規則的支援度-置信度框架。

相關規則不僅用支援度和置信度度量,而且還用項集A和B之間的相關性度量。一個典型的相關性度量的方法是:提升度(lift)

1. A 和 B是互相獨立的:P(A∪B) = P(A)P(B);
2. 項集A和B是依賴的(dependent)和相關的(correlated):P(A∪B) != P(A)P(B);

A和B出現之間的提升度定義為:lift(A,B) = P(A∪B) / P(A) * P(B)

如果lift(A,B)<1,則說明A的出現和B的出現是負相關的;

如果lift(A,B)>1,則A和B是正相關的,意味每一個的出現蘊涵另一個的出現;

如果lift(A,B)=1,則說明A和B是獨立的,沒有相關性。

Relevant Link:

https://blog.csdn.net/fjssharpsword/article/details/78291638
https://blog.csdn.net/dq_dm/article/details/38145075

 

5. 在實際工程專案中的思考

0x1:你的輸入資料集是什麼?是否單純?包含了哪些概率分佈假設?

在實際的機器學習工程專案中,要注意的一點是,Apriori和FP-growth是面向一個概率分佈純粹的資料集進行共現模式和關聯模式的挖掘的,例如商品交易資料中,所有的每一條資料都是交易資料,演算法是從這些商品交易資料中挖掘有趣關係。

如果要再入侵檢測場景中使用該演算法,同樣也要注意純度的問題,不要引入噪音資料,例如我們提供的資料集應該是所有發生了異常入侵事件的時間視窗內的op序列,這裡單個op序列可以抽象為商品單品,每臺機器可以抽象為一次交易。這種假設沒太大問題。它基於的假設是全網的被入侵伺服器,在大資料的場景下,都具有類似的IOC模式。

記住一句話,關聯分析演算法只是在單純從統計機器學習層面去挖掘資料集中潛在的規律和關聯,你傳入什麼資料,它就給你挖掘出什麼。所以在使用演算法的時候,一定要思考清楚你傳入的資料意味著什麼?資料中可能蘊含了哪些規則但是你不想或者沒法人肉地去自動化挖掘出來,演算法只是幫你自動化地完成了這個過程,千萬不能把演算法當成魔法,把一堆資料扔進去,妄想可以自動挖掘出0day。

0x2:頻繁項集和關聯規則對你的專案來說意味著什麼?

關聯挖掘演算法是從交易資料商機挖掘的場景中被開發出來的,它的出發點是找到交易資料中的伴隨購買以及購買推導關係鏈。這種挖掘模式在其他專案中是否能對映到一個類似的場景?這是需要開發者要去思考的。

例如,在入侵檢測場景中,我們通過Apriori挖掘得到的頻繁項集和關聯規則可能是如下的形式:

這種結果的解釋性在於:
入侵以及伴隨入侵的惡意指令碼植入及執行,都是成對出現的,並且滿足一定的先後關係。

但是在入侵檢測領域,我們知道,一次入侵往往會通過包含多種指令序列模式,我們並不需要強制在一個機器日誌中完整匹配到整個頻繁項集。
一個可行的做法是:
只取演算法得到結果的1-頻繁項集或者將所有k-頻繁項集split拆分成1-頻繁項集後,直接根據1-頻繁項集在原始日誌進行匹配,其實如果只要發現了一個頻繁項集對應的op seq序列,基本上就是認為該時間點發生了入侵事件。

0x3:其他思考

專案開發過程中,我們發現有一篇paper用的方案是非常類似的,只是業務場景稍有不同。

http://www.paper.edu.cn/scholar/showpdf/NUD2UNyINTz0MxeQh

它這有幾點很有趣的,值得去思考的:

1. 利用關聯挖掘演算法先挖掘出正常行為模式,用於進行白名單過濾
2. 加入了弱規則挖掘,即低支援度,高置信度的弱規則。根據網路攻擊的實際特點,有些攻擊是異常行為比較頻繁的攻擊,如DDOS攻擊等,通過強規則挖掘能檢測出此類攻擊;而有些攻擊異常行為不太頻繁,如慢攻擊在單位時間內異常掃描數量很少。強規則挖掘適合抓出批量大範圍行為,弱規則挖礦適合抓出0day攻擊
3. 下游stacking了貝葉斯網路來進行異常行為的最終判斷

 

相關文章