参考

重排(二)个性化重排

上一节我们探讨了基于贪心策略的重排序方法,如MMR (Maximal Marginal Relevance) 和 DPP (Determinantal Point Processes)。这些方法通过显式定义多样性、相关性或覆盖度的优化目标,在初始排序列表上进行局部调整。它们计算高效且可解释性强,但在处理复杂的物品间相互影响和深度个性化方面存在局限:目标函数往往需要手工设计,难以捕捉高阶、非线性的交互模式;同时,将用户个性化信息深度融入列表级优化也颇具挑战。

接下来会介绍两个经典的个性化重排模型:PRM和PRS。

1、PRM:基于Transformer的个性化重排

原理

PRM (Personalized Re-Ranking Model) 的提出,标志着重排序技术从基于规则/启发式向数据驱动、端到端学习的重要转变。其核心思想是:利用强大的序列建模能力(Transformer)自动学习列表中物品间复杂的相互影响,并将细粒度的用户个性化信息深度融入整个重排序过程,通过最大化列表级效用目标(如点击率)进行全局优化。 PRM不再依赖预设的多样性公式,而是让模型直接从数据中学习最优的物品组合方式,同时精准反映用户的独特偏好。下图展示了PRM的整体架构:

PRM模型架构

输入层

输入层的核心任务是为初始列表 S=[i1,i2,...,in]S = [i_1, i_2, ..., i_n] 中的每个物品 iji_j 准备一个信息丰富且适合后续处理的初始表示。这个表示需要包含两个至关重要的方面:

  1. 物品自身特征 (XX): 例如物品ID嵌入、类别、标签、统计特征等基础信息。

  2. 用户对该物品的个性化偏好 (PVPV): 这是PRM实现"个性化"重排序的灵魂所在!它编码了用户uu与物品iji_j之间独特的互动关系和偏好程度。PV 的生成是其关键创新点,我们将在后面详细探讨。

PRM采用了一个直观且有效的方法:将物品的原始特征向量 xjx_j 与其对应的个性化向量 pvjpv_j 拼接(Concatenate)起来,形成一个更全面的基础表示 [xj;pvj][x_j; pv_j]

然而,仅有个性化和物品特征还不够。基础排序模型给出的初始列表 S=[i1,i2,...,in]S = [i_1, i_2, ..., i_n] 本身包含潜在的序列信息(例如,排名靠前的物品可能相关性更高)。为了利用这一信息,PRM引入了标准的位置嵌入 (Positional Embedding, PE)。这就像给列表中的每个位置(第1位、第2位…第n位)赋予一个独特的、可学习的向量标识。最终,每个物品进入编码层之前的输入表示EE是其融合特征与位置信息的叠加:

E=[物品自身特征(xj);个性化向量(pvj)]+位置嵌入(pej)E = [\text{物品自身特征}(x_j) ; \text{个性化向量}(pv_j)] + \text{位置嵌入}(pe_j)

这个组合结果通常会经过一个简单的前馈网络(线性变换)进行维度调整,以适应后续Transformer编码器的输入要求。

编码层

输入层提供了带有个性化烙印和位置信息的物品序列。编码层的核心使命是利用 Transformer 架构的强大序列建模能力,让列表中的所有物品能够充分"沟通"和"互动",从而捕捉它们之间复杂的、高阶的相互影响。这对于重排序至关重要,因为:

  • 用户是否点击列表中的第 j 个物品,很可能受到第 k 个(甚至更远)物品的显著影响(例如,它们是替代品、互补品,或者提供了多样性)。

  • 这种影响往往是长距离的,不受物品在列表中初始物理位置的限制。

Transformer的核心武器是 自注意力机制 (Self-Attention)。它赋予了序列中每个物品一项能力:可以动态地"关注"序列中的所有其他物品(包括它自己)。其工作原理是计算每个物品的查询向量(Query)与其他物品的键向量(Key)的相似度,得到一个注意力权重。这个权重决定了在更新当前物品表示时,应该聚合(Value) 多少来自其他物品的信息。公式 Attention(Q,K,V)=softmax(QKTd)VAttention(Q, K, V) = softmax(\frac{QK^T}{\sqrt{d}}) V精炼地描述了这个加权聚合过程。

为了更全面、更鲁棒地捕捉列表中物品间复杂的相互影响,PRM采用了多头注意力,这些多头注意力模块被组织在标准的 Transformer 编码器块(Block) 中,每个块包含一个多头自注意力层和一个前馈神经网络层。PRM通过堆叠多层,使模型能够在初始交互表示的基础上,逐层提炼更复杂、更高阶的物品间依赖关系。最终,编码层输出每个物品的高级表示FNxF^{N_x},它深度融合了物品自身特征、用户个性化偏好以及在整个列表上下文感知到的深度交互信息。

输出层

PRM采用了一个轻量级但有效的输出结构:对每个物品的高级表示 FNxF^{N_x} 应用一个线性变换(WfFNx+bfW^f \cdot F^{N_x} + b^f),将其映射为一个标量分数(或称 logit)。这个分数初步反映了该物品在重排序后的列表中的相对价值。将列表中所有物品的标量分数输入一个 Softmax 函数。Softmax 在此扮演两个关键角色:

  1. 归一化: 将所有分数转换为一个概率分布 P(yiX,PV;θ^)P(y_i | X, PV; \hat{\theta}),其中 yiy_i 表示物品 ii 在最终列表中被认为是最合适(或最可能被点击)的概率。所有物品的概率之和为1。

  2. 隐含相对关系建模: Softmax 函数的特性使得每个物品的最终概率不仅取决于它自身的分数,也取决于它与列表中所有其他物品分数的相对比较。这天然地符合重排序需要评估物品间相对重要性的需求

个性化向量 (PV) 的生成

回顾整个流程,个性化向量 PVPV 是PRM区别于普通重排序模型、实现真正"个性化"的关键所在。那么,PVPV 从何而来?PRM采用了一个巧妙且实用的策略:利用预训练的点击率预估模型来生成PV。

  1. 预训练模型的作用: 这个模型在海量的用户历史行为数据(用户ID、物品ID、上下文特征、历史点击/转化日志)上进行训练。它的核心任务是学习预测:给定用户 uu 及其行为历史 HuH_u,用户点击某个候选物品 ii 的概率 P(yiHu,u;θ)P(y_i | H_u, u; \theta')

  2. 提取个性化向量: PRM 并不直接使用预训练模型预测的点击概率本身。相反,它提取该模型在输出最终点击概率(通常经过Sigmoid激活)之前的那个隐藏层的激活值。这个隐藏层的向量,蕴含了预训练模型学习到的、关于"用户 uu 对物品 ii 偏好程度"的丰富、抽象的信息,将其作为物品 ii 相对于用户 uu 的个性化向量 pvipv_i

  3. 输入PRM: 对于初始列表 S=[i1,i2,...,in]S = [i_1, i_2, ..., i_n] 中的每个物品 iji_j,都通过上述预训练模型计算出其对应的 pvjpv_j,然后作为关键输入送入PRM的输入层。

代码

prm.py 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import tensorflow as tf

from .layers import PositionEncodingLayer, TransformerEncoder
from .utils import (
build_input_layer,
build_group_feature_embedding_table_dict,
concat_group_embedding,
concat_func,
add_tensor_func,
)


def build_prm_model(feature_columns, model_config):
"""构建与FunRec训练流水线兼容的PRM重排序模型。

Args:
feature_columns: FeatureColumn列表
model_config: 包含参数的字典:
- max_seq_len: int (默认30)
- transformer_blocks: int (默认2)
- nums_head: int (默认1)
- dropout_rate: float (默认0.1)
- intermediate_dim: int (默认64)
- pos_emb_trainable: bool (默认True)

Returns:
(model, None, None): 重排序模型元组
"""
max_seq_len = model_config.get("max_seq_len", 30)
transformer_blocks = model_config.get("transformer_blocks", 2)
nums_head = model_config.get("nums_head", 1)
dropout_rate = model_config.get("dropout_rate", 0.1)
intermediate_dim = model_config.get("intermediate_dim", 64)
pos_emb_trainable = model_config.get("pos_emb_trainable", True)

# 输入层
input_layer_dict = build_input_layer(feature_columns)
group_embedding_feature_dict = build_group_feature_embedding_table_dict(
feature_columns, input_layer_dict, prefix="embedding/"
)

user_part_embedding = concat_group_embedding(
group_embedding_feature_dict, "user_part"
) # B x D
# 将用户嵌入在序列长度维度上进行复制
user_part_embedding = tf.keras.layers.Lambda(
lambda x: tf.tile(tf.expand_dims(x, axis=1), [1, max_seq_len, 1])
)(
user_part_embedding
) # [B, max_len, D]

# 物品侧序列特征
item_part_embedding = concat_group_embedding(
group_embedding_feature_dict, "item_part", axis=-1, flatten=False
) # [B, max_len, K]
pv_embeddings = input_layer_dict["pv_emb"] # [B, max_len, D_pv]
item_embeddings = input_layer_dict["item_emb"] # [B, max_len, D_item]

page_embedding = concat_func(
[user_part_embedding, item_part_embedding, pv_embeddings, item_embeddings],
axis=-1,
) # [B, max_len, dim]

position_embedding = PositionEncodingLayer(
dims=feature_columns[0].emb_dim,
max_len=max_seq_len,
trainable=pos_emb_trainable,
initializer="glorot_uniform",
)(page_embedding)

enc_inputs = add_tensor_func([page_embedding, position_embedding])

for _ in range(transformer_blocks):
enc_inputs = TransformerEncoder(
intermediate_dim,
nums_head,
dropout_rate,
activation="relu",
normalize_first=True,
is_residual=True,
)(enc_inputs)

enc_output = tf.keras.layers.Dense(intermediate_dim, activation="tanh")(enc_inputs)
enc_output = tf.keras.layers.Dense(1)(enc_output)
flat = tf.keras.layers.Flatten()(enc_output)
score_output = tf.keras.layers.Activation(activation="softmax")(flat)

model = tf.keras.Model(inputs=list(input_layer_dict.values()), outputs=score_output)
return model, None, None

性能

1
2
3
4
5
6
PRM:
+----------+---------+--------------+-------------+------------+-----------+--------------+-------------+------------+-----------+--------+--------+
| map@10 | map@5 | new_map@10 | new_map@5 | new_p@10 | new_p@5 | old_map@10 | old_map@5 | old_p@10 | old_p@5 | p@10 | p@5 |
+==========+=========+==============+=============+============+===========+==============+=============+============+===========+========+========+
| 0.2626 | 0.2537 | 0.2626 | 0.2537 | 0.0828 | 0.1008 | 0.2954 | 0.2824 | 0.0936 | 0.1196 | 0.0828 | 0.1008 |
+----------+---------+--------------+-------------+------------+-----------+--------------+-------------+------------+-----------+--------+--------+

2、PRS:基于排列组合的重排

原理

虽然PRM通过Transformer架构实现了端到端的个性化重排序,但它仍然存在一个根本性的局限:缺乏对排列组合影响的深度理解。 想象这样一个场景:用户面对商品列表 [A, B, C] 时毫无购买欲望,但当看到 [B, A, C] 这个排列时却购买了商品A。这种现象被称为 排列变异影响 (Permutation-Variant Influence)。一个可能的解释是:将价格较高的商品B放在前面,会让用户觉得商品A相对便宜,从而激发购买欲望。

排列变异影响

这个观察引出了一个重要问题:传统的重排序方法(包括PRM)主要关注单个物品的分数优化,却忽略了物品排列顺序本身对用户行为的影响

PRS 的设计哲学可以用一个简单而深刻的题目来概括:如果我们能够评估所有可能的物品排列组合,并选择其中用户体验最佳的那一个,会怎样? 当然,对于包含n个物品的列表,所有可能的排列数量是n!,这在计算上是不可行的。PRS的巧妙之处在于提出了一个两阶段的解决方案:

  1. PMatch阶段:通过智能搜索算法快速筛选出少数几个"有希望"的候选排列
  2. PRank阶段:使用专门的神经网络模型精确评估这些候选排列的质量,选出最优解

这种设计既保证了计算效率,又能够捕捉到排列组合对用户体验的深层影响。

PRS整体架构

PRS框架整体架构

PMatch阶段:智能候选排列生成

PMatch (Permutation-Matching) 阶段的使命是从指数级的排列空间中,高效地识别出最有潜力的候选排列。这个阶段采用了一种名为FPSA (Fast Permutation Searching Algorithm) 的创新算法,它结合了目标导向的beam search和两个关键的用户行为预测模型。

离线训练:双模型预测体系

PMatch阶段需要两个point-wise预测模型的支持:

  1. CTR模型:预测用户点击某个物品的概率 PCTR(iu)P_{CTR}(i|u)
  2. Next模型:预测用户在浏览完当前物品后继续浏览下一个物品的概率 PNext(iu)P_{Next}(i|u)

Next模型的引入是PRS的一个重要创新。它反映了这样一个现实:用户的浏览行为具有连续性,某个物品不仅要能吸引用户点击,还要能够"引导"用户继续探索列表中的后续内容。这两个模型都采用标准的point-wise建模方式:

fCTR(xu,xi)=σ(WCTR[xu;xi]+bCTR)f_{CTR}(x_u, x_i) = \sigma(W_{CTR} \cdot [x_u; x_i] + b_{CTR})

fNext(xu,xi)=σ(WNext[xu;xi]+bNext)f_{Next}(x_u, x_i) = \sigma(W_{Next} \cdot [x_u; x_i] + b_{Next})

其中 xux_uxix_i 分别表示用户和物品的特征向量,σ\sigma 是sigmoid激活函数。两个模型都使用交叉熵损失进行训练。

在线服务:FPSA算法

FPSA结构图

上图展示了FPSA算法在整个PRS框架中的位置和核心组件。我们可以看到:

  1. 输入处理:算法接收来自ranking阶段的候选物品集合C,每个物品都有丰富的特征表示
  2. 双模型预测体系
    • CTR模型:预测每个物品的点击概率 PiCTRP^{CTR}_i
    • Next模型:预测用户浏览完物品i后继续浏览的概率 PiNEXTP^{NEXT}_i
  3. Beam Search核心:通过树状搜索逐步构建候选排列,每一步都基于奖励函数进行剪枝
  4. 奖励计算机制:融合rPV和rIPV两个指标,平衡浏览深度和点击收益

FPSA算法的核心创新在于将用户的浏览行为建模为一个序列决策过程。传统的重排序方法往往独立地评估每个物品,而FPSA认识到:物品在序列中的价值不仅取决于自身特征,更取决于它在整个浏览路径中的作用

算法定义了两个关键的奖励指标:

  • rPV (Page View Reward):衡量排列能够带来的总浏览深度,鼓励选择那些能引导用户深度浏览的物品组合
  • rIPV (Item Page View Reward):衡量排列中物品被点击的总概率,确保排列具有足够的商业价值

FPSA核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
def fpsa_algorithm(items, ctr_scores, next_scores, beam_size=5, max_length=10, alpha=0.5, beta=0.5):
"""
Fast Permutation Searching Algorithm (根据Algorithm 1实现)

Args:
items: 候选物品列表 (对应Algorithm 1的Input ranking list C)
ctr_scores: 每个物品的CTR分数字典 (对应P^CTR)
next_scores: 每个物品的Next分数字典 (对应P^NEXT)
beam_size: beam search的大小 (对应Beam size integer k)
max_length: 输出序列的最大长度 (对应Output length n)
alpha, beta: 融合系数 (对应Fusion coefficient float $\alpha, \beta$)

Returns:
候选排列集合 (对应Output: Candidate list set S)
"""
S = [()] # 候选列表集合,初始包含空序列
R = {} # 估计奖励集合

for i in range(1, max_length + 1):
St = S.copy()

S = []
R = {}

for O in St:
for ci in items:
if ci not in O:
Ot = O + (ci,)

r = calculate_estimated_reward(Ot, ctr_scores, next_scores, alpha, beta)

R[Ot] = r
S.append(Ot)

S = sorted(S, key=lambda x: R[x], reverse=True)[:beam_size]

return S

def calculate_estimated_reward(O, ctr_scores, next_scores, alpha, beta):
"""
计算估计奖励 (对应Algorithm 1的第19-28行 Calculate-Estimated-Reward函数)

Args:
O: 当前排列序列
ctr_scores: CTR分数字典
next_scores: Next分数字典
alpha, beta: 融合系数

Returns:
估计奖励值
"""
if not O:
return 0.0

r_pv = 1.0
r_ipv = 0.0

p_expose = 1.0

for ci in O:
p_ctr_ci = ctr_scores[ci]
p_next_ci = next_scores[ci]

r_ipv = r_ipv + p_expose * p_ctr_ci

p_expose *= p_next_ci

r_pv = p_expose

r_sum = alpha * r_pv + beta * r_ipv

return r_sum

# 使用示例保持不变
def get_ctr_score(item):
"""获取物品的CTR分数"""
# 这里应该调用预训练的CTR模型
pass

def get_next_score(item):
"""获取物品的Next分数"""
# 这里应该调用预训练的Next模型
pass

# 完整的FPSA调用
def run_fpsa(items, beam_size=5, max_length=10):
# 预计算所有物品的CTR和Next分数
ctr_scores = {item: get_ctr_score(item) for item in items}
next_scores = {item: get_next_score(item) for item in items}

# 运行FPSA算法
candidates = fpsa_algorithm(
items=items,
ctr_scores=ctr_scores,
next_scores=next_scores,
beam_size=beam_size,
max_length=max_length,
alpha=0.5, # 可调节的融合系数
beta=0.5
)

return candidates

这个算法的精妙之处在于:

  • rPV 鼓励选择那些能够引导用户深度浏览的物品排列

  • rIPV 确保排列中的物品具有足够的吸引力

  • 曝光概率的递减 真实地模拟了用户浏览行为:越往后的物品,被用户看到的概率越低

PRank阶段:深度排列评估

PRank (Permutation-Ranking) 阶段接收PMatch生成的候选排列,使用一个专门设计的神经网络模型DPWN (Deep Permutation-Wise Network) 来精确评估每个排列的质量。

DPWN模型架构

DPWN的设计理念是:排列中每个物品的价值不仅取决于它自身的特征,更取决于它在整个序列上下文中的位置和作用。为了捕捉这种复杂的序列依赖关系,DPWN采用了Bi-LSTM架构:

DPWN模型架构

模型结构介绍

  1. 序列编码层:对于输入序列中的第t个物品,DPWN使用双向LSTM计算其上下文表示:

ht=LSTMforward(xvt,ht1)\overrightarrow{h_t} = LSTM_{forward}(x_{v_t}, \overrightarrow{h_{t-1}})

ht=LSTMbackward(xvt,ht+1)\overleftarrow{h_t} = LSTM_{backward}(x_{v_t}, \overleftarrow{h_{t+1}})

ht=[ht;ht]h_t = [\overrightarrow{h_t}; \overleftarrow{h_t}]

其中 xvtx_{v_t} 是第t个物品的特征向量,hth_t 是融合了前向和后向信息的隐状态。

  1. 特征融合层:将序列表示与用户特征和物品特征进行融合:

zt=[ht;xu;xvt]z_t = [h_t; x_u; x_{v_t}]

其中 xux_u 是用户特征向量。

  1. 预测层:通过多层感知机预测每个位置的点击概率:

pt=σ(MLP(zt))p_t = \sigma(MLP(z_t))

List Reward (LR) 计算

PRank阶段的核心评估指标是List Reward,它被定义为排列中所有物品预测点击概率的总和:

LR(O)=t=1OptLR(O) = \sum_{t=1}^{|O|} p_t

这个简单而有效的指标反映了整个排列的预期收益。在线服务时,PRank会计算每个候选排列的LR值,并选择LR最高的排列作为最终输出。

重排全总结

本章深入探讨了推荐系统重排序阶段的核心技术,从传统的启发式方法发展到现代的深度学习模型。重排序作为推荐系统的“最后一公里”,其核心价值在于在保持相关性的基础上优化多样性、个性化和业务目标,从而显著提升用户体验和平台收益。

我们首先介绍了基于贪心策略的重排方法,包括MMR通过线性组合平衡相关性与多样性,以及DPP利用行列式几何意义实现更精确的多样性控制。随后深入分析了基于个性化的重排技术,PRM采用Transformer架构实现端到端学习,通过个性化向量深度融合用户偏好;而PRS进一步考虑排列变异影响,通过PMatch和PRank两阶段设计,在保证计算效率的同时捕捉排列组合对用户行为的深层影响。

从技术演进角度看,重排序技术经历了从简单规则到复杂模型的发展历程:MMR提供了快速有效的基础方案,DPP实现了理论严谨的多样性建模,PRM引入了深度学习的个性化能力,PRS则代表了当前最前沿的排列感知技术。