MapReduce 算法 - 反转排序 (Order Inversion)

本文另一地址请见MapReduce算法-反转排序

本文译自 MapReduce Algorithms - Order Inversion排序

译者注:在刚开始翻译的时候,我将Order Inversion按照字面意思翻译成“反序”或者“倒序”,但是翻译完整篇文章之后,我感觉到,将Order Inversion翻译成反序模式是不恰当的,根据本文的内容,很显然,Inversion并非是将顺序倒排的意思,而是如同Spring的IOC一样,表明的是一种控制权的反转。Spring将对象的实例化责任从业务代码反转给了框架,而在本文的模式中,在mapreduce的sorting过程中,原来由框架负责的数据的排序以及shuffle规则被用户定制化了,控制权从框架反转到了user,实际上这种模式就是由用户控制sorting过程的意思

本文是一系列有关MapReduce算法的文章中的一篇,这些算法都在《Data-Intensive Text Processing with MapReduce》中提到过。这系列文章在本文之前已经发表的有 本地聚合本地聚合二 和 建立共生矩阵。在这篇文章里我们要讨论的是排序反转模式。这种模式利用MapReduce的排序(sorting)阶段,让一部分数据提前发送到reducer端以利于后续计算,如果你对MapReduce了解不多,我劝你读下去,因为我将展示给你如何使用排序(sorting)和partitioner来实现我们的目的,这将会大有益处。

尽管已经有许多MapReduce框架提供了高层次的抽象,例如Hive和Pig,理解底层是如何运行的仍然是有好处的。反序模式出现在《Data-Intensive Text Processing with MapReduce》这本书的第三章, 为了说明反序模式,我们要用共生矩阵模式中出现过的配对方法。建立共生矩阵的时候我们可以记录下词共同出现的次数,我门会对配对方法做一个小小的修改,mapper不止输出诸如(“foo”,”bar”) 这样的词对,还会额外输出(“foo”,”*”)这样的词对,对于每个词都依此法办理,这样可以很容易的得出左边的这个词的总共出现次数,用这个就可以计算出相对频率。这种方法会带来两个问题,首先我们需要想办法保证让 (“foo”,”*”) 成为reducer 的第一条记录,其次我们要保证左边的词相同的所有的词对都被同一个reducer所处理,我们先来看mapper代码再解决这两个问题。


Mapper Code

首先我们要对mapper做一些有别于配对方法的修改。在每次循环的最后,输出了某个词的所有的词对之后,输出一个特殊的词对(“word”,”*”), 计数就是这个词作为左边词的词对出现的次数。

01 public class PairsRelativeOccurrenceMapper extendsMapper<LongWritable, Text, WordPair, IntWritable> {
02     private WordPair wordPair = new WordPair();
03     private IntWritable ONE = new IntWritable(1);
04     private IntWritable totalCount = new IntWritable();
05  
06     @Override
07     protected void map(LongWritable key, Text value, Context context) throwsIOException, InterruptedException {
08         int neighbors = context.getConfiguration().getInt('neighbors', 2);
09         String[] tokens = value.toString().split('\\s+');
10         if (tokens.length > 1) {
11             for (int i = 0; i < tokens.length; i++) {
12                     tokens[i] = tokens[i].replaceAll('\\W+','');
13  
14                     if(tokens[i].equals('')){
15                         continue;
16                     }
17  
18                     wordPair.setWord(tokens[i]);
19  
20                     int start = (i - neighbors < 0) ? 0 : i - neighbors;
21                     int end = (i + neighbors >= tokens.length) ? tokens.length - 1 : i + neighbors;
22                     for (int j = start; j <= end; j++) {
23                         if (j == i) continue;
24                         wordPair.setNeighbor(tokens[j].replaceAll('\\W',''));
25                         context.write(wordPair, ONE);
26                     }
27                     wordPair.setNeighbor('*');
28                     totalCount.set(end - start);
29                     context.write(wordPair, totalCount);
30             }
31         }
32     }
33 }

现在我们找到了统计特定词出现次数的办法,我们还需要想办法让这个特定的词对称为reduce处理的第一条记录以便计算相对频度。我们可以通过修改WordPair对象的compareTo方法在MapReduce 的sorting阶段来实现这个目的。

修改排序

修改WordPair类的compareTo方法,让发现 “*” 为右词的对象排到前列。

01 @Override
02 public int compareTo(WordPair other) {
03     int returnVal = this.word.compareTo(other.getWord());
04     if(returnVal != 0){
05         return returnVal;
06     }
07     if(this.neighbor.toString().equals('*')){
08         return -1;
09     }else if(other.getNeighbor().toString().equals('*')){
10         return 1;
11     }
12     return this.neighbor.compareTo(other.getNeighbor());
13 }

通过修改compareTo方法,我们可以保证含有特殊字符的WordPair 都排在比较靠前的位置并会首先被reducer处理。这引出了第二个问题,我们怎样使具有相同左词的所有WordPai对象被发送到同一个reducer? 答案是定制一个partitioner。

定制 Partitioner

用key的hashcode对reducer数取模,就把key分配到了不同的reducer,这就是shuffle过程。但我们的WordPair 对象包含2个词,计算整个对象的hashcode是行不通的。我们需要写一个自己的Partitioner, 它在选择将输出发送到哪个reducer的时候只考虑左边的词。

1 public class WordPairPartitioner extends Partitioner<WordPair,IntWritable> {
2  
3     @Override
4     public int getPartition(WordPair wordPair, IntWritable intWritable, int numPartitions) {
5         return wordPair.getWord().hashCode() % numPartitions;
6     }
7 }

Reducer

写一个reducer来实现倒序模式很简单。引入一个计数变量以及一个表示当前词的“current”变量。reducer会检查作为输入key的WordPair 右边是不是特殊字符“*”。假如左边的词不等于“current”表示的词就重置计数变量,并且计算current表示的词的总次数。然后处理下一个WordPair对象,在同一个current范围内,计数之和与各个不同右词的计数结合就可以得到相对频率。继续这个过程直到发现另一个词(左词)然后再重新开始。

01 public class PairsRelativeOccurrenceReducer extendsReducer<WordPair, IntWritable, WordPair, DoubleWritable> {
02     private DoubleWritable totalCount = new DoubleWritable();
03     private DoubleWritable relativeCount = new DoubleWritable();
04     private Text currentWord = new Text('NOT_SET');
05     private Text flag = new Text('*');
06  
07     @Override
08     protected void reduce(WordPair key, Iterable<IntWritable> values, Context context) throwsIOException, InterruptedException {
09         if (key.getNeighbor().equals(flag)) {
10             if (key.getWord().equals(currentWord)) {
11                 totalCount.set(totalCount.get() + getTotalCount(values));
12             } else {
13                 currentWord.set(key.getWord());
14                 totalCount.set(0);
15                 totalCount.set(getTotalCount(values));
16             }
17         } else {
18             int count = getTotalCount(values);
19             relativeCount.set((double) count / totalCount.get());
20             context.write(key, relativeCount);
21         }
22     }
23   private int getTotalCount(Iterable<IntWritable> values) {
24         int count = 0;
25         for (IntWritable value : values) {
26             count += value.get();
27         }
28         return count;
29     }
30 }

通过控制sort阶段的逻辑和建立定制partitioner,我们可以把执行计算的reducer需要的数据在计算所需的数据到达之前发送到reducer,虽然这里没有展示,不过combiner在MapReduce中是经常会用到的。而且这个方法(使用combiner)也是mapper端合并模式的的一个非常好的实现。

现在我们可以保证有着相同左词的所有WordPair对象都被发到了同一个reducer。剩下的就是建立一个reducer来使用发送到reducer的数据。

例子和结果

在假期的这段时间里,我用查尔斯狄更斯的小说《圣诞颂歌》作为样例来运行了一下反序模式。我知道这可能没什么实际意义,但我们的目的就是这样。

01 new-host-2:sbin bbejeck$ hdfs dfs -cat relative/part* | grep Humbug
02 {word=[Humbug] neighbor=[Scrooge]}  0.2222222222222222
03 {word=[Humbug] neighbor=[creation]} 0.1111111111111111
04 {word=[Humbug] neighbor=[own]}  0.1111111111111111
05 {word=[Humbug] neighbor=[said]} 0.2222222222222222
06 {word=[Humbug] neighbor=[say]}  0.1111111111111111
07 {word=[Humbug] neighbor=[to]}   0.1111111111111111
08 {word=[Humbug] neighbor=[with]} 0.1111111111111111
09 {word=[Scrooge] neighbor=[Humbug]}  0.0020833333333333333
10 {word=[creation] neighbor=[Humbug]} 0.1
11 {word=[own] neighbor=[Humbug]}  0.006097560975609756
12 {word=[said] neighbor=[Humbug]} 0.0026246719160104987
13 {word=[say] neighbor=[Humbug]}  0.010526315789473684
14 {word=[to] neighbor=[Humbug]}   3.97456279809221E-4
15 {word=[with] neighbor=[Humbug]} 9.372071227741331E-4

结论

即使在工作中计算相对词频的需求可能并不常见,我们也能够用这个来展示sorting和定制partitioner的用法,这可是我们写 MapReduce 程序时候的得力工具。如前所述,即使你的MapReduce都是用像Hive和Pig这样的高层次抽象语言写成的,了解一些底层的机制仍然是有好处的,谢谢。





转载于:https://my.oschina.net/juliashine/blog/105412

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值