一、算法简介
Apriori算法是一种经典的频繁项集挖掘算法,用于从大规模数据集中发现频繁出现的项集。该算法基于一种叫做Apriori原理的性质,该原理指出如果一个项集是频繁的,那么它的所有子集也必须是频繁的。这个原理是算法的核心思想,它允许通过逐层增加项集的大小来逐步发现频繁项集。
以下是Apriori算法的基本步骤:
- 生成候选项集:
- 初始阶段,将每个单个项作为候选项集,统计它们的支持度(在数据集中出现的频率)。
- 然后,通过组合频繁的项集来生成更大的候选项集。这一步骤的目的是产生所有可能的项集,以便后续筛选。
- 筛选候选项集:
- 对生成的候选项集进行筛选,剔除支持度低于预设阈值的项集,得到频繁项集。
- 频繁项集是在数据集中出现频率高于阈值(最小支持度)的项集。
- 生成关联规则:
- 基于频繁项集,生成可能的关联规则。
- 关联规则是形如 A->B 的规则,表示在项集A出现时,项集B也经常出现。
- 评估关联规则:
- 对生成的关联规则进行评估,通常使用支持度、置信度等指标来衡量规则的好坏程度。
- 支持度表示包含A和B的项集在总项集中出现的频率,置信度表示在A出现的情况下B出现的概率。
Apriori算法的关键优势在于它能够通过先验知识(即Apriori原理)来减少搜索空间,从而降低计算复杂度。它被广泛应用于市场篮子分析、网络流量分析、生物信息学等领域,用于发现数据集中的潜在关联关系和规律。
Apriori原理是频繁项集挖掘算法中的一项重要原则,它用于减少搜索空间,加速频繁项集的发现过程。Apriori原理的核心思想是:如果一个项集是频繁的,那么它的所有子集也必须是频繁的。
具体来说,Apriori原理包含以下两个重要观点:
- 支持度下界性质: 如果一个项集是频繁的,那么它的所有子集一定是频繁的。这是因为对于一个项集来说,它的支持度(即在数据集中出现的频率)不能超过其子集的支持度。换句话说,如果一个项集的支持度低于设定的阈值,那么它的所有超集也一定不会达到阈值,因此不可能是频繁的。
- 连接性质: 如果一个项集是频繁的,那么它的所有子集的连接(即组合)结果也一定是频繁的。这是因为如果一个项集是频繁的,那么它的子集一定在数据集中出现过,那么它们的连接结果也一定会在数据集中出现,从而成为频繁项集。
基于这两个性质,Apriori算法可以通过不断迭代地生成并筛选候选项集来发现频繁项集。首先从单个项开始,然后逐层增加项集的大小,直到无法找到更大的频繁项集为止。这样的策略有效地减少了搜索空间,加快了频繁项集的发现过程,使得算法能够处理大规模数据集。
二、基本概念
-
项(Item): 项是指数据集中的一个元素或属性,通常用来描述一个事件或事务的特征。在市场篮子分析中,项可以是商品、产品或者属性。
-
项集(Itemset): 项集是指一个或多个项的集合。项集可以是单个项(单项集)或多个项的组合(多项集)。
-
频繁项集(Frequent Itemset): 频繁项集是指在数据集中频繁出现的项集,即其支持度(Support)不低于预设阈值的项集。频繁项集是Apriori算法的目标之一,用于发现数据集中的关联关系。
-
支持度(Support): 支持度是指一个项集在数据集中出现的频率,即项集的出现次数与总事务数之间的比率。支持度反映了项集在数据集中的普遍程度,是衡量项集重要性的指标。
-
最小支持度(Minimum Support): 最小支持度是用户事先设定的一个阈值,用于筛选频繁项集。只有支持度不低于最小支持度的项集才被认为是频繁的。
S u p p o r t ( X ) = X 出现在数据集中的项集数目 总的数据集数目 Support(X) = \frac{X出现在数据集中的项集数目}{总的数据集数目} Support(X)=总的数据集数目X出现在数据集中的项集数目 -
关联规则(Association Rules): 关联规则是指描述项集之间关联关系的规则,通常以“前项 -> 后项”的形式表示。例如,{牛奶, 面包} -> {黄油} 表示购买牛奶和面包的顾客也有可能购买黄油。
-
置信度(Confidence): 置信度是指一个关联规则的可信程度,即在前项出现的情况下,后项出现的概率。置信度反映了关联规则的强度,是衡量规则质量的指标。
C o n f i d e n c e ( X → Y ) = S u p p o r t ( X ∪ Y ) S u p p o r t ( X ) Confidence(X \rightarrow Y) = \frac{Support(X \cup Y)}{Support(X)} Confidence(X→Y)=Support(X)Support(X∪Y)
三、MapReduce
MapReduce基本语法:
-
Mapper类:Mapper负责将输入数据转换成键值对<Key, Value>。需要实现
map()
方法来定义具体的映射逻辑。 -
Reducer类:Reducer负责对Mapper输出的键值对<Key, List> 进行处理,并输出最终的结果。需要实现
reduce()
方法来定义具体的归约逻辑。 -
主程序类:通常包含
main()
方法,用于设置和运行MapReduce任务。需要创建一个Job对象,设置Mapper类、Reducer类、输入路径、输出路径等参数,并运行任务。
实现逻辑:
- 数据分片:输入数据被分成若干个数据块,每个数据块称为一个输入分片(Input Split)。
- Map阶段:
- 对于每个输入分片,MapReduce框架会创建一个Mapper实例。
- Mapper通过
map()
方法处理输入数据,将其转换成中间键值对<Intermediate Key, Intermediate Value>。
- 分区(Partition):根据中间键值对的键(Intermediate Key)进行分区,将具有相同键的键值对发送到同一个Reducer。
- 排序(Shuffle and Sort):对每个分区内的键值对进行排序,以便Reducer可以按照键的顺序处理。
- Reduce阶段:
- 对于每个分区,MapReduce框架会创建一个Reducer实例。
- Reducer通过
reduce()
方法处理相同键的键值对,将它们归约成最终的结果,然后输出。
- 输出:Reducer输出的结果被写入输出文件。
流程图:
示例:
以下是一个简单的Word Count示例,演示了MapReduce的基本实现逻辑:
javaCopy code// Mapper类
class WordCountMapper extends Mapper<LongWritable, Text, Text, IntWritable> {
private final static IntWritable one = new IntWritable(1);
private Text word = new Text();
public void map(LongWritable key, Text value, Context context) throws IOException, InterruptedException {
String[] words = value.toString().split(" ");
for (String w : words) {
word.set(w);
context.write(word, one);
}
}
}
// Reducer类
class WordCountReducer extends Reducer<Text, IntWritable, Text, IntWritable> {
private IntWritable result = new IntWritable();
public void reduce(Text key, Iterable<IntWritable> values, Context context) throws IOException, InterruptedException {
int sum = 0;
for (IntWritable val : values) {
sum += val.get();
}
result.set(sum);
context.write(key, result);
}
}
// 主程序类
public class WordCount {
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
Job job = Job.getInstance(conf, "word count");
job.setJarByClass(WordCount.class);
job.setMapperClass(WordCountMapper.class);
job.setCombinerClass(WordCountReducer.class);
job.setReducerClass(WordCountReducer.class);
job.setOutputKeyClass(Text.class);
job.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(job, new Path(args[0]));
FileOutputFormat.setOutputPath(job, new Path(args[1]));
System.exit(job.waitForCompletion(true) ? 0 : 1);
}
}
以上是一个简单的Word Count示例,演示了MapReduce的基本实现逻辑。
四、使用MapReduce实现Apriori的优点
- 可扩展性:MapReduce框架适用于处理大规模数据集,可以方便地在分布式环境中运行,因此能够处理非常大的数据集。这对于频繁项集挖掘这种需要对数据进行多次扫描的任务来说尤为重要。
- 并行处理:Apriori算法中的大部分计算可以并行处理,每个Mapper可以处理数据集的一部分,并生成局部的候选项集和频繁项集。Reducer可以将局部结果合并,得到全局的频繁项集。这样可以加快算法的运行速度。
- 容错性:MapReduce框架提供了自动的容错机制,能够处理节点故障和任务失败。这使得在大规模集群上运行Apriori算法更加可靠。
- 灵活性:MapReduce框架提供了丰富的API和工具,可以方便地对任务进行配置和管理。同时,可以通过调整MapReduce任务的数量和集群的规模来优化算法的性能。
- 与分布式存储集成:MapReduce框架与分布式存储系统(如Hadoop的HDFS)集成紧密,能够高效地读取和写入大规模数据集。这使得在处理大数据集时更加高效和方便。
综上所述,使用MapReduce实现Apriori算法能够充分发挥其在大规模数据处理方面的优势,提高算法的效率和可扩展性,从而更好地应对大数据环境下的频繁项集挖掘任务。
五、使用MapReduce实现Apriori算法
主要实现流程:
- AprioriPass1Mapper类:
- 继承自Mapper类,负责第一次pass的映射。
- 在
map()
方法中,将每一行数据按逗号分隔,并将每个项映射为键值对<项, 1>。 - 在
cleanup()
方法中,统计输入文件的行数,并将结果写入文件。
- AprioriReducer类:
- 继承自Reducer类,负责统计频繁项集和筛选。
- 在
setup()
方法中,从上一次pass的输出中获取数据,计算最小支持度minSup。 - 在
reduce()
方法中,对每个项集的计数进行累加,并筛选出支持度大于minSup的项集。
- AprioriPassKMapper类:
- 继承自Mapper类,负责后续pass的映射。
- 在
setup()
方法中,根据上一次pass的输出构建候选项集。 - 在
map()
方法中,将输入数据按行处理,与候选项集进行匹配,并映射为<项集, 1>的键值对。
- Apriori类:
- 实现了Configured接口和Tool接口,用于配置和运行MapReduce任务。
- 在
run()
方法中,循环执行K次pass,每次执行一个MapReduce任务。 - 在
runPassKMRJob()
方法中,配置MapReduce任务的参数,包括Mapper类、Reducer类、输入路径和输出路径等。 - 在
main()
方法中,解析命令行参数,调用ToolRunner.run()
方法运行MapReduce任务。
总体流程如下:
- 主程序读取输入数据路径、输出路径、支持度阈值和pass次数等参数。
- 循环执行K次pass,每次pass都调用
runPassKMRJob()
方法配置和运行MapReduce任务。 - MapReduce任务根据不同的pass选择不同的Mapper类,并执行相应的映射和归约操作。
- 在Mapper中根据候选项集与数据进行匹配映射,Reducer统计项集的频率并筛选出频繁项集。
- 输出频繁项集到指定的输出路径。
- 最终统计任务运行时间并输出。
这个实现流程利用MapReduce框架并行处理数据,将频繁项集挖掘任务分解成多个MapReduce任务,实现了大规模数据的高效处理。
流程图
实现代码:
import java.io.IOException;
import java.util.Iterator;
import java.util.StringTokenizer;
import java.util.List;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.HashMap;
import java.util.HashSet;
import java.io.*;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.io.IntWritable;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
import org.apache.hadoop.mapreduce.Mapper.Context;
import org.apache.hadoop.mapreduce.Reducer;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.mapreduce.lib.jobcontrol.JobControl;
import org.apache.hadoop.mapreduce.lib.jobcontrol.ControlledJob;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;
import org.apache.hadoop.io.*;
import org.apache.hadoop.mapreduce.*;
class AprioriPass1Mapper extends Mapper<LongWritable,Text,Text,IntWritable>{
private final static IntWritable one = new IntWritable(1);
private Text number = new Text();
private long lineCount = 0; // 添加计数器用于统计行数
//第一次pass的Mapper只要把每个item映射为1
public void map(LongWritable key,Text value,Context context) throws IOException,InterruptedException{
long lineNumber = key.get();
String line = value.toString();
if (lineNumber > 0) {
String[] parts = line.split(",");
lineCount++; // 每读取一行,增加计数器的值
for (int i = 1; i < parts.length; i++) {
context.write(new Text(parts[i]), new IntWritable(1));
}
}
}
@Override
protected void cleanup(Context context) throws IOException, InterruptedException {
String prefix = context.getConfiguration().get("hdfsOutputDirPrefix","");
String linesFile = context.getConfiguration().get("fs.default.name") + prefix + "0/part-r-00000";
try {
Configuration conf = context.getConfiguration();
FileSystem fs = FileSystem.get(conf);
Path outFile = new Path(linesFile);
OutputStream os = fs.create(outFile, true);
BufferedWriter br = new BufferedWriter(new OutputStreamWriter(os));
br.write(Long.toString(lineCount) + "\n");
br.close();
} catch (IOException e) {
e.printStackTrace();
}
super.cleanup(context);
}
}
class AprioriReducer extends Reducer<Text,IntWritable,Text,IntWritable>{
private IntWritable result = new IntWritable();
private int minSup=0;
// 所有Pass的job共用一个reducer,即统计一种itemset的个数,并筛选除大于s的
public void setup(Context context) throws IOException, InterruptedException {
String prefix = context.getConfiguration().get("hdfsOutputDirPrefix","");
String linesFile = context.getConfiguration().get("fs.default.name") + prefix + "0/part-r-00000";
double sup = context.getConfiguration().getDouble("sup",0);
try {
Configuration conf = context.getConfiguration();
FileSystem fs = FileSystem.get(conf);
Path inFile = new Path(linesFile);
BufferedReader br = new BufferedReader(new InputStreamReader(fs.open(inFile)));
String line;
long lineNumber;
if ((line = br.readLine()) != null) {
lineNumber=Long.parseLong(line);
}
br.close();
minSup = sup*(int)lineNumber;
} catch (IOException e) {
e.printStackTrace();
}
}
//所有Pass的job共用一个reducer,即统计一种itemset的个数,并筛选除大于s的
public void reduce(Text key,Iterable<IntWritable> values,Context context) throws IOException,InterruptedException{
int sum = 0;
for(IntWritable val : values){
sum += val.get();
}
result.set(sum);
if(sum > minSup){
context.write(key,result);
}
}
}
class AprioriPassKMapper extends Mapper<LongWritable,Text,Text,IntWritable>{
private final static IntWritable one = new IntWritable(1);
private Text item = new Text();
private List< HashSet<String> > prevItemsets = new ArrayList< HashSet<String> >();
private List< HashSet<String> > candidateItemsets = new ArrayList< HashSet<String> >();
//第一个以后的pass使用该Mapper,在map函数执行前会执行setup来从k-1次pass的输出中构建候选itemsets,对应于apriori算法
@Override
public void setup(Context context) throws IOException, InterruptedException{
int passNum = context.getConfiguration().getInt("passNum",2);
String prefix = context.getConfiguration().get("hdfsOutputDirPrefix","");
String lastPass = context.getConfiguration().get("fs.default.name") + prefix + (passNum - 1) + "/part-r-00000";
try{
Path path = new Path(lastPass);
FileSystem fs = FileSystem.get(context.getConfiguration());
BufferedReader fis = new BufferedReader(new InputStreamReader(fs.open(path)));
String line = null;
while((line = fis.readLine()) != null){
HashSet<String> itemset = new HashSet<String>();
String itemsStr = line.split("\\t")[0];
for(String itemStr : itemsStr.split(",")){
itemset.add(itemStr);
}
prevItemsets.add(itemset);
}
}catch (Exception e){
e.printStackTrace();
}
//get candidate itemsets from the prev itemsets
candidateItemsets = getCandidateItemsets(prevItemsets,passNum - 1);
}
public void map(LongWritable key,Text value,Context context) throws IOException,InterruptedException{
HashSet<String> itemset = new HashSet<String>();
long lineNumber = key.get();
String line = value.toString();
if (lineNumber > 0) {
String[] parts = line.split(",");
for (String part : parts) {
itemset.add(part);
}
}
for(HashSet<String> candidateItemset : candidateItemsets){
// 如果输入的一行中包含该候选集合,则映射1,这样来统计候选集合被包括的次数
// 子集合,消耗掉了大部分时间
if(itemset.containsAll(candidateItemset)){
StringBuilder outputKeyBuilder = new StringBuilder();
for(String item : candidateItemset){
outputKeyBuilder.append(item).append(",");
}
String outputKey = outputKeyBuilder.toString();
outputKey = outputKey.substring(0, outputKey.length() - 1);
context.write(new Text(outputKey), one);
}
}
}
//获取所有候选集合,参考apriori算法
private List< HashSet<String> > getCandidateItemsets(List< HashSet<String> > prevItemsets, int passNum){
List< HashSet<String> > candidateItemsets = new ArrayList<HashSet<String> >();
//上次pass的输出中选取连个itemset构造大小为k + 1的候选集合
for(int i = 0;i < prevItemsets.size();i++){
for(int j = i + 1;j < prevItemsets.size();j++) {
HashSet<String> outerItems = prevItemsets.get(i);
HashSet<String> innerItems = prevItemsets.get(j);
HashSet<String> newItems = new HashSet<>();
newItems.addAll(outerItems);
newItems.addAll(innerItems);
if (newItems.size() == outerItems.size() + 1) {
if(newItems == null){continue;}
//候选集合必须满足所有的子集都在上次pass的输出中,调用isCandidate进行检测,通过后加入到候选子集和列表
if(!hasInfrequentSubSet(newItems,prevItemsets) && !candidateItemsets.contains(newItems)){
candidateItemsets.add(newItems);
}
}
}
}
return candidateItemsets;
}
//判断是否有子集是非频繁的
public boolean hasInfrequentSubSet(HashSet<String> newItemset, List<HashSet<String>> prevItemsets) {
List<String> newItemsetList = new ArrayList<>(newItemset);
for (int k = 0; k < newItemsetList.size(); k++) {
HashSet<String> subItemset = new HashSet<>();
for (int m = 0; m < newItemsetList.size(); m++) {
if (m != k) {
subItemset.add(newItemsetList.get(m));
}
}
if (!prevItemsets.contains(subItemset)) {
return true;
}
}
return false;
}
}
public class Apriori extends Configured implements Tool{
public static double s;
public static int k;
public int run(String[] args)throws IOException,InterruptedException,ClassNotFoundException{
long startTime = System.currentTimeMillis();
String hdfsInputDir = args[0]; //从参数1中读取输入数据
String hdfsOutputDirPrefix = args[1]; //参数2为输出数据前缀,和第pass次组成输出目录
s = Double.parseDouble(args[2]); //阈值
k = Integer.parseInt(args[3]); //k次pass
//循环执行K次pass
for(int pass = 1; pass <= k;pass++){
long passStartTime = System.currentTimeMillis();
//配置执行该job
if(!runPassKMRJob(hdfsInputDir,hdfsOutputDirPrefix,pass)){
return -1;
}
long passEndTime = System.currentTimeMillis();
System.out.println("pass " + pass + " time : " + (passEndTime - passStartTime));
}
long endTime = System.currentTimeMillis();
System.out.println("total time : " + (endTime - startTime));
return 0;
}
private static boolean runPassKMRJob(String hdfsInputDir,String hdfsOutputDirPrefix,int passNum)
throws IOException,InterruptedException,ClassNotFoundException{
Configuration passNumMRConf = new Configuration();
passNumMRConf.setInt("passNum",passNum);
passNumMRConf.set("hdfsOutputDirPrefix",hdfsOutputDirPrefix);
passNumMRConf.setDouble("sup",s);
Job passNumMRJob = new Job(passNumMRConf,"" + passNum);
passNumMRJob.setJarByClass(Apriori.class);
if(passNum == 1){
//第一次pass的Mapper类特殊对待,不许要构造候选itemsets
passNumMRJob.setMapperClass(AprioriPass1Mapper.class);
}
else{
//第一次之后的pass的Mapper类特殊对待,不需要构造候选itemsets
passNumMRJob.setMapperClass(AprioriPassKMapper.class);
}
passNumMRJob.setReducerClass(AprioriReducer.class);
passNumMRJob.setOutputKeyClass(Text.class);
passNumMRJob.setOutputValueClass(IntWritable.class);
FileInputFormat.addInputPath(passNumMRJob,new Path(hdfsInputDir));
FileOutputFormat.setOutputPath(passNumMRJob,new Path(hdfsOutputDirPrefix + passNum));
return passNumMRJob.waitForCompletion(true);
}
public static void main(String[] args) throws Exception{
int exitCode = ToolRunner.run(new Apriori(),args);
System.exit(exitCode);
}
}
运行指令
// 编译
javac Apriori.java
// 打包
jar -cf Apriori.jar Apriori*.class
// 运行
hadoop jar Apriori.jar Apriori /data/groceries.csv /Apriori/ 0.005 5
/data/groceries.csv替换成输入文件的位置,/Apriori 替换成输出文件的位置,0.005替换成支持度,5替换为频繁项的阶数