打散是在推荐、广告、搜索系统的结果基础上,提升用户视觉体验的一种处理。主要方法是对结果进行一个呈现顺序上的重排序,令相似品类的对象分散开,避免用户疲劳。在互联网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 +
'}';
}
}
}