📉 第一章:痛点剖析——准确率的“回声室效应”
在推荐系统的早期,我们追求的是极致的准确率。协同过滤、逻辑回归、深度学习模型,都在试图回答一个问题:“用户喜欢什么?”
但当准确率达到一定阈值后,问题出现了:
过滤气泡: 系统只展示用户已知的兴趣,将用户封闭在信息茧房中。
- 长尾埋没: 冷门但优质的内容因为与用户历史行为相似度低,永远没有出头之日。
新颖性缺失: 用户感到乏味,因为系统太“听话”了。
我们的目标:
在推荐列表的后处理阶段(重排序),引入多样性约束。我们要在保留Top-K个最相关物品的同时,确保这些物品之间不能“长得太像”。
技术选型:最大边缘相关性算法
核心思想:在每一步选择下一个推荐项时,不仅考虑它与用户查询(或历史)的相关性,还要考虑它与已经选中的推荐项的相似性。如果它和已经选中的某个项太像,它的得分就会被惩罚。
🛠️ 第二章:数学建模——用公式定义“惊喜”
假设我们有一个候选集R,我们想要从中选出一个子集S作为最终推荐,使得S既相关又多样。
对于候选集中的每一个物品d,我们计算它的MMR得分:
Score(d) = lambda cdot Sim_{rel}(d, Q) – (1 – lambda) cdot max_{d’ in S} Sim_{div}(d, d’)
参数解析:
Sim_{rel}(d, Q):物品d与用户查询/兴趣Q的相关性得分(例如:预测评分、点击率)。
Sim_{div}(d, d’):物品d与已选集合S中某个物品d’的相似性(例如:基于内容的特征余弦相似度)。
lambda:平衡因子(0到1之间)。lambda越大,越重视准确性;越小,越重视多样性。
算法流程:
初始化空的结果列表S。
当S的大小小于目标数量N时:
a. 遍历候选集R中所有不在S中的物品d。
b. 计算d的Score。
c. 选择Score最高的d加入S。
d. 从R中移除d。
🧪 第三章:Java实战——手撕MMR重排序引擎
我们将模拟一个“电影推荐”场景。用户喜欢“科幻”和“动作”片,但系统不应该只推《星际穿越》,也应该试着推一部“科幻+爱情”的《HER》。
第一步:定义实体类
我们需要电影实体,包含ID、名称、特征向量(这里简化为一个数组)以及原始的相关性得分。
import lombok.Data;
import java.util.Arrays;
/**
电影实体类
为了演示多样性,我们用一个"特征向量"来表示电影的内容属性
例如:[动作指数, 科幻指数, 爱情指数, 喜剧指数]
*/
@Data
public class Movie {
private Long id;
private String title;
// 电影的内容特征向量 (归一化后的值)
private double[] features;
// 基于协同过滤或CTR预估的原始相关性得分 (准确性得分)
private double relevanceScore;
public Movie(Long id, String title, double[] features, double relevanceScore) {
this.id = id;
this.title = title;
this.features = features;
this.relevanceScore = relevanceScore;
}
@Override
public String toString() {
return String.format("《%s》(ID:%d) [动:%.2f,科:%.2f,爱:%.2f,喜:%.2f] 得分:%.3f",
title, id,
features[0], features[1], features[2], features[3],
relevanceScore);
}
}
第二步:核心工具类——向量计算与MMR算法
这是最核心的部分。我们将实现余弦相似度计算和MMR选择逻辑。
import java.util.*;
public class DiversityRecommender {
/**
计算两个向量之间的余弦相似度
用于衡量两部电影的"相似程度"
结果范围:-1 到 1,这里因为是非负特征,所以是 0 到 1
*/
public static double cosineSimilarity(double[] vec1, double[] vec2) {
if (vec1.length != vec2.length) {
throw new IllegalArgumentException("向量维度不匹配");
}
double dotProduct = 0.0;
double norm1 = 0.0;
double norm2 = 0.0;
for (int i = 0; i mmrSelection(List candidates, int n, double lambda) {
// 结果集
List selected = new ArrayList();
// 剩余候选集 (使用LinkedHashSet保持插入顺序,方便移除)
Set remaining = new LinkedHashSet(candidates);
// 如果候选数量不足,直接返回(虽然通常不会发生)
n = Math.min(n, candidates.size());
// MMR核心循环
for (int i = 0; i bestScore) {
bestScore = mmrScore;
bestCandidate = candidate;
}
}
// 5. 将选中的电影加入结果集,并从候选集中移除
if (bestCandidate != null) {
selected.add(bestCandidate);
remaining.remove(bestCandidate);
System.out.printf("第%d轮选择: %s (MMR得分: %.4f)%n", i+1, bestCandidate.getTitle(), bestScore);
}
}
return selected;
}
}
第三步:模拟测试场景
我们构建一个极端的场景:前3名都是纯科幻片,第4名是科幻爱情片。
public class DiversityTest {
public static void main(String[] args) {
// 模拟候选集 (假设已经经过召回和初步打分,按相关性降序排列)
List candidates = Arrays.asList(
// 1. 纯科幻动作 (特征向量: [动作, 科幻, 爱情, 喜剧])
new Movie(1L, “星际穿越”, new double[]{0.1, 0.9, 0.2, 0.0}, 0.98),
new Movie(2L, “星球大战”, new double[]{0.8, 0.9, 0.1, 0.0}, 0.95),
new Movie(3L, “银翼杀手”, new double[]{0.3, 0.9, 0.3, 0.0}, 0.92),
// 4. 科幻+爱情 (混合类型,与前三个略有不同)
new Movie(4L, “HER”, new double[]{0.1, 0.6, 0.9, 0.2}, 0.85), // 原始相关性稍低
// 5. 纯爱情喜剧
new Movie(5L, “恋恋笔记本”, new double[]{0.0, 0.1, 0.9, 0.8}, 0.70)
);
System.out.println("=== 原始候选集 (按相关性排序) ===");
candidates.forEach(System.out::println);
System.out.println("n=== 开始MMR重排序 (λ=0.6) ===");
// λ=0.6 表示稍微偏向准确率,但保留一定多样性
List result = DiversityRecommender.mmrSelection(candidates, 3, 0.6);
System.out.println("n=== 最终推荐列表 ===");
result.forEach(movie -> System.out.println("✅ 推荐: " + movie.getTitle()));
}
}
📊 第四章:结果分析与调优
运行输出模拟:
=== 原始候选集 (按相关性排序) ===
《星际穿越》… 得分:0.98
《星球大战》… 得分:0.95
《银翼杀手》… 得分:0.92
《HER》… 得分:0.85
《恋恋笔记本》… 得分:0.70
=== 开始MMR重排序 (λ=0.6) ===
第1轮选择: 星际穿越 (最相关,直接入选)
第2轮选择: 星球大战 (虽然相关,但和星际穿越太像,得分被扣)
第3轮选择: HER (因为HER与前两部的相似度较低,尽管原始得分0.85 < 0.92,但MMR得分反超!)
调优策略:
调节 lambda (Lambda):
lambda to 1.0:退化为纯贪心算法,只看相关性。(准确率优先)
lambda to 0.0:只看多样性,推荐一堆不相关但五花八门的东西。(探索优先)
建议: 从0.5开始A/B测试,根据业务指标调整。
特征工程:
上述例子用了简单的向量。在生产环境,特征可以是:
类别的One-Hot编码。
标签的TF-IDF权重。
甚至是从深度学习模型(如BERT)提取的Embedding向量。
分桶策略 (Hybrid Approach):
不要把所有鸡蛋放在MMR里。更稳健的做法是:
70% 的位置留给纯相关性排序(保底转化率)。
30% 的位置留给MMR或纯多样性探索(拉新)。
🚀 总结
小李看着屏幕上输出的推荐列表,若有所思:“原来不是要抛弃准确率,而是要在排序的过程中加入‘嫉妒心’。如果一个物品和已经选中的太像,就‘惩罚’它。”
我拍了拍他的肩膀:“没错。推荐系统的高阶玩法,就是做用户兴趣的‘拓扑学家’——既要沿着用户已知的路径深挖(准确率),又要时不时打通新的隧道,连接到未知的大陆(多样性)。”
“那这个算法上线后,监控指标要注意什么?”
“主要看两个:一是覆盖率(是否有更多种类的物品被推荐出去),二是惊喜度(用户是否点击了那些不在他历史兴趣列表里的物品)。只要这两个指标上升,且CTR没有断崖式下跌,我们就成功了。”
“明白!这就去改代码!”
“去吧,记住,好的算法不是让用户看到他们‘已经知道’的世界,而是帮他们发现他们‘可能喜欢’的世界。”


