FP-Tree算法的實現
在關聯規則挖掘領域最經典的算法法是Apriori,其致命的缺點是需要多次掃描事務數據庫。於是人們提出了各種裁剪(prune)數據集的方法以減少I/O開支,韓嘉煒老師的FP-Tree算法就是其中非常高效的一種。
支持度和置信度
嚴格地說Apriori和FP-Tree都是尋找頻繁項集的算法,頻繁項集就是所謂的“支持度”比較高的項集,下面解釋一下支持度和置信度的概念。
設事務數據庫爲:
A E F G
A F G
A B E F G
E F G
則{A,F,G}的支持度數爲3,支持度爲3/4。
{F,G}的支持度數爲4,支持度爲4/4。
{A}的支持度數爲3,支持度爲3/4。
{F,G}=>{A}的置信度爲:{A,F,G}的支持度數 除以 {F,G}的支持度數,即3/4
{A}=>{F,G}的置信度爲:{A,F,G}的支持度數 除以 {A}的支持度數,即3/3
強關聯規則挖掘是在滿足一定支持度的情況下尋找置信度達到閾值的所有模式。
FP-Tree算法
我們舉個例子來詳細講解FP-Tree算法的完整實現。
事務數據庫如下,一行表示一條購物記錄:
牛奶,雞蛋,麪包,薯片
雞蛋,爆米花,薯片,啤酒
雞蛋,麪包,薯片
牛奶,雞蛋,麪包,爆米花,薯片,啤酒
牛奶,麪包,啤酒
雞蛋,麪包,啤酒
牛奶,麪包,薯片
牛奶,雞蛋,麪包,黃油,薯片
牛奶,雞蛋,黃油,薯片
我們的目的是要找出哪些商品總是相伴出現的,比如人們買薯片的時候通常也會買雞蛋,則[薯片,雞蛋]就是一條頻繁模式(frequent pattern)。
FP-Tree算法第一步:掃描事務數據庫,每項商品按頻數遞減排序,並刪除頻數小於最小支持度MinSup的商品。(第一次掃描數據庫)
薯片:7雞蛋:7麪包:7牛奶:6啤酒:4 (這裏我們令MinSup=3)
以上結果就是頻繁1項集,記爲F1。
第二步:對於每一條購買記錄,按照F1中的順序重新排序。(第二次也是最後一次掃描數據庫)
薯片,雞蛋,麪包,牛奶
薯片,雞蛋,啤酒
薯片,雞蛋,麪包
薯片,雞蛋,麪包,牛奶,啤酒
麪包,牛奶,啤酒
雞蛋,麪包,啤酒
薯片,麪包,牛奶
薯片,雞蛋,麪包,牛奶
薯片,雞蛋,牛奶
第三步:把第二步得到的各條記錄插入到FP-Tree中。剛開始時後綴模式爲空。
插入每一條(薯片,雞蛋,麪包,牛奶)之後
插入第二條記錄(薯片,雞蛋,啤酒)
插入第三條記錄(麪包,牛奶,啤酒)
估計你也知道怎麼插了,最終生成的FP-Tree是:
上圖中左邊的那一叫做表頭項,樹中相同名稱的節點要鏈接起來,鏈表的第一個元素就是表頭項裏的元素。
如果FP-Tree爲空(只含一個虛的root節點),則FP-Growth函數返回。
此時輸出表頭項的每一項+postModel,支持度爲表頭項中對應項的計數。
第四步:從FP-Tree中找出頻繁項。
遍歷表頭項中的每一項(我們拿“牛奶:6”爲例),對於各項都執行以下(1)到(5)的操作:
(1)從FP-Tree中找到所有的“牛奶”節點,向上遍歷它的祖先節點,得到4條路徑:
薯片:7,雞蛋:6,牛奶:1 薯片:7,雞蛋:6,麪包:4,牛奶:3 薯片:7,麪包:1,牛奶:1 麪包:1,牛奶:1
對於每一條路徑上的節點,其count都設置爲牛奶的count
薯片:1,雞蛋:1,牛奶:1 薯片:3,雞蛋:3,麪包:3,牛奶:3 薯片:1,麪包:1,牛奶:1 麪包:1,牛奶:1
因爲每一項末尾都是牛奶,可以把牛奶去掉,得到條件模式基(Conditional Pattern Base,CPB),此時的後綴模式是:(牛奶)。
薯片:1,雞蛋:1 薯片:3,雞蛋:3,麪包:3 薯片:1,麪包:1 麪包:1
(2)我們把上面的結果當作原始的事務數據庫,返回到第3步,遞歸迭代運行。
沒講清楚,你可以參考這篇博客,直接看核心代碼吧:
public void FPGrowth(List<List<String>> transRecords, List<String> postPattern,Context context) throws IOException, InterruptedException { // 構建項頭表,同時也是頻繁1項集 ArrayList<TreeNode> HeaderTable = buildHeaderTable(transRecords); // 構建FP-Tree TreeNode treeRoot = buildFPTree(transRecords, HeaderTable); // 如果FP-Tree爲空則返回 if (treeRoot.getChildren()==null || treeRoot.getChildren().size() == 0) return; //輸出項頭表的每一項+postPattern if(postPattern!=null){ for (TreeNode header : HeaderTable) { String outStr=header.getName(); int count=header.getCount(); for (String ele : postPattern) outStr+="\t" + ele; context.write(new IntWritable(count), new Text(outStr)); } } // 找到項頭表的每一項的條件模式基,進入遞歸迭代 for (TreeNode header : HeaderTable) { // 後綴模式增加一項 List<String> newPostPattern = new LinkedList<String>(); newPostPattern.add(header.getName()); if (postPattern != null) newPostPattern.addAll(postPattern); // 尋找header的條件模式基CPB,放入newTransRecords中 List<List<String>> newTransRecords = new LinkedList<List<String>>(); TreeNode backnode = header.getNextHomonym(); while (backnode != null) { int counter = backnode.getCount(); List<String> prenodes = new ArrayList<String>(); TreeNode parent = backnode; // 遍歷backnode的祖先節點,放到prenodes中 while ((parent = parent.getParent()).getName() != null) { prenodes.add(parent.getName()); } while (counter-- > 0) { newTransRecords.add(prenodes); } backnode = backnode.getNextHomonym(); } // 遞歸迭代 FPGrowth(newTransRecords, newPostPattern,context); } }
對於FP-Tree已經是單枝的情況,就沒有必要再遞歸調用FPGrowth了,直接輸出整條路徑上所有節點的各種組合+postModel就可了。例如當FP-Tree爲:
我們直接輸出:
3 A+postModel
3 B+postModel
3 A+B+postModel
就可以了。
如何按照上面代碼裏的做法,是先輸出:
3 A+postModel
3 B+postModel
然後把B插入到postModel的頭部,重新建立一個FP-Tree,這時Tree中只含A,於是輸出
3 A+(B+postModel)
兩種方法結果是一樣的,但畢竟重新建立FP-Tree計算量大些。
Java實現
FP樹節點定義
挖掘頻繁模式
輸入文件
牛奶,雞蛋,麪包,薯片
雞蛋,爆米花,薯片,啤酒
雞蛋,麪包,薯片
牛奶,雞蛋,麪包,爆米花,薯片,啤酒
牛奶,麪包,啤酒
雞蛋,麪包,啤酒
牛奶,麪包,薯片
牛奶,雞蛋,麪包,黃油,薯片
牛奶,雞蛋,黃油,薯片
輸出
6 薯片 雞蛋 5 薯片 麪包 5 雞蛋 麪包 4 薯片 雞蛋 麪包 5 薯片 牛奶 5 麪包 牛奶 4 雞蛋 牛奶 4 薯片 麪包 牛奶 4 薯片 雞蛋 牛奶 3 麪包 雞蛋 牛奶 3 薯片 麪包 雞蛋 牛奶 3 雞蛋 啤酒 3 麪包 啤酒
用Hadoop來實現
在上面的代碼我們把整個事務數據庫放在一個List<List<String>>裏面傳給FPGrowth,在實際中這是不可取的,因爲內存不可能容下整個事務數據庫,我們可能需要從關係關係數據庫中一條一條地讀入來建立FP-Tree。但無論如何 FP-Tree是肯定需要放在內存中的,但內存如果容不下怎麼辦?另外FPGrowth仍然是非常耗時的,你想提高速度怎麼辦?解決辦法:分而治之,並行計算。
我們把原始事務數據庫分成N部分,在N個節點上並行地進行FPGrowth挖掘,最後把關聯規則彙總到一起就可以了。關鍵問題是怎麼“劃分”纔會不遺露任何一條關聯規則呢?參見這篇博客。這裏爲了達到並行計算的目的,採用了一種“冗餘”的劃分方法,即各部分的並集大於原來的集合。這種方法最終求出來的關聯規則也是有冗餘的,比如在節點1上得到一條規則(6:啤酒,尿布),在節點2上得到一條規則(3:尿布,啤酒),顯然節點2上的這條規則是冗餘的,需要採用後續步驟把冗餘的規則去掉。
代碼:
Record.java
DC_FPTree.java
結束語
在實踐中,關聯規則挖掘可能並不像人們期望的那麼有用。一方面是因爲支持度置信度框架會產生過多的規則,並不是每一個規則都是有用的。另一方面大部分的關聯規則並不像“啤酒與尿布”這種經典故事這麼普遍。關聯規則分析是需要技巧的,有時需要用更嚴格的統計學知識來控制規則的增殖。