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

打散是在推荐、广告、搜索系统的结果基础上,提升用户视觉体验的一种处理。主要方法是对结果进行一个呈现顺序上的重排序,令相似品类的对象分散开,避免用户疲劳。在互联网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<Item> 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<Item>() {
            @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 guoyanchao@eqxiu.com
     * @param numbers
     * @param length
     * @return
     */
    public static List<Item> windowsScatter(List<Item> numbers, Integer length){
//        List<Item> ls = new ArrayList<>();
        if(length == null || length > groupByType(numbers).size()){
            length = groupByType(numbers).size();
        }
        for(int i=0; i<numbers.size()-length; i++){
            List<Item> subls = numbers.subList(i, i+length);
            List<String> keys = new ArrayList<>();
            int j = length+i;
            for(int m=0; m<length; m++){
                Item item = subls.get(m);
                if(keys.contains(item.getType())){
                    numbers.set(i+m, numbers.get(j));
                    numbers.set(j, item);
                    keys.add(item.getType());
                    j+=1;
                }else{
                    keys.add(item.getType());
                }
            }
        }
        return numbers;
    }
    /**
     * 通过分桶对全局数据打散
     * @author guoyanchao@eqxiu.com
     * @param numbers
     * @return
     */
    public static List<Item> bucketScatter(List<Item> numbers){
        Map<String, List<Item>> map = groupByType(numbers);
        List<Item> 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<maxSize; i++){
            List<Item> tmp = new ArrayList<>();
            for(String k: map.keySet()){
                List<Item> gls = map.get(k);
                if(i<gls.size()){
                    tmp.add(gls.get(i));
                }
            }
            Collections.sort(tmp, new Comparator<Item>() {
                @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 guoyanchao@eqxiu.com
     * @param numbers
     * @return
     */
    public static List<Item> scoreScatter(List<Item> numbers) {
        List<Item> ls = new ArrayList<>();
        Map<String, List<Item>> 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<Item>() {
            @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<String, List<Item>> groupByType(List<Item> numbers){
        Map<String, List<Item>> map = new HashMap<>();

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

    private static List<Item> cumulativeSum(List<Item> 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 +
                    '}';
        }
    }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容