搜索&推荐打散排序算法实战

打散是在推荐、广告、搜索系统的结果基础上,提升用户视觉体验的一种处理。主要方法是对结果进行一个呈现顺序上的重排序,令相似品类的对象分散开,避免用户疲劳。在互联网APP中,例如电商(淘宝、京东、拼多多)、信息流(头条、微博、看一看)、短视频(抖音、快手、微信视频号)等,搜索推荐的多样性对优化点击转化效率、用户体验、浏览深度、停留时长、回访、留存等目标至关重要。

商品打散能解决如下效果

1、相似品类的商品易扎堆这是必然的,如果商品的各特征相似,其获得的推荐分数也容易相近,导致推荐的商品缺乏多样性,而满目的同款肯定不是用户期望的结果。

2、用户心理层面,对于隐私或者偏好被完美捕捉这件事是敏感的,过于精准的结果不但容易导致用户的反感,也容易限制用户潜力的转化。

3、对于行为稀疏的用户,很容易出现对仅有特征的放大,从而就容易产生错误推荐。

多样性评价指标

ILS(intra-list similarity)

ILS主要是针对单个用户,一般来说ILS值越大,单个用户推荐列表多样性越差。

其中,i 和 j 为Item,k 为推荐列表长度,Sim() 为相似性度量方法

方案

通过三种方案进行实现

按列打散法

既然要避免相似属性的内容在呈现时相邻,很直接的思路是我们将不同属性的装在不同的桶里,每次要拿的时候尽量选择不同的桶。这样就可以实现将元素尽量打散。如下图所示,在这个例子中,初始的列表是共有三类(蓝、黄、红):

将他们按序装到桶里(通常是HashMap):

这个时候,我们把每个桶按列取出元素,即可以保证元素被最大程度打散,最终结果为


为了保证对原算法结果的保留,我们在取每一列时都有一次按原序排序的过程。这种算法的优点为:

简单直接,容易实现

打散效果好,虽然排序可能导致元素在列的开头和结尾偶然相邻,但是在末尾之前,最多相邻元素为2,不影响体验

性能比较稳定,不易受输入结构影响

缺点为:

1、末尾打散失效,容易出现扎堆

2、对原序的尊重性不算强,即使有推荐系数非常低的对象也强制出现在前面

3、只能考虑一种维度的分类,无法综合考虑别的因素

同时也可以看出,这个算法对类目数量有着相当的依赖,如果类目足够细致,这个算法的缺点就可以被部分掩盖,性能上,时间和空间消耗都是O(n)的

窗口滑动法

实际场景中,用户并不会一下看到整个序列,往往一次返回topN个,填满用户窗口就可以了。这个时候,我们应当发掘一种只参考局部的方法,基本思想就是滑动窗口。

如下图所示,我们开启一个size为3的窗口,以此来类比用户的接收窗口,规定窗口内不能有元素重复,即模拟用户看到的一个展示页面没有重复,如果窗口内发现重复元素,则往后探测一个合适的元素与当前元素交换。在第一步时,我们探测到2、3同类,于是将3拿出来,往后探测到4符合3处的要求,于是交换3、4,窗口往后滑动一格。第二步时,发现还存在窗口中2、3同类,再将3、5交换,窗口往后滑动一格,发现窗口内无冲突,再滑动一格。第三步时,发现5、6同类,将6、7交换,窗口滑动到最后,尽管还发现了7、8同类,但彼时已无可交换元素,便不作处理。

定义离散函数

一个比较好用的内容打散算法如下所示,它能够拉大同类内容的区分度,从而使得不同的内容实现混插。其中V(k,j)代表推荐结果中,分类k中排序为j的商品的推荐分数。V(k,j)”代表最终修正后的推荐分数。u代表离散率,取值范围(0,1),越接近于0,则离散性越强。该算法要求先对数据进行分桶,如第一种案列打散方法,对桶内数据按照如下公式重新计算分值:

实际应用中不单纯使用其中任何一种,一定要明确需求,然后结合需求来分析,取三者的优势。

Java实现

 
import java.util.*;

public class DataSorted {
    static double u = 0.5;
    public static void main(String[] args) {
        List ls = new ArrayList<>();
        ls.add(new Item("1","A",11.0));
        ls.add(new Item("2","A",10.0));
        ls.add(new Item("3","B",10.1));
        ls.add(new Item("4","A",4.0));
        ls.add(new Item("4","C",9.0));
        ls.add(new Item("4","C",11.0));
        ls.add(new Item("4","B",11.0));
        Collections.sort(ls, new Comparator() {
            @Override
            public int compare(Item o1, Item o2) {
                Double diff = o1.getScore() - o2.getScore();
                if(diff>0){
                    return -1;
                }else{
                    return 1;
                }
            }
        });
        System.out.println(ls);
        System.out.println(scoreScatter(ls));
        System.out.println(bucketScatter(ls));
        System.out.println(windowsScatter(ls, 2));
    }

    /**
     * 通过设置滑动窗口,对窗口内元素一定程度打散
     * @author [email protected]
     * @param numbers
     * @param length
     * @return
     */
    public static List windowsScatter(List numbers, Integer length){
//        List ls = new ArrayList<>();
        if(length == null || length > groupByType(numbers).size()){
            length = groupByType(numbers).size();
        }
        for(int i=0; i subls = numbers.subList(i, i+length);
            List keys = new ArrayList<>();
            int j = length+i;
            for(int m=0; m bucketScatter(List numbers){
        Map> map = groupByType(numbers);
        List ls = new ArrayList<>();
        int maxSize = 0;
        for(String key : map.keySet()) {
            if(map.get(key).size()>maxSize){
                maxSize = map.get(key).size();
            }
        }
        for(int i=0; i tmp = new ArrayList<>();
            for(String k: map.keySet()){
                List gls = map.get(k);
                if(i() {
                @Override
                public int compare(Item o1, Item o2) {
                    Double diff = o1.getScore() - o2.getScore();
                    if(diff>0){
                        return -1;
                    }else{
                        return 1;
                    }
                }
            });
            ls.addAll(tmp);
        }
        return ls;
    }
    /**
     * 通过重新计算得分,使用新的排名进行打散
     * @author [email protected]
     * @param numbers
     * @return
     */
    public static List scoreScatter(List numbers) {
        List ls = new ArrayList<>();
        Map> map = groupByType(numbers);
//        System.out.println(map);
        for(String key : map.keySet()) {
            numbers = map.get(key);
            numbers = cumulativeSum(numbers);
            for (int i = 0; i < numbers.size(); i++) {
                Item item = numbers.get(i);
                if (i < numbers.size() - 1) {
                    item.setNewScore(Math.pow(Math.pow(item.getNewScore(), 1 / u) - Math.pow(numbers.get(i + 1).getNewScore(), 1 / u), u));
                } else {
                    item.setNewScore(Math.pow(Math.pow(item.getNewScore(), 1 / u), u));
                }
            }
            ls.addAll(numbers);
        }
        Collections.sort(ls, new Comparator() {
            @Override
            public int compare(Item o1, Item o2) {
                Double diff = o1.getNewScore() - o2.getNewScore();
                if(diff>0){
                    return -1;
                }else{
                    return 1;
                }
            }
        });
        return ls;
    }

    public static Map> groupByType(List numbers){
        Map> map = new HashMap<>();

        for(Item item : numbers){
            if(map.containsKey(item.getType())){
                map.get(item.getType()).add(item);
            }else{
                List ls = new ArrayList<>();
                ls.add(item);
                map.put(item.getType(), ls);
            }
        }
        return map;
    }

    private static List cumulativeSum(List numbers) {

        double sum = 0;
        for (int i = numbers.size()-1; i >= 0; i--) {
            Item item = numbers.get(i);
            sum += item.getScore(); // find sum
            item.setNewScore(sum);
//            numbers.set(i, item); // replace
        }

        return numbers;
    }

    static class Item{
        String pid = null;
        String type = null;
        Double score = 0.0;
        Double newScore = null;
        public Item(String pid, String type, Double score) {
            this.pid = pid;
            this.score = score;
            this.type = type;
        }

        public void setType(String type) {
            this.type = type;
        }

        public void setScore(Double score) {
            this.score = score;
        }

        public void setNewScore(Double newScore) {
            this.newScore = newScore;
        }

        public String getType() {
            return type;
        }

        public Double getScore() {
            return score;
        }

        public Double getNewScore() {
            return newScore;
        }

        public void setPid(String pid) {
            this.pid = pid;
        }

        public String getPid() {
            return pid;
        }

        @Override
        public String toString() {
            return "Item{" +
                    "pid='" + pid + '\'' +
                    ", type='" + type + '\'' +
                    ", score=" + score +
                    ", newScore=" + newScore +
                    '}';
        }
    }
}

你可能感兴趣的:(搜索&推荐打散排序算法实战)