基于关联规则的分类(一):频繁项集挖掘的介绍与实现

一、基于关联规则的分类介绍

频繁模式和对应的关联或相关规则在一定程度上刻画了属性条件与类标号之间的有趣联系,因此将关联规则挖掘用于分类也会产生比较好的效果。

关联规则就是在给定训练项集上频繁出现的项集与项集之间的一种紧密的联系。其中“频繁”是由人为设定的一个阈值即支持度(support)来衡量,“紧密”也是由人为设定的一个关联阈值即置信度(confidence)来衡量的。这两种度量标准是频繁项集挖掘中两个至关重要的因素,也是挖掘算法的关键所在。对项集支持度和规则置信度的计算是影响挖掘算法效率的决定性因素,也是对频繁项集挖掘进行改进的入口点和研究热点。

基于关联规则的分类主要分为以下以个步骤:

  • 1. 对训练数据进行预处理(包括离散化、缺失值处理等)
  • 2. 关联规则挖掘
  • 2.1 频繁项集挖掘
  • 2.2 关联规则生成
  • 3. 规则处理
  • 4. 对测试集进行测试

二、频繁项集挖掘

目前频繁项集挖掘已经有很多比较成熟的算法,在网上也可以找到相关的优秀论文或源代码。算法中最经典的莫过于Apriori算法,它可以算得上是频繁项集挖掘算法的鼻祖,后续很多的改进算法也是基于Apriori算法的。但是遗憾的是Apriori算法的性能实在不咋的,当当玩具玩玩还可以,但是即使如此,该算法却是频繁项集挖掘必须要掌握的入门算法。

题外话:关健是要了解算法的思想,你可以不了解一个东西是怎样具体实现的,但是一定得了解它是如何出来的。这样遇到相关的问题,你可以有一个参考的解决方法,或者在关键时刻可以跟别人忽悠忽悠。当然,了解思想的最佳途径就是自己动手去实现实现了,哪怕实现得不咋样,起码思想掌握了,也是个不小的收获。

下面就要具体介绍如何利用Apriori算法进行频繁项集挖掘了。

(1)相关概念

  • 项集:“属性-值”对的集合,一般情况下在实际操作中会省略属性。
  • 候选项集:用来获取频繁项集的候选项集,候选项集中满足支持度条件的项集保留,不满足条件的舍弃。
  • 频繁项集:在所有训练元组中同时出现的次数超过人工定义的阈值的项集称为频繁项集。
  • 极大频繁项集:不存在包含当前频繁项集的频繁超集,则当前频繁项集就是极大频繁项集。
  • 支持度:项集在所有训练元组中同时出现的次数。
  • 置信度:形如A->B,置信度为60%表示60%的A出现的同时也出现B。
  • k项集:项集中的每个项有k个“属性-值”对的组合。

(2)两个定理

  • i:连接定理。若有两个k-1项集,每个项集按照“属性-值”(一般按值)的字母顺序进行排序。如果两个k-1项集的前k-2个项相同,而最后一个项不同,则证明它们是可连接的,即这个k-1项集可以联姻,即可连接生成k项集。使如有两个3项集:{a, b, c}{a, b, d},这两个3项集就是可连接的,它们可以连接生成4项集{a, b, c, d}。又如两个3项集{a, b, c}{a, d, e},这两个3项集显示是不能连接生成3项集的。要记住,强扭的瓜是不甜的,也结不了果的。
  • ii:频繁子集定理。若一个项集的子集不是频繁项集,则该项集肯定也不是频繁项集。这个很好理解,举一个例子,若存在3项集{a, b, c},如果它的2项子集{a, b}的支持度即同时出现的次数达不到阈值,则{a, b, c}同时出现的次数显然也是达不到阈值的。因此,若存在一个项集的子集不是频繁项集,那么该项集就应该被无情的舍弃。倘若你舍不得,那么这个项集就会无情的影响你的效率以及处理器资源,所以在这时,你必须无情,斩立决!

(3)Apriori算法流程

1. 扫描数据库,生成候选1项集和频繁1项集。

2. 从2项集开始循环,由频繁k-1项集生成频繁频繁k项集。

2.1 频繁k-1项集生成2项子集,这里的2项指的生成的子集中有两个k-1项集。使如有3个2项频繁集{a, b}{b, c}{c, f},则它所有的2项子集为{{a, b}{b, c}}{{a, b}{e, f}}{{b, c}{c, f}}

2.2 对由2.1生成的2项子集中的两个项集根据上面所述的定理 i 进行连接,生成k项集。

2.3 对k项集中的每个项集根据如上所述的定理 ii 进行计算,舍弃掉子集不是频繁项集即不在频繁k-1项集中的项集。

2.4 扫描数据库,计算2.3步中过滤后的k项集的支持度,舍弃掉支持度小于阈值的项集,生成频繁k项集。

3. 当当前生成的频繁k项集中只有一个项集时循环结束。

(3)Apriori算法实现

请注意:由于本人能力有限,因此算法实现显得比较臃肿笨拙,算法中没有什么亮点可言,仅仅是根据算法的思想来完成了算法本身的功能操作。因此只适合用于了解Apriori算法的思想及其实现流程。实现的算法中有很多地方可以进行优化,但是由于时间有限,没有进行深入的考量,这有待以后慢慢研究。

算法是用java实现的,实验数据作了简化,不需要预处理。实现的代码如下:

package Apriori.Me;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;

/**
* Apriori算法类
* @author Rowen
* @mail luowen3405@163.com
* @blog blog.csdn.net/luowen3405
* @data 2011.04.10
*/
public class Apriori {
private int MIN_SUPPORT = 2; //支持度
private double MIN_CONFIDENCE = 0.5; //置信度
private List> datas; //训练数据集
/*
* 生成频繁1项集
*/
public Map, Integer> getFreq1Set(){
//生成候选1项集
Map cand1Set = new HashMap();
for (int i = 0; i < datas.size(); i++) {
List data = datas.get(i);
for (int j = 0; j < data.size(); j++) {
String item = data.get(j);
if (cand1Set.containsKey(item)) {
cand1Set.put(item, cand1Set.get(item) + 1);
} else {
cand1Set.put(item, 1);
}
}
}
//生成频繁1项集
Map, Integer> freq1Set = new HashMap, Integer>();
Iterator> iter = cand1Set.entrySet().iterator();
List item = null;
while (iter.hasNext()) {
Entry entry = iter.next();
String key = entry.getKey();
int support = entry.getValue();
if (support >= MIN_SUPPORT) {
item = new ArrayList();
item.add(key);
freq1Set.put(item, support);
}
}
return freq1Set;
}
/**
* 对k项集进行剪裁,去掉k-1项子集不是频繁的k项集
* @param kItems 未经剪裁的k项集
* @param k_1Items 频繁k-1项集
* @param k_1
* @return
*/
public List> prune(List> kItems, Set> k_1Items, int k_1){
FindSubset fs = new FindSubset();
boolean isExist = false;
for (int i = 0; i < kItems.size(); i++) {
List kItem = kItems.get(i);
List> subItems = fs.execute(k_1, kItem);
for (int j = 0; j < subItems.size(); j++) {
List subItem = subItems.get(j);
Iterator> it = k_1Items.iterator();
while (it.hasNext()) {
List k_1Item = it.next();
int m;
for (m = 0; m < subItem.size(); m++) {
if (k_1Item.contains(subItem.get(m))) {
continue;
} else {
break;
}
}
if (m == subItem.size()) {
isExist = true;
break;
} else {
continue;
}
}
if (!isExist) {
kItems.remove(i);
}
}
}
return kItems;
}

/**
* 获取k项集
* @param freqK_1Set
* @param k_1 频繁k-1项集
* @return
*/
public List> getKItems(Map, Integer> freqK_1Set, int k_1){
List> kItems = new ArrayList>();
FindSubset fs = new FindSubset();
List>> subsets = fs.execute(2, freqK_1Set.keySet());
for (int i = 0; i < subsets.size(); i++) {
List> subset = subsets.get(i);
List k_1Set1 = subset.get(0);
List k_1Set2 = subset.get(1);
List kSet = new ArrayList();
for (int j = 0; j < k_1; j++) {
String k_1Items1 = k_1Set1.get(j);
String k_1Items2 = k_1Set2.get(j);
if (!kSet.contains(k_1Items1)) {
kSet.add(k_1Items1);
}
if (!kSet.contains(k_1Items2)) {
kSet.add(k_1Items2);
}
}
if (kSet.size() == k_1 + 1) {
int j;
for (j = 0; j < kItems.size(); j++) {
List kItem = kItems.get(j);
int k;
for (k = 0; k < kSet.size(); k++) {
if (kItem.contains(kSet.get(k))) {
continue;
} else {
break;
}
}
if (k == kSet.size()) {
break;
} else {
continue;
}
}
if (j == kItems.size()) {
kItems.add(kSet);
}
}
}
return kItems;
}
/**
* 获取候选k项集
* @param freqK_1Set 频繁k-1项集
* @param k_1
* @return
*/
public Map, Integer> getCandKSet(Map, Integer> freqK_1Set, int k_1){
Map, Integer> candKSet = new HashMap, Integer>();
List> kItems = getKItems(freqK_1Set, k_1);
kItems = prune(kItems, freqK_1Set.keySet(), k_1);
int[] support = new int[kItems.size()];
for (int i = 0; i < kItems.size(); i++) {
support[i] = 0;
}
for (int i = 0; i < datas.size(); i++) {
List data = datas.get(i);
for (int j = 0; j < kItems.size(); j++) {
List kItem = kItems.get(j);
if (kItem.size() > data.size()) {
continue;
} else {
int k;
for (k = 0; k < kItem.size(); k++) {
if (data.contains(kItem.get(k))) {
continue;
} else {
break;
}
}
if (k == kItem.size()) {
support[j]++;
}
}
}
}
for (int i = 0; i < kItems.size(); i++) {
candKSet.put(kItems.get(i), support[i]);
}
return candKSet;
}
/**
* 获取频繁K项集
* @param freqK_1Set 频繁k-1项集
* @param k_1
* @return
*/
public Map, Integer> getFreqKSet(Map, Integer> freqK_1Set, int k_1){
Map, Integer> candKSet = getCandKSet(freqK_1Set, k_1);
Map, Integer> freqKSet = new HashMap, Integer>();
Iterator, Integer>> iter = candKSet.entrySet().iterator();
while (iter.hasNext()) {
Entry, Integer> entry = iter.next();
List key = entry.getKey();
int support = entry.getValue();
if (support >= MIN_SUPPORT) {
freqKSet.put(key, support);
}
}
return freqKSet;
}
/**
* 运行Apriori算法
* @param datas 训练数据集
*/
public void run(List> datas) {
this.datas = datas;
Map, Integer>> freqItemsSet = new HashMap, Integer>>();
Map, Integer> currentFreqKSet = getFreq1Set();
print(currentFreqKSet, 1);
freqItemsSet.put(1, currentFreqKSet);
for (int k = 2; currentFreqKSet.size() > 1; k++) {
currentFreqKSet = getFreqKSet(currentFreqKSet, k - 1);
if (currentFreqKSet.size() >= 1) {
print(currentFreqKSet, k);
freqItemsSet.put(k, currentFreqKSet);
} else {
break;
}
}
}

/**
* 打印频繁k项集
* @param freqKSet 频繁k项集
* @param k
*/
public void print(Map, Integer> freqKSet, int k){
System.out.println("频繁" + k + "项集:");
Iterator, Integer>> iter = freqKSet.entrySet().iterator();
List item = null;
while (iter.hasNext()) {
Entry, Integer> entry = iter.next();
List key = entry.getKey();
int support = entry.getValue();
System.out.print(key.toString() + " " + support);
System.out.println();
}
}
}

package Apriori.Me;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.ArrayList;
import java.util.List;

/**
* Apriori算法测试类
* @author Rowen
* @mail luowen3405@163.com
* @blog blog.csdn.net/luowen3405
* @data 2011.04.10
*/
public class TestApriori {

public void readData(List> datas, String datafile){
BufferedReader reader = null;
try {
reader = new BufferedReader(new FileReader(new File(datafile)));
List data = null;
String line = reader.readLine();
while(line != null){
data = new ArrayList();
String[] items = line.split(" ");
for (int i = 0; i < items.length; i++) {
data.add(items[i]);
}
datas.add(data);
line = reader.readLine();
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
List> datas = new ArrayList>();
String datafile = new File("").getAbsolutePath() + File.separator + "apriori-data2";
TestApriori ta = new TestApriori();
ta.readData(datas, datafile);
Apriori apriori = new Apriori();
apriori.run(datas);
}

}

注:代码中用到的FindSubset类用于获取一个集合的任意2项子集,详情请参考本人的博文《java实现求一个项目集合任意元子集的通用算法》。在Apriori算法的使用中,对FindSubset作了一些改变,主要是因为传递的集合参数类型不一致所导致的。因此对FindSubset算法,还不算是通用的,但是可以做一些小小的改进,来实现通用性。

(4)Apriori算法测试

1. 测试数据1(类似于购物蓝数据)

a b e
b d
b c
a b d
a c
b c
a c
a b c e
a b c
a b c e

测试结果如下:([]内表示项集,后面的数字表示支持度)

频繁1项集:
[b] 8
[a] 7
[d] 2
[c] 7
[e] 3
频繁2项集:
[c, e] 2
[b, a] 5
[b, d] 2
[a, c] 5
[b, c] 5
[a, e] 3
[b, e] 3
频繁3项集:
[c, a, e] 2
[c, b, e] 2
[b, a, c] 3
[b, a, e] 3
频繁4项集:
[c, a, b, e] 2

2. 测试数据2(ID3算法实现用过的客户信息数据,yes和no表示客户是否会买电脑)

youth high stu_no fair no
youth high stu_no excellent no
middle_aged high stu_no fair yes
senior medium stu_no fair yes
senior low stu_yes fair yes
senior low stu_yes excellent no
middle_aged low stu_yes excellent yes
youth medium stu_no fair no
youth low stu_yes fair yes
senior medium stu_yes fair yes
youth medium stu_yes excellent yes
middle_aged medium stu_no excellent yes
middle_aged high stu_yes fair yes
senior medium stu_no excellent no

测试结果::([]内表示项集,后面的数字表示支持度)

频繁1项集:
[medium] 6
[stu_no] 7
[no] 5
[senior] 5
[youth] 5
[low] 4
[stu_yes] 7
[fair] 8
[middle_aged] 4
[yes] 9
[high] 4
[excellent] 6
频繁2项集:
[stu_no, high] 3
[no, senior] 2
[youth, yes] 2
[medium, stu_no] 4
[senior, fair] 3
[medium, fair] 3
[medium, excellent] 3
[fair, middle_aged] 2
[low, yes] 3
[yes, high] 2
[stu_yes, middle_aged] 2
[stu_no, yes] 3
[stu_no, middle_aged] 2
[stu_no, no] 4
[stu_yes, yes] 6
[senior, low] 2
[medium, no] 2
[youth, fair] 3
[medium, yes] 4
[stu_no, senior] 2
[stu_yes, excellent] 3
[low, stu_yes] 4
[yes, excellent] 3
[medium, senior] 3
[medium, youth] 2
[stu_no, fair] 4
[stu_yes, fair] 4
[fair, high] 3
[senior, yes] 3
[no, youth] 3
[stu_no, youth] 3
[fair, yes] 6
[middle_aged, high] 2
[medium, stu_yes] 2
[youth, high] 2
[stu_no, excellent] 3
[middle_aged, yes] 4
[senior, excellent] 2
[no, high] 2
[low, excellent] 2
[no, fair] 2
[senior, stu_yes] 3
[low, fair] 2
[middle_aged, excellent] 2
[no, excellent] 3
[youth, stu_yes] 2
[youth, excellent] 2
频繁3项集:
[stu_yes, low, excellent] 2
[no, senior, excellent] 2
[stu_yes, yes, excellent] 2
[stu_no, high, no] 2
[medium, excellent, yes] 2
[stu_no, high, fair] 2
[senior, medium, fair] 2
[stu_no, no, excellent] 2
[stu_yes, medium, yes] 2
[low, stu_yes, fair] 2
[medium, stu_no, excellent] 2
[senior, fair, yes] 3
[medium, stu_no, fair] 2
[stu_no, yes, middle_aged] 2
[fair, middle_aged, yes] 2
[medium, stu_no, senior] 2
[low, stu_yes, yes] 3
[fair, middle_aged, high] 2
[stu_yes, senior, yes] 2
[medium, yes, senior] 2
[senior, low, stu_yes] 2
[yes, middle_aged, high] 2
[stu_no, no, youth] 3
[youth, stu_no, fair] 2
[stu_yes, middle_aged, yes] 2
[yes, middle_aged, excellent] 2
[stu_no, no, fair] 2
[low, fair, yes] 2
[stu_no, high, youth] 2
[senior, stu_yes, fair] 2
[medium, stu_no, yes] 2
[stu_no, yes, fair] 2
[youth, no, fair] 2
[youth, stu_yes, yes] 2
[medium, stu_no, no] 2
[yes, fair, high] 2
[medium, fair, yes] 2
[no, youth, high] 2
[stu_yes, yes, fair] 4
频繁4项集:
[fair, middle_aged, yes, high] 2
[senior, stu_yes, fair, yes] 2
[stu_no, high, no, youth] 2
[low, stu_yes, fair, yes] 2
[senior, medium, fair, yes] 2
[stu_no, youth, no, fair] 2

测试的结果还是比较正确的,呵呵O)_(O

(5)算法实现可以改进的地方

1. 对k项集的元素按字母顺序进行排序存储在集合中,这样有利于连接的效率。因为{a, b, c, d}{a, b, e, f}判断是否能够进行连接肯定要比{a, c, d, b}{f, e, b, a}要容易而且快的多。是不是,有木有?

2. 两个2项子集进行连接之后,不可避免的会出现与其它两个2项子集连接之后的项集是相同的这种情况。例如{a, b}{b, c}连接后生成{a, b, c},而{a, c}{b, c}连接之后也会生成{a, b, c}。因此如何更快速的判断一个新连接生成的项集是否在已有的项集集合中出现过,是一个值得考虑的问题。解决办法有一个,就是将连接生成的每个项集按字母顺序排列之后加上一个“$”作为结尾字符构造一个前缀trie树(trie树的构造见本人的博文:标准trie树(前缀树)的介绍及java实现),trie树的结点是一个属性值,这样每次新连接生成的项集就可以在trie树中快速的查找到是否已经出现过。若出现,则舍弃;若没出现过,则保留并将其加入到trie树中。这个办法应该不错,是不是?有木有更好的办法呢,还有待学习和考虑?

3. 可以改进的关键之处就在于计算项集支持度了。解决办法有两个,一个是用所有的训练元组来构造一棵改进的trie树,当然要先对每个元组的属性值集合按字线顺序排好序,trie树结点为属性值,并包含一个统计次数的变量。第二个办法就是hash了,具体的就不多说了,看看书,上网找找相关的资料。

三、总结

频繁项集就挖掘到此,其实挖得很辛苦,写得也很辛苦,泪奔!下一步就是就是生成关联规则了。

另外还要说明的是,频繁项集挖掘是用来生成关联规则,而关联规则是用来分类。因此最终目的是分类,那么分类肯定要有类啊,没类还分个毛啊。类从哪里来,从生成关联规则中来,关联规则中的类从哪里来,从挖掘出的频繁项集中来。因此,大家知道该怎么做了吧,那就是毫不留情的扔掉不包含类别的频繁项集,尽管这些频繁项集是自己辛辛苦苦挖出来的,但是一点用处都没有还留个毛用啊啊啊啊啊啊!!!

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值