首先介绍下SPMF,SPMF是一个采用Java开发的开源数据挖掘平台。它提供了51种数据挖掘算法实现,用于:
•序列模式挖掘,
•关联规则挖掘,
•frequent itemset 挖掘,
•顺序规则挖掘,
•聚类
这几天放假研究了下Apriori算法的源代码,把总结写下,好记性不如一个烂笔头,防止以后忘。
Apriori算法的主要步骤:
- 数据读取
- 生成频繁1项集
- 如何由频繁K-1项集生成候选Ck项集(候选项集内部剪枝:确保k阶候选项集的每一个k-1阶子集都是频繁的)
- 计数、剪枝生成频繁k项集
现在将以上步骤的核心源代码的注释和分析贴在下面,Apriori算法的源代码在此处下载(eclipse编译过,可用eclipse直接打开使用):DataMiningApriori
(1)数据读取
根据输入文件IO路径,按行读取文件,每一行按照空格“ ”分割,分割后存入数组中,然后存入map中,key是每一个项,value是key出现的次数,即使支持度计数。通过这个操作,便可获得每一个项及其支持度,并且把数据库存入database中。输入文件为本地txt文件,内容为:
1 3 4
2 3 5
1 2 3 5
2 5
1 2 3 5
Map<Integer, Integer> mapItemCount = new HashMap<Integer, Integer>();
database = new ArrayList<int[]>();
BufferedReader reader = new BufferedReader(new FileReader(input));
String line;
while (((line = reader.readLine()) != null)) {
if (line.isEmpty() == true ||
line.charAt(0) == '#' || line.charAt(0) == '%'
|| line.charAt(0) == '@') {
continue;
}
String[] lineSplited = line.split(" ");
int transaction[] = new int[lineSplited.length];
for (int i=0; i< lineSplited.length; i++) {
Integer item = Integer.parseInt(lineSplited[i]);
transaction[i] = item;
Integer count = mapItemCount.get(item);
if (count == null) {
mapItemCount.put(item, 1);
} else {
mapItemCount.put(item, ++count);
}
}
database.add(transaction);
}
(2)如何由频繁K-1项集生成候选Ck项集
当k=2时,由频繁1项集生成候选2项集。这个情况时比较简单,直接依次比较组合即可,源代码也是这么实现的,在生成候选项集前,对候选项集进行排列,使后选项集按照字典序列排列:
Collections.sort(frequent1, new Comparator<Integer>() {
public int compare(Integer o1, Integer o2) {
return o1 - o2;
}
});
//生成候选2项集,存于candidate中
private List<Itemset> generateCandidate2(List<Integer> frequent1) {
List<Itemset> candidates = new ArrayList<Itemset>();
// For each itemset I1 and I2 of level k-1
for (int i = 0; i < frequent1.size(); i++) {
Integer item1 = frequent1.get(i);
for (int j = i + 1; j < frequent1.size(); j++) {
Integer item2 = frequent1.get(j);
// Create a new candidate by combining itemset1 and itemset2
candidates.add(new Itemset(new int []{item1, item2}));
}
}
return candidates;
}
当k>2时,情况比较复杂了。因为k阶候选项集的生成的前提是,前k-1阶相等,而第k阶不等,此时才能合并生成候选k项集
protected List<Itemset> generateCandidateSizeK(List<Itemset> levelK_1) {
// 创建临时变量,存储k阶候选项集
List<Itemset> candidates = new ArrayList<Itemset>();
// 从候选项集取两个存于itemset1,itemset2中
loop1: for (int i = 0; i < levelK_1.size(); i++) {
int[] itemset1 = levelK_1.get(i).itemset;
loop2: for (int j = i + 1; j < levelK_1.size(); j++) {
int[] itemset2 = levelK_1.get(j).itemset;
//合并原则: 比较 itemset1 和 itemset2.如果itemset1的前k-1项与itemset2的相同,并且最后
//一项比itemset2的小,这合并itemset1、itemset2
for (int k = 0; k < itemset1.length; k++) {
//当 k == itemset1.length - 1时,说明前k-1项不同
if (k == itemset1.length - 1) {
//如果最后一项itemset2大,违反合并原则,终止此次比较
if (itemset1[k] >= itemset2[k]) {
continue loop1;
}
}
// 不是最后一项时,如果itemset1当前项较itemset2小,继续比较
//如果如果itemset1当前项较itemset2当前项大.由于候选项集
//是字典序列,所以不符,应终止
else if (itemset1[k] < itemset2[k]) {
continue loop2;
} else if (itemset1[k] > itemset2[k]) {
continue loop1;
}
}
// 符合以上条件的,合并产生K候选项集
int newItemset[] = new int[itemset1.length+1];
System.arraycopy(itemset1, 0, newItemset, 0, itemset1.length);
newItemset[itemset1.length] = itemset2[itemset2.length -1];
//检查新产生的候选项集的所有子集是不是频繁项集,如果子集中有一项不是,则,立刻删除
//比如频繁项集1 2和1 3合并产生1 2 3,而1 2 3的子集2 3 并不是频繁项集,所以1 2 3应删除
if (allSubsetsOfSizeK_1AreFrequent(newItemset, levelK_1)) {
candidates.add(new Itemset(newItemset));
}
}
}
return candidates; // return the set of candidates
}
(3)如何扫描数据库,计算每个候选K项集的支持度
for(int[] transaction: database){
//如果当前事务的长度小于K,这终止本次扫描(这是一定的,很容易理解,如果当前事务长度小于候选项集长度,它根本就不可能包含候选项集)
if(transaction.length < k) {
System.out.println("test");
continue;
}
//接下来用从数据库中取出的事务项transaction与每个候选项依次比较,
//如果包含该候选项,则该候选支持度+1
loopCand: for(Itemset candidate : candidatesK){
int pos=0;
for(int item: transaction){
// 如果事务项transaction的item与candidate相对应的第pos个项相等
//则继续比较下一个
if(item == candidate.itemset[pos]){
pos++;
// 当pos == candidate.itemset.length说明candidate包含在transaction中
if(pos == candidate.itemset.length){
// we increase the support of this candidate
candidate.support++;
continue loopCand;
}
//如果item > candidate.itemset[pos],即当前candidate的pos位置的项比item小,
//则candidate便不可能再包含在transaction中(合并时的字典顺序决定),
//比如transaction为{ 1 3 4 6}而candidate为{1 2},当pos=1时,3<2,
//2便不可能存在于transaction中,所以不需要再进行比较,
// 而当candidate为{1 4}时,虽然3>4,但是在3后任然可能找到和4相等的数
}else if(item > candidate.itemset[pos]){
continue loopCand;
}
}
}
}
(4) 如何判断新生成的候选Ck的k-1子集都在频繁K-1项集中
至于为什么要判断,在上面的代码的注释中已经说过,由K-1阶频繁项集合并生成的k阶候选项集的子集不一定是频繁项集,可以根据这个性质进行剪枝,所以我们根据合并产生的候选k项集,检查它的所有子集是不是都在k-1频繁项集中,若在,则说明候选项集的符合条件,若不在则应删除:
protected boolean allSubsetsOfSizeK_1AreFrequent(int[] candidate, List<Itemset> levelK_1) {
// 对于某一个k阶候选项集,依次产生它的所有的k-1阶候选项集
//spfm使用了一个很让我佩服的策略是设置一个下标posRemoved,
//用该标志来指明应该被忽视的项,假设候选项candidate有三个元素{1 2 3}。
//则当posRemoved=0时,第一个元素1被忽略,即产生子集{2 3}。同理依次产生子集
for(int posRemoved=0; posRemoved< candidate.length; posRemoved++){
// 由于频繁k-1阶项集也是字典排序产生,是符合大小单调性的,因此
//这里采用了折半快速查找k阶候选项集的子集在频繁k-1阶项集中的位置
int first = 0;
int last = levelK_1.size() - 1;
boolean found = false;
while( first <= last )
{
// >>1是除2的意思
int middle = ( first + last ) >>1 ;
//samAs()是自定义的比较函数,比较k-1阶子集与middle所指的k-1阶频繁项集的大小
//若相等则返回为0,小于则返回1,大于则返回-1,下面的代码就是
//普通的折半查找算法,很容易理解,如果找到,就把found置true
int comparison = ArraysAlgos.sameAs(levelK_1.get(middle).getItems(), candidate, posRemoved);
if(comparison < 0 ){
first = middle + 1;
}
else if(comparison > 0 ){
last = middle - 1;
}
else{
found = true;
break;
}
}
if(found == false){
return false;
}
}
return true;
}
下面是sameAs()函数的源码:
//itemset1为middle指向的k-1阶频繁项集,itemsets2为k阶候选项集,
//posRemoved为需要被忽略的项的下标
public static int sameAs(int [] itemset1, int [] itemsets2, int posRemoved) {
int j=0;
for(int i=0; i<itemset1.length; i++){
//忽略posRemoved所指的项
if(j == posRemoved){
j++;
}
// 依次比较项集中的每一个项的是否相等,如果相等,则继续比较
if(itemset1[i] == itemsets2[j]){
j++;
}else if (itemset1[i] > itemsets2[j]){
return 1;
}else{
return -1;
}
}
return 0;
}