嚴格地說Apriori和FP-Tree都是尋找頻繁項集的算法,頻繁項集就是所謂的「支持度」比較高的項集,下面解釋一下支持度和置信度的概念。html
設事務數據庫爲:java
A E F G A F G A B E F G E F G
則{A,F,G}的支持度數爲3,支持度爲3/4。node
{F,G}的支持度數爲4,支持度爲4/4。算法
{A}的支持度數爲3,支持度爲3/4。數據庫
{F,G}=>{A}的置信度爲:{A,F,G}的支持度數 除以 {F,G}的支持度數,即3/4apache
{A}=>{F,G}的置信度爲:{A,F,G}的支持度數 除以 {A}的支持度數,即3/3app
強關聯規則挖掘是在知足必定支持度的狀況下尋找置信度達到閾值的全部模式。框架
咱們舉個例子來詳細講解FP-Tree算法的完整實現。ide
事務數據庫以下,一行表示一條購物記錄:函數
牛奶,雞蛋,麪包,薯片 雞蛋,爆米花,薯片,啤酒 雞蛋,麪包,薯片 牛奶,雞蛋,麪包,爆米花,薯片,啤酒 牛奶,麪包,啤酒 雞蛋,麪包,啤酒 牛奶,麪包,薯片 牛奶,雞蛋,麪包,黃油,薯片 牛奶,雞蛋,黃油,薯片
咱們的目的是要找出哪些商品老是相伴出現的,好比人們買薯片的時候一般也會買雞蛋,則[薯片,雞蛋]就是一條頻繁模式(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計算量大些。
FP樹節點定義
挖掘頻繁模式
輸入文件
牛奶,雞蛋,麪包,薯片 雞蛋,爆米花,薯片,啤酒 雞蛋,麪包,薯片 牛奶,雞蛋,麪包,爆米花,薯片,啤酒 牛奶,麪包,啤酒 雞蛋,麪包,啤酒 牛奶,麪包,薯片 牛奶,雞蛋,麪包,黃油,薯片 牛奶,雞蛋,黃油,薯片
輸出
6 薯片 雞蛋5 薯片 麪包5 雞蛋 麪包4 薯片 雞蛋 麪包5 薯片 牛奶5 麪包 牛奶4 雞蛋 牛奶4 薯片 麪包 牛奶4 薯片 雞蛋 牛奶3 麪包 雞蛋 牛奶3 薯片 麪包 雞蛋 牛奶3 雞蛋 啤酒3 麪包 啤酒
在上面的代碼咱們把整個事務數據庫放在一個List<List<String>>裏面傳給FPGrowth,在實際中這是不可取的,由於內存不可能容下整個事務數據庫,咱們可能須要從關係關係數據庫中一條一條地讀入來創建FP-Tree。但不管如何 FP-Tree是確定須要放在內存中的,但內存若是容不下怎麼辦?另外FPGrowth仍然是很是耗時的,你想提升速度怎麼辦?解決辦法:分而治之,並行計算。
按照論文《FP-Growth 算法MapReduce 化研究》中介紹的方法,咱們來看看語料中哪些詞老是常常出現,一句話做爲一個事務,這句話中的詞做爲項。
MR_FPTree.java
import imdm.bean.TreeNode; import ioformat.EncryptFieInputFormat; import java.io.IOException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Calendar; import java.util.Collections; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import org.apache.hadoop.conf.Configuration; import org.apache.hadoop.fs.FSDataInputStream; import org.apache.hadoop.fs.FileSystem; import org.apache.hadoop.fs.Path; import org.apache.hadoop.io.IntWritable; import org.apache.hadoop.io.LongWritable; import org.apache.hadoop.io.Text; import org.apache.hadoop.mapreduce.Job; import org.apache.hadoop.mapreduce.Mapper; import org.apache.hadoop.mapreduce.Reducer; import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat; import org.apache.hadoop.util.GenericOptionsParser; import org.apache.hadoop.util.LineReader; import org.wltea.analyzer.dic.Dictionary; import text.outservice.WordSegService; public class MR_FPTree { private static final int minSuport = 30; // 最小支持度 public static class GroupMapper extends Mapper<LongWritable, Text, Text, Text> { LinkedHashMap<String, Integer> freq = new LinkedHashMap<String, Integer>(); // 頻繁1項集 org.wltea.analyzer.cfg.Configuration cfg = null; Dictionary ikdict = null; /** * 讀取頻繁1項集 */ @Override public void setup(Context context) throws IOException { // 初始化IK分詞器 cfg = org.wltea.analyzer.cfg.DefaultConfig.getInstance(); ikdict = Dictionary.initial(cfg); // 從HDFS文件讀入頻繁1項集,即讀取IMWordCount的輸出文件,要求已經按詞頻降序排好 Configuration conf = context.getConfiguration(); FileSystem fs = FileSystem.get(conf); Calendar cad = Calendar.getInstance(); cad.add(Calendar.DAY_OF_MONTH, -1); // 昨天 SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd"); String yes_day = sdf.format(cad.getTime()); Path freqFile = new Path("/dsap/resultdata/content/WordCount/" + yes_day + "/part-r-00000"); FSDataInputStream fileIn = fs.open(freqFile); LineReader in = new LineReader(fileIn, conf); Text line = new Text(); while (in.readLine(line) > 0) { String[] arr = line.toString().split("\\s+"); if (arr.length == 2) { int count = Integer.parseInt(arr[1]); // 只讀取詞頻大於最小支持度的 if (count > minSuport) { String word = arr[0]; freq.put(word, count); } } } in.close(); } @Override public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException { String[] arr = value.toString().split("\\s+"); if (arr.length == 4) { String content = arr[3]; List<String> result = WordSegService.wordSeg(content); List<String> list = new LinkedList<String>(); for (String ele : result) { // 若是在頻繁1項集中 if (freq.containsKey(ele)) { list.add(ele.toLowerCase()); // 若是包含英文字母,則統一轉換爲小寫 } } // 對事務項中的每一項按頻繁1項集排序 Collections.sort(list, new Comparator<String>() { @Override public int compare(String s1, String s2) { return freq.get(s2) - freq.get(s1); } }); /** * 好比對於事務(中國,人民,人民,廣場),輸出(中國,人民)、(中國,人民,廣場) */ List<String> newlist = new ArrayList<String>(); newlist.add(list.get(0)); for (int i = 1; i < list.size(); i++) { // 去除list中的重複項 if (!list.get(i).equals(list.get(i - 1))) { newlist.add(list.get(i)); } } for (int i = 1; i < newlist.size(); i++) { StringBuilder sb = new StringBuilder(); for (int j = 0; j <= i; j++) { sb.append(newlist.get(j) + "\t"); } context.write(new Text(newlist.get(i)), new Text(sb.toString())); } } } } public static class FPReducer extends Reducer<Text, Text, Text, IntWritable> { public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException { List<List<String>> trans = new LinkedList<List<String>>(); // 事務數據庫 while (values.iterator().hasNext()) { String[] arr = values.iterator().next().toString() .split("\\s+"); LinkedList<String> list = new LinkedList<String>(); for (String ele : arr) list.add(ele); trans.add(list); } List<TreeNode> leafNodes = new LinkedList<TreeNode>(); // 收集FPTree中的葉節點 buildFPTree(trans, leafNodes); for (TreeNode leaf : leafNodes) { TreeNode tmpNode = leaf; List<String> associateRrule = new ArrayList<String>(); int frequency = 0; while (tmpNode.getParent() != null) { associateRrule.add(tmpNode.getName()); frequency = tmpNode.getCount(); tmpNode = tmpNode.getParent(); } // Collections.sort(associateRrule); //從根節點到葉節點已經按F1排好序了,不須要再排序了 StringBuilder sb = new StringBuilder(); for (String ele : associateRrule) { sb.append(ele + "|"); } // 由於一句話可能包含重複的詞,因此即便這些詞都是從F1中取出來的,到最後其支持度也可能小於最小支持度 if (frequency > minSuport) { context.write(new Text(sb.substring(0, sb.length() - 1) .toString()), new IntWritable(frequency)); } } } // 構建FP-Tree public TreeNode buildFPTree(List<List<String>> records, List<TreeNode> leafNodes) { TreeNode root = new TreeNode(); // 建立樹的根節點 for (List<String> record : records) { // 遍歷每一項事務 // root.printChildrenName(); insertTransToTree(root, record, leafNodes); } return root; } // 把record做爲ancestor的後代插入樹中 public void insertTransToTree(TreeNode root, List<String> record, List<TreeNode> leafNodes) { if (record.size() > 0) { String ele = record.get(0); record.remove(0); if (root.findChild(ele) != null) { root.countIncrement(1); root = root.findChild(ele); insertTransToTree(root, record, leafNodes); } else { TreeNode node = new TreeNode(ele); root.addChild(node); node.setCount(1); node.setParent(root); if (record.size() == 0) { leafNodes.add(node); // 把葉節點都放在一個鏈表中 } insertTransToTree(node, record, leafNodes); } } } } public static void main(String[] args) throws IOException, InterruptedException, ClassNotFoundException { Configuration conf = new Configuration(); String[] argv = new GenericOptionsParser(conf, args).getRemainingArgs(); if (argv.length < 2) { System.err .println("Usage: MR_FPTree EcryptedChartContent AssociateRules"); System.exit(1); } FileSystem fs = FileSystem.get(conf); Path inpath = new Path(argv[0]); Path outpath = new Path(argv[1]); fs.delete(outpath, true); Job FPTreejob = new Job(conf, "MR_FPTree"); FPTreejob.setJarByClass(MR_FPTree.class); FPTreejob.setInputFormatClass(EncryptFieInputFormat.class); EncryptFieInputFormat.addInputPath(FPTreejob, inpath); FileOutputFormat.setOutputPath(FPTreejob, outpath); FPTreejob.setMapperClass(GroupMapper.class); FPTreejob.setMapOutputKeyClass(Text.class); FPTreejob.setMapOutputValueClass(Text.class); FPTreejob.setReducerClass(FPReducer.class); FPTreejob.setOutputKeyClass(Text.class); FPTreejob.setOutputKeyClass(IntWritable.class); FPTreejob.waitForCompletion(true); } }
在實踐中,關聯規則挖掘可能並不像人們指望的那麼有用。一方面是由於支持度置信度框架會產生過多的規則,並非每個規則都是有用的。另外一方面大部分的關聯規則並不像「啤酒與尿布」這種經典故事這麼廣泛。關聯規則分析是須要技巧的,有時須要用更嚴格的統計學知識來控制規則的增殖。