搜索&推薦打散排序算法實戰(zhàn)

打散是在推薦红省、廣告额各、搜索系統(tǒng)的結(jié)果基礎上,提升用戶視覺體驗的一種處理吧恃。主要方法是對結(jié)果進行一個呈現(xiàn)順序上的重排序虾啦,令相似品類的對象分散開,避免用戶疲勞痕寓。在互聯(lián)網(wǎng)APP中傲醉,例如電商(淘寶、京東厂抽、拼多多)需频、信息流(頭條、微博筷凤、看一看)、短視頻(抖音苞七、快手藐守、微信視頻號)等,搜索推薦的多樣性對優(yōu)化點擊轉(zhuǎn)化效率蹂风、用戶體驗卢厂、瀏覽深度、停留時長惠啄、回訪慎恒、留存等目標至關(guān)重要。

商品打散能解決如下效果

1撵渡、相似品類的商品易扎堆這是必然的融柬,如果商品的各特征相似,其獲得的推薦分數(shù)也容易相近趋距,導致推薦的商品缺乏多樣性粒氧,而滿目的同款肯定不是用戶期望的結(jié)果。

2节腐、用戶心理層面外盯,對于隱私或者偏好被完美捕捉這件事是敏感的摘盆,過于精準的結(jié)果不但容易導致用戶的反感,也容易限制用戶潛力的轉(zhuǎn)化饱苟。

3孩擂、對于行為稀疏的用戶,很容易出現(xiàn)對僅有特征的放大箱熬,從而就容易產(chǎn)生錯誤推薦肋殴。

多樣性評價指標

ILS(intra-list similarity)

ILS主要是針對單個用戶,一般來說ILS值越大坦弟,單個用戶推薦列表多樣性越差护锤。

其中,i 和 j 為Item酿傍,k 為推薦列表長度烙懦,Sim() 為相似性度量方法

方案

通過三種方案進行實現(xiàn)

按列打散法

既然要避免相似屬性的內(nèi)容在呈現(xiàn)時相鄰,很直接的思路是我們將不同屬性的裝在不同的桶里赤炒,每次要拿的時候盡量選擇不同的桶氯析。這樣就可以實現(xiàn)將元素盡量打散。如下圖所示莺褒,在這個例子中掩缓,初始的列表是共有三類(藍、黃遵岩、紅):

將他們按序裝到桶里(通常是HashMap):

這個時候你辣,我們把每個桶按列取出元素,即可以保證元素被最大程度打散尘执,最終結(jié)果為


為了保證對原算法結(jié)果的保留舍哄,我們在取每一列時都有一次按原序排序的過程。這種算法的優(yōu)點為:

簡單直接誊锭,容易實現(xiàn)

打散效果好表悬,雖然排序可能導致元素在列的開頭和結(jié)尾偶然相鄰,但是在末尾之前丧靡,最多相鄰元素為2蟆沫,不影響體驗

性能比較穩(wěn)定,不易受輸入結(jié)構(gòu)影響

缺點為:

1温治、末尾打散失效饭庞,容易出現(xiàn)扎堆

2、對原序的尊重性不算強罐盔,即使有推薦系數(shù)非常低的對象也強制出現(xiàn)在前面

3但绕、只能考慮一種維度的分類,無法綜合考慮別的因素

同時也可以看出,這個算法對類目數(shù)量有著相當?shù)囊蕾嚹笏常绻惸孔銐蚣氈铝酰@個算法的缺點就可以被部分掩蓋,性能上幅骄,時間和空間消耗都是O(n)的

窗口滑動法

實際場景中劫窒,用戶并不會一下看到整個序列,往往一次返回topN個拆座,填滿用戶窗口就可以了主巍。這個時候,我們應當發(fā)掘一種只參考局部的方法挪凑,基本思想就是滑動窗口孕索。

如下圖所示,我們開啟一個size為3的窗口躏碳,以此來類比用戶的接收窗口搞旭,規(guī)定窗口內(nèi)不能有元素重復,即模擬用戶看到的一個展示頁面沒有重復菇绵,如果窗口內(nèi)發(fā)現(xiàn)重復元素肄渗,則往后探測一個合適的元素與當前元素交換。在第一步時咬最,我們探測到2翎嫡、3同類,于是將3拿出來永乌,往后探測到4符合3處的要求惑申,于是交換3、4铆遭,窗口往后滑動一格硝桩。第二步時,發(fā)現(xiàn)還存在窗口中2枚荣、3同類,再將3啼肩、5交換橄妆,窗口往后滑動一格,發(fā)現(xiàn)窗口內(nèi)無沖突祈坠,再滑動一格害碾。第三步時,發(fā)現(xiàn)5赦拘、6同類慌随,將6、7交換,窗口滑動到最后阁猜,盡管還發(fā)現(xiàn)了7丸逸、8同類,但彼時已無可交換元素剃袍,便不作處理黄刚。

定義離散函數(shù)

一個比較好用的內(nèi)容打散算法如下所示,它能夠拉大同類內(nèi)容的區(qū)分度民效,從而使得不同的內(nèi)容實現(xiàn)混插憔维。其中V(k,j)代表推薦結(jié)果中畏邢,分類k中排序為j的商品的推薦分數(shù)业扒。V(k,j)”代表最終修正后的推薦分數(shù)舒萎。u代表離散率程储,取值范圍(0,1),越接近于0逆甜,則離散性越強虱肄。該算法要求先對數(shù)據(jù)進行分桶,如第一種案列打散方法交煞,對桶內(nèi)數(shù)據(jù)按照如下公式重新計算分值:

實際應用中不單純使用其中任何一種咏窿,一定要明確需求,然后結(jié)合需求來分析素征,取三者的優(yōu)勢集嵌。

Java實現(xiàn)

 
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));
    }

    /**
     * 通過設置滑動窗口,對窗口內(nèi)元素一定程度打散
     * @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;
    }
    /**
     * 通過分桶對全局數(shù)據(jù)打散
     * @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 +
                    '}';
        }
    }
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末根欧,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子端蛆,更是在濱河造成了極大的恐慌凤粗,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件今豆,死亡現(xiàn)場離奇詭異嫌拣,居然都是意外死亡,警方通過查閱死者的電腦和手機呆躲,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門异逐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人插掂,你說我怎么就攤上這事灰瞻⌒壤” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵酝润,是天一觀的道長燎竖。 經(jīng)常有香客問我,道長袍祖,這世上最難降的妖魔是什么底瓣? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮蕉陋,結(jié)果婚禮上捐凭,老公的妹妹穿的比我還像新娘。我一直安慰自己凳鬓,他們只是感情好茁肠,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著缩举,像睡著了一般垦梆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上仅孩,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天托猩,我揣著相機與錄音,去河邊找鬼辽慕。 笑死京腥,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的溅蛉。 我是一名探鬼主播公浪,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼船侧!你這毒婦竟也來了欠气?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤镜撩,失蹤者是張志新(化名)和其女友劉穎预柒,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體袁梗,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡卫旱,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了围段。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡投放,死狀恐怖奈泪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤涝桅,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布拜姿,位于F島的核電站,受9級特大地震影響冯遂,放射性物質(zhì)發(fā)生泄漏蕊肥。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一蛤肌、第九天 我趴在偏房一處隱蔽的房頂上張望壁却。 院中可真熱鬧,春花似錦裸准、人聲如沸展东。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽盐肃。三九已至,卻和暖如春权悟,著一層夾襖步出監(jiān)牢的瞬間砸王,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工峦阁, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留谦铃,地道東北人。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓拇派,卻偏偏與公主長得像荷辕,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子件豌,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

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