使用Apriori算法和FP-growth算法進行關聯分析

系列文章:《機器學習實戰》學習筆記html

最近看了《機器學習實戰》中的第11章(使用Apriori算法進行關聯分析)和第12章(使用FP-growth算法來高效發現頻繁項集)。正如章節標題所示,這兩章講了無監督機器學習方法中的關聯分析問題。關聯分析能夠用於回答"哪些商品常常被同時購買?"之類的問題。書中舉了一些關聯分析的例子:node

  • 經過查看哪些商品常常在一塊兒購買,能夠幫助商店瞭解用戶的購買行爲。這種從數據海洋中抽取的知識能夠用於商品訂價、市場促銷、存活管理等環節。
  • 在美國國會投票記錄中發現關聯規則。在一個國會投票記錄的數據集中發現議案投票的相關性,(原文:這裏只是出於娛樂的目的,不過也能夠……)使用分析結果來爲政治競選活動服務,或者預測選舉官員會如何投票。
  • 發現毒蘑菇的類似特徵。這裏只對包含某個特定元素(有毒性)的項集感興趣,從中尋找毒蘑菇中的一些公共特徵,利用這些特徵來避免吃到那些有毒蘑菇。
  • 在Twitter源中發現一些共現詞。對於給定搜索詞,發現推文中頻繁出現的單詞集合。
  • 重新聞網站點擊流中挖掘新聞流行趨勢,挖掘哪些新聞普遍被用戶瀏覽到。
  • 搜索引擎推薦,在用戶輸入查詢詞時推薦同相關的查詢詞項。

從大規模數據集中尋找物品間的隱含關係被稱做關聯分析(association analysis)或者關聯規則學習(association rule learning)。這裏的主要問題在於,尋找物品的不一樣組合是一項十分耗時的任務,所需的計算代價很高,蠻力搜索方法並不能解決這個問題,因此須要用更智能的方法在合理的時間範圍內找到頻繁項集。本文分別介紹如何使用Apriori算法和FP-growth算法來解決上述問題。git

1. 關聯分析

關聯分析是在大規模數據集中尋找有趣關係的任務。這些關係能夠有兩種形式:github

  • 頻繁項集
  • 關聯規則

頻繁項集(frequent item sets)是常常出如今一起的物品的集合,關聯規則(association rules)暗示兩種物品之間可能存在很強的關係。web

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

交易號碼數據庫

商品數組

0數據結構

豆奶,萵苣app

1

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

2

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

3

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

4

萵苣,豆奶,尿布,橙汁

圖1 某雜貨店交易清單

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

咱們用支持度和可信度來度量這些有趣的關係。一個項集的支持度(support)被定義數據集中包含該項集的記錄所佔的比例。如上圖中,{豆奶}的支持度爲4/5,{豆奶,尿布}的支持度爲3/5。支持度是針對項集來講的,所以能夠定義一個最小支持度,而只保留知足最小值尺度的項集。

可信度置信度(confidence)是針對關聯規則來定義的。規則{尿布}➞{啤酒}的可信度被定義爲"支持度({尿布,啤酒})/支持度({尿布})",因爲{尿布,啤酒}的支持度爲3/5,尿布的支持度爲4/5,因此"尿布➞啤酒"的可信度爲3/4。這意味着對於包含"尿布"的全部記錄,咱們的規則對其中75%的記錄都適用。

2. Apriori原理

假設咱們有一家經營着4種商品(商品0,商品1,商品2和商品3)的雜貨店,2圖顯示了全部商品之間全部的可能組合:

圖2 集合{0,1,2,3,4}中全部可能的項集組合

對於單個項集的支持度,咱們能夠經過遍歷每條記錄並檢查該記錄是否包含該項集來計算。對於包含N中物品的數據集共有\( 2^N-1 \)種項集組合,重複上述計算過程是不現實的。

研究人員發現一種所謂的Apriori原理,能夠幫助咱們減小計算量。Apriori原理是說若是某個項集是頻繁的,那麼它的全部子集也是頻繁的。更經常使用的是它的逆否命題,即若是一個項集是非頻繁的,那麼它的全部超集也是非頻繁的

在圖3中,已知陰影項集{2,3}是非頻繁的。利用這個知識,咱們就知道項集{0,2,3},{1,2,3}以及{0,1,2,3}也是非頻繁的。也就是說,一旦計算出了{2,3}的支持度,知道它是非頻繁的後,就能夠緊接着排除{0,2,3}、{1,2,3}和{0,1,2,3}。

圖3 圖中給出了全部可能的項集,其中非頻繁項集用灰色表示。

3. 使用Apriori算法來發現頻繁集

前面提到,關聯分析的目標包括兩項:發現頻繁項集和發現關聯規則。首先須要找到頻繁項集,而後才能得到關聯規則(正如前文所講,計算關聯規則的可信度須要用到頻繁項集的支持度)。

Apriori算法是發現頻繁項集的一種方法。Apriori算法的兩個輸入參數分別是最小支持度和數據集。該算法首先會生成全部單個元素的項集列表。接着掃描數據集來查看哪些項集知足最小支持度要求,那些不知足最小支持度的集合會被去掉。而後,對剩下來的集合進行組合以生成包含兩個元素的項集。接下來,再從新掃描交易記錄,去掉不知足最小支持度的項集。該過程重複進行直到全部項集都被去掉。

3.1 生成候選項集

數據集掃描的僞代碼大體以下:

對數據集中的每條交易記錄tran:
    對每一個候選項集can:
        檢查can是不是tran的子集
        若是是,則增長can的計數
對每一個候選項集:
    若是其支持度不低於最小值,則保留該項集
返回全部頻繁項集列表

下面看一下實際代碼,創建一個apriori.py文件並加入一下代碼:

# coding=utf-8
from numpy import *

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

其中numpy爲程序中須要用到的Python庫。

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)

其中C1即爲元素個數爲1的項集(非頻繁項集,由於尚未同最小支持度比較)。map(frozenset, C1)的語義是將C1由Python列表轉換爲不變集合(frozenset,Python中的數據結構)。

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

其中D爲所有數據集,Ck爲大小爲k(包含k個元素)的候選項集,minSupport爲設定的最小支持度。返回值中retList爲在Ck中找出的頻繁項集(支持度大於minSupport的),supportData記錄各頻繁項集的支持度。

retList.insert(0, key)一行將頻繁項集插入返回列表的首部。

3.2 完整的Apriori算法

整個Apriori算法的僞代碼以下:

當集合中項的個數大於0時:
    構建一個由k個項組成的候選項集的列表(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

該函數經過頻繁項集列表$ L_k $和項集個數k生成候選項集$ C_{k+1} $。

注意其生成的過程當中,首選對每一個項集按元素排序,而後每次比較兩個項集,只有在前k-1項相同時纔將這兩項合併。這樣作是由於函數並不是要兩兩合併各個集合,那樣生成的集合並不是都是k+1項的。在限制項數爲k+1的前提下,只有在前k-1項相同、最後一項不相同的狀況下合併才爲所須要的新候選項集。

因爲Python中使用下標0表示第一個元素,所以代碼中的[:k-2]的實際做用爲取列表的前k-1個元素。

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

該函數爲Apriori算法的主函數,按照前述僞代碼的邏輯執行。Ck表示項數爲k的候選項集,最初的C1經過createC1()函數生成。Lk表示項數爲k的頻繁項集,supK爲其支持度,Lk和supK由scanD()函數經過Ck計算而來。

函數返回的L和supportData爲全部的頻繁項集及其支持度,所以在每次迭代中都要將所求得的Lk和supK添加到L和supportData中。

代碼測試(在Python提示符下輸入):

>>> import apriori
>>> dataSet = apriori.loadDataSet()
>>> dataSet
>>> C1 = apriori.createC1(dataSet)
>>> D = map(set, dataSet)
>>> D
>>> L1, suppDat = apriori.scanD(D, C1, 0.5)
>>> L1
>>> L, suppData = apriori.apriori(dataSet)
>>> L
>>> L, suppData = apriori.apriori(dataSet, minSupport=0.7)
>>> L

L返回的值爲frozenset列表的形式:

[[frozenset([1]), frozenset([3]), frozenset([2]), frozenset([5])],
[frozenset([1, 3]), frozenset([2, 5]), frozenset([2, 3]), frozenset([3, 5])],
[frozenset([2, 3, 5])], []]

即L[0]爲項數爲1的頻繁項集:

[frozenset([1]), frozenset([3]), frozenset([2]), frozenset([5])]

L[1]爲項數爲2的頻繁項集:

[frozenset([1, 3]), frozenset([2, 5]), frozenset([2, 3]), frozenset([3, 5])]

依此類推。

suppData爲一個字典,它包含項集的支持度。

3.3 從頻繁集中挖掘相關規則

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

要找到關聯規則,咱們首先從一個頻繁項集開始。從雜貨店的例子能夠獲得,若是有一個頻繁項集{豆奶, 萵苣},那麼就可能有一條關聯規則「豆奶➞萵苣」。這意味着若是有人購買了豆奶,那麼在統計上他會購買萵苣的機率較大。注意這一條反過來並不老是成立,也就是說,可信度(「豆奶➞萵苣」)並不等於可信度(「萵苣➞豆奶」)。

前文也提到過,一條規則P➞H的可信度定義爲support(P | H)/support(P),其中「|」表示P和H的並集。可見可信度的計算是基於項集的支持度的。

圖4給出了從項集{0,1,2,3}產生的全部關聯規則,其中陰影區域給出的是低可信度的規則。能夠發現若是{0,1,2}➞{3}是一條低可信度規則,那麼全部其餘以3做爲後件(箭頭右部包含3)的規則均爲低可信度的。

image

圖4 頻繁項集{0,1,2,3}的關聯規則網格示意圖

 

能夠觀察到,若是某條規則並不知足最小可信度要求,那麼該規則的全部子集也不會知足最小可信度要求。以圖4爲例,假設規則{0,1,2} ➞ {3}並不知足最小可信度要求,那麼就知道任何左部爲{0,1,2}子集的規則也不會知足最小可信度要求。能夠利用關聯規則的上述性質屬性來減小須要測試的規則數目,相似於Apriori算法求解頻繁項集。

1 書中的原始代碼

1 關聯規則生成函數:

def generateRules(L, supportData, minConf=0.7):
	bigRuleList = []
	for i in range(1, len(L)):
		for freqSet in L[i]:
			H1 = [frozenset([item]) for item in freqSet]
			if (i > 1):
				# 三個及以上元素的集合
				rulesFromConseq(freqSet, H1, supportData, bigRuleList, minConf)
			else:
				# 兩個元素的集合
				calcConf(freqSet, H1, supportData, bigRuleList, minConf)
	return bigRuleList

這個函數是主函數,調用其餘兩個函數。其餘兩個函數是rulesFromConseq()和calcConf(),分別用於生成候選規則集合以及對規則進行評估(計算支持度)

函數generateRules()有3個參數:頻繁項集列表L、包含那些頻繁項集支持數據的字典supportData、最小可信度閾值minConf。函數最後要生成一個包含可信度的規則列表bigRuleList,後面能夠基於可信度對它們進行排序。L和supportData正好爲函數apriori()的輸出。該函數遍歷L中的每個頻繁項集,並對每一個頻繁項集構建只包含單個元素集合的列表H1。代碼中的i指示當前遍歷的頻繁項集包含的元素個數,freqSet爲當前遍歷的頻繁項集(回憶L的組織結構是先把具備相同元素個數的頻繁項集組織成列表,再將各個列表組成一個大列表,因此爲遍歷L中的頻繁項集,須要使用兩層for循環)。

2 輔助函數——計算規則的可信度,並過濾出知足最小可信度要求的規則

def calcConf(freqSet, H, supportData, brl, minConf=0.7):
	''' 對候選規則集進行評估 '''
	prunedH = []
	for conseq in H:
		conf = supportData[freqSet] / supportData[freqSet - conseq]
		if conf >= minConf:
			print freqSet - conseq, '-->', conseq, 'conf:', conf
			brl.append((freqSet - conseq, conseq, conf))
			prunedH.append(conseq)
	return prunedH

計算規則的可信度以及找出知足最小可信度要求的規則。函數返回一個知足最小可信度要求的規則列表,並將這個規則列表添加到主函數的bigRuleList中(經過參數brl)。返回值prunedH保存規則列表的右部,這個值將在下一個函數rulesFromConseq()中用到。

3 輔助函數——根據當前候選規則集H生成下一層候選規則集

def rulesFromConseq(freqSet, H, supportData, brl, minConf=0.7):
	''' 生成候選規則集 '''
	m = len(H[0])
	if (len(freqSet) > (m + 1)):
		Hmpl = aprioriGen(H, m + 1)
		Hmpl = calcConf(freqSet, Hmpl, supportData, brl, minConf)
		if (len(Hmpl) > 1):
			rulesFromConseq(freqSet, Hmpl, supportData, brl, minConf)

從最初的項集中生成更多的關聯規則。該函數有兩個參數:頻繁項集freqSet,能夠出如今規則右部的元素列表H。其他參數:supportData保存項集的支持度,brl保存生成的關聯規則,minConf同主函數。函數先計算H中的頻繁項集大小m。接下來查看該頻繁項集是否大到能夠移除大小爲m的子集。若是能夠的話,則將其移除。使用函數aprioriGen()來生成H中元素的無重複組合,結果保存在Hmp1中,這也是下一次迭代的H列表。

 

實際運行效果:

>>> import apriori
>>> dataSet = apriori.loadDataSet()
>>> L, suppData = apriori.apriori(dataSet, minSupport=0.5)
>>> rules = apriori.generateRules(L, suppData, minConf=0.7)
>>> rules

frozenset([1]) --> frozenset([3]) conf: 1.0
frozenset([5]) --> frozenset([2]) conf: 1.0
frozenset([2]) --> frozenset([5]) conf: 1.0

>>> rules = apriori.generateRules(L, suppData, minConf=0.5)
>>> rules

frozenset([3]) --> frozenset([1]) conf: 0.666666666667
frozenset([1]) --> frozenset([3]) conf: 1.0
frozenset([5]) --> frozenset([2]) conf: 1.0
frozenset([2]) --> frozenset([5]) conf: 1.0
frozenset([3]) --> frozenset([2]) conf: 0.666666666667
frozenset([2]) --> frozenset([3]) conf: 0.666666666667
frozenset([5]) --> frozenset([3]) conf: 0.666666666667
frozenset([3]) --> frozenset([5]) conf: 0.666666666667
frozenset([5]) --> frozenset([2, 3]) conf: 0.666666666667
frozenset([3]) --> frozenset([2, 5]) conf: 0.666666666667
frozenset([2]) --> frozenset([3, 5]) conf: 0.666666666667

到目前爲止,若是代碼同書中同樣的話,輸出就是這樣。在這裏首先使用參數最小支持度minSupport = 0.5計算頻繁項集L和支持度suppData,而後分別計算最小可信度minConf = 0.7和minConf = 0.5的關聯規則。

2 關於rulesFromConseq()函數的問題

若是仔細看下上述代碼和輸出,會發現這裏面是一些問題的。

1 問題的提出

頻繁項集L的值前面提到過。咱們在其中計算經過{2, 3, 5}生成的關聯規則,能夠發現關聯規則{3, 5}➞{2}和{2, 3}➞{5}的可信度都應該爲1.0的,於是也應該包括在當minConf = 0.7時的rules中——可是這在前面的運行結果中並無體現出來。minConf = 0.5時也是同樣,{3, 5}➞{2}的可信度爲1.0,{2, 5}➞{3}的可信度爲2/3,{2, 3}➞{5}的可信度爲1.0,也沒有體如今rules中。

經過分析程序代碼,咱們能夠發現:

  • 當i = 1時,generateRules()函數直接調用了calcConf()函數直接計算其可信度,由於這時L[1]中的頻繁項集均包含兩個元素,能夠直接生成和判斷候選關聯規則。好比L[1]中的{2, 3},生成的候選關聯規則爲{2}➞{3}、{3}➞{2},這樣就能夠了。
  • 當i > 1時,generateRules()函數調用了rulesFromConseq()函數,這時L[i]中至少包含3個元素,如{2, 3, 5},對候選關聯規則的生成和判斷的過程須要分層進行(圖4)。這裏,將初始的H1(表示初始關聯規則的右部,即箭頭右邊的部分)做爲參數傳遞給了rulesFromConseq()函數。

例如,對於頻繁項集{a, b, c, …},H1的值爲[a, b, c, …](代碼中實際爲frozenset類型)。若是將H1帶入計算可信度的calcConf()函數,在函數中會依次計算關聯規則{b, c, d, …}➞{a}、{a, c, d, …}➞{b}、{a, b, d, …}➞{c}……的支持度,並保存支持度大於最小支持度的關聯規則,並保存這些規則的右部(prunedH,即對H的過濾,刪除支持度太小的關聯規則)。

當i > 1時沒有直接調用calcConf()函數計算經過H1生成的規則集。在rulesFromConseq()函數中,首先得到當前H的元素數m = len(H[0])(記當前的H爲$ H_m $)。當$ H_m $能夠進一步合併爲m+1元素數的集合$ H_{m+1} $時(判斷條件:len(freqSet) > (m + 1)),依次:

  • 生成$ H_{m+1} $:Hmpl = aprioriGen(H, m + 1)
  • 計算$ H_{m+1} $的可信度:Hmpl = calcConf(freqSet, Hmpl, …)
  • 遞歸計算由$ H_{m+1} $生成的關聯規則:rulesFromConseq(freqSet, Hmpl, …)

因此這裏的問題是,在i>1時,rulesFromConseq()函數中並無調用calcConf()函數計算H1的可信度,而是直接由H1生成H2,從H2開始計算關聯規則——因而由元素數>3的頻繁項集生成的{a, b, c, …}➞{x}形式的關聯規則(圖4中的第2層)均缺失了。因爲代碼示例數據中的對H1的剪枝prunedH沒有刪除任何元素,結果只是「巧合」地缺失了一層。正常狀況下若是沒有對H1進行過濾,直接生成H2,將給下一層帶入錯誤的結果(如圖4中的012➞3會被錯誤得留下來)。

2 對問題代碼的修改

在i>1時,將對H1調用calcConf()的過程加上就能夠了。好比能夠這樣:

def generateRules2(L, supportData, minConf=0.7):
	bigRuleList = []
	for i in range(1, len(L)):
		for freqSet in L[i]:
			H1 = [frozenset([item]) for item in freqSet]
			if (i > 1):
				# 三個及以上元素的集合
				H1 = calcConf(freqSet, H1, supportData, bigRuleList, minConf)
				rulesFromConseq(freqSet, H1, supportData, bigRuleList, minConf)
			else:
				# 兩個元素的集合
				calcConf(freqSet, H1, supportData, bigRuleList, minConf)
	return bigRuleList

這裏就只須要修改generateRules()函數。這樣實際運行效果中,剛纔丟失的那幾個關聯規則就都出來了。

 

進一步修改:當i=1時的else部分並無獨特的邏輯,這個if語句能夠合併,而後再修改rulesFromConseq()函數,保證其會調用calcConf(freqSet, H1, …):

def generateRules3(L, supportData, minConf=0.7):
	bigRuleList = []
	for i in range(1, len(L)):
		for freqSet in L[i]:
			H1 = [frozenset([item]) for item in freqSet]
			rulesFromConseq2(freqSet, H1, supportData, bigRuleList, minConf)
	return bigRuleList

def rulesFromConseq2(freqSet, H, supportData, brl, minConf=0.7):
	m = len(H[0])
	if (len(freqSet) > m): # 判斷長度改成 > m,這時便可以求H的可信度
		Hmpl = calcConf(freqSet, H, supportData, brl, minConf)
		if (len(Hmpl) > 1): # 判斷求完可信度後是否還有可信度大於閾值的項用來生成下一層H
			Hmpl = aprioriGen(Hmpl, m + 1)
			rulesFromConseq2(freqSet, Hmpl, supportData, brl, minConf) # 遞歸計算,不變

運行結果和generateRules2相同。

 

進一步修改:消除rulesFromConseq2()函數中的遞歸項。這個遞歸純粹是偷懶的結果,沒有簡化任何邏輯和增長任何可讀性,能夠直接用一個循環代替:

def rulesFromConseq3(freqSet, H, supportData, brl, minConf=0.7):
	m = len(H[0])
	while (len(freqSet) > m): # 判斷長度 > m,這時便可求H的可信度
		H = calcConf(freqSet, H, supportData, brl, minConf)
		if (len(H) > 1): # 判斷求完可信度後是否還有可信度大於閾值的項用來生成下一層H
			H = aprioriGen(H, m + 1)
			m += 1
		else: # 不能繼續生成下一層候選關聯規則,提早退出循環
			break

另外一個主要的區別是去掉了多餘的Hmpl變量。運行的結果和generateRules2相同。

至此,一個完整的Apriori算法就完成了。

3.4 小結

關聯分析是用於發現大數據集中元素間有趣關係的一個工具集,能夠採用兩種方式來量化這些有趣的關係。第一種方式是使用頻繁項集,它會給出常常在一塊兒出現的元素項。第二種方式是關聯規則,每條關聯規則意味着元素項之間的「若是……那麼」關係。

發現元素項間不一樣的組合是個十分耗時的任務,不可避免須要大量昂貴的計算資源,這就須要一些更智能的方法在合理的時間範圍內找到頻繁項集。可以實現這一目標的一個方法是Apriori算法,它使用Apriori原理來減小在數據庫上進行檢查的集合的數目。Apriori原理是說若是一個元素項是不頻繁的,那麼那些包含該元素的超集也是不頻繁的。Apriori算法從單元素項集開始,經過組合知足最小支持度要求的項集來造成更大的集合。支持度用來度量一個集合在原始數據中出現的頻率。

關聯分析能夠用在許多不一樣物品上。商店中的商品以及網站的訪問頁面是其中比較常見的例子。

每次增長頻繁項集的大小,Apriori算法都會從新掃描整個數據集。當數據集很大時,這會顯著下降頻繁項集發現的速度。下面會介紹FP-growth算法,和Apriori算法相比,該算法只須要對數據庫進行兩次遍歷,可以顯著加快發現頻繁項集的速度。

4. 使用FP-growth算法來高效發現頻繁項集

FP-growth算法基於Apriori構建,但採用了高級的數據結構減小掃描次數,大大加快了算法速度。FP-growth算法只須要對數據庫進行兩次掃描,而Apriori算法對於每一個潛在的頻繁項集都會掃描數據集斷定給定模式是否頻繁,所以FP-growth算法的速度要比Apriori算法快。

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

  • 構建FP樹
  • 從FP樹中挖掘頻繁項集

FP-growth算法

  • 優勢:通常要快於Apriori。
  • 缺點:實現比較困難,在某些數據集上性能會降低。
  • 適用數據類型:離散型數據。

4.1 FP樹:用於編碼數據集的有效方式

FP-growth算法將數據存儲在一種稱爲FP樹的緊湊數據結構中。FP表明頻繁模式(Frequent Pattern)。一棵FP樹看上去與計算機科學中的其餘樹結構相似,可是它經過連接(link)來鏈接類似元素,被連起來的元素項能夠當作一個鏈表。圖5給出了FP樹的一個例子。

機器學習實戰.pdf - Adobe Acrobat Pro

圖5 一棵FP樹,和通常的樹結構相似,包含着鏈接類似節點(值相同的節點)的鏈接

與搜索樹不一樣的是,一個元素項能夠在一棵FP樹種出現屢次。FP樹輝存儲項集的出現頻率,而每一個項集會以路徑的方式存儲在數中。存在類似元素的集合會共享樹的一部分。只有當集合之間徹底不一樣時,樹纔會分叉。 樹節點上給出集合中的單個元素及其在序列中的出現次數,路徑會給出該序列的出現次數。

類似項之間的連接稱爲節點連接(node link),用於快速發現類似項的位置。

舉例說明,下表用來產生圖5的FP樹:

用於生成圖5中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樹的解讀:

圖5中,元素項z出現了5次,集合{r, z}出現了1次。因而能夠得出結論:z必定是本身自己或者和其餘符號一塊兒出現了4次。集合{t, s, y, x, z}出現了2次,集合{t, r, y, x, z}出現了1次,z自己單獨出現1次。就像這樣,FP樹的解讀方式是讀取某個節點開始到根節點的路徑。路徑上的元素構成一個頻繁項集,開始節點的值表示這個項集的支持度。根據圖5,咱們能夠快速讀出項集{z}的支持度爲五、項集{t, s, y, x, z}的支持度爲二、項集{r, y, x, z}的支持度爲一、項集{r, s, x}的支持度爲1。FP樹中會屢次出現相同的元素項,也是由於同一個元素項會存在於多條路徑,構成多個頻繁項集。可是頻繁項集的共享路徑是會合並的,如圖中的{t, s, y, x, z}和{t, r, y, x, z}

和以前同樣,咱們取一個最小閾值,出現次數低於最小閾值的元素項將被直接忽略。圖5中將最小支持度設爲3,因此q和p沒有在FP中出現。

FP-growth算法的工做流程以下。首先構建FP樹,而後利用它來挖掘頻繁項集。爲構建FP樹,須要對原始數據集掃描兩遍。第一遍對全部元素項的出現次數進行計數。數據庫的第一遍掃描用來統計出現的頻率,而第二遍掃描中只考慮那些頻繁元素

4.2 構建FP樹

1 建立FP樹的數據結構

因爲樹節點的結構比較複雜,咱們使用一個類表示。建立文件fpGrowth.py並加入下列代碼:

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)

每一個樹節點由五個數據項組成:

  • name:節點元素名稱,在構造時初始化爲給定值
  • count:出現次數,在構造時初始化爲給定值
  • nodeLink:指向下一個類似節點的指針,默認爲None
  • parent:指向父節點的指針,在構造時初始化爲給定值
  • children:指向子節點的字典,以子節點的元素名稱爲鍵,指向子節點的指針爲值,初始化爲空字典

成員函數:

  • inc():增長節點的出現次數值
  • disp():輸出節點和子節點的FP樹結構

測試代碼:

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

2 構建FP樹

頭指針表

FP-growth算法還須要一個稱爲頭指針表的數據結構,其實很簡單,就是用來記錄各個元素項的總出現次數的數組,再附帶一個指針指向FP樹中該元素項的第一個節點。這樣每一個元素項都構成一條單鏈表。圖示說明:

機器學習實戰.pdf - Adobe Acrobat Pro

圖6 帶頭指針表的FP樹,頭指針表做爲一個起始指針來發現類似元素項

這裏使用Python字典做爲數據結構,來保存頭指針表。以元素項名稱爲鍵,保存出現的總次數和一個指向第一個類似元素項的指針。

第一次遍歷數據集會得到每一個元素項的出現頻率,去掉不知足最小支持度的元素項,生成這個頭指針表。

 

元素項排序

上文提到過,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樹了。從空集開始,將過濾和重排序後的頻繁項集一次添加到樹中。若是樹中已存在現有元素,則增長現有元素的值;若是現有元素不存在,則向樹添加一個分支。對前兩條事務進行添加的過程:

機器學習實戰.pdf - Adobe Acrobat Pro

圖7 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的過程

代碼(在fpGrowth.py中加入下面的代碼):

1 總函數:createTree

def createTree(dataSet, minSup=1):
	''' 建立FP樹 '''
	# 第一次遍歷數據集,建立頭指針表
	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) # 根節點
	# 第二次遍歷數據集,建立FP樹
	for tranSet, count in dataSet.items():
		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

(代碼比較寬,你們的顯示器都那麼大,應該不要緊吧……)

須要注意的是,參數中的dataSet的格式比較奇特,不是直覺上得集合的list,而是一個集合的字典,以這個集合爲鍵,值部分記錄的是這個集合出現的次數。因而要生成這個dataSet還須要後面的createInitSet()函數輔助。所以代碼中第7行中的dataSet[trans]實際得到了這個trans集合的出現次數(在本例中均爲1),一樣第21行的「for tranSet, count in dataSet.items():」得到了tranSet和count分別表示一個項集和該項集的出現次數。——這樣作是爲了適應後面在挖掘頻繁項集時生成的條件FP樹。

2 輔助函數:updateTree

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)

3 輔助函數:updateHeader

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

這個函數其實只作了一件事,就是獲取頭指針表中該元素項對應的單鏈表的尾節點,而後將其指向新節點targetNode。

 

生成數據集

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

生成的樣例數據同文中用得同樣。這個詭異的輸入格式就是createInitSet()函數中這樣來得。

 

測試代碼

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

結果是這樣的(連字都懶得打了,直接截圖……):

image

獲得的FP樹也和圖5中的同樣。

4.3 從一棵FP樹種挖掘頻繁項集

到如今爲止大部分比較困難的工做已經處理完了。有了FP樹以後,就能夠抽取頻繁項集了。這裏的思路與Apriori算法大體相似,首先從單元素項集合開始,而後在此基礎上逐步構建更大的集合。

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

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

1 抽取條件模式基

(這個翻譯是什麼鬼……英文是conditional pattern base)

首先從頭指針表中的每一個頻繁元素項開始,對每一個元素項,得到其對應的條件模式基(conditional pattern base)。條件模式基是以所查找元素項爲結尾的路徑集合。每一條路徑其實都是一條前綴路徑(prefix path)。簡而言之,一條前綴路徑是介於所查找元素項與樹根節點之間的全部內容。

將圖5從新貼在這裏:

image

則每個頻繁元素項的全部前綴路徑(條件模式基)爲:

頻繁項 前綴路徑
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樹,暫時先不考慮。如何發現某個頻繁元素項的所在的路徑?利用先前建立的頭指針表和FP樹中的類似元素節點指針,咱們已經有了每一個元素對應的單鏈表,於是能夠直接獲取。

下面的程序給出了建立前綴路徑的代碼:

1 主函數:findPrefixPath

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

該函數代碼用於爲給定元素項生成一個條件模式基(前綴路徑),這經過訪問樹中全部包含給定元素項的節點來完成。參數basePet表示輸入的頻繁項,treeNode爲當前FP樹種對應的第一個節點(可在函數外部經過headerTable[basePat][1]獲取)。函數返回值即爲條件模式基condPats,用一個字典表示,鍵爲前綴路徑,值爲計數值。

2 輔助函數:ascendTree

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

這個函數直接修改prefixPath的值,將當前節點leafNode添加到prefixPath的末尾,而後遞歸添加其父節點。最終結果,prefixPath就是一條從treeNode(包括treeNode)到根節點(不包括根節點)的路徑。在主函數findPrefixPath()中再取prefixPath[1:],即爲treeNode的前綴路徑。

測試代碼:

>>> fpGrowth.findPrefixPath('x', myHeaderTab['x'][1])
>>> fpGrowth.findPrefixPath('z', myHeaderTab['z'][1])
>>> fpGrowth.findPrefixPath('r', myHeaderTab['r'][1])

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」。

代碼(直接調用createTree()函數):

condPattBases = findPrefixPath(basePat, headerTable[basePat][1])
myCondTree, myHead = createTree(condPattBases, minSup)

示例:t的條件FP樹

image

圖8 t的條件FP樹的建立過程

在圖8中,注意到元素項s以及r是條件模式基的一部分,可是它們並不屬於條件FP樹。由於在當前的輸入中,s和r不知足最小支持度的條件。

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,遞歸這個過程

函數以下:

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)

輸入參數:

  • inTree和headerTable是由createTree()函數生成的數據集的FP樹
  • minSup表示最小支持度
  • preFix請傳入一個空集合(set([])),將在函數中用於保存當前前綴
  • freqItemList請傳入一個空列表([]),將用來儲存生成的頻繁項集

測試代碼:

>>> freqItems = []
>>> fpGrowth.mineTree(myFPtree, myHeaderTab, 3, set([]), freqItems)
>>> freqItems

[set(['y']), set(['y', 'x']), set(['y', 'z']), set(['y', 'x', 'z']), set(['s']), set(['x', 's']), set(['t']), set(['z', 't']), set(['x', 'z', 't']), set(['y', 'x', 'z', 't']), set(['y', 'z', 't']), set(['x', 't']), set(['y', 'x', 't']), set(['y', 't']), set(['r']), set(['x']), set(['x', 'z']), set(['z'])]

想這一段代碼解釋清楚比較難,由於中間涉及到不少遞歸。直接舉例說明,咱們在這裏分解輸入myFPtree和myHeaderTab後,「for basePat in bigL:」一行當basePat爲’t’時的過程:

image

圖9 mineTree函數解構圖(basePat = ‘t’)

圖中紅色加粗的部分即實際添加到freqItemList中的頻繁項集。

4 封裝

至此,完整的FP-growth算法已經能夠運行。封裝整個過程以下:

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

注意,這裏直接使用了上節(4.2)中的createInitSet()函數,這裏有個問題:上節中的loadSimpDat()函數返回了一組簡單的樣例數據,沒有相同的事務,因此createInitSet()函數中直接賦值「retDict[frozenset(trans)] = 1」沒有問題。可是若是要封裝成一個通用的FP-growth算法,就還須要處理輸入數據有相同事務的情形,createInitSet()函數中須要累加retDict[frozenset(trans)]。(謝謝@xanxuslam的回覆)

測試代碼:

>>> import fpGrowth
>>> dataSet = fpGrowth.loadSimpDat()
>>> freqItems = fpGrowth.fpGrowth(dataSet)
>>> freqItems

和以前的輸出相同。

5 總結

FP-growth算法是一種用於發現數據集中頻繁模式的有效方法。FP-growth算法利用Apriori原則,執行更快。Apriori算法產生候選項集,而後掃描數據集來檢查它們是否頻繁。因爲只對數據集掃描兩次,所以FP-growth算法執行更快。在FP-growth算法中,數據集存儲在一個稱爲FP樹的結構中。FP樹構建完成後,能夠經過查找元素項的條件基及構建條件FP樹來發現頻繁項集。該過程不斷以更多元素做爲條件重複進行,直到FP樹只包含一個元素爲止。

FP-growth算法還有一個map-reduce版本的實現,它也很不錯,能夠擴展到多臺機器上運行。Google使用該算法經過遍歷大量文原本發現頻繁共現詞,其作法和咱們剛纔介紹的例子很是相似(參見擴展閱讀:FP-growth算法)。

5. 示例:重新聞網站點擊流中挖掘新聞報道

書中的這兩章有很多精彩的示例,這裏只選取比較有表明性的一個——重新聞網站點擊流中挖掘熱門新聞報道。這是一個很大的數據集,有將近100萬條記錄(參見擴展閱讀:kosarak)。在源數據集合保存在文件kosarak.dat中。該文件中的每一行包含某個用戶瀏覽過的新聞報道。新聞報道被編碼成整數,咱們可使用Apriori或FP-growth算法挖掘其中的頻繁項集,查看那些新聞ID被用戶大量觀看到。

首先,將數據集導入到列表:

>>> parsedDat = [line.split() for line in open('kosarak.dat').readlines()]

接下來須要對初始集合格式化:

>>> import fpGrowth
>>> initSet = fpGrowth.createInitSet(parsedDat)

而後構建FP樹,並從中尋找那些至少被10萬人瀏覽過的新聞報道。

>>> myFPtree, myHeaderTab = fpGrowth.createTree(initSet, 100000)

下面建立一個空列表來保存這些頻繁項集:

>>> myFreqList = []
>>> fpGrowth.mineTree(myFPtree, myHeaderTab, 100000, set([]), myFreqList)

接下來看下有多少新聞報道或報道集合曾經被10萬或者更多的人瀏覽過:

>>> len(myFreqList)

9

總共有9個。下面看看都是那些:

>>> myFreqList

[set(['1']), set(['1', '6']), set(['3']), set(['11', '3']), set(['11', '3', '6']), set(['3', '6']), set(['11']), set(['11', '6']), set(['6'])]

擴展閱讀

在看這兩章的過程當中和以後又看到的一些相關的東西:

注:

相關文章
相關標籤/搜索