MapReduce设计模式:Numerical Computation

 

MapReduce设计模式:Numerical Computation

 

我们已经学习了Local Aggregation和Filtering这两种设计模式,本节我们继续学习另外一种:Numerical Computation。这种模式实际上是进行数学运算,即对于一系列输入(v1,v2,v3,...vn),f定义为某种运算法则,使得x=f(v1,v2,v3,...,vn),f运算法则包括max、min、avg、median、stddev等,本节内容比较简单,所以进展比较快。

 

目前使用MapReduce进行Numerical Computation的实例主要有:

Word Count:这个是作为Hadoop的示例发布的,看起来很是简单啊。No,其实这个例子并不简单,最后会再讲到这个问题的一个扩展,你会发现其中的不简单之处。

Record Count:记录数目统计,实际上就是统计记录数,如单位时间(weekly/daily/hourly)内Log数目、用户交易笔数、用户访问次数等。

min/max/count:最大值、最小值、满足条件值统计,这个实际上是极值统计,这个统计功能我们在生活中也是比价关注的,如微博上的粉丝数、最受关注话题等。

avg/median/stddev:这个在直接统计中可能用处不大,但是在机器学习中都是常用的统计量;如tf-idf中就统计了词频和文档信息

 

Word Count的例子在http://isilic.iteye.com/blog/1770340中已经有伪码,略过不在细提;我们看下最大最小值得统计,其实也很简单:

class Mapper
  method map(tern t, Value val)
    emit(t,val)
    
class Reducer
  method reduce(term t, Value[val1,val2,...])
		min =  INF;
		max = -INF;
		for val in [val1,val2,val3,...] do:
		  if(val < min)
		    min = val
		  if(val > max)
		    max = val
		emit(t,pair(min,max))

 

这个有没有感觉眼熟呢,这个和TOP-K算法的思路是一致的,其实TOP-K算法更为通用。

 

看下avg和median还有stddev的计算,由于这个计算类似,我就不再分开写,直接在一个Reducer里面实现,可能会有不太协调的地方:

class Mapper
  method map(tern t, Value val)
    emit(t,val)
    
class Reducer
  method reduce(term t, Value[val1,val2,...])
    H = sort([val1,val2,val3,...])
    size = len(H)
    if(size % 2 == 0)
      median = (H[size/2 - 1] + H(size/2))/2
    else 
      median = H[int(size/2)]
    emit(t,median)
    sum = 0;
    count = 0;
    for val in [val1,val2,val3,...] do:
	      sum = sum + val
	      count = count + 1
    mean =  sum / count
    emit(t,mean)	
    squares = 0.0
    for val in [val1,val2,val3,...] do:
         squares = squares + (val - mean) * (val - mean)
    stddev = sqrt(squares/(count - 1))
    emit(t,stddev)

 

其中标准偏差可能需要一点数学知识,其它的都比较简单。

值得注意的是,我们在计算过程中计算出了count,实际上如果想直接计算count的值,或者是符合要求的值,还可以使用另外一种counter的方法。Counter方法是使用一种Hadoop提供的全局计数器,能够在各个Mapper中更改某个预定义好的Counter,使其递增,这个增量会在最后MapReduce程序执行完毕之后,进行合并。

Counter的使用是全局性的,在MapReduce执行完成输出的信息中,就有预定义好的Counter,如输入记录、输出记录、输入byte数、输出byte数、Map task数目、reduce task数目等。这个也算是应该知道的方法。使用该Counter计数时,速度还是不错的,没有输出、也没有各种IO,只是个数据传递;所以执行速度依赖于Map Task数目和Mapper内处理记录的速度。

 

下面我们通过计算文档词汇相对频率来说明计算过程,这个过程和上面提到的word count有关,但是在计算过程中还有个非常巧妙的地方,大家可以仔细体会下。

文档词汇相对频率是指,当wi出现时,wj同时出现的频率,用公式可以表示为:

 f(wj|wi) = N(wi,wj) / sum(N(wi,w))

 其中w为文档中出现的词汇,N(wi,wj)表示wi和wj同时出现的总次数。

 

这个最初的实现是应该是怎么想的呢?大家根据前面的MapReduce框架的学习,这个实现应该是这样:

class Mapper
  method map(docid a)
    for all term w in a do:
      for all term u in w.neighbour:
        emit(pair(w,u),count 1)
    
class Reducer
  method setup:
    key = null
    keycnt = 0;
    pairmap = new map
  method reduce(pair (w,u), Value[val1,val2,...])
    if(key == null || key != w)
      if(len(pairset) > 0)
        for pair,paircnt in pairmap do:
          ratio = paircnt / keycnt
          emit(pair,ratio)
      key = null
      keycnt = 0
    sum = 0
    for val in [val1,val2,val3,...] do:
      sum = sum + val
    pairmap.put(pair,sum)
    keycnt = keycnt + sum

 

内容还是比较容易简单的,不过这么绕口的逻辑可不符合MapReduce程序的初衷,这种精巧的设计总是给人一种特殊的感觉,如果想更加一般化的话,难度就有点大了。

逻辑复杂性先不论,紧接着的疑问就是:这个能得到结果吗?很遗憾的回答不能。

我们回忆一下http://isilic.iteye.com/blog/1770340里面提到的MapReduce流程,可以看到在shuffle和sort阶段是根据key进行Reducer选择的,注意我们的key是个pair(w,u),我们希望在shuffle过程中得到类似下面的这种排列:

......
(dog,Greyhound)
(dog,IbizanHound)
(dog,Weimaraner)
(dog,schnauzer)
......

 

也就是说,我们不希望按照pair的整体值来进行分环选择shuffle,我们希望按照pair中值进行shuffle,这个要求还是比较常见的。那么种种要求能实现吗?当然可以,其实这种实现被称为是secondary-sorting。

如果想实现secondary-sorting,需要自己编写一个额外的Partitioner类,提供shuffle过程的选择是按照单个key进行而不是按照pair进行;同时提供PartitionerComparator,提供升序或者降序排列,这个PartitionerComparator能够提供整个整个pair的排列顺序。在Hadoop教程中有个SecondarySort实例,大家仔细学习下。Hadoop提供的实例非常不错,每个实例能够讲明白一个很实用的功能点,理解了这个功能点,才能更好的使用。如果将所有的特性都集中在一个实例中,估计学习曲线会陡峭很多。

Secondary-Sorting的细节大家仔细学习,设置好启动程序就能确保对于每个pair都是按照w来进行排序的,同时能够将输出结果。不过这里面保存了所有的pairmap,实际含义是对于单个w,保存了所有的w的相邻词,这个有可能词汇量非常大,存在OOM的风险。并且必须处理完整个w之后,才能输出。

 

我们需要改进这种模式,主要问题在于pairmap保存,如果w的值还没有显示完全,相对频度就无法计算。我们通过另外一种形式来改进上面提到的这种方案:

 

......
(dog,*)
(dog,Greyhound)
(dog,IbizanHound)
(dog,Weimaraner)
(dog,schnauzer)
......

 

 

这种改进的效果还是很明显的,通过这种形式,我们能最先算出来关于w的所有词频数目,然后在后面的处理过程中再跟别计算w和u的单词数,计算完成就可以直接得到w和u的相对频度。

 

 

 

在上面的实例上修改,可以直接得到改进后的MapReduce程序,伪码如下:

 

 

class Mapper
  method map(docid a)
    for all term w in a do:
      for all term u in w.neighbour:
        emit(pair(w,u),count 1)
        emit(pair(u,*),count 1)
    
class Reducer
  method setup:
    key = null
    keycnt = 0;
  method reduce(pair (w,u), Value[val1,val2,...])
    if(key != w && u == *)
      for val in [val1,val2,val3,...] do:
        keycnt = keycnt + val
    else 
       sum = 0
       for val in [val1,val2,val3,...] do:
         sum = sum + val
       ratio = sum / keycnt
       emit(pair(w,u),ratio)

 

Ok,仔细理解下这两个问题的解决方法,各有巧妙之处,后者能够很好的解决前者的瓶颈问题,所以还是推荐使用后者;当然后者也依赖了Partitioner的选择和PartitionerComparator的使用。确保Reducer处理过程的顺序和准确性。

 

Numerical Computation的设计模式还是比较简单的,如果只是考虑算数运算的话。实际上,特别是这些Numerical Computation,我们在使用时使用在大规模文档处理中,速度、效率、时间都是要考虑的因素,如果能通过这些方法尽可能的提升其中某一方面的性能,所做的努力都是值得的,大家可以仔细体会下。

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(mapreduce)