利用MapReduce仿QQ音樂實(shí)現(xiàn)"今日推薦歌曲"系統(tǒng)

大數(shù)據(jù)無處不在,推薦系統(tǒng)無處不在帆谍。

QQ音樂的今日推薦歌曲伪朽;人人網(wǎng)的好友推薦;新浪微博的你可能感覺興趣的人汛蝙;優(yōu)酷烈涮,土豆的電影推薦朴肺;豆瓣的圖書推薦;大從點(diǎn)評(píng)的餐飲推薦坚洽;世紀(jì)佳緣的相親推薦戈稿;天際網(wǎng)的職業(yè)推薦等都用到了大數(shù)據(jù)。
今天利用MapReduce簡(jiǎn)單寫個(gè)仿QQ音樂的推薦系統(tǒng)讶舰,希望能給在座各位在工作中或面試中一點(diǎn)幫助鞍盗!轉(zhuǎn)載請(qǐng)注明出處:Michael孟良

今日推薦歌曲

原理:

通過歷史對(duì)歌曲操作記錄,計(jì)算得出每首歌相對(duì)其他歌曲同時(shí)出現(xiàn)在同一用戶的次數(shù)跳昼,每件歌曲都有自己相對(duì)全部歌曲的同現(xiàn)列表般甲,用戶會(huì)對(duì)部分歌曲有過點(diǎn)擊,收藏等實(shí)際操作鹅颊,經(jīng)過計(jì)算會(huì)得到用戶對(duì)這部分歌曲的評(píng)分向量列表敷存。
使用用戶評(píng)分向量列表中的分值:
依次乘以每首歌同現(xiàn)列表中該分值的代表歌曲的同現(xiàn)值
求和便是該歌曲的推薦向量

具體算法:

推薦系統(tǒng)——協(xié)同過濾(Collaborative Filtering)算法

UserCF
基于用戶的協(xié)同過濾,通過不同用戶對(duì)歌曲的評(píng)分來評(píng)測(cè)用戶之間的相似性堪伍,基于用戶之間的相似性做出推薦锚烦。簡(jiǎn)單來講就是:給用戶推薦和他興趣相似的其他用戶喜歡的歌曲。


基于用戶的協(xié)同過濾

基于用戶的協(xié)同過濾

-ItemCF
基于item的協(xié)同過濾帝雇,通過用戶對(duì)不同item的評(píng)分來評(píng)測(cè)item之間的相似性涮俄,基于item之間的相似性做出推薦。簡(jiǎn)單來講就是:給用戶推薦和他之前喜歡的歌曲相似的歌曲尸闸。


基于item的協(xié)同過濾

基于item的協(xié)同過濾

—Co-occurrence Matrix(同現(xiàn)矩陣)和User Preference Vector(用戶評(píng)分向量)相乘得到的這個(gè)Recommended Vector(推薦向量)

—基于全量數(shù)據(jù)的統(tǒng)計(jì)彻亲,產(chǎn)生同現(xiàn)矩陣

1.體現(xiàn)歌曲間的關(guān)聯(lián)性
2.每首歌都有自己對(duì)其他全部歌曲的關(guān)聯(lián)性(每件歌曲的特征)

用戶評(píng)分向量體現(xiàn)的是用戶對(duì)一些歌曲的評(píng)分

任一歌曲需要:

1.用戶評(píng)分向量乘以基于該歌曲的其他歌曲關(guān)聯(lián)值
2.求和得出針對(duì)該歌曲的推薦向量
3.排序取TopN即可
公式

同現(xiàn)矩陣乘以用戶評(píng)分

好了,邏輯知道了室叉,怎么把它搬到代碼中睹栖,我們一步一步來:

數(shù)據(jù):

item_id,user_id,action,vtime
i161,u2625,click,2018/8/18 15:03
i161,u2626,click,2018/8/23 22:40
i161,u2627,click,2018/8/25 19:09
i161,u2628,click,2018/8/28 21:35
i161,u2629,click,2018/8/27 16:33
i161,u2630,click,2018/8/5 18:45
i161,u2631,click,2018/8/29 16:57
i161,u2632,click,2018/8/24 21:58
i161,u2633,click,2018/8/25 22:41
i161,u2634,click,2018/8/16 13:30
i161,u2635,click,2018/8/20 9:23
i161,u2636,click,2018/8/21 1:00
i161,u2637,click,2018/8/24 22:51
...
...
...
歌曲id:item_id
用戶id:user_id
對(duì)歌曲操作:action
操作時(shí)間:vtime

代碼:

啟動(dòng)類StartRun

public class StartRun {

    public static void main(String[] args) {
        Configuration config = new Configuration(true);
        
        config.set("mapreduce.framework.name", "local");
        config.set("mapreduce.app-submission.cross-platform", "true");
//      config.set("fs.defaultFS", "hdfs://node1:8020");
//      config.set("yarn.resourcemanager.hostname", "node3");
        
        // 所有mr的輸入和輸出目錄定義在map集合中
        Map<String, String> paths = new HashMap<String, String>();
        paths.put("Step1Input", "/user/root/m_log");
        paths.put("Step1Output", "/data/itemcf/output/step1");
        paths.put("Step2Input", paths.get("Step1Output"));
        paths.put("Step2Output", "/data/itemcf/output/step2");
        paths.put("Step3Input", paths.get("Step2Output"));
        paths.put("Step3Output", "/data/itemcf/output/step3");
        paths.put("Step4Input1", paths.get("Step2Output"));
        paths.put("Step4Input2", paths.get("Step3Output"));
        paths.put("Step4Output", "/data/itemcf/output/step4");
        paths.put("Step5Input", paths.get("Step4Output"));
        paths.put("Step5Output", "/data/itemcf/output/step5");
        paths.put("Step6Input", paths.get("Step5Output"));
        paths.put("Step6Output", "/data/itemcf/output/step6");

        Step1.run(config, paths);
//      Step2.run(config, paths);
//      Step3.run(config, paths);
//      Step4.run(config, paths);
//      Step5.run(config, paths);
//      Step6.run(config, paths);
    }

    public static Map<String, Integer> R = new HashMap<String, Integer>();
    static {
        R.put("click", 1);
        R.put("share", 2);
        R.put("like", 3);
        R.put("download", 4);
    }
}

我們分為6步:
第一步:

  public class Step1 {

    public static boolean run(Configuration config, Map<String, String> paths) {
        try {
            FileSystem fs = FileSystem.get(config);
            Job job = Job.getInstance(config);
            
            job.setJobName("step1");

//          config.set("mapred.jar", "D:\\MR\\item.jar");

            job.setJarByClass(Step1.class);
            job.setMapperClass(Step1_Mapper.class);
            job.setReducerClass(Step1_Reducer.class);

            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(NullWritable.class);

            FileInputFormat.addInputPath(job, new Path(paths.get("Step1Input")));
            
            Path outpath = new Path(paths.get("Step1Output"));
            if (fs.exists(outpath)) {
                fs.delete(outpath, true);
            }
            FileOutputFormat.setOutputPath(job, outpath);

            boolean f = job.waitForCompletion(true);
            return f;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    static class Step1_Mapper extends Mapper<LongWritable, Text, Text, NullWritable> {

        
        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws IOException, InterruptedException {
            if (key.get() != 0) {
                context.write(value, NullWritable.get());
            }
        }
    }

    static class Step1_Reducer extends Reducer<Text, IntWritable, Text, NullWritable> {

        @Override
        protected void reduce(Text key, Iterable<IntWritable> i, Context context)
                throws IOException, InterruptedException {
            context.write(key, NullWritable.get());
        }
    }
}

第一步去重硫惕,一進(jìn)一出茧痕,沒什么好說的。

第二步:
public class Step2 {

    public static boolean run(Configuration config, Map<String, String> paths) {
        try {
            FileSystem fs = FileSystem.get(config);
            Job job = Job.getInstance(config);
            
            job.setJobName("step2");
            job.setJarByClass(StartRun.class);
            job.setMapperClass(Step2_Mapper.class);
            job.setReducerClass(Step2_Reducer.class);

            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(Text.class);

            FileInputFormat.addInputPath(job, new Path(paths.get("Step2Input")));
            Path outpath = new Path(paths.get("Step2Output"));
            if (fs.exists(outpath)) {
                fs.delete(outpath, true);
            }
            FileOutputFormat.setOutputPath(job, outpath);

            boolean f = job.waitForCompletion(true);
            return f;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    static class Step2_Mapper extends Mapper<LongWritable, Text, Text, Text> {

        // 如果使用:用戶+歌曲的id恼除,同時(shí)作為輸出key踪旷,更好
        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws IOException, InterruptedException {
            //i1,u2723,click,2014/9/14 9:31
            String[] tokens = value.toString().split(",");
            String item = tokens[0];
            String user = tokens[1];
            String action = tokens[2];
            Text k = new Text(user);
            Integer rv = StartRun.R.get(action);
            // if(rv!=null){
            Text v = new Text(item + ":" + rv.intValue());
            //u2750 i160:1
            context.write(k, v);
        }
    }

    static class Step2_Reducer extends Reducer<Text, Text, Text, Text> {

        @Override
        protected void reduce(Text key, Iterable<Text> i, Context context)
                throws IOException, InterruptedException {
            Map<String, Integer> r = new HashMap<String, Integer>();

            //迭代同一用戶關(guān)注的歌曲的id
            for (Text value : i) {
                //u2750 i160:1
                String[] vs = value.toString().split(":");
                String item = vs[0];
                Integer action = Integer.parseInt(vs[1]);
                action = ((Integer) (r.get(item) == null ? 0 : r.get(item))).intValue() + action;
                r.put(item, action);
            }
            StringBuffer sb = new StringBuffer();
            for (Entry<String, Integer> entry : r.entrySet()) {
                sb.append(entry.getKey() + ":" + entry.getValue().intValue() + ",");
            }
            
            //u2756 i105:1,i79:1,i341:1,i319:1,i332:1,i160:1,i342:1,i94:1,
            context.write(key, new Text(sb.toString()));
        }
    }
}

第二步,按用戶分組豁辉,計(jì)算所有歌曲的id出現(xiàn)的組合列表令野,得到用戶對(duì)歌曲的id的喜愛度得分矩陣。
運(yùn)行結(jié)果:


第二步

第三步:

     public class Step3 {

    private final static Text K = new Text();
    private final static IntWritable V = new IntWritable(1);

    public static boolean run(Configuration config, Map<String, String> paths) {
        try {
            FileSystem fs = FileSystem.get(config);
            Job job = Job.getInstance(config);
            job.setJobName("step3");
            job.setJarByClass(StartRun.class);
            job.setMapperClass(Step3_Mapper.class);
            job.setReducerClass(Step3_Reducer.class);
            job.setCombinerClass(Step3_Reducer.class);

            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(IntWritable.class);

            FileInputFormat
                    .addInputPath(job, new Path(paths.get("Step3Input")));
            Path outpath = new Path(paths.get("Step3Output"));
            if (fs.exists(outpath)) {
                fs.delete(outpath, true);
            }
            FileOutputFormat.setOutputPath(job, outpath);

            boolean f = job.waitForCompletion(true);
            return f;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    // 第二個(gè)MR執(zhí)行的結(jié)果--作為本次MR的輸入  樣本: u2837  i541:1,i331:1,i314:1,i125:1,
    static class Step3_Mapper extends
            Mapper<LongWritable, Text, Text, IntWritable> {

        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws IOException, InterruptedException {
            String[] tokens = value.toString().split("\t");
            String[] items = tokens[1].split(",");
            //嵌套循環(huán)徽级,每一個(gè)歌曲與其他歌曲組合輸出一次气破,val的值為1
            //WC的思維邏輯
            for (int i = 0; i < items.length; i++) {
                String itemA = items[i].split(":")[0];
                for (int j = 0; j < items.length; j++) {
                    String itemB = items[j].split(":")[0];
                    K.set(itemA + ":" + itemB);
                    context.write(K, V);
                }
            }

        }
    }


    static class Step3_Reducer extends
            Reducer<Text, IntWritable, Text, IntWritable> {

        @Override
        protected void reduce(Text key, Iterable<IntWritable> i, Context context)
                throws IOException, InterruptedException {
            int sum = 0;
            for (IntWritable v : i) {
                sum = sum + v.get();
            }
            V.set(sum);
            context.write(key, V);
            //  執(zhí)行結(jié)果
//          i100:i181   1
//          i100:i184   2
        }
    }
}

第三步,對(duì)歌曲id組合列表進(jìn)行計(jì)數(shù)餐抢,建立歌曲id的同現(xiàn)矩陣现使。
運(yùn)行加過如下:


第三步

第四步:

public class Step4 {

public static boolean run(Configuration config, Map<String, String> paths) {
    try {
        FileSystem fs = FileSystem.get(config);
        Job job = Job.getInstance(config);
        job.setJobName("step4");
        job.setJarByClass(StartRun.class);
        job.setMapperClass(Step4_Mapper.class);
        job.setReducerClass(Step4_Reducer.class);
        job.setMapOutputKeyClass(Text.class);
        job.setMapOutputValueClass(Text.class);

        FileInputFormat.setInputPaths(job,
                new Path[] { new Path(paths.get("Step4Input1")),
                        new Path(paths.get("Step4Input2")) });
        Path outpath = new Path(paths.get("Step4Output"));
        if (fs.exists(outpath)) {
            fs.delete(outpath, true);
        }
        FileOutputFormat.setOutputPath(job, outpath);

        boolean f = job.waitForCompletion(true);
        return f;
    } catch (Exception e) {
        e.printStackTrace();
    }
    return false;
}

static class Step4_Mapper extends Mapper<LongWritable, Text, Text, Text> {
    private String flag;// A同現(xiàn)矩陣 or B得分矩陣

    // 每個(gè)maptask低匙,初始化時(shí)調(diào)用一次
    @Override
    protected void setup(Context context) throws IOException,
            InterruptedException {
        FileSplit split = (FileSplit) context.getInputSplit();
        flag = split.getPath().getParent().getName();// 判斷讀的數(shù)據(jù)集

        System.out.println(flag + "**********************");
    }

    @Override
    protected void map(LongWritable key, Text value, Context context)
            throws IOException, InterruptedException {
        String[] tokens = Pattern.compile("[\t,]").split(value.toString());

        if (flag.equals("step3")) {// 同現(xiàn)矩陣
            // 樣本:  i100:i181   1
            //       i100:i184  2
            String[] v1 = tokens[0].split(":");
            String itemID1 = v1[0];
            String itemID2 = v1[1];
            String num = tokens[1];

            Text k = new Text(itemID1);// 以前一個(gè)歌曲id為key 比如i100
            Text v = new Text("A:" + itemID2 + "," + num);// A:i109,1
            // 樣本:  i100    A:i181,1
            context.write(k, v);

        } else if (flag.equals("step2")) {// 用戶對(duì)歌曲id喜愛得分矩陣
            // 樣本:  u24  i64:1,i218:1,i185:1,
            String userID = tokens[0];
            for (int i = 1; i < tokens.length; i++) {
                String[] vector = tokens[i].split(":");
                String itemID = vector[0];// 歌曲idid
                String pref = vector[1];// 喜愛分?jǐn)?shù)

                Text k = new Text(itemID); // 以歌曲id為key 比如:i100
                Text v = new Text("B:" + userID + "," + pref); // B:u401,2
                // 樣本:  i64 B:u24,1
                context.write(k, v);
            }
        }
    }
}

static class Step4_Reducer extends Reducer<Text, Text, Text, Text> {
    @Override
    protected void reduce(Text key, Iterable<Text> values, Context context)
            throws IOException, InterruptedException {
        // A同現(xiàn)矩陣 or B得分矩陣
        // 某一個(gè)歌曲id,針對(duì)它和其他所有歌曲id的同現(xiàn)次數(shù)碳锈,都在mapA集合中
        Map<String, Integer> mapA = new HashMap<String, Integer>();
        //和該歌曲id(key中的itemID)同現(xiàn)的其他歌曲id的同現(xiàn)集合//
        //其他歌曲idID為map的key顽冶,同現(xiàn)數(shù)字為值
        
        Map<String, Integer> mapB = new HashMap<String, Integer>();
        //該歌曲id(key中的itemID),所有用戶的推薦權(quán)重分?jǐn)?shù)

        for (Text line : values) {
            String val = line.toString();
            if (val.startsWith("A:")) {// 表示歌曲id同現(xiàn)數(shù)字// 樣本:  i100    A:i181,1
                String[] kv = Pattern.compile("[\t,]").split(val.substring(2));
                try {
                    mapA.put(kv[0], Integer.parseInt(kv[1]));//mapA:"i181" -> "1"
                } catch (Exception e) {
                    e.printStackTrace();
                }

            } else if (val.startsWith("B:")) {// 樣本:  i64   B:u24,1
                String[] kv = Pattern.compile("[\t,]").split(
                        val.substring(2));
                try {
                    mapB.put(kv[0], Integer.parseInt(kv[1]));//mapB:"u24" -> "1"
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

        
        double result = 0;
        //同現(xiàn)矩陣A
        Iterator<String> iter = mapA.keySet().iterator();
        //MR原語特征售碳,這里只有一種歌曲的同現(xiàn)列表
        while (iter.hasNext()) {
            String mapk = iter.next();// itemID

            int num = mapA.get(mapk).intValue();
            Iterator<String> iterb = mapB.keySet().iterator();
            //MR原語特征强重,這里是所有用戶的同一歌曲的評(píng)分,迭代之
            while (iterb.hasNext()) {//迭代用戶名
                String mapkb = iterb.next();// userID
                int pref = mapB.get(mapkb).intValue();
                //注意這里的計(jì)算思維理解:
                    //針對(duì)A歌曲
                    //使用用戶對(duì)A歌曲的分值
                    //逐一乘以與A歌曲有同現(xiàn)的歌曲的次數(shù)
                    //但是計(jì)算推薦向量的時(shí)候需要的是A歌曲同現(xiàn)的歌曲贸人,用同現(xiàn)次數(shù)乘以各自的分值
                result = num * pref;// 矩陣乘法相乘計(jì)算

                                    //Text k = new Text(mapkb);
                                    //Text v = new Text(mapk + "," + result);
                                    //結(jié)果樣本:  u2723      i9,8.0
                                    //context.write(k, v);
                
                Text k = new Text(mapkb+","+mapk);
                Text v = new Text( key.toString() + "," + result);
                //key:101
                //  結(jié)果樣本:   u3,101   101,4.0   *
                //  結(jié)果樣本:   u3,102   101,4.0   
                //  結(jié)果樣本:   u3,103   101,4.0
                //key:102
                //  結(jié)果樣本:   u3,101   102,4.0   *
                //  結(jié)果樣本:   u3,102   102,4.0   
                //  結(jié)果樣本:   u3,103   102,4.0
                
                context.write(k, v);
            }
        }
    }
}

}
第四步比較飄

  • 把同現(xiàn)矩陣和得分矩陣相乘
  • 利用MR原語特征间景,按歌曲分組
  • 這樣相同歌曲的同現(xiàn)列表和所有用戶對(duì)該歌曲的評(píng)分進(jìn)到一個(gè)reduce中

這一步也可以理解為,之前邏輯我們是打橫計(jì)算的艺智,現(xiàn)在我們先打豎把所有參數(shù)計(jì)算好拱燃,把整個(gè)矩陣填好,第五步再打橫的一條數(shù)據(jù)一條數(shù)據(jù)技術(shù)出來:


先把整個(gè)矩陣的數(shù)計(jì)算出來

運(yùn)算結(jié)果:


第四步結(jié)果

第五步:

    public class Step5 {
    private final static Text K = new Text();
    private final static Text V = new Text();

    public static boolean run(Configuration config, Map<String, String> paths) {
        try {
            FileSystem fs = FileSystem.get(config);
            Job job = Job.getInstance(config);
            job.setJobName("step5");
            job.setJarByClass(StartRun.class);
            job.setMapperClass(Step5_Mapper.class);
            job.setReducerClass(Step5_Reducer.class);
            job.setMapOutputKeyClass(Text.class);
            job.setMapOutputValueClass(Text.class);

            FileInputFormat
                    .addInputPath(job, new Path(paths.get("Step5Input")));
            Path outpath = new Path(paths.get("Step5Output"));
            if (fs.exists(outpath)) {
                fs.delete(outpath, true);
            }
            FileOutputFormat.setOutputPath(job, outpath);

            boolean f = job.waitForCompletion(true);
            return f;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    static class Step5_Mapper extends Mapper<LongWritable, Text, Text, Text> {

        /**
         * 原封不動(dòng)輸出
         */
        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws IOException, InterruptedException {
            //  樣本:  u2723:101  i9,8.0
            String[] tokens = Pattern.compile("[\t]").split(value.toString());
            Text k = new Text(tokens[0]);// 用戶為key
            Text v = new Text(tokens[1] );
            context.write(k, v);
        }
    }

    static class Step5_Reducer extends Reducer<Text, Text, Text, Text> {
        @Override
        protected void reduce(Text key, Iterable<Text> values, Context context)
                throws IOException, InterruptedException {
            Map<String, Double> map = new HashMap<String, Double>();// 結(jié)果
            Double score = 0.0;
            for (Text line : values) {// i9,4.0
                String[] tokens = line.toString().split(",");
//              String itemID = tokens[0];
                score += Double.parseDouble(tokens[1]);

            }

            String[] tmp = StringUtils.split(key.toString(),',');
            key.set(tmp[0]);
            Text v = new Text(tmp[1]+","+String.valueOf(score));
            context.write(key, v);

        }
        // 樣本:  u13 i9,5.0
    }
}
  • 把相乘之后的矩陣相加獲得結(jié)果矩陣
  • 還是按用戶分組力惯,將該用戶所有歌曲的推薦向量求和
    運(yùn)行結(jié)果:


    第五步結(jié)果

第六步:

    public class Step6 {
    private final static Text K = new Text();
    private final static Text V = new Text();

    public static boolean run(Configuration config, Map<String, String> paths) {
        try {
            FileSystem fs = FileSystem.get(config);
            Job job = Job.getInstance(config);
            job.setJobName("step6");
            job.setJarByClass(StartRun.class);
            job.setMapperClass(Step6_Mapper.class);
            job.setReducerClass(Step6_Reducer.class);
            job.setSortComparatorClass(NumSort.class);
            job.setGroupingComparatorClass(UserGroup.class);
            job.setMapOutputKeyClass(PairWritable.class);
            job.setMapOutputValueClass(Text.class);

            FileInputFormat
                    .addInputPath(job, new Path(paths.get("Step6Input")));
            Path outpath = new Path(paths.get("Step6Output"));
            if (fs.exists(outpath)) {
                fs.delete(outpath, true);
            }
            FileOutputFormat.setOutputPath(job, outpath);

            boolean f = job.waitForCompletion(true);
            return f;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return false;
    }

    static class Step6_Mapper extends
            Mapper<LongWritable, Text, PairWritable, Text> {

        @Override
        protected void map(LongWritable key, Text value, Context context)
                throws IOException, InterruptedException {
            String[] tokens = Pattern.compile("[\t,]").split(value.toString());
            String u = tokens[0];
            String item = tokens[1];
            String num = tokens[2];
            PairWritable k = new PairWritable();
            k.setUid(u);
            k.setNum(Double.parseDouble(num));
            V.set(item + ":" + num);
            context.write(k, V);
        }
    }

    static class Step6_Reducer extends Reducer<PairWritable, Text, Text, Text> {
        @Override
        protected void reduce(PairWritable key, Iterable<Text> values,
                Context context) throws IOException, InterruptedException {
            int i = 0;
            StringBuffer sb = new StringBuffer();
            for (Text v : values) {
                if (i == 10)
                    break;
                sb.append(v.toString() + ",");
                i++;
            }
            K.set(key.getUid());
            V.set(sb.toString());
            context.write(K, V);
        }

    }

    static class PairWritable implements WritableComparable<PairWritable> {

        private String uid;
        private double num;

        @Override
        public void write(DataOutput out) throws IOException {
            out.writeUTF(uid);
            out.writeDouble(num);
        }

        @Override
        public void readFields(DataInput in) throws IOException {
            this.uid = in.readUTF();
            this.num = in.readDouble();
        }

        @Override
        public int compareTo(PairWritable o) {
            int r = this.uid.compareTo(o.getUid());
            if (r == 0) {
                return Double.compare(this.num, o.getNum());
            }
            return r;
        }

        public String getUid() {
            return uid;
        }

        public void setUid(String uid) {
            this.uid = uid;
        }

        public double getNum() {
            return num;
        }

        public void setNum(double num) {
            this.num = num;
        }

    }

    static class NumSort extends WritableComparator {
        public NumSort() {
            super(PairWritable.class, true);
        }

        @Override
        public int compare(WritableComparable a, WritableComparable b) {
            PairWritable o1 = (PairWritable) a;
            PairWritable o2 = (PairWritable) b;

            int r = o1.getUid().compareTo(o2.getUid());
            if (r == 0) {
                return -Double.compare(o1.getNum(), o2.getNum());
            }
            return r;
        }
    }

    static class UserGroup extends WritableComparator {
        public UserGroup() {
            super(PairWritable.class, true);
        }

        @Override
        public int compare(WritableComparable a, WritableComparable b) {
            PairWritable o1 = (PairWritable) a;
            PairWritable o2 = (PairWritable) b;
            return o1.getUid().compareTo(o2.getUid());
        }
    }
}

第六步碗誉,按照推薦得分降序排序,每個(gè)用戶列出10個(gè)推薦歌曲父晶。
運(yùn)行結(jié)果:


最終結(jié)果

最終結(jié)果哮缺,key作為用戶id,value為推薦分?jǐn)?shù)由高到低的歌曲ID甲喝。

后語:

1.第四步可能有點(diǎn)繞尝苇,看代碼可能看不懂,如果是這樣子的話我建議你拿支筆埠胖,在紙上寫寫那張表的計(jì)算過程糠溜,找找其中規(guī)律,就發(fā)現(xiàn)直撤,妙胺歉汀!
2.用戶的action操作評(píng)分是我暫時(shí)亂寫的谋竖,具體要看你公司業(yè)務(wù)需要红柱,設(shè)計(jì)具體操作的分值。
2.生產(chǎn)中肯定不會(huì)這么簡(jiǎn)單蓖乘,例如要考慮一開始用戶沒數(shù)據(jù)時(shí)個(gè)推薦什么歌曲锤悄;推薦歌曲里不應(yīng)該有已收藏的歌曲,但收藏這個(gè)操作的分值不能低嘉抒,所以要去到下一步的過濾零聚。。。具體怎么優(yōu)化歡迎大家留言~


代碼下載地址:https://github.com/MichaelYipInGitHub/RecommendDemo


Thanks for watching隶症!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末容诬,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子沿腰,更是在濱河造成了極大的恐慌览徒,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,651評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件颂龙,死亡現(xiàn)場(chǎng)離奇詭異习蓬,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)措嵌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門躲叼,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人企巢,你說我怎么就攤上這事枫慷。” “怎么了浪规?”我有些...
    開封第一講書人閱讀 162,931評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵或听,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我笋婿,道長(zhǎng)誉裆,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,218評(píng)論 1 292
  • 正文 為了忘掉前任缸濒,我火速辦了婚禮足丢,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘庇配。我一直安慰自己斩跌,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評(píng)論 6 388
  • 文/花漫 我一把揭開白布捞慌。 她就那樣靜靜地躺著耀鸦,像睡著了一般。 火紅的嫁衣襯著肌膚如雪卿闹。 梳的紋絲不亂的頭發(fā)上揭糕,一...
    開封第一講書人閱讀 51,198評(píng)論 1 299
  • 那天萝快,我揣著相機(jī)與錄音锻霎,去河邊找鬼。 笑死揪漩,一個(gè)胖子當(dāng)著我的面吹牛旋恼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播奄容,決...
    沈念sama閱讀 40,084評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼冰更,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼产徊!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起蜀细,我...
    開封第一講書人閱讀 38,926評(píng)論 0 274
  • 序言:老撾萬榮一對(duì)情侶失蹤舟铜,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后奠衔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體谆刨,經(jīng)...
    沈念sama閱讀 45,341評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評(píng)論 2 333
  • 正文 我和宋清朗相戀三年归斤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了痊夭。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,731評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡脏里,死狀恐怖她我,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情迫横,我是刑警寧澤番舆,帶...
    沈念sama閱讀 35,430評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站矾踱,受9級(jí)特大地震影響合蔽,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜介返,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評(píng)論 3 326
  • 文/蒙蒙 一拴事、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧圣蝎,春花似錦刃宵、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至关面,卻和暖如春坦袍,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背等太。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工捂齐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人缩抡。 一個(gè)月前我還...
    沈念sama閱讀 47,743評(píng)論 2 368
  • 正文 我出身青樓奠宜,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子压真,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容