Python機器學習算法 — 關聯規則(Apriori、FP-growth)


關聯規則 -- 簡介

        關聯規則挖掘是一種基於規則的機器學習算法,該算法可以在大數據庫中發現感興趣的關系。它的目的是利用一些度量指標來分辨數據庫中存在的強規則。也即是說關聯規則挖掘是用於知識發現,而非預測,所以是屬於無監督的機器學習方法。
        Apriori算法是一種挖掘關聯規則的頻繁項集算法,其核心思想是通過候選集生成和情節的向下封閉檢測兩個階段來挖掘頻繁項集。

        關聯規則的一般步驟:
              1、找到頻繁集;
              2、在頻繁集中通過可信度篩選獲得關聯規則。
        關聯規則應用:
              1、Apriori算法應用廣泛,可用於消費市場價格分析,猜測顧客的消費習慣,比如較有名的“尿布和啤酒”的故事;
              2、網絡安全領域中的入侵檢測技術;
              3、可用在用於高校管理中,根據挖掘規則可以有效地輔助學校管理部門有針對性的開展貧困助學工作;
              4、也可用在移動通信領域中,指導運營商的業務運營和輔助業務提供商的決策制定。
        關聯規則算法的主要應用是購物籃分析,是為了從大量的訂單中發現商品潛在的關聯。其中常用的一個算法叫Apriori先驗算法。

關聯規則 -- 概念

        關聯分析(Association Analysis):在大規模數據集中尋找有趣的關系。
        頻繁項集(Frequent Item Sets):經常出現在一塊的物品的集合,即包含0個或者多個項的集合稱為項集。
        支持度(Support):數據集中包含該項集的記錄所占的比例,是針對項集來說的。
        置信度(Confidence):出現某些物品時,另外一些物品必定出現的概率,針對規則而言。
        關聯規則(Association Rules):暗示兩個物品之間可能存在很強的關系。形如A->B的表達式,規則A->B的度量包括支持度和置信度
        項集支持度:一個項集出現的次數與數據集所有事物數的百分比稱為項集的支持度
        支持度反映了A和B同時出現的概率,關聯規則的支持度等於頻繁集的支持度。
        項集置信度:包含A的數據集中包含B的百分比

        置信度反映了如果交易中包含A,則交易包含B的概率。也可以稱為在A發生的條件下,發生B的概率,成為條件概率。
        只有支持度和置信度(可信度)較高的關聯規則才是用戶感興趣的。

關聯規則 --支持度和置信度

1、支持度(Support)

        支持度揭示了A與B同時出現的概率。如果A與B同時出現的概率小,說明A與B的關系不大;如果A與B同時出現的非常頻繁,則說明A與B總是相關的。
        支持度: P(A∪B),即A和B這兩個項集在事務集D中同時出現的概率。

2、可信度(Confidence)

        置信度揭示了A出現時,B是否也會出現或有多大概率出現。如果置信度度為100%,則A和B可以捆綁銷售了。如果置信度太低,則說明A的出現與B否出現關系不大。
        置信度: P(B|A),即在出現項集A的事務集D中,項集B也同時出現的概率。

3、設定合理的支持度和置信度

        對於某條規則:(A = a)−>(B = b)(support=30%,confident=60%);其中support=30%表示在所有的數據記錄中,同時出現A=a和B=b的概率為30%;confident=60%表示在所有的數據記錄中,在出現A=a的情況下出現B=b的概率為60%,也就是條件概率。支持度揭示了A=a和B=b同時出現的概率,置信度揭示了當A=a出現時,B=b是否會一定出現的概率。
        (1)如果支持度和置信度閉值設置的過高,雖然可以減少挖掘時間,但是容易造成一些隱含在數據中非頻繁特征項被忽略掉,難以發現足夠有用的規則;
        (2)如果支持度和置信度閉值設置的過低,又有可能產生過多的規則,甚至產生大量冗余和無效的規則,同時由於算法存在的固有問題,會導致高負荷的計算量,大大增加挖掘時間。

關聯規則 -- Apriori算法

Apriori算法簡介

Apriori算法:使用候選項集找頻繁項集
         Apriori算法是一種最有影響的挖掘布爾關聯規則頻繁項集的算法。其核心是基於兩階段頻集思想的遞推算法。該關聯規則在分類上屬於單維、單層、布爾關聯規則。在這里,所有支持度大於最小支持度的項集稱為頻繁項集,簡稱頻集。
Apriori原理如果某個項集是頻繁的,那么它的所有子集也是頻繁的。該定理的逆反定理為:如果某一個項集是非頻繁的,那么它的所有超集(包含該集合的集合)也是非頻繁的。Apriori原理的出現,可以在得知某些項集是非頻繁之后,不需要計算該集合的超集,有效地避免項集數目的指數增長,從而在合理時間內計算出頻繁項集。
在圖中,已知陰影項集{2,3}是非頻繁的。利用這個知識,我們就知道項集{0,2,3},{1,2,3}以及{0,1,2,3}也是非頻繁的。也就是說,一旦計算出了{2,3}的支持度,知道它是非頻繁的后,就可以緊接着排除{0,2,3}、{1,2,3}和{0,1,2,3}。

算法思想

        ①找出所有的頻集,這些項集出現的頻繁性至少和預定義的最小支持度一樣。
        ②由頻集產生強關聯規則,這些規則必須滿足最小支持度和最小可信度。
        ③使用第1步找到的頻集產生期望的規則,產生只包含集合的項的所有規則,其中每一條規則的右部只有一項,這里采用的是中規則的定義。
        ④一旦這些規則被生成,那么只有那些大於用戶給定的最小可信度的規則才被留下來。為了生成所有頻集,使用了遞推的方法。


算法步驟

Ariori算法有兩個主要步驟:
       
 1、連接:(將項集進行兩兩連接形成新的候選集)
            利用已經找到的個項的頻繁項集,通過兩兩連接得出候選集,注意進行連接的,必須有屬性值相同,然后另外兩個不同的分別分布在中,這樣的求出的的候選集。
        
2、剪枝:(去掉非頻繁項集)
             候選集中的並不都是頻繁項集,必須剪枝去掉,越早越好以防止所處理的數據無效項越來越多。只有當子集都是頻繁集的候選集才是頻繁集,這是剪枝的依據

代碼實現

#Apriori算法實現
from numpy import *

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

# 獲取候選1項集,dataSet為事務集。返回一個list,每個元素都是set集合
def createC1(dataSet):
    C1 = []   # 元素個數為1的項集(非頻繁項集,因為還沒有同最小支持度比較)
    for transaction in dataSet:
        for item in transaction:
            if not [item] in C1:
                C1.append([item])
    C1.sort()  # 這里排序是為了,生成新的候選集時可以直接認為兩個n項候選集前面的部分相同
    # 因為除了候選1項集外其他的候選n項集都是以二維列表的形式存在,所以要將候選1項集的每一個元素都轉化為一個單獨的集合。
    return list(map(frozenset, C1))   #map(frozenset, C1)的語義是將C1由Python列表轉換為不變集合(frozenset,Python中的數據結構)

# 找出候選集中的頻繁項集
# dataSet為全部數據集,Ck為大小為k(包含k個元素)的候選項集,minSupport為設定的最小支持度
def scanD(dataSet, Ck, minSupport):
    ssCnt = {}   # 記錄每個候選項的個數
    for tid in dataSet:
        for can in Ck:
            if can.issubset(tid):
                ssCnt[can] = ssCnt.get(can, 0) + 1   # 計算每一個項集出現的頻率
    numItems = float(len(dataSet))
    retList = []
    supportData = {}
    for key in ssCnt:
        support = ssCnt[key] / numItems
        if support >= minSupport:
            retList.insert(0, key)  #將頻繁項集插入返回列表的首部
        supportData[key] = support
    return retList, supportData   #retList為在Ck中找出的頻繁項集(支持度大於minSupport的),supportData記錄各頻繁項集的支持度

# 通過頻繁項集列表Lk和項集個數k生成候選項集C(k+1)。
def aprioriGen(Lk, k):
    retList = []
    lenLk = len(Lk)
    for i in range(lenLk):
        for j in range(i + 1, lenLk):
            # 前k-1項相同時,才將兩個集合合並,合並后才能生成k+1項
            L1 = list(Lk[i])[:k-2]; L2 = list(Lk[j])[:k-2]   # 取出兩個集合的前k-1個元素
            L1.sort(); L2.sort()
            if L1 == L2:
                retList.append(Lk[i] | Lk[j])
    return retList

# 獲取事務集中的所有的頻繁項集
# Ck表示項數為k的候選項集,最初的C1通過createC1()函數生成。Lk表示項數為k的頻繁項集,supK為其支持度,Lk和supK由scanD()函數通過Ck計算而來。
def apriori(dataSet, minSupport=0.5):
    C1 = createC1(dataSet)  # 從事務集中獲取候選1項集
    D = list(map(set, dataSet))  # 將事務集的每個元素轉化為集合
    L1, supportData = scanD(D, C1, minSupport)  # 獲取頻繁1項集和對應的支持度
    L = [L1]  # L用來存儲所有的頻繁項集
    k = 2
    while (len(L[k-2]) > 0): # 一直迭代到項集數目過大而在事務集中不存在這種n項集
        Ck = aprioriGen(L[k-2], k)   # 根據頻繁項集生成新的候選項集。Ck表示項數為k的候選項集
        Lk, supK = scanD(D, Ck, minSupport)  # Lk表示項數為k的頻繁項集,supK為其支持度
        L.append(Lk);supportData.update(supK)  # 添加新頻繁項集和他們的支持度
        k += 1
    return L, supportData

if __name__=='__main__':
    dataSet = loadDataSet()  # 獲取事務集。每個元素都是列表
    # C1 = createC1(dataSet)  # 獲取候選1項集。每個元素都是集合
    # D = list(map(set, dataSet))  # 轉化事務集的形式,每個元素都轉化為集合。
    # L1, suppDat = scanD(D, C1, 0.5)
    # print(L1,suppDat)

    L, suppData = apriori(dataSet,minSupport=0.7)
    print(L,suppData)

關聯規則 -- FP樹頻集算法

算法簡介

        在關聯分析中,頻繁項集的挖掘最常用到的就是Apriori算法。Apriori算法是一種先產生候選項集再檢驗是否頻繁的“產生-測試”的方法。這種方法有種弊端:當數據集很大的時候,需要不斷掃描數據集造成運行效率很低。 
        而FP-Growth算法就很好地解決了這個問題。它的思路是把數據集中的事務映射到一棵FP-Tree上面,再根據這棵樹找出頻繁項集。FP-Tree的構建過程只需要掃描兩次數據集。 

算法步驟

        FP-growth算法發現頻繁項集的基本過程如下:
                ①構建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的過程

算法詳解

1、用FP樹編碼數據集

        FP-growth算法將數據存儲在一個稱為FP樹的緊湊數據結構中,它與計算機科學中的其他樹的結構類似,但是它通過鏈接來鏈接相似元素,被連起來的元素可以看做一個鏈表,如下圖:


 圖1--FP樹的結構示意圖

        FP樹會存儲項集出現的頻率,每個項集都會以路徑的形式存儲在樹中,存在相似元素的集合會共享樹的一部分。只有當集合之間完全不同時樹才會分叉,樹節點上給出集合中單個元素及其在序列中出現的次數,路徑會給出該序列的出現次數。
        相似項之間的鏈接即節點鏈接,用於快速發現相似項的位置,下面的例子:
圖2--用於發現圖1中FP樹的事務數據樣例
        第一列是事務的ID,第二列是事務中的元素項,在圖1中z出現了5次,而{r,z}項支出項了一次,所以z一定自己本身或者和其他的符號一起出現了4次,而由圖1同樣可知:集合{t,s,y,x,z}出現了2次,集合{t,r,y,x,z}出現了1次,所以z一定本身出現了1次。看了圖2可能會有疑問,為什么在圖1中沒有p,q,w,v等元素呢?這是因為通常會給所有的元素設置一個閾度值(Apriori里的支持度),低於這個閾值的元素不加以研究。暫時不理解沒關系,看了下面的內容可能會對這一段的內容有比較好的理解。

1.1、構建FP樹

        構建FP樹是算法的第一步,在FP樹的基礎之上再對頻繁項集進行挖掘。為了構建FP樹,要對數據集掃描兩次,第一次對所有元素項出現次數進行計數,記住如果一個元素不是頻繁的,那么包含這個元素的超集也不是頻繁的,所以不需要考慮這些超集,第二遍的掃描只考慮那些頻繁元素。
        除了圖1給出的FP樹之外,還需要一個頭指針表來指向給定類型的第一個實例。利用頭指針表可以快速訪問FP樹中一個給定類型的所有元素,發現相似元素項,如下圖所示:
圖3--帶頭指針的FP樹
        頭指針表的數據結構是字典,除了存放頭指針元素之外,還可以存放FP中每類元素的個數。第一次遍歷數據集得到每個元素項出現的頻率,接下來去掉不滿足最小值支持度的元素項,在接下來就可以創建FP樹了,構建時,將每個項集添加到一個已經存在的路徑中,如果該路徑不存在,則創建一個新的路徑。每個事務都是一個無序的集合,然而在FP樹中相同項只會出現一次,{x,y,z}和{y,z,x}應該在同一個路徑上,所以在將集合添加到樹之前要對每個集合進行排序,排序是基於各個元素出現的頻率來進行的,使用圖3頭指針表中單個元素的出現值,對圖2中的數據進行過濾,重排后的新數據如下:


圖4--移除非頻繁項,重新排序后的事務表

        現在,就可以構建FP樹了,從空集開始,向其中不斷添加頻繁項集。過濾,排序后的事務依次添加到樹中,如果樹中已有現有元素,則增加該元素的值;如果元素不存在,則添加新分枝。圖4中事務表前兩條事務添加的過程如下圖所示:


圖5--FP構建過程示例圖

2、從FP樹中挖掘頻繁項

        有了FP樹之后就可以抽取頻繁項集了,思想與Apriori算法大致一樣,從單元素項集開始,逐步構建更大的集合,只不過不需要原始的數據集了。
        從FP樹中抽取頻繁項集的三個基本步驟:
            1)從FP樹中獲得條件模式基;
            2)利用條件模式基,構建一個條件FP樹;
            3)迭代重復步驟(1)(2)直到樹只包含一個元素項為止

2.1抽取條件模式基

        條件模式基是以所查找元素項為結尾的路徑集合,每一條路徑包含一條前綴路徑和結尾元素,圖3中,符號r的前綴路徑有{x,s}、{z,x,y}和{z},每一條前綴路徑都與一個數據值關聯,這個值等於路徑上r的數目,下表中列出單元素頻繁項的所有前綴路徑。

圖6--每個頻繁項的前綴路徑

        前綴路徑將被用於構建條件FP樹。為了獲得這些路徑,可以對FP樹進行窮舉式搜索,直到獲得想要的頻繁項為止,但可以使用一個更為有效的方法加速搜索過程。可以用先前的頭指針表來創建一種更為有效的方法,頭指針表中包含相同類型元素鏈表的起始指針。一旦達了每一個元素項,就可以上溯這棵樹直到根節點為止。

2.2、創建條件FP樹

        對於每一個頻繁項,都要創建一個條件FP樹,將上面的條件模式基作為輸入,通過相同的建樹方法來構建這些條件樹,然后遞歸地發現頻繁項、發現條件模式基,以及發現另外的條件樹。舉個例子,假定為頻繁項t創建一個條件FP樹,然后對{t,y},{t,x}、...重復該過程。元素項t的條件FP樹的構建過程如下:


圖7

       s,r雖然是條件模式基的一部分,且單獨看都是頻繁項,但是在t的條件樹中,他卻是不頻繁的,分別出現了2次和一次,小於閾值3,所以{t,r},{t,s}不是頻繁的。接下來對集合{t,z},{t,x},{t,y}來挖掘對應的條件樹,會產生更復雜的頻率項集,該過程重復進行,直到條件樹中沒有元素 為止,停止。
代碼實現
# FP樹類
class treeNode:
    def __init__(self, nameValue, numOccur, parentNode):
        self.name = nameValue  #節點元素名稱,在構造時初始化為給定值
        self.count = numOccur   # 出現次數,在構造時初始化為給定值
        self.nodeLink = None   # 指向下一個相似節點的指針,默認為None
        self.parent = parentNode   # 指向父節點的指針,在構造時初始化為給定值
        self.children = {}  # 指向子節點的字典,以子節點的元素名稱為鍵,指向子節點的指針為值,初始化為空字典

    # 增加節點的出現次數值
    def inc(self, numOccur):
        self.count += numOccur

    # 輸出節點和子節點的FP樹結構
    def disp(self, ind=1):
        print(' ' * ind, self.name, ' ', self.count)
        for child in self.children.values():
            child.disp(ind + 1)

# =======================================================構建FP樹==================================================

# 對不是第一個出現的節點,更新頭指針塊。就是添加到相似元素鏈表的尾部
def updateHeader(nodeToTest, targetNode):
    while (nodeToTest.nodeLink != None):
        nodeToTest = nodeToTest.nodeLink
    nodeToTest.nodeLink = targetNode

# 根據一個排序過濾后的頻繁項更新FP樹
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)

# 主程序。創建FP樹。dataSet為事務集,為一個字典,鍵為每個事物,值為該事物出現的次數。minSup為最低支持度
def createTree(dataSet, minSup=1):
    # 第一次遍歷數據集,創建頭指針表
    headerTable = {}
    for trans in dataSet:
        for item in trans:
            headerTable[item] = headerTable.get(item, 0) + dataSet[trans]
    # 移除不滿足最小支持度的元素項
    keys = list(headerTable.keys()) # 因為字典要求在迭代中不能修改,所以轉化為列表
    for k in 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) # 根節點
    # 第二次遍歷數據集,創建FP樹
    for tranSet, count in dataSet.items():
        localD = {} # 記錄頻繁1項集的全局頻率,用於排序
        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

# =================================================查找元素條件模式基===============================================

# 直接修改prefixPath的值,將當前節點leafNode添加到prefixPath的末尾,然后遞歸添加其父節點。
# prefixPath就是一條從treeNode(包括treeNode)到根節點(不包括根節點)的路徑
def ascendTree(leafNode, prefixPath):
    if leafNode.parent != None:
        prefixPath.append(leafNode.name)
        ascendTree(leafNode.parent, prefixPath)

# 為給定元素項生成一個條件模式基(前綴路徑)。basePet表示輸入的頻繁項,treeNode為當前FP樹中對應的第一個節點
# 函數返回值即為條件模式基condPats,用一個字典表示,鍵為前綴路徑,值為計數值。
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

# =================================================遞歸查找頻繁項集===============================================
# 根據事務集獲取FP樹和頻繁項。
# 遍歷頻繁項,生成每個頻繁項的條件FP樹和條件FP樹的頻繁項
# 這樣每個頻繁項與他條件FP樹的頻繁項都構成了頻繁項集

# inTree和headerTable是由createTree()函數生成的事務集的FP樹。
# minSup表示最小支持度。
# preFix請傳入一個空集合(set([])),將在函數中用於保存當前前綴。
# freqItemList請傳入一個空列表([]),將用來儲存生成的頻繁項集。
def mineTree(inTree, headerTable, minSup, preFix, freqItemList):
    # 對頻繁項按出現的數量進行排序進行排序
    sorted_headerTable = sorted(headerTable.items(), key=lambda p: p[1][0])  #返回重新排序的列表。每個元素是一個元組,[(key,[num,treeNode],())
    bigL = [v[0] for v in sorted_headerTable]  # 獲取頻繁項
    for basePat in bigL:
        newFreqSet = preFix.copy()  # 新的頻繁項集
        newFreqSet.add(basePat)     # 當前前綴添加一個新元素
        freqItemList.append(newFreqSet)  # 所有的頻繁項集列表
        condPattBases = findPrefixPath(basePat, headerTable[basePat][1])  # 獲取條件模式基。就是basePat元素的所有前綴路徑。它像一個新的事務集
        myCondTree, myHead = createTree(condPattBases, minSup)  # 創建條件FP樹

        if myHead != None:
            # 用於測試
            print('conditional tree for:', newFreqSet)
            myCondTree.disp()
            mineTree(myCondTree, myHead, minSup, newFreqSet, freqItemList)  # 遞歸直到不再有元素

# 生成數據集
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

if __name__=='__main__':
    minSup =3
    simpDat = loadSimpDat()  # 加載數據集
    initSet = createInitSet(simpDat)  # 轉化為符合格式的事務集
    myFPtree, myHeaderTab = createTree(initSet, minSup)  # 形成FP樹
    # myFPtree.disp()  # 打印樹

    freqItems = []  # 用於存儲頻繁項集
    mineTree(myFPtree, myHeaderTab, minSup, set([]), freqItems)  # 獲取頻繁項集
    print(freqItems)  # 打印頻繁項集



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM