1. 什么是 MMR? #
最大边际相关性(Maximal Marginal Relevance,MMR) 是一种用于信息检索和文本摘要的算法,由 Carbonell 和 Goldstein 在 1998 年提出。
MMR 的核心目标是:在返回的结果中,既要保证相关性(结果与查询相关),又要保证多样性(结果之间不重复、不冗余)。
1.1 为什么需要 MMR? #
想象一下,你在搜索引擎中搜索"人工智能最新进展",如果没有 MMR,可能会返回 10 篇都是关于"GPT-4 发布"的相似文章。虽然这些文章都很相关,但信息重复,你无法获得全面的信息。
使用 MMR 后,返回的结果可能是:
- GPT-4 最新进展
- 深度学习硬件突破
- AI 伦理讨论
- 计算机视觉新应用
- 医疗 AI 成功案例
这样你就能获得更全面、更多样化的信息。
2. MMR 的核心思想 #
MMR 算法的核心思想可以用一句话概括:在保证相关性的同时,最大化结果的多样性。
2.1 两个关键目标 #
- 相关性(Relevance):选出的文档应该与查询高度相关
- 多样性(Diversity):选出的文档之间应该尽可能不同,避免重复
2.2 如何平衡? #
MMR 通过一个公式来平衡这两个目标:
$$ \text{MMR分数} = \lambda \times \text{相关性} - (1 - \lambda) \times \text{与已选文档的最大相似度} $$
这个公式的含义是:
- 相关性部分($\lambda \times \text{相关性}$):我们希望文档与查询相关,这部分越大越好
- 多样性部分($(1 - \lambda) \times \text{最大相似度}$):我们希望文档与已选文档不同,这部分越小越好(所以用减法)
通过调整参数 $\lambda$,我们可以控制更偏向相关性还是多样性。
3. 算法公式 #
MMR 的完整公式如下:
$$ \text{MMR} = \arg\max_{D_i \in R \setminus S} \left[ \lambda \cdot \text{Sim}_1(D_i, Q) - (1 - \lambda) \cdot \max_{D_j \in S} \text{Sim}_2(D_i, D_j) \right] $$
3.1 公式符号说明 #
- $D_i$:当前正在评估的候选文档
- $Q$:查询(用户输入的问题或关键词)
- $S$:已经选择的结果集(已选文档的集合)
- $R$:所有候选文档的集合
- $R \setminus S$:候选文档中还未被选中的部分
- $\text{Sim}_1(D_i, Q)$:文档 $D_i$ 与查询 $Q$ 的相关性评分
- $\text{Sim}_2(D_i, D_j)$:文档 $D_i$ 与已选文档 $D_j$ 的相似度
- $\max_{D_j \in S} \text{Sim}_2(D_i, D_j)$:文档 $D_i$ 与所有已选文档的最大相似度
- $\lambda$:权衡参数,取值范围是 0 到 1
3.2 公式含义 #
这个公式的意思是:在所有未选中的候选文档中,找到 MMR 分数最高的那个文档。
MMR 分数由两部分组成:
- 相关性奖励:$\lambda \times \text{Sim}_1(D_i, Q)$,文档与查询越相关,这部分越大
- 多样性惩罚:$(1 - \lambda) \times \max_{D_j \in S} \text{Sim}_2(D_i, D_j)$,文档与已选文档越相似,这部分越大,但因为是减法,所以会降低总分
4. 参数 λ 的作用 #
参数 $\lambda$(lambda)是 MMR 算法的关键参数,它控制着相关性和多样性之间的平衡。
4.1 λ 的取值范围 #
λ = 1:完全偏向相关性
- 只考虑文档与查询的相关性,不考虑多样性
- 相当于传统的相关性排序
- 可能返回很多相似的文档
λ = 0:完全偏向多样性
- 只考虑文档之间的差异,不考虑与查询的相关性
- 可能返回与查询不太相关但彼此不同的文档
λ = 0.5:平衡相关性和多样性
- 相关性和多样性的权重相等
λ = 0.7:略微偏向相关性(常用设置)
- 更重视相关性,但也会考虑多样性
- 这是实际应用中最常用的设置
4.2 如何选择 λ? #
- 信息检索:通常使用 0.5-0.7,保证结果相关且多样
- 文档摘要:可以使用 0.3-0.5,更重视多样性
- 推荐系统:通常使用 0.6-0.8,保证推荐相关
5. 算法步骤 #
MMR 算法采用贪心策略,逐步选择文档。下面是详细的步骤:
5.1 算法流程 #
输入:
- R:候选文档集合
- Q:查询
- k:需要选择的文档数量
- λ:权衡参数
输出:
- S:选择的结果集
步骤:
1. 初始化 S = ∅(空集合)
2. 计算所有候选文档与查询的相关性分数
3. 选择第一个文档:
- 选择与查询最相关的文档
- 将其加入 S,并从 R 中移除
4. 重复以下步骤,直到选择了 k 个文档:
a. 对 R 中每个未选文档 D_i:
计算 MMR 分数:
分数 = λ × Sim1(D_i, Q) - (1-λ) × max(Sim2(D_i, D_j)),其中 D_j ∈ S
b. 选择 MMR 分数最高的文档
c. 将该文档加入 S,并从 R 中移除
5. 返回 S5.2 为什么先选最相关的? #
第一个文档直接选择最相关的,因为此时还没有已选文档,无法计算多样性。从第二个文档开始,才需要考虑与已选文档的差异。
5.3 代码实现 #
# 导入NumPy库
import numpy as np
# 定义余弦相似度计算函数
def cosine_similarity(from_vec, to_vecs):
"""
计算一个向量与多个向量的余弦相似度
参数:
from_vec: 单个向量(1D或2D数组或列表)
to_vecs: 多个向量(2D或3D数组,每一行为一个向量)
返回:
一维数组,包含单个向量与每个多个向量的相似度
示例:
sims = cosine_similarity(from_vec, to_vecs)
"""
# 将from_vec转换为NumPy数组并强制类型为float
from_vec = np.array(from_vec, dtype=float)
# 将to_vecs转换为NumPy数组并强制类型为float
to_vecs = np.array(to_vecs, dtype=float)
# 计算from_vec的范数(模长)
norm1 = np.linalg.norm(from_vec)
# 如果from_vec是零向量,直接返回全0相似度数组
if norm1 == 0:
return np.zeros(len(to_vecs))
# 初始化用于存储相似度的列表
similarities = []
# 遍历每一个待比较的向量to_vec
for to_vec in to_vecs:
# 计算from_vec和to_vec的点积
dot_product = np.sum(from_vec * to_vec)
# 计算to_vec的模长(范数)
norm_vec = np.linalg.norm(to_vec)
# 如果to_vec为零向量,相似度设为0
if norm_vec == 0:
similarities.append(0.0)
else:
# 计算余弦相似度
similarity = dot_product / (norm1 * norm_vec)
similarities.append(similarity)
# 将列表转换为NumPy数组后返回
return np.array(similarities)
# 封装MMR算法为函数
def mmr_select(query_vector, doc_vectors, k=3, lambda_mult=0.5):
"""
使用最大边际相关性(MMR)算法选择文档
参数:
query_vector: 查询向量(1D数组)
doc_vectors: 文档向量集合(2D数组,每行一个文档向量)
k: 要选择的文档数量(默认3)
lambda_mult: λ参数,平衡相关性与多样性(默认0.5)
- λ=1: 只看相关性
- λ=0: 只看多样性
- λ=0.5: 平衡相关性和多样性
返回:
selected: 选中的文档索引列表(从0开始)
示例:
selected = mmr_select(query_vector, doc_vectors, k=3, lambda_mult=0.5)
"""
# 计算所有文档与查询向量的余弦相似度
query_similarities = cosine_similarity(query_vector, doc_vectors)
# 打印所有文档与查询向量的余弦相似度分数
print("文档与查询的相关性分数:")
for i, sim in enumerate(query_similarities):
# 依次输出文档编号(从1开始)及其相似度分数
print(f"文档{i+1}: {sim:.4f}")
# 找到与查询最相关的文档的下标,作为初始已选文档
selected = [int(np.argmax(query_similarities))]
# 不断循环直到已选择k个文档
while len(selected) < k:
# 存储每个未选文档的MMR分数(二元组)
mmr_scores = []
# 遍历所有文档编号
for i in range(len(doc_vectors)):
# 如果当前文档未被选择
if i not in selected:
# 获取当前文档与查询的相关性分数
relevance = query_similarities[i]
# 获取当前已选文档的向量集合
selected_vecs = doc_vectors[selected]
# 计算当前文档与所有已选文档的余弦相似度
sims = cosine_similarity(doc_vectors[i], selected_vecs)
# 取对已选集中最大相似度(最“不多样性的”)
max_sim = np.max(sims)
# 按MMR公式计算分数
mmr_score = lambda_mult * relevance - (1 - lambda_mult) * max_sim
# 存储该文档的编号和分数
mmr_scores.append((i, mmr_score))
# 如果没有可选文档则跳出循环
if not mmr_scores:
break
# 选取MMR分数最高的文档编号
best_idx, best_score = max(mmr_scores, key=lambda x: x[1])
# 将选中的文档添加到已选列表
selected.append(best_idx)
# 返回最终已选文档的索引
return selected
# 使用示例区域
# 设置λ参数,权衡相关性与多样性
lambda_mult = 1
# 指定需要选择的文档数量k
k = 3
# 构造查询向量(比如“人工智能”主题)
query_vector = np.array([4, 2])
# 构造待选文档的向量列表
doc_vectors = np.array([
[9, 2],
[2, 9],
[7, 8],
[1, 3],
[6, 1]
])
# 调用MMR算法选择文档
selected = mmr_select(query_vector, doc_vectors, k=k, lambda_mult=lambda_mult)
# 打印最终选中的所有文档的编号(转换为从1计算的序号)
print(f"\nλ={lambda_mult:.1f}: 最终选择文档 {[int(s)+1 for s in selected]}")5.4 贪心策略的优缺点 #
优点:
- 简单高效,容易实现
- 计算速度快
缺点:
- 可能不是全局最优解
- 早期选择会影响后续选择
6. 实际应用示例 #
6.1 搜索引擎结果多样化 #
场景:用户搜索"Python 教程"
不使用 MMR:可能返回 10 个都是"Python 基础教程"的相似结果
使用 MMR:返回多样化的结果
- Python 基础语法教程
- Python 数据分析教程
- Python Web 开发教程
- Python 机器学习教程
- Python 爬虫教程
6.2 推荐系统 #
场景:用户喜欢看科幻电影
不使用 MMR:可能推荐 10 部都是"太空科幻"的相似电影
使用 MMR:推荐多样化的科幻电影
- 太空科幻(星际穿越)
- 时间旅行(回到未来)
- 人工智能(机械姬)
- 反乌托邦(银翼杀手)
- 外星人(E.T.)
6.3 文档摘要 #
场景:为一篇长文章生成摘要
不使用 MMR:可能选择的句子都来自同一段落,信息重复
使用 MMR:选择覆盖文章不同方面的句子,信息更全面
7. 优缺点分析 #
7.1 优点 #
- 简单有效:算法逻辑清晰,容易理解和实现
- 可调节:通过 λ 参数可以灵活控制相关性和多样性的平衡
- 通用性强:适用于多种场景(检索、推荐、摘要等)
- 效果明显:能够有效减少结果冗余,提高信息覆盖度
7.2 缺点 #
- 贪心策略:采用贪心算法,可能不是全局最优解
- 计算复杂度:时间复杂度为 O(kn²),当文档数量 n 很大时,计算较慢
- 需要调参:λ 参数需要根据具体任务调整,没有统一的最优值
- 依赖相似度计算:效果很大程度上依赖于相似度计算的准确性
7.3 适用场景 #
适合使用 MMR 的场景:
- 需要返回多样化结果的检索系统
- 推荐系统(避免推荐相似商品)
- 文档摘要(选择覆盖不同方面的句子)
- 结果数量较少(k < 20)的场景
不适合使用 MMR 的场景:
- 只需要相关性排序,不需要多样性
- 文档数量非常大(n > 10000),计算成本高
- 对实时性要求极高的场景