最近在工作中遇到一个问题:需要求几千个商品类目之间的相关性,自然而然相当了关联规则算法。
由于商品类目只有几千个,所以起初并没有考虑性能问题,所以就自己实现了一个mapreduce版的Priori算法,结果用到实际数据集上就悲剧了。
3000个1-频繁项目集做连接,可以得到400多万个2-候选项目集, 然后为了判断在400万中每个候选项集是不是频繁的,需要遍历700万条事务记录。然后依次求2,3,4频繁项集,每次都 需要遍历整个数据库(700万条记录),即使分布式也极度耗时的。所以,把目光转向只需要遍历两次数据库的FP-Growth算法。
说实话,涉及到树、图等复杂数据结构的算法,想改成成mapreduce版,鄙人真的没有信息。所以谷歌了一下,知道mahout里面有fp-growth算法mapreduce版。然后就傻不拉几下了一个最新版的mahout.0.9, 按照网上找来的教程:mahout fpg ..........
直接得到下面这个错误:
java.lang.IllegalArgumentException: Unknown program 'fpg' chosen.
病急乱投医,赶忙谷歌一下,有人说是因为mahout从0.8版开始就不支持fpg了(貌似不是这样的)
因为赶时间,也没去验证真假,就按照网上0.5版的教程,下了一个0.5版,继续运行命令:mahout fpg .....
又出现下面的错误:
java.lang.RuntimeException: java.lang.reflect.InvocationTargetException
at org.apache.hadoop.util.ReflectionUtils.newInstance(ReflectionUtils.java:126)
at org.apache.hadoop.mapred.MapTask.runNewMapper(MapTask.java:616)
at org.apache.hadoop.mapred.MapTask.run(MapTask.java:322)
at org.apache.hadoop.mapred.Child.main(Child.java:165)
Caused by: java.lang.reflect.InvocationTargetException
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at org.apache.hadoop.util.ReflectionUtils.newInstance(ReflectionUtils.java:124)
... 3 more
Caused by: java.lang.NoClassDefFoundError: org/apache/mahout/math/map/OpenObjectIntHashMap
at org.apache.mahout.fpm.pfpgrowth.TransactionSortingMapper.<init>(TransactionSortingMapper.java:42)
... 8 more
Caused by: java.lang.ClassNotFoundException: org.apache.mahout.math.map.OpenObjectIntHashMap
at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
at java.security.AccessController.doPrivileged(Native Method)
at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
at java.lang.ClassLoader.loadClass(ClassLoader.java:306)
at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
at java.lang.ClassLoader.loadClass(ClassLoader.java:247)
... 9 more
火大,继续谷歌了一下,并且认真看了一下0.5包里面文件的结构,确实没有
<pre name="code" class="java">org/apache/mahout/math/map/OpenObjectIntHashMap 这个类
继续研究了一下0.5下面几个jar包的内部结构,觉得可以尝试下面的调用方式:
hadoop jar mahout-examples-0.5-job.jar org.apache.mahout.fpm.pfpgrowth.FPGrowthDriver -i /group/admin/input/train_records.txt -o /group/admin/output -k 4 -method mapreduce -regex '[\ ]' -s 40 -m 20 -r 10
成功! -i 后面跟输入文件路径, -o 后面跟输出目录,而且运行前不需要删除该目录,mahout源码里面在运行之前会确保该目录不存在; -k表示多想求出前几个频繁项;-s 表示支持度阈值(我是这么认为);;-method mapreduce表示执行分布式的fpg算法; -regex ‘[\ ]’ 表示输入文件中item之间的分隔符。后面的-m -r mahout是不识别的,我自己加的`(*∩_∩*)′
因为pfpg默认的mapper和reduce的数目貌似只有两个,数据量特别大的情况下,这绝对是瓶颈。而且, 我找来找去没找到mahout命令行中,指定mapper和reducer个数的参数,-Dmapred.map.tasks这个参数也不接受。只好去改源码了。
下载一个mahout 0.5的源码,编译成maven工程:maven clean install -Dmaven.test.skip
编译好之后,打开eclipse, import --->exist maven project, 这里貌似要装m2e的插件。
然后打开org.apache.mahout.fpm.pfpgrowth.FPGrowthDriver, 可以看到main函数里面定义了很多Option对象,每一个Option对象表示一个支持的命令行参数。我们想加入指定mapper和reducer个数参数,首先模仿着以后的例子,添加下面两个:
Option mapTasksOpt = obuilder.withLongName("mapTasks").withArgument(
abuilder.withName("mapTasks").withMinimum(1).withMaximum(100).create()).withDescription(
"(Optional)Minium Mapred.map.tasks. Default Value: 1").withShortName("m").create();
Option reduceTasksOpt = obuilder.withLongName("reduceTasks").withArgument(
abuilder.withName("reduceTasks").withMinimum(1).withMaximum(10).create()).withDescription(
"(Optional)Minium Mapred.reduce.tasks. Default Value: 1").withShortName("r").create();
其中withLongName和withShortName分别表示了两种指定参数的方式:--mapTasks 5, 或者-m 5.
然后再Group group后面添加上这两个参数
Group group = gbuilder.withName("Options").withOption(minSupportOpt).withOption(inputDirOpt).withOption(
outputOpt).withOption(maxHeapSizeOpt).withOption(numGroupsOpt).withOption(methodOpt).withOption(
encodingOpt).withOption(helpOpt).withOption(treeCacheOpt).withOption(recordSplitterOpt).withOption(reduceTasksOpt).withOption(mapTasksOpt).create();
;
这样,当我们在命令行加入-m -r 的时候, 这两个参数就可以别识别的,不会报错说不合法参数了。
上面做的只能保证-m -r可以被识别,还得获取我们输入的参数值
if(cmdLine.hasOption(mapTasksOpt)) {
String numMapTasks = (String)cmdLine.getValue(mapTasksOpt);
params.set("numMapTasks", numMapTasks);
}
if(cmdLine.hasOption(reduceTasksOpt)) {
String numReduceTasks = (String)cmdLine.getValue(reduceTasksOpt);
params.set("numReduceTasks", numReduceTasks);
}
这样,就把命令行输入的Mapper和Reducer数目,存在params里面,传给后面的mapper和reducer任务用。
在往后面执行就到了-method方法起作用的地方了:
String classificationMethod = (String) cmdLine.getValue(methodOpt);
if ("sequential".equalsIgnoreCase(classificationMethod)) {
runFPGrowth(params);
} else if ("mapreduce".equalsIgnoreCase(classificationMethod)) {
Configuration conf = new Configuration();
HadoopUtil.delete(conf, outputDir);
PFPGrowth.runPFPGrowth(params);
}
如果不执行分布式版本,method可以写成sequential, 这里应该表示“串行”的意思。否则-method mapreduce
首先删除output目录,这就是为什么在运行命令之前,不需要手动去删除output目录。
然后运行runPFPGrowth方法。改方法里面有四个mapreduce任务(startGroupingItems非MR任务),每个任务的作用在PFP: Parallel FP-Growth for Query Recommendation这篇论文里面有论述,我们在这里不关注。
startParallelCounting(params);
startGroupingItems(params);
startTransactionSorting(params);
startParallelFPGrowth(params);
startAggregating(params);
再每一个方法里面加上下面几行代码:
int numMapTasks = Integer.parseInt(params.get("numMapTasks", "20"));
int numReduceTasks = Integer.parseInt(params.get("numReduceTasks", "10"));
conf.setInt("mapred.map.tasks", numMapTasks);
job.setNumReduceTasks(numReduceTasks);
位置任意,param.get("paramName" , defaultValue) 后面的默认值可以根据每个MR任务的特点自己设定。可以设置一个合理的默认值,比如第一个任务只是统计item的频率,map和reduce的数目都可以设置的很小,而startParallelFPGrowth()任务reducer的计算任务比较重,可以把默认的reduce个数设置的大一点。这样在运行的时候,不需要在命令行指定了。推荐这种方式,因此我们用-m -r来指定,这四个MR任务都使用了统一的map和reduce个数,这很不合理。当然你的任务如果非常有讲究,可以为每一个MR任务指定单独的mapper和reducer个数,这样你需要在命令行为四个MR任务指定八个参数。
上面完工之后,编译一下整个项目:maven clean install -Dmaven.test.skip
然后进入maven的本地仓库找到:mahout-examples-0.5-job.jar这个包。把这个包拷贝到工作目录,在运行:
hadoop jar mahout-examples-0.5-job.jar org.apache.mahout.fpm.pfpgrowth.FPGrowthDriver -i /group/admin/input/train_records.txt -o /group/admin/output -k 4 -method mapreduce -regex '[\ ]' -s 40 -m 20 -r 10
这样 -m 和 -r就可以起作用了。