打散是在推薦红省、廣告额各、搜索系統(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 +
'}';
}
}
}