關聯分析算法(Association Analysis)Apriori算法和FP-growth算法初探

1. 關聯分析是什麼?

關聯分析,也叫關聯規則挖掘,屬於無監督算法的一種,它用於從數據中挖掘出潛在的關聯關係,例如經典的啤酒與尿布的關聯關係。html

本文將要重點介紹的Apriori和FP-growth算法就是一種關聯算法,,它們能夠高效自動地從數據集中挖掘出潛在的屬性關聯組合規則。java

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

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

例如,下表給出了一個這種數據集的例子,咱們一般稱其爲購物籃交易(market basket transaction)。表中每一行對應一個交易,包含一個惟一標識TID和特定顧客購買的商品集合。python

零售商對分析這些數據很感興趣,以便了解其顧客的購買行爲。可使用這種有價值的信息來支持各類商業中的實際應用,如市場促銷,庫存管理和顧客關係管理等等。算法

如今,零售商但願從這些交易記錄中發現「某種商業規律」,所謂的商業規律,是一個經濟學術語,簡單來講是由於某些事物間存在的彼此關聯和依賴的關係,從而致使這些事物成對或者按照某種肯定的前後關係成對出現的狀況。shell

例如:數據庫

  • if 豆奶 then 萵苣
  • if 豆奶 and 尿布 then 萵苣
  • .....

理論上說,任何屬性均可以伴隨着任何可能的屬性值出如今右邊,而一個單獨的關聯規則常常可以預測出不止一個屬性的值。數組

要找出這些規則,就必須對右邊的每一種可能的屬性組合,用每種可能的屬性值的組合執行一次規則概括過程。但這是理論上的分析,實際上學者們已經研究出不少更高效的算法,大大加速了這個搜索過程,咱們接下來就來討論它們。bash

可是在討論具體算法以前,筆者須要先介紹一下關聯分析算法的兩個基本分析元素。服務器

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

關聯分析是在大規模數據集中尋找關聯關係的任務。這些關係能夠有兩種形式,它們是2種遞進的抽象形式,而且前者是後者的抽象基礎:

  • 表明共現關係的頻繁項集頻繁項集(frequent item sets)是常常出如今一起的物品的集合,它暗示了某些事物之間老是結伴或成對出現。本質上來講,無論是因果關係仍是相關關係,都是共現關係,因此從這點上來說,頻繁項集是覆蓋量(coverage)這個指標的一種度量關係。
  • 表明因果/相關關係的關聯規則關聯規則(association rules)暗示兩種物品之間可能存在很強的關係,它更關注的是事物之間的互相依賴條件先驗關係。它暗示了組內某些屬性間不只共現,並且還存在明顯的相關和因果關係,關聯關係一種更強的共現關係。因此從這點上來將,關聯規則是準確率(accuracy)這個指標的一種度量關係。

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

從表中能夠看出:

  • 頻繁項集是指那些常常出如今一塊兒的商品集合,圖中的集合{葡萄酒,尿布,豆奶}就是頻繁項集的一個例子;
  • 從這個數據集中也能夠找到諸如「尿布->葡萄酒」的關聯規則,即若是有人買了尿布,那麼他極可能也會買葡萄酒。

這裏咱們注意,爲何是說尿布->葡萄酒的關聯規則,而不是葡萄酒->尿布的關聯規則呢?由於咱們注意到,在第4行,出現了尿布,可是沒有出現葡萄酒,因此這個關聯推導是不成立的,反之卻成立(至少在這個樣本數據集裏是成立的)。

在實際的關聯分析中,經常會分紅兩部分進行:

  • 第一階段:產生一個達到指定最小覆蓋量的項集
  • 第二階段:從每個項集中找出可以達到指定最小準確率的規則

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

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

1. 項與項集

是購物籃數據中全部項的集合,而是全部交易的集合。包含0個或多個項的集合被稱爲項集(itemset)

若是一個項集包含 k 個項,則稱它爲 k-項集。顯然,每一個交易包含的項集都是 I 的子集,每一個頻繁項集都是一個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=∅。

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

通俗地說,可信度置信度(confidence)是針對關聯規則來定義的。

例如咱們定義一個規則:

{尿布}➞{葡萄酒},即購買尿布的顧客也會購買啤酒

這是一個關聯規則,這個關聯規則的可信度被定義爲:

"支持度({尿布,葡萄酒}) / 支持度({尿布})"

因爲{尿布,葡萄酒}的支持度爲3/5,尿布的支持度爲4/5,因此"尿布➞葡萄酒"的可信度爲3/4。

從訓練數據的統計角度來看,這意味着對於包含"尿布"的全部記錄,咱們的規則對其中75%的記錄都適用,或者說「{尿布}➞{葡萄酒},即購買尿布的顧客也會購買啤酒」這條規則的準確度有75%。

從關聯規則的可信程度角度來看,「購買尿布的顧客會購買葡萄酒」這個商業推測,有75%的可能性是成立的,也能夠理解爲作這種商業決策,能夠得到75%的回報率指望。

筆者思考

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

0x4:關聯分析算法過程

純粹的項集是一個指數級的排列組合過程,每一個數據集均可以獲得一個天文數字的項集,而其實大多數的項集都是咱們不感興趣的,所以,分析的過程須要加入閾值判斷,對搜索進行剪枝,具體來講:

  • 頻繁項集發現階段:按照「support ≥ minsup threshold」的標準篩選知足最小支持度的頻繁項集(frequent itemset)。
  • 關聯規則發現階段:按照「confidence ≥ minconf threshold」的標準篩選知足最小置信度的強規則(strong rule)。

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

這其實是在工程化項目中須要關心的,由於咱們在一個龐大的數據集中,頻繁項集合關聯規則是很是多的,咱們不可能採納全部的這些關係,特別是在入侵檢測中,咱們每每須要提取TOP N的關聯,並將其轉化爲規則,這個過程也能夠自動化完成。

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了貝葉斯網絡來進行異常行爲的最終判斷
相關文章
相關標籤/搜索