一、在新浪微博、人人网等社交网站上,为了使用户在网络上认识更多的朋友,社交网站往往提供类似“你可能感兴趣的人”、“间接关注推荐”等好友推荐的功能。一直很好奇这个功能是怎么实现的。
其实,社交网站上的各个用户以及用户之间的相互关注可以抽象为一个图。以下图为例:
顶点A、B、C到I分别是社交网站的用户,两顶点之间的边表示两顶点代表的用户之间相互关注。那么如何根据用户之间相互关注所构成的图,来向每个用户推荐好友呢?可能大家都听说过六度人脉的说法,所谓六度人脉是指:地球上所有的人都可以通过五层以内的熟人链和任何其他人联系起来。通俗地讲:“你和任何一个陌生人之间所间隔的人不会超过六个,也就是说,最多通过六个人你就能够认识任何一个陌生人。”这个理论在社交网络中同样成立。
现在我们以上图为例,介绍下如何利用用户之间相互关注所构成的图,来向每个用户推荐好友。首先我们不得不假设的是如果两用户之间相互关注,那么我们认为他们认识或者说是现实中的好友,至少应该认识。假设我们现在需要向用户I推荐好友,我们发现用户I的好友有H、G、C。其中H的好友还有A,G的好友还有 F,C的好友还有B、F。那么用户I、H、G、C、A、B、F极有可能是同一个圈子里的人。我们应该把用户A、B、F推荐给用户I认识。进一步的想,用户 F跟两位I的好友C、G是好友,而用户A、B都分别只跟一位I的好友是好友,那么相对于A、B来说,F当然更应该推荐给用户I认识。
可能你会发现,在上面的分析中,我们使用了用户I的二度人脉作为他的推荐好友,而且我们对用户I的每个二度人脉进行了投票处理,选举出最优推荐。其实,我觉得,二度人脉的结果只能看看某个用户的在社交网站上的人际关系链,而基于投票选举产生的二度人脉才是好友推荐功能中所需要的好友。
另外你也可能已经认识到所谓的N度人脉,其实就是图算法里面的宽度优先搜索。宽度优先搜索的主要思想是From Center To Outer,我们以用户I为起点,在相互关注所构成的图上往外不退回地走N步所能到的顶点,就是用户I的N度好友。
下面是Python写的N度人脉的算法,可以输出某个用户的N度好友,代码详见这里。
下面几点是其与宽度优先搜索的不同之处:
1. 宽度优先搜索搜索的是起始顶点可达的所有顶点,N度人脉不需要,它只需要向外走N步,走到N步的顶点处便停止,不需要再往外走了。
2. 走过N步之后,结果中包含起始顶点往外走1、2……N-1步所能到达的所有顶点,返回结果之前需将这些点删除。
3. 变量pathLenFromStart记录这N步具体的走法。
上诉的算法看似可行,其实在实际中并不适用。社交网站上的用户量至少是千万级别的,不可能把所有用户之间相互关注的关系图放进内存中,这个时候就可以依赖 Hadoop了。下面的实例中,我们的输入是deg2friend.txt,保存用户之间相互关注的信息。每行有两个用户ID,以逗号分割,表示这两个用户之间相互关注即认识。
二度好友的计算需要两轮的MapReduce。第一轮MapReduce的Map中,如果输入是“H,I”,我们的输出是 key=H,value=“H,I”跟key=I,value=“H,I”两条结果。前者表示I可以通过H去发现他的二度好友,后者表示H可以通过I去发现他的二度好友。
根据第一轮MapReduce的Map,第一轮MapReduce的Reduce 的输入是例如key =I,value={“H,I”、“C,I”、“G,I”} 。其实Reduce 的输入是所有与Key代表的结点相互关注的人。如果H、C、G是与I相互关注的好友,那么H、C、G就可能是二度好友的关系,如果他们之间不是相互关注的。对应最上面的图,H与C是二度好友,G与C是二度好友,但G与H不是二度好友,因为他们是相互关注的。第一轮MapReduce的Reduce的处理就是把相互关注的好友对标记为一度好友(“deg1friend”)并输出,把有可能是二度好友的好友对标记为二度好友(“deg2friend”)并输出。
第二轮MapReduce则需要根据第一轮MapReduce的输出,即每个好友对之间是否是一度好友(“deg1friend”),是否有可能是二度好友(“deg2friend”)的关系,确认他们之间是不是真正的二度好友关系。如果他们有deg1friend的标签,那么不可能是二度好友的关系;如果有deg2friend的标签、没有deg1friend的标签,那么他们就是二度好友的关系。另外,特别可以利用的是,某好友对deg2friend标签的个数就是他们成为二度好友的支持数,即他们之间可以通过多少个都相互关注的好友认识。
两轮MapReduce的代码,详见这里。
根据上述两轮的MapReduce的方法,我以部分微博的数据进行了测试,测试的部分结果如下:
通过与我(@Intergret)相互关注的138位好友,两轮的MapReduce向我推荐的二度好友前三位是:2010963993(@可乐要改变),2022127621(@琥珀露珠)和2572979357(@赵鸿泽),他们都是我本科的同学,有很多共同的好友,但我跟他们三目前尚未相互关注,所以推荐结果还算靠谱。
原文链接:http://www.datalab.sinaapp.com/?p=192
二、代码如下:
<pre name="code" class="java">import java.io.IOException;
import java.util.Random;
import java.util.Vector;
import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.fs.FileSystem;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.Mapper;
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.util.GenericOptionsParser;
public class deg2friend {
public static class job1Mapper extends Mapper<Object, Text, Text, Text>{
private Text job1map_key = new Text();
private Text job1map_value = new Text();
public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
String eachterm[] = value.toString().split(",");
if(eachterm[0].compareTo(eachterm[1])<0){
job1map_value.set(eachterm[0]+"\t"+eachterm[1]);
}
else if(eachterm[0].compareTo(eachterm[1])>0){
job1map_value.set(eachterm[1]+"\t"+eachterm[0]);
}
job1map_key.set(eachterm[0]);
context.write(job1map_key, job1map_value);
job1map_key.set(eachterm[1]);
context.write(job1map_key, job1map_value);
}
}
public static class job1Reducer extends Reducer<Text,Text,Text,Text> {
private Text job1reduce_key = new Text();
private Text job1reduce_value = new Text();
public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
String someperson = key.toString();
Vector<String> hisfriends = new Vector<String>();
for (Text val : values) {
String eachterm[] = val.toString().split("\t");
if(eachterm[0].equals(someperson)){
hisfriends.add(eachterm[1]);
job1reduce_value.set("deg1friend");
context.write(val, job1reduce_value);
}
else if(eachterm[1].equals(someperson)){
hisfriends.add(eachterm[0]);
job1reduce_value.set("deg1friend");
context.write(val, job1reduce_value);
}
}
for(int i = 0; i<hisfriends.size(); i++){
for(int j = 0; j<hisfriends.size(); j++){
if (hisfriends.elementAt(i).compareTo(hisfriends.elementAt(j))<0){
job1reduce_key.set(hisfriends.elementAt(i)+"\t"+hisfriends.elementAt(j));
job1reduce_value.set("deg2friend");
context.write(job1reduce_key, job1reduce_value);
}
// else if(hisfriends.elementAt(i).compareTo(hisfriends.elementAt(j))>0){
// job1reduce_key.set(hisfriends.elementAt(j)+"\t"+hisfriends.elementAt(i));
// }
}
}
}
}
public static class job2Mapper extends Mapper<Object, Text, Text, Text>{
private Text job2map_key = new Text();
private Text job2map_value = new Text();
public void map(Object key, Text value, Context context) throws IOException, InterruptedException {
String lineterms[] = value.toString().split("\t");
if(lineterms.length == 3){
job2map_key.set(lineterms[0]+"\t"+lineterms[1]);
job2map_value.set(lineterms[2]);
context.write(job2map_key,job2map_value);
}
}
}
public static class job2Reducer extends Reducer<Text,Text,Text,Text> {
private Text job2reducer_key = new Text();
private Text job2reducer_value = new Text();
public void reduce(Text key, Iterable<Text> values, Context context) throws IOException, InterruptedException {
Vector<String> relationtags = new Vector<String>();
String deg2friendpair = key.toString();
for (Text val : values) {
relationtags.add(val.toString());
}
boolean isadeg1friendpair = false;
boolean isadeg2friendpair = false;
int surport = 0;
for(int i = 0; i<relationtags.size(); i++){
if(relationtags.elementAt(i).equals("deg1friend")){
isadeg1friendpair = true;
}else if(relationtags.elementAt(i).equals("deg2friend")){
isadeg2friendpair = true;
surport += 1;
}
}
if ((!isadeg1friendpair) && isadeg2friendpair){
job2reducer_key.set(String.valueOf(surport));
job2reducer_value.set(deg2friendpair);
context.write(job2reducer_key,job2reducer_value);
}
}
}
public static void main(String[] args) throws Exception {
Configuration conf = new Configuration();
String[] otherArgs = new GenericOptionsParser(conf, args).getRemainingArgs();
if (otherArgs.length != 2) {
System.err.println("Usage: deg2friend <in> <out>");
System.exit(2);
}
Job job1 = new Job(conf, "deg2friend");
job1.setJarByClass(deg2friend.class);
job1.setMapperClass(job1Mapper.class);
job1.setReducerClass(job1Reducer.class);
job1.setOutputKeyClass(Text.class);
job1.setOutputValueClass(Text.class);
//定义一个临时目录,先将任务的输出结果写到临时目录中, 下一个排序任务以临时目录为输入目录。
FileInputFormat.addInputPath(job1, new Path(otherArgs[0]));
Path tempDir = new Path("deg2friend-temp-" + Integer.toString(new Random().nextInt(Integer.MAX_VALUE)));
FileOutputFormat.setOutputPath(job1, tempDir);
if(job1.waitForCompletion(true))
{
Job job2 = new Job(conf, "deg2friend");
job2.setJarByClass(deg2friend.class);
FileInputFormat.addInputPath(job2, tempDir);
job2.setMapperClass(job2Mapper.class);
job2.setReducerClass(job2Reducer.class);
FileOutputFormat.setOutputPath(job2, new Path(otherArgs[1]));
job2.setOutputKeyClass(Text.class);
job2.setOutputValueClass(Text.class);
FileSystem.get(conf).deleteOnExit(tempDir);
System.exit(job2.waitForCompletion(true) ? 0 : 1);
}
System.exit(job1.waitForCompletion(true) ? 0 : 1);
}
}