關聯分析,也叫關聯規則挖掘,屬於無監督算法的一種,它用於從數據中挖掘出潛在的關聯關係,例如經典的啤酒與尿布的關聯關係。html
本文將要重點介紹的Apriori和FP-growth算法就是一種關聯算法,,它們能夠高效自動地從數據集中挖掘出潛在的屬性關聯組合規則。java
許多商業企業在日復一日的運營中積聚了大量的交易數據。例如,超市的收銀臺天天都收集大量的顧客購物數據。node
例如,下表給出了一個這種數據集的例子,咱們一般稱其爲購物籃交易(market basket transaction)。表中每一行對應一個交易,包含一個惟一標識TID和特定顧客購買的商品集合。python
零售商對分析這些數據很感興趣,以便了解其顧客的購買行爲。可使用這種有價值的信息來支持各類商業中的實際應用,如市場促銷,庫存管理和顧客關係管理等等。算法
如今,零售商但願從這些交易記錄中發現「某種商業規律」,所謂的商業規律,是一個經濟學術語,簡單來講是由於某些事物間存在的彼此關聯和依賴的關係,從而致使這些事物成對或者按照某種肯定的前後關係成對出現的狀況。shell
例如:數據庫
理論上說,任何屬性均可以伴隨着任何可能的屬性值出如今右邊,而一個單獨的關聯規則常常可以預測出不止一個屬性的值。數組
要找出這些規則,就必須對右邊的每一種可能的屬性組合,用每種可能的屬性值的組合執行一次規則概括過程。但這是理論上的分析,實際上學者們已經研究出不少更高效的算法,大大加速了這個搜索過程,咱們接下來就來討論它們。bash
可是在討論具體算法以前,筆者須要先介紹一下關聯分析算法的兩個基本分析元素。服務器
關聯分析是在大規模數據集中尋找關聯關係的任務。這些關係能夠有兩種形式,它們是2種遞進的抽象形式,而且前者是後者的抽象基礎:
下面用一個例子來講明這兩種概念:下圖給出了某個雜貨店的交易清單。
從表中能夠看出:
這裏咱們注意,爲何是說尿布->葡萄酒的關聯規則,而不是葡萄酒->尿布的關聯規則呢?由於咱們注意到,在第4行,出現了尿布,可是沒有出現葡萄酒,因此這個關聯推導是不成立的,反之卻成立(至少在這個樣本數據集裏是成立的)。
在實際的關聯分析中,經常會分紅兩部分進行:
咱們用支持度和可信度來度量事物間的關聯關係,雖然事物間的關聯關係十分複雜,可是咱們基於統計規律以及貝葉斯條件機率理論的基礎進行抽象,獲得一種數值化的度量描述。
令是購物籃數據中全部項的集合,而是全部交易的集合。包含0個或多個項的集合被稱爲項集(itemset)。
若是一個項集包含 k 個項,則稱它爲 k-項集。顯然,每一個交易包含的項集都是 I 的子集,每一個頻繁項集都是一個k-項集。
關聯規則的支持度定義以下:
其中表示事務包含集合A和B的並(即包含A和B中的每一個項)的機率。這裏的支持度也能夠理解爲項集A和項集B的共現機率。
通俗的說,一個項集的支持度(support)被定義數據集中包含該項集(多個項的組合集合)的記錄所佔的比例,也即覆蓋度。
如上圖中,
在實際的業務場景中,支持度能夠幫助咱們發現潛在的規則集合:
例如在異常進程檢測中,當同時出現{ java->bash、bash->bash }這種事件序列集合會常常在發生了反彈shell惡性入侵的機器日誌中出現(即這種組合的支持度會較高),這種頻繁項集暗示了咱們這是一個有表明性的序列標誌,極可能是exploited IOC標誌。
關聯規則是形如 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的推導關聯幾乎沒有實際意義。
純粹的項集是一個指數級的排列組合過程,每一個數據集均可以獲得一個天文數字的項集,而其實大多數的項集都是咱們不感興趣的,所以,分析的過程須要加入閾值判斷,對搜索進行剪枝,具體來講:
知足最小支持度和最小置信度的關聯規則,即待挖掘的最終關聯規則。也是咱們指望模型產出的業務結果。
這其實是在工程化項目中須要關心的,由於咱們在一個龐大的數據集中,頻繁項集合關聯規則是很是多的,咱們不可能採納全部的這些關係,特別是在入侵檢測中,咱們每每須要提取TOP N的關聯,並將其轉化爲規則,這個過程也能夠自動化完成。
一種最直接的進行關聯關係挖掘的方法或許就是暴力搜索(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
在上一小節的末尾,咱們已經討論說明了Brute-force在實際中並不可取。咱們必須設法下降產生頻繁項集的計算複雜度。
此時咱們能夠利用支持度對候選項集進行剪枝,它的核心思想是在上一輪中已經明確不能成功頻繁項集的項集就不要進入下一輪浪費時間了,只保留上一輪中的頻繁項集,在本輪繼續進行統計。
假設一個集合{A,B}是頻繁項集,即A、B同時出如今一條記錄的次數大於等於最小支持度min_support,則它的子集{A},{B}出現次數一定大於等於min_support,即它的子集都是頻繁項集。
假設集合{A}不是頻繁項集,即A出現的次數小於 min_support,則它的任何超集如{A,B}出現的次數一定小於min_support,所以其超集一定也不是頻繁項集
下圖表示當咱們發現{A,B}是非頻繁集時,就表明全部包含它的超集也是非頻繁的,便可以將它們都剪除(剪紙)
通常而言,關聯規則的挖掘是一個兩步的過程:
1. 找出全部的頻繁項集 2. 由頻繁項集產生強關聯規則
下面是一個具體的例子,最開始數據庫裏有4條交易,{A、C、D},{B、C、E},{A、B、C、E},{B、E},使用min_support=2做爲支持度閾值,最後咱們篩選出來的頻繁集爲{B、C、E}。
# 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
須要注意的是,閾值設置的越小,總體算法的運行時間就越短,由於閾值設置的越小,剪紙會更早介入。
解決了頻繁項集問題,下一步就能夠解決相關規則問題。
從前面對置信度的形式化描述咱們知道,關聯規則來源於每一輪迭代中產生的頻繁項集(從C1開始,由於空集對單項集的支持推導是沒有意義的)
從公式中能夠看到,計算關聯規則置信度的分子和分母咱們都有了,就是上一步計算獲得的頻繁項集。因此,關聯規則的搜索就是圍繞頻繁項集展開的。
一條規則 S➞H 的可信度定義爲:
P(H | S)= support(P 並 S) / support(S)
可見,可信度的計算是基於項集的支持度的。
既然關聯規則來源於全部頻繁項集 ,那要怎麼搜索呢?全部的組合都暴力窮舉嘗試一遍嗎?
顯然不是的,關聯規則的搜索同樣能夠遵循頻繁項集的層次迭代搜索方法,即按照頻繁項集的層次結構,進行逐層搜索
下圖給出了從項集{0,1,2,3}產生的全部關聯規則,其中陰影區域給出的是低可信度的規則。能夠發現:
若是{0,1,2}➞{3}是一條低可信度規則,那麼全部其餘以3做爲後件(箭頭右部包含3)的規則均爲低可信度的。即若是某條規則並不知足最小可信度要求,那麼該規則的全部子集也不會知足最小可信度要求。
反之,若是{0,1,3}->{2},則說明{2}這個頻繁項做爲後件,能夠進入到下一輪的迭代層次搜索中,繼續和本輪獲得的規則列表的右部進行組合。直到搜索一中止爲止
能夠利用關聯規則的上述性質屬性來減小須要測試的規則數目,相似於Apriori算法求解頻繁項集的剪紙策略。
# 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
FP-growth算法基於Apriori構建,但採用了高級的數據結構減小掃描次數,大大加快了算法速度。FP-growth算法只須要對數據庫進行兩次掃描,而Apriori算法對於每一個潛在的頻繁項集都會掃描數據集斷定給定模式是否頻繁,所以FP-growth算法的速度要比Apriori算法快。
FP-growth算法發現頻繁項集的基本過程以下:
1. 構建FP樹
2. 從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樹的解讀方式是:讀取某個節點開始到根節點的路徑。路徑上的元素構成一個頻繁項集,開始節點的值表示這個項集的支持度。
根據上圖,咱們能夠快速讀出:
FP樹中會屢次出現相同的元素項,也是由於同一個元素項會存在於多條路徑,構成多個頻繁項集。可是頻繁項集的共享路徑是會合並的,如圖中的{t, s, y, x, z}和{t, r, y, x, z}
和Apriori同樣,咱們須要設定一個最小閾值,出現次數低於最小閾值的元素項將被直接忽略(提早剪枝)。上圖中將最小支持度設爲3,因此q和p沒有在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()
FP-growth算法須要一個稱爲頭指針表的數據結構,就是用來記錄各個元素項的總出現次數的數組,再附帶一個指針指向FP樹中該元素項的第一個節點。這樣每一個元素項都構成一條單鏈表。圖示說明:
這裏使用Python字典做爲數據結構,來保存頭指針表。以元素項名稱爲鍵,保存出現的總次數和一個指向第一個類似元素項的指針。
第一次遍歷數據集會得到每一個元素項的出現頻率,去掉不知足最小支持度的元素項,生成這個頭指針表。這個過程至關於Apriori裏的1-頻繁項集的生成過程。
上文提到過,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 |
在對事務記錄過濾和排序以後,就能夠構建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()
有了FP樹以後,接下來能夠抽取頻繁項集了。這裏的思路與Apriori算法大體相似,首先從單元素項集合開始,而後在此基礎上逐步構建更大的集合。
從FP樹中抽取頻繁項集的三個基本步驟以下:
1. 從FP樹中得到條件模式基; 2. 利用條件模式基,構建一個條件FP樹; 3. 迭代重複步驟1步驟2,直到樹包含一個元素項爲止。
首先從頭指針表中的每一個頻繁元素項開始,對每一個元素項,得到其對應的條件模式基(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的條件模式基;
以此類推。
對於每個頻繁項,都要建立一棵條件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」。
有了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,遞歸這個過程
# 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
關聯規則挖掘算法基本都使用支持度-置信度框架。可是在實際工程項目中,咱們可能會指望從數據集中挖掘潛在的未知模式(0day),可是低支持度閾值挖掘或挖掘長模式時,會產生不少無趣的規則,這是關聯規則挖掘應用的瓶頸之一。
基於支持度-置信度框架識別出的強關聯規則,不足以過濾掉無趣的關聯規則,它可能僅僅是數據集中包含的一個顯而易見的統計規律,或者僅僅是咱們傳入的數據集中包含了髒數據。統計有時候就是魔鬼。
爲識別規則的有趣性,需使用相關性度量來擴充關聯規則的支持度-置信度框架。
相關規則不只用支持度和置信度度量,並且還用項集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)
Relevant Link:
https://blog.csdn.net/fjssharpsword/article/details/78291638 https://blog.csdn.net/dq_dm/article/details/38145075
在實際的機器學習工程項目中,要注意的一點是,Apriori和FP-growth是面向一個機率分佈純粹的數據集進行共現模式和關聯模式的挖掘的,例如商品交易數據中,全部的每一條數據都是交易數據,算法是從這些商品交易數據中挖掘有趣關係。
若是要再入侵檢測場景中使用該算法,一樣也要注意純度的問題,不要引入噪音數據,例如咱們提供的數據集應該是全部發生了異常入侵事件的時間窗口內的op序列,這裏單個op序列能夠抽象爲商品單品,每臺機器能夠抽象爲一次交易。這種假設沒太大問題。它基於的假設是全網的被入侵服務器,在大數據的場景下,都具備相似的IOC模式。
記住一句話,關聯分析算法只是在單純從統計機器學習層面去挖掘數據集中潛在的規律和關聯,你傳入什麼數據,它就給你挖掘出什麼。因此在使用算法的時候,必定要思考清楚你傳入的數據意味着什麼?數據中可能蘊含了哪些規則可是你不想或者無法人肉地去自動化挖掘出來,算法只是幫你自動化地完成了這個過程,千萬不能把算法當成魔法,把一堆數據扔進去,妄想能夠自動挖掘出0day。
關聯挖掘算法是從交易數據商機挖掘的場景中被開發出來的,它的出發點是找到交易數據中的伴隨購買以及購買推導關係鏈。這種挖掘模式在其餘項目中是否能映射到一個相似的場景?這是須要開發者要去思考的。
例如,在入侵檢測場景中,咱們經過Apriori挖掘獲得的頻繁項集和關聯規則多是以下的形式:
這種結果的解釋性在於:
入侵以及伴隨入侵的惡意腳本植入及執行,都是成對出現的,而且知足必定的前後關係。
可是在入侵檢測領域,咱們知道,一次入侵每每會經過包含多種指令序列模式,咱們並不須要強制在一個機器日誌中完整匹配到整個頻繁項集。
一個可行的作法是:
只取算法獲得結果的1-頻繁項集或者將全部k-頻繁項集split拆分紅1-頻繁項集後,直接根據1-頻繁項集在原始日誌進行匹配,其實若是隻要發現了一個頻繁項集對應的op seq序列,基本上就是認爲該時間點發生了入侵事件。
項目開發過程當中,咱們發現有一篇paper用的方案是很是相似的,只是業務場景稍有不一樣。
http://www.paper.edu.cn/scholar/showpdf/NUD2UNyINTz0MxeQh
它這有幾點頗有趣的,值得去思考的:
1. 利用關聯挖掘算法先挖掘出正常行爲模式,用於進行白名單過濾 2. 加入了弱規則挖掘,即低支持度,高置信度的弱規則。根據網絡攻擊的實際特色,有些攻擊是異常行爲比較頻繁的攻擊,如DDOS攻擊等,經過強規則挖掘能檢測出此類攻擊;而有些攻擊異常行爲不太頻繁,如慢攻擊在單位時間內異常掃描數量不多。強規則挖掘適合抓出批量大範圍行爲,弱規則挖礦適合抓出0day攻擊 3. 下游stacking了貝葉斯網絡來進行異常行爲的最終判斷