跳转至

第20章 强化学习与Bandit推荐

⚠️ 时效性说明:本章涉及前沿模型/价格/榜单等信息,可能随版本快速变化;请以论文原文、官方发布页和 API 文档为准。

前置知识: 需要了解推荐系统基础(第1-10章)和强化学习基础(见强化学习教程)。本章聚焦RL/Bandit方法在推荐系统中的具体应用,是字节跳动等公司面试的高频考点。


20.1 推荐系统中的探索与利用

20.1.1 为什么推荐系统需要探索

推荐系统的核心目标是为用户推荐最相关的内容,但"最相关"依赖于对用户偏好的准确估计。如果系统只推荐当前认为最好的内容(利用 Exploitation),就永远无法发现用户潜在的新兴趣;如果系统经常推荐未知内容(探索 Exploration),短期用户体验会受损。

这就是经典的 Exploration vs Exploitation (EE) 问题。

Text Only
推荐系统中的EE困境:

用户画像 → [已知偏好: 科技、编程]  →  Exploitation: 推荐科技文章
                                    →  Exploration: 尝试推荐旅行、美食

问题:如何在不损害用户体验的前提下,持续发现用户的新兴趣?

20.1.2 纯利用的问题

只做 Exploitation 会导致严重的系统性问题:

问题 描述 后果
Filter Bubble 用户只能看到与历史行为相似的内容 信息茧房,用户视野变窄
马太效应 热门内容越推越热,冷门内容永无出头之日 内容生态不健康,创作者流失
估计偏差 未被展示的内容缺乏反馈数据,其价值永远被低估 系统无法自我修正
用户疲劳 反复推荐同类内容导致用户厌倦 留存率下降

20.1.3 EE在推荐各阶段的应用

EE策略可以在推荐系统的不同阶段实施:

召回阶段: - 在候选集中混入一定比例的随机/新颖物品 - 使用多路召回,其中一路专门负责探索(如随机召回、新品召回)

排序阶段: - 在排序模型的预估分上加噪声(ε-Greedy 思想) - 使用 Bandit 模型替代或辅助排序模型(如 LinUCB) - Thompson Sampling 对排序分进行采样

重排阶段: - 在最终推荐列表中插入探索性内容 - 使用 MMR(Maximal Marginal Relevance)增加多样性 - Determinantal Point Process (DPP) 保证列表多样性

20.1.4 面试考点

Q: 如何在推荐系统中平衡 Exploration 和 Exploitation?

A: 核心思路是在保证用户体验的前提下进行有策略的探索: 1. 流量分配:用一小部分流量(如5%)做探索,大部分流量做利用 2. 算法层面:使用 Bandit 算法(UCB/Thompson Sampling)自动平衡EE 3. 分阶段策略:召回阶段大胆探索(成本低),排序/重排阶段保守探索 4. 用户分群:对活跃用户多探索,对低活用户少探索 5. 置信度驱动:对模型不确定性高的物品进行探索(UCB思想) 6. 时间衰减:新用户多探索,随着数据积累逐渐减少探索比例


20.2 Multi-Armed Bandit基础

20.2.1 MAB问题形式化

Multi-Armed Bandit(多臂老虎机)是探索与利用问题的经典抽象:

  • \(K\) 个臂(arm),对应 \(K\) 个候选物品
  • 每轮选择一个臂拉动,获得一个随机奖励 \(r \sim P_a\)
  • 目标:在 \(T\) 轮内最大化累积奖励,等价于最小化累积遗憾(Regret)
\[R_T = T \cdot \mu^* - \sum_{t=1}^{T} \mu_{a_t}\]

其中 \(\mu^* = \max_a \mu_a\) 是最优臂的期望奖励,\(\mu_{a_t}\) 是第 \(t\) 轮选择的臂的期望奖励。

20.2.2 ε-Greedy策略

最简单的EE策略:以 \(\epsilon\) 的概率随机选择一个臂(探索),以 \(1-\epsilon\) 的概率选择当前估计最优的臂(利用)。

Python
import numpy as np

class EpsilonGreedy:
    """ε-Greedy策略"""
    def __init__(self, n_arms, epsilon=0.1):
        self.n_arms = n_arms
        self.epsilon = epsilon
        self.counts = np.zeros(n_arms)     # 每个臂被选次数
        self.values = np.zeros(n_arms)     # 每个臂的估计价值

    def select_arm(self):
        if np.random.random() < self.epsilon:
            return np.random.randint(self.n_arms)  # 探索
        else:
            return np.argmax(self.values)            # 利用

    def update(self, arm, reward):
        self.counts[arm] += 1
        n = self.counts[arm]
        # 增量更新均值: Q_new = Q_old + (r - Q_old) / n
        self.values[arm] += (reward - self.values[arm]) / n

缺点:探索是完全随机的,不考虑已有信息;\(\epsilon\) 是固定的,不会随时间衰减。

20.2.3 Upper Confidence Bound (UCB)

UCB 的核心思想:乐观面对不确定性。选择的不是估计值最高的臂,而是估计值+置信上界最高的臂。

\[a_t = \arg\max_a \left[ Q(a) + c\sqrt{\frac{\ln t}{N(a)}} \right]\]
  • \(Q(a)\):臂 \(a\) 的估计期望奖励
  • \(c\):探索系数,控制探索程度
  • \(\sqrt{\frac{\ln t}{N(a)}}\):置信半径,被选次数少的臂置信区间更宽
Python
class UCB:
    """Upper Confidence Bound策略"""
    def __init__(self, n_arms, c=2.0):
        self.n_arms = n_arms
        self.c = c
        self.counts = np.zeros(n_arms)
        self.values = np.zeros(n_arms)
        self.t = 0

    def select_arm(self):
        self.t += 1
        # 先让每个臂至少被选一次
        for arm in range(self.n_arms):
            if self.counts[arm] == 0:
                return arm

        ucb_values = self.values + self.c * np.sqrt(
            np.log(self.t) / self.counts
        )
        return np.argmax(ucb_values)

    def update(self, arm, reward):
        self.counts[arm] += 1
        n = self.counts[arm]
        self.values[arm] += (reward - self.values[arm]) / n

优点:探索有方向性,优先探索不确定性高的臂;有理论保证(Regret上界为 \(O(\sqrt{KT\ln T})\))。

20.2.4 Thompson Sampling

Thompson Sampling 使用贝叶斯方法,为每个臂维护一个奖励分布的后验,每轮从后验中采样来做决策。

Beta-Bernoulli模型(适用于0/1奖励,如点击/不点击): - 先验:\(\text{Beta}(\alpha=1, \beta=1)\)(均匀分布) - 观测到一次成功(点击):\(\alpha \leftarrow \alpha + 1\) - 观测到一次失败(不点击):\(\beta \leftarrow \beta + 1\) - 后验:\(\text{Beta}(\alpha, \beta)\),均值为 \(\frac{\alpha}{\alpha + \beta}\)

Python
class ThompsonSampling:
    """Thompson Sampling (Beta-Bernoulli)"""
    def __init__(self, n_arms):
        self.n_arms = n_arms
        self.alpha = np.ones(n_arms)  # 成功次数 + 1
        self.beta = np.ones(n_arms)   # 失败次数 + 1

    def select_arm(self):
        # 从每个臂的后验Beta分布中采样
        samples = np.random.beta(self.alpha, self.beta)
        return np.argmax(samples)

    def update(self, arm, reward):
        # reward为0或1
        if reward == 1:
            self.alpha[arm] += 1
        else:
            self.beta[arm] += 1

优点:自动平衡EE(不确定性大的臂采样方差大,被选概率高);实现简洁;经验效果通常最好。

20.2.5 三种策略对比实验

Python
import numpy as np
import matplotlib.pyplot as plt
# EpsilonGreedy, UCB, ThompsonSampling 类定义同上

class BanditEnv:
    """模拟K臂老虎机环境"""
    def __init__(self, true_probs):
        self.true_probs = np.array(true_probs)  # np.array创建NumPy数组
        self.n_arms = len(true_probs)
        self.best_arm = np.argmax(true_probs)
        self.best_prob = true_probs[self.best_arm]

    def pull(self, arm):
        return 1 if np.random.random() < self.true_probs[arm] else 0

# ----- 运行实验 -----
def run_experiment(env, policy, n_rounds=5000):
    """运行一次实验,返回累积遗憾"""
    regrets = []
    cumulative_regret = 0
    for t in range(n_rounds):
        arm = policy.select_arm()
        reward = env.pull(arm)
        policy.update(arm, reward)
        instant_regret = env.best_prob - env.true_probs[arm]
        cumulative_regret += instant_regret
        regrets.append(cumulative_regret)
    return regrets

# 设置环境:10个臂的Bandit
np.random.seed(42)
true_probs = [0.05, 0.08, 0.12, 0.15, 0.20, 0.25, 0.30, 0.35, 0.10, 0.45]
env = BanditEnv(true_probs)
n_rounds = 5000
n_experiments = 20  # 多次实验取平均

policies = {
    'ε-Greedy (ε=0.1)': lambda: EpsilonGreedy(env.n_arms, epsilon=0.1),
    'UCB (c=2)': lambda: UCB(env.n_arms, c=2.0),
    'Thompson Sampling': lambda: ThompsonSampling(env.n_arms),
}

plt.figure(figsize=(10, 6))
for name, policy_fn in policies.items():
    all_regrets = []
    for _ in range(n_experiments):
        policy = policy_fn()
        regrets = run_experiment(env, policy, n_rounds)
        all_regrets.append(regrets)
    mean_regrets = np.mean(all_regrets, axis=0)
    plt.plot(mean_regrets, label=name)

plt.xlabel('Round')
plt.ylabel('Cumulative Regret')
plt.title('Bandit策略对比 - 累积遗憾曲线')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('bandit_comparison.png', dpi=150)
plt.show()
print(f"最优臂: arm {env.best_arm}, 真实概率: {env.best_prob}")

实验观察: - Thompson Sampling 通常 Regret 增长最慢,收敛最快 - UCB 表现稳定,有理论保证 - ε-Greedy 因为持续随机探索,Regret 线性增长

20.2.6 面试考点

Q: UCB 与 Thompson Sampling 的优劣对比?

维度 UCB Thompson Sampling
理论保证 有严格Regret上界证明 有渐近最优性证明
实现复杂度 简单,确定性选择 需要采样,略复杂
经验效果 通常更好
超参数 探索系数 \(c\) 需要调 仅需选择先验(不敏感)
批量更新 不太自然 天然支持批量/延迟更新
扩展性 易扩展到连续动作 后验复杂时需近似采样
工业应用 较多用于广告、推荐 广泛用于A/B测试、推荐

结论:工业界更偏好 Thompson Sampling,因为效果好、对超参数不敏感、天然支持批量延迟反馈。


20.3 Contextual Bandit

20.3.1 从MAB到Contextual Bandit

经典MAB的局限:不考虑上下文。给所有用户推荐同一个最优臂,没有个性化。

Contextual Bandit 引入上下文向量 \(\mathbf{x}_{t,a}\),每轮: 1. 观察上下文 \(\mathbf{x}_t\)(用户特征、物品特征、交叉特征等) 2. 根据上下文选择动作 \(a_t\) 3. 观察奖励 \(r_t\) 4. 更新策略

Text Only
经典MAB:       用户 → 选择arm → 获得reward
Contextual:    用户 + 上下文特征 → 选择arm → 获得reward

在推荐场景中: - 上下文:用户特征(年龄、性别、历史行为)+ 环境特征(时间、地点、设备) - 臂/动作:候选物品或推荐策略 - 奖励:点击、购买、观看时长等

20.3.2 LinUCB算法详解

LinUCB(Li et al., 2010)是最经典的 Contextual Bandit 算法,Yahoo! 用它做新闻推荐。

核心假设:每个臂 \(a\) 的期望奖励是上下文特征的线性函数:

\[\mathbb{E}[r_{t,a} | \mathbf{x}_{t,a}] = \mathbf{x}_{t,a}^\top \boldsymbol{\theta}_a^*\]

其中 \(\boldsymbol{\theta}_a^*\) 是臂 \(a\) 的未知参数向量。

岭回归参数估计

通过收集的数据 \(\{(\mathbf{x}_s, r_s)\}\) 进行岭回归估计:

\[\hat{\boldsymbol{\theta}}_a = \mathbf{A}_a^{-1} \mathbf{b}_a\]

其中: - \(\mathbf{A}_a = \mathbf{I}_d + \sum_{s: a_s=a} \mathbf{x}_s \mathbf{x}_s^\top\)(正则化协方差矩阵) - \(\mathbf{b}_a = \sum_{s: a_s=a} r_s \mathbf{x}_s\)(特征加权奖励累积)

UCB置信区间

选择策略加入置信上界:

\[a_t = \arg\max_a \left[ \mathbf{x}_{t,a}^\top \hat{\boldsymbol{\theta}}_a + \alpha \sqrt{\mathbf{x}_{t,a}^\top \mathbf{A}_a^{-1} \mathbf{x}_{t,a}} \right]\]

\(\sqrt{\mathbf{x}_{t,a}^\top \mathbf{A}_a^{-1} \mathbf{x}_{t,a}}\) 衡量的是在当前上下文下对臂 \(a\) 奖励估计的不确定性。

完整 LinUCB 实现

Python
import numpy as np

class LinUCB:
    """
    Disjoint LinUCB算法
    每个臂有独立的参数向量θ_a
    """
    def __init__(self, n_arms, d, alpha=1.0):
        """
        Args:
            n_arms: 候选臂数量
            d: 上下文特征维度
            alpha: 探索系数,控制置信区间宽度
        """
        self.n_arms = n_arms
        self.d = d
        self.alpha = alpha

        # 每个臂的参数
        self.A = {}  # A_a = I_d + Σ x_s x_s^T
        self.b = {}  # b_a = Σ r_s * x_s

        for a in range(n_arms):
            self.A[a] = np.eye(d)        # d×d 单位矩阵
            self.b[a] = np.zeros(d)       # d维零向量

    def select_arm(self, context_vectors):
        """
        选择一个臂
        Args:
            context_vectors: dict or list, {arm_id: context_vector}
                             或 shape=(n_arms, d) 的数组
        Returns:
            chosen_arm: 选择的臂编号
        """
        ucb_values = np.zeros(self.n_arms)

        for a in range(self.n_arms):
            if isinstance(context_vectors, dict):  # isinstance检查类型
                x = context_vectors[a]
            else:
                x = context_vectors[a]

            A_inv = np.linalg.inv(self.A[a])  # np.linalg线性代数运算
            theta_hat = A_inv @ self.b[a]        # 参数估计

            # UCB = x^T θ_hat + α * sqrt(x^T A^{-1} x)
            pred = x @ theta_hat                   # 预测奖励
            confidence = self.alpha * np.sqrt(x @ A_inv @ x)  # 置信宽度
            ucb_values[a] = pred + confidence

        return np.argmax(ucb_values)

    def update(self, arm, context, reward):
        """
        更新臂的参数
        Args:
            arm: 选择的臂
            context: 上下文向量 shape=(d,)
            reward: 获得的奖励
        """
        self.A[arm] += np.outer(context, context)  # A_a += x x^T
        self.b[arm] += reward * context              # b_a += r * x

# ----- LinUCB 模拟实验 -----
def simulate_linucb():
    """模拟新闻推荐场景"""
    np.random.seed(42)

    n_arms = 5       # 5篇候选新闻
    d = 6            # 6维用户特征
    n_rounds = 3000
    alpha = 0.5

    # 每个臂的真实参数(未知,模拟用)
    true_theta = np.random.randn(n_arms, d) * 0.5

    agent = LinUCB(n_arms, d, alpha=alpha)

    cumulative_reward = 0
    rewards_history = []
    optimal_rewards = []

    for t in range(n_rounds):
        # 生成随机用户上下文
        user_context = np.random.randn(d)
        user_context = user_context / np.linalg.norm(user_context)  # 归一化

        # 构建每个臂的上下文(此处简化为共享用户特征)
        contexts = {a: user_context for a in range(n_arms)}

        # 计算真实期望奖励
        true_rewards = [user_context @ true_theta[a] for a in range(n_arms)]
        optimal_arm = np.argmax(true_rewards)

        # LinUCB选择
        chosen_arm = agent.select_arm(contexts)

        # 生成奖励(真实期望 + 噪声)
        reward = user_context @ true_theta[chosen_arm] + np.random.randn() * 0.1
        reward = max(0, min(1, reward))  # 裁剪到[0,1]

        # 更新
        agent.update(chosen_arm, user_context, reward)

        cumulative_reward += reward
        rewards_history.append(cumulative_reward / (t + 1))
        optimal_rewards.append(
            max(0, min(1, true_rewards[optimal_arm]))
        )

    # 绘制平均奖励曲线
    plt.figure(figsize=(10, 5))
    plt.plot(rewards_history, label='LinUCB Avg Reward', alpha=0.8)
    window = 100
    rolling_optimal = np.convolve(
        optimal_rewards, np.ones(window)/window, mode='valid'
    )
    plt.plot(range(window-1, n_rounds), rolling_optimal,
             label='Optimal (rolling avg)', alpha=0.8, linestyle='--')
    plt.xlabel('Round')
    plt.ylabel('Average Reward')
    plt.title('LinUCB 新闻推荐模拟')
    plt.legend()
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig('linucb_simulation.png', dpi=150)
    plt.show()
    print(f"最终平均奖励: {rewards_history[-1]:.4f}")  # [-1]负索引取最后元素

import matplotlib.pyplot as plt
simulate_linucb()

20.3.3 Disjoint vs Hybrid LinUCB

类型 参数结构 适用场景
Disjoint 每个臂独立的 \(\boldsymbol{\theta}_a\) 臂之间无共享特征时
Hybrid 共享参数 \(\boldsymbol{\beta}\) + 独立参数 \(\boldsymbol{\theta}_a\) 臂之间有共享特征(如物品共有属性)

Hybrid LinUCB 的奖励模型:

\[\mathbb{E}[r_{t,a}] = \mathbf{z}_{t,a}^\top \boldsymbol{\beta}^* + \mathbf{x}_{t,a}^\top \boldsymbol{\theta}_a^*\]
  • \(\mathbf{z}_{t,a}\):用户-物品共享特征
  • \(\mathbf{x}_{t,a}\):臂特有的特征

共享参数 \(\boldsymbol{\beta}\) 允许跨臂的知识迁移,对冷启动物品特别有帮助。

20.3.4 面试考点

Q: LinUCB如何处理冷启动?

A: LinUCB 天然具有冷启动能力: 1. 新物品冷启动:新物品的 \(\mathbf{A}_a = \mathbf{I}_d\)\(\mathbf{b}_a = \mathbf{0}\),因此 \(\hat{\boldsymbol{\theta}}_a = \mathbf{0}\),预测值为0,但置信宽度 \(\sqrt{\mathbf{x}^\top \mathbf{A}_a^{-1} \mathbf{x}}\) 很大,UCB值高,系统自动倾向于探索新物品 2. Hybrid模式:共享参数 \(\boldsymbol{\beta}\) 可以利用已有物品的知识帮助估计新物品 3. 特征泛化:即使物品是新的,只要有上下文特征(如内容特征),模型也能给出合理的初始估计 4. 与协同过滤对比:协同过滤需要用户-物品交互数据,新物品无数据时完全无法预测;LinUCB 只需特征即可


20.4 深度强化学习推荐

20.4.1 DQN在推荐中的应用

当状态空间和动作空间很大时,传统 Bandit 方法难以扩展。深度强化学习(Deep RL)使用神经网络逼近价值函数或策略函数,可以处理高维连续状态空间。

推荐系统的MDP建模

要素 定义 示例
状态 \(s_t\) 用户当前状态 用户历史行为序列、画像特征
动作 \(a_t\) 推荐决策 推荐一个物品 / 一个列表
奖励 \(r_t\) 即时反馈 点击=1,购买=5,跳过=0
转移 \(P\) 状态转换 用户行为后状态更新
折扣 \(\gamma\) 长期收益权重 0.9~0.99

核心优势:DQN 可以优化长期用户价值(如用户留存),而不仅是即时点击率。

20.4.2 DQN推荐Agent

Python
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from collections import deque
import random
import matplotlib.pyplot as plt

class QNetwork(nn.Module):  # 继承nn.Module定义网络层
    """Q网络:输入状态,输出每个动作的Q值"""
    def __init__(self, state_dim, n_actions, hidden_dim=128):
        super().__init__()  # super()调用父类方法
        self.net = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, n_actions)
        )

    def forward(self, x):
        return self.net(x)

class DQNRecommender:
    """基于DQN的推荐Agent"""
    def __init__(self, state_dim, n_items, hidden_dim=128,
                 lr=1e-3, gamma=0.99, epsilon_start=1.0,
                 epsilon_end=0.01, epsilon_decay=0.995,
                 buffer_size=10000, batch_size=64,
                 target_update_freq=100):
        self.state_dim = state_dim
        self.n_items = n_items
        self.gamma = gamma
        self.epsilon = epsilon_start
        self.epsilon_end = epsilon_end
        self.epsilon_decay = epsilon_decay
        self.batch_size = batch_size
        self.target_update_freq = target_update_freq
        self.step_count = 0

        # 主网络和目标网络
        self.q_net = QNetwork(state_dim, n_items, hidden_dim)
        self.target_net = QNetwork(state_dim, n_items, hidden_dim)
        self.target_net.load_state_dict(self.q_net.state_dict())
        self.target_net.eval()

        self.optimizer = optim.Adam(self.q_net.parameters(), lr=lr)
        self.loss_fn = nn.SmoothL1Loss()  # Huber Loss

        # 经验回放
        self.replay_buffer = deque(maxlen=buffer_size)

    def select_item(self, state):
        """ε-Greedy选择推荐物品"""
        if random.random() < self.epsilon:
            return random.randint(0, self.n_items - 1)

        with torch.no_grad():  # 禁用梯度计算,节省内存
            state_t = torch.FloatTensor(state).unsqueeze(0)  # unsqueeze增加一个维度
            q_values = self.q_net(state_t)
            return q_values.argmax(dim=1).item()  # 将单元素张量转为Python数值

    def store_transition(self, state, action, reward, next_state, done):
        """存入经验回放"""
        self.replay_buffer.append((state, action, reward, next_state, done))

    def train_step(self):
        """从经验回放中采样并训练"""
        if len(self.replay_buffer) < self.batch_size:
            return None

        batch = random.sample(self.replay_buffer, self.batch_size)
        states, actions, rewards, next_states, dones = zip(*batch)  # zip按位置配对

        states = torch.FloatTensor(np.array(states))
        actions = torch.LongTensor(actions).unsqueeze(1)
        rewards = torch.FloatTensor(rewards).unsqueeze(1)
        next_states = torch.FloatTensor(np.array(next_states))
        dones = torch.FloatTensor(dones).unsqueeze(1)

        # 当前Q值
        q_values = self.q_net(states).gather(1, actions)

        # 目标Q值 (Double DQN)
        with torch.no_grad():
            next_actions = self.q_net(next_states).argmax(dim=1, keepdim=True)
            next_q = self.target_net(next_states).gather(1, next_actions)
            target_q = rewards + self.gamma * next_q * (1 - dones)

        loss = self.loss_fn(q_values, target_q)

        self.optimizer.zero_grad()  # 清零梯度
        loss.backward()  # 反向传播计算梯度
        torch.nn.utils.clip_grad_norm_(self.q_net.parameters(), 1.0)
        self.optimizer.step()  # 更新参数

        # 更新目标网络
        self.step_count += 1
        if self.step_count % self.target_update_freq == 0:
            self.target_net.load_state_dict(self.q_net.state_dict())

        # 衰减ε
        self.epsilon = max(self.epsilon_end,
                          self.epsilon * self.epsilon_decay)

        return loss.item()

# ----- 简化模拟 -----
class SimpleRecEnv:
    """简化的推荐环境模拟"""
    def __init__(self, n_items=20, state_dim=16):
        self.n_items = n_items
        self.state_dim = state_dim
        # 模拟用户偏好
        self.user_pref = np.random.randn(state_dim)
        self.item_embeddings = np.random.randn(n_items, state_dim) * 0.5
        self.step = 0
        self.max_steps = 50

    def reset(self):
        self.user_pref = np.random.randn(self.state_dim)
        self.step = 0
        return self._get_state()

    def _get_state(self):
        return self.user_pref.copy()

    def act(self, item_id):
        """模拟用户对推荐物品的反应"""
        # 相似度越高,奖励越高
        sim = np.dot(self.user_pref, self.item_embeddings[item_id])  # np.dot矩阵/向量点乘
        prob_click = 1 / (1 + np.exp(-sim))  # sigmoid
        clicked = np.random.random() < prob_click
        reward = 1.0 if clicked else 0.0

        # 更新用户状态(模拟兴趣漂移)
        if clicked:
            self.user_pref = 0.9 * self.user_pref + \
                             0.1 * self.item_embeddings[item_id]

        self.step += 1
        done = self.step >= self.max_steps
        return self._get_state(), reward, done

def train_dqn_recommender():
    """训练DQN推荐Agent"""
    env = SimpleRecEnv(n_items=20, state_dim=16)
    agent = DQNRecommender(
        state_dim=16, n_items=20,
        lr=1e-3, gamma=0.95, buffer_size=5000, batch_size=32
    )

    n_episodes = 300
    episode_rewards = []

    for ep in range(n_episodes):
        state = env.reset()
        total_reward = 0

        while True:
            action = agent.select_item(state)
            next_state, reward, done = env.act(action)
            agent.store_transition(state, action, reward, next_state, done)

            loss = agent.train_step()
            state = next_state
            total_reward += reward

            if done:
                break

        episode_rewards.append(total_reward)

        if (ep + 1) % 50 == 0:
            avg = np.mean(episode_rewards[-50:])
            print(f"Episode {ep+1}, Avg Reward: {avg:.2f}, "
                  f"Epsilon: {agent.epsilon:.3f}")

    # 绘制学习曲线
    plt.figure(figsize=(10, 5))
    window = 20
    smoothed = np.convolve(episode_rewards,
                           np.ones(window)/window, mode='valid')
    plt.plot(smoothed)
    plt.xlabel('Episode')
    plt.ylabel('Total Reward (smoothed)')
    plt.title('DQN推荐Agent学习曲线')
    plt.grid(True, alpha=0.3)
    plt.tight_layout()
    plt.savefig('dqn_recommender.png', dpi=150)
    plt.show()

train_dqn_recommender()

20.4.3 Actor-Critic在推荐中的应用

Actor-Critic 框架中,Actor(策略网络)输出推荐策略,Critic(价值网络)评估长期收益。优势函数 \(A = r + \gamma V(s') - V(s)\) 指导Actor更新。

优势:比DQN更适合大动作空间;可输出概率分布天然支持探索;训练更稳定。


20.5 策略梯度与列表推荐

20.5.1 Policy Gradient用于列表排序

推荐系统通常需要输出一个有序列表(slate),而不是单个物品。策略梯度方法可以直接优化整个列表的质量。

REINFORCE算法在推荐中的应用

将推荐列表的生成建模为序列决策过程——依次选择列表中的每个位置放什么物品。

策略梯度定理:

\[\nabla_\theta J(\theta) = \mathbb{E}_{\pi_\theta} \left[ \sum_{t=0}^{T} \nabla_\theta \log \pi_\theta(a_t|s_t) \cdot G_t \right]\]

其中 \(G_t = \sum_{k=t}^{T} \gamma^{k-t} r_k\) 是折扣回报。

Python
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

class SlatePolicy(nn.Module):
    """
    推荐列表策略网络
    输入用户状态,输出每个候选物品的选择概率
    """
    def __init__(self, state_dim, n_candidates, hidden_dim=64):
        super().__init__()
        self.encoder = nn.Sequential(
            nn.Linear(state_dim, hidden_dim),
            nn.ReLU(),
            nn.Linear(hidden_dim, hidden_dim),
            nn.ReLU(),
        )
        self.scorer = nn.Linear(hidden_dim, n_candidates)

    def forward(self, state):
        h = self.encoder(state)
        logits = self.scorer(h)
        return logits

    def select_slate(self, state, slate_size, greedy=False):
        """
        选择一个推荐列表(无放回采样)
        Returns: (slate, log_probs)
        """
        logits = self.forward(state)
        slate = []
        log_probs = []
        mask = torch.zeros_like(logits)  # 已选物品的mask

        for _ in range(slate_size):
            masked_logits = logits + mask
            probs = F.softmax(masked_logits, dim=-1)  # F.xxx PyTorch函数式API

            if greedy:
                idx = probs.argmax(dim=-1)
            else:
                dist = torch.distributions.Categorical(probs)
                idx = dist.sample()
                log_probs.append(dist.log_prob(idx))

            slate.append(idx.item())
            mask[0, idx] = float('-inf')  # 屏蔽已选物品

        return slate, log_probs

def train_slate_reinforce():
    """REINFORCE训练推荐列表策略"""
    state_dim = 16
    n_candidates = 50
    slate_size = 5
    n_episodes = 500
    lr = 1e-3
    gamma = 0.99

    policy = SlatePolicy(state_dim, n_candidates)
    optimizer = torch.optim.Adam(policy.parameters(), lr=lr)

    # 模拟物品质量(未知)
    item_quality = np.random.rand(n_candidates)

    episode_rewards = []

    for ep in range(n_episodes):
        # 随机用户状态
        state = torch.randn(1, state_dim)
        slate, log_probs = policy.select_slate(state, slate_size)

        # 模拟用户反馈:位置衰减 + 物品质量
        rewards = []
        for pos, item_id in enumerate(slate):  # enumerate同时获取索引和元素
            position_bias = 1.0 / np.log2(pos + 2)  # DCG位置权重
            click_prob = item_quality[item_id] * position_bias
            reward = 1.0 if np.random.random() < click_prob else 0.0
            rewards.append(reward)

        # 计算折扣回报
        returns = []
        G = 0
        for r in reversed(rewards):
            G = r + gamma * G
            returns.insert(0, G)
        returns = torch.FloatTensor(returns)
        returns = (returns - returns.mean()) / (returns.std() + 1e-8)

        # REINFORCE更新
        policy_loss = 0
        for lp, G in zip(log_probs, returns):
            policy_loss -= lp * G

        optimizer.zero_grad()
        policy_loss.backward()
        optimizer.step()

        episode_rewards.append(sum(rewards))

        if (ep + 1) % 100 == 0:
            avg_r = np.mean(episode_rewards[-100:])
            print(f"Episode {ep+1}, Avg Reward: {avg_r:.3f}")

    print(f"\n学到的Top物品: {slate}")
    print(f"真实最优Top-{slate_size}: "
          f"{np.argsort(item_quality)[-slate_size:][::-1].tolist()}")

train_slate_reinforce()

20.5.2 离线策略评估(Off-Policy Evaluation, OPE)

在推荐系统中,离线评估至关重要——我们不可能每次都上线测试新策略。

为什么OPE在RL推荐中特别重要? - 在线A/B测试成本高、周期长 - 错误的策略可能严重损害用户体验 - 需要在部署前评估新策略的效果

Inverse Propensity Scoring (IPS)

\[\hat{V}_{IPS}(\pi_e) = \frac{1}{n} \sum_{i=1}^{n} \frac{\pi_e(a_i | x_i)}{\pi_b(a_i | x_i)} r_i\]
  • \(\pi_e\):待评估策略(evaluation policy)
  • \(\pi_b\):行为策略(behavior policy,收集数据时使用的策略)
  • 重要性采样权重 \(w_i = \frac{\pi_e(a_i|x_i)}{\pi_b(a_i|x_i)}\) 修正分布偏差

问题:当 \(\pi_e\)\(\pi_b\) 差异大时,方差极大。

Doubly Robust Estimator

\[\hat{V}_{DR}(\pi_e) = \frac{1}{n}\sum_{i=1}^{n}\left[\hat{r}(x_i, \pi_e) + \frac{\pi_e(a_i|x_i)}{\pi_b(a_i|x_i)}(r_i - \hat{r}(x_i, a_i))\right]\]
  • \(\hat{r}(x, a)\):奖励预测模型(Direct Method 部分)
  • 同时使用 IPS 和 DM,当其中一个准确时,估计就是准确的
Python
def ips_estimator(rewards, pi_e_probs, pi_b_probs, clip=None):
    """
    IPS离线策略评估
    Args:
        rewards: 观测到的奖励
        pi_e_probs: 评估策略在观测动作上的概率 π_e(a|x)
        pi_b_probs: 行为策略在观测动作上的概率 π_b(a|x)
        clip: 重要性权重裁剪阈值
    """
    weights = pi_e_probs / (pi_b_probs + 1e-10)
    if clip is not None:
        weights = np.clip(weights, 0, clip)
    return np.mean(weights * rewards)

def doubly_robust_estimator(rewards, pi_e_probs, pi_b_probs,
                             reward_model_preds, pi_e_expected_reward):
    """
    Doubly Robust离线策略评估
    Args:
        reward_model_preds: 奖励模型对(x,a)的预测 r_hat(x,a)
        pi_e_expected_reward: 奖励模型在评估策略下的期望 E_π_e[r_hat(x,a)]
    """
    weights = pi_e_probs / (pi_b_probs + 1e-10)
    residuals = rewards - reward_model_preds
    return np.mean(pi_e_expected_reward + weights * residuals)

# 示例
np.random.seed(42)
n = 1000
rewards = np.random.binomial(1, 0.3, n).astype(float)
pi_b_probs = np.ones(n) * 0.2       # 行为策略:均匀分布(5选1)
pi_e_probs = np.random.uniform(0.1, 0.5, n)  # 评估策略
reward_preds = np.ones(n) * 0.3     # 简单奖励模型
pi_e_expected = np.ones(n) * 0.3

v_ips = ips_estimator(rewards, pi_e_probs, pi_b_probs, clip=10)
v_dr = doubly_robust_estimator(
    rewards, pi_e_probs, pi_b_probs, reward_preds, pi_e_expected
)
print(f"IPS估计: {v_ips:.4f}")
print(f"DR估计:  {v_dr:.4f}")
print(f"真实均值: {rewards.mean():.4f}")

20.5.3 面试考点

Q: 为什么推荐系统的RL方法特别需要离线评估?

A: 1. 在线实验代价高:RL策略需要大量交互才能评估,线上实验影响真实用户 2. 安全约束:未经验证的策略可能推荐不当内容 3. 反馈延迟:推荐的长期效果(留存、LTV)需要很长时间才能观测 4. 部署周期:工业系统上线流程复杂,不可能频繁部署-评估-回滚 5. Counterfactual需求:"如果当时用了新策略,效果会怎样"只能离线估计


20.6 工业界RL推荐实践

20.6.1 字节跳动推荐系统中的Bandit应用

字节跳动(抖音/今日头条)大量使用 Contextual Bandit:

应用场景: - 冷启动探索:新内容/新用户使用 Bandit 做快速探索 - 流量调控:通过 Bandit 动态分配不同策略的流量比例 - 创作者激励:用 Bandit 在内容质量和创作者多样性之间平衡 - 多目标权衡:在点击、时长、互动等多个目标间使用 Bandit 动态调权

技术细节: - 使用 Thompson Sampling 做后验采样,天然支持延迟反馈 - 在粗排层使用 Bandit 做策略选择,精排层用深度模型

20.6.2 阿里妈妈的RL出价/排序

阿里将出价建模为MDP,用DQN/DDPG学习实时出价策略(RLBID),并用RL在一天内动态分配广告预算最大化ROI。

20.6.3 YouTube的强化学习推荐

YouTube的 SlateQ 方法将推荐看作选择slate的问题,使用分解Q值处理组合动作空间,优化长期用户满意度。

20.6.4 线上RL推荐的挑战

挑战 描述 应对方案
延迟奖励 用户购买/留存信号可能延迟数天 Reward Shaping、短期代理指标
安全约束 不能推荐违规/低质内容 约束优化(CMDP)、安全层过滤
样本效率 在线学习需要快速收敛 离线预训练 + 在线微调
非平稳性 用户兴趣和内容分布持续变化 滑动窗口、遗忘因子
大动作空间 候选物品数量可达百万级 层级动作空间、候选生成+排序
部分可观测 用户真实意图不可完全观测 信念状态、RNN/Transformer编码历史

20.6.5 Reward Shaping技巧

直接使用原始奖励(如购买)训练RL推荐系统效果很差,需要精心设计奖励:

Python
def shaped_reward(action_type, duration=0, **kwargs):  # *args接收任意位置参数,**kwargs接收任意关键字参数
    """
    推荐系统中的Reward Shaping示例

    将稀疏的长期奖励转化为密集的短期信号
    """
    reward_map = {
        'impression': 0.0,         # 曝光:无奖励
        'click': 1.0,              # 点击
        'like': 2.0,               # 点赞
        'comment': 3.0,            # 评论
        'share': 5.0,              # 分享(高价值行为)
        'follow': 4.0,             # 关注创作者
        'purchase': 10.0,          # 购买
        'skip': -0.5,              # 快速跳过(负反馈)
        'dislike': -2.0,           # 不喜欢
        'report': -5.0,            # 举报
    }

    base_reward = reward_map.get(action_type, 0.0)

    # 观看时长加成(视频推荐场景)
    if action_type == 'click' and duration > 0:
        # 完播给额外奖励
        completion_ratio = min(duration / kwargs.get('video_len', 60), 1.0)
        base_reward += completion_ratio * 2.0

    return base_reward

设计原则:奖励反映真实业务目标;短期代理指标要与长期目标对齐;负反馈同样重要;多目标权重动态调整。注意避免Goodhart问题(优化代理指标而非真实目标)。

20.6.6 面试考点

Q: RL推荐系统部署的主要挑战有哪些?

A: 从工程和算法两个维度回答:

算法挑战: 1. 离线-在线差距(Sim-to-Real Gap):离线训练的模拟环境与真实用户行为有偏差 2. 延迟奖励:购买、留存等关键指标反馈延迟,导致信用分配困难 3. 样本效率:在线学习不能用太多轮次探索,否则损害用户体验 4. 非平稳环境:用户兴趣漂移、内容池持续更新

工程挑战: 1. 延迟要求:推荐系统要求毫秒级响应,RL推理不能太慢 2. 大规模动作空间:候选集可达百万,不能对每个物品计算Q值 3. AB测试框架:需要可靠的在线实验平台评估RL策略 4. 回滚机制:RL策略可能突然变差,需要自动降级和回滚


20.7 前沿方向

20.7.1 LLM作为推荐策略Agent

大语言模型为推荐系统带来新范式:

  • LLM作为排序器:用prompt让LLM对候选物品排序
  • 对话式推荐:LLM理解用户自然语言需求,主动提问并推荐
  • Agent-based推荐:LLM作为Agent,调用检索/排序工具完成推荐,可生成推荐理由

挑战:延迟高、成本大、可控性差、幻觉问题。

20.7.2 Offline RL在推荐中的应用

Offline RL 从历史日志数据中学习策略,不需要在线交互:

  • Conservative Q-Learning (CQL):惩罚超出数据分布的Q值,防止过高估计
  • Batch-Constrained Q-Learning (BCQ):限制策略只选择数据中出现过的动作
  • Decision Transformer:将RL建模为序列预测问题,用Transformer处理
\[\mathcal{L}_{CQL} = \alpha \left(\mathbb{E}_{s \sim \mathcal{D}} \left[\log \sum_a \exp(Q(s,a))\right] -\mathbb{E}_{(s,a) \sim \mathcal{D}}[Q(s,a)]\right) + \mathcal{L}_{TD}\]

优势:充分利用海量日志数据;不需在线探索,安全性好;适合推荐等高风险场景。

20.7.3 多目标RL推荐

推荐需同时优化点击、时长、互动等多个可能冲突的目标。方法路线:

  • Pareto最优:搜索Pareto前沿上的策略
  • 约束优化:将部分目标作为约束(如"时长≥30s前提下最大化点击")
  • 动态加权:用Meta-Learning学习目标权重的自适应调整
  • Conditioned Policy:策略以目标权重为条件输入,一个模型支持多种偏好

20.7.4 公平性约束下的RL推荐

公平性包括用户侧(不同群体获得相似质量推荐)、物品侧(创作者公平曝光)和双侧公平。技术方案:Constrained MDP (CMDP) 添加公平约束、Lagrangian方法转化为惩罚项、后处理方法在RL输出后调整。


20.8 面试高频题

题目1:Exploration vs Exploitation

Q: 推荐系统中为什么需要 Exploration?纯 Exploitation 策略有什么问题?举例说明在工业推荐系统中如何实施探索。

A:

纯 Exploitation 会导致三个核心问题:(1) Filter Bubble——用户只能看到历史偏好的内容;(2) 马太效应——热门越推越热,新内容没有机会;(3) 估计偏差——未曝光物品缺乏数据,价值永远被低估。

工业实践:字节跳动召回层设置“探索通道”混入新内容,排序层用TS采样;淘宝5%流量做探索桶;新用户多探索(70%),老用户少探索(5%)。


题目2:UCB公式推导与直觉

Q: 写出UCB1的选择公式,解释每一项的含义,以及为什么它能平衡探索与利用。

A:

\[a_t = \arg\max_a \left[\underbrace{Q_t(a)}_{\text{利用项}} + \underbrace{c\sqrt{\frac{\ln t}{N_t(a)}}}_{\text{探索项}}\right]\]
  • \(Q_t(a)\):臂 \(a\) 截至第 \(t\) 轮的平均奖励估计。越高说明这个臂越好 → 利用
  • \(c\):探索系数,手动设置,典型值为 \(\sqrt{2}\)
  • \(\ln t\):总轮次的对数。随时间增长,鼓励探索
  • \(N_t(a)\):臂 \(a\) 被选次数。被选次数少 → 该项大 → 鼓励探索

EE平衡机制: - 如果某个臂很少被选(\(N_t(a)\) 小),探索项很大,UCB值高,被选概率大 → 探索 - 如果某个臂估计奖励很高(\(Q_t(a)\) 大),即使探索项不大,UCB也高 → 利用 - 随着 \(N_t(a)\) 增加,探索项缩小,系统逐渐从探索转向利用


题目3:Thompson Sampling原理

Q: 解释Thompson Sampling在推荐场景中如何工作?为什么工业界偏好它?

A:

工作流程(以CTR预估为例): 1. 为每个候选物品维护点击率的后验分布 \(\text{Beta}(\alpha_i, \beta_i)\) 2. 每次推荐时,从每个物品的后验中采样一个CTR值 \(\tilde{p}_i \sim \text{Beta}(\alpha_i, \beta_i)\) 3. 推荐采样CTR最高的物品 4. 观察用户是否点击,更新后验:点击 → \(\alpha_i + 1\),未点击 → \(\beta_i + 1\)

工业偏好的原因:天然支持批量/延迟反馈(不需每次立即更新);易于分布式实现(独立采样,天然并行);无需调参(不像UCB调\(c\)、ε-Greedy调\(\epsilon\));效果好且收敛快;可与深度模型的不确定性估计结合。


题目4:LinUCB算法

Q: LinUCB和传统UCB有什么区别?它在推荐系统中如何处理个性化?

A:

核心区别:UCB对每个臂维护一个标量估计,不考虑上下文;LinUCB 为每个臂维护一个参数向量 \(\boldsymbol{\theta}_a\),奖励是上下文特征的线性函数。

个性化机制:上下文向量 \(\mathbf{x}\) 包含用户特征,不同用户的 \(\mathbf{x}\) 不同 → 每个臂的UCB值对不同用户不同 → 个性化排序。活跃用户数据多 → 不确定性小 → 更倾向利用;新用户不确定性大 → 更倾向探索。矩阵更新复杂度:\(\mathbf{A}_a\) 更新 \(O(d^2)\),选择时求逆 \(O(d^3)\)(或用 Sherman-Morrison 增量更新 \(O(d^2)\))。


题目5:DQN推荐与传统推荐的区别

Q: 用DQN做推荐和传统的CTR预估模型有什么本质区别?

A:

维度 CTR预估模型 DQN推荐
优化目标 即时指标(点击/转化) 长期累积奖励(留存/LTV)
决策范围 贪心:每次推最可能被点击的 全局最优:可能推"当前点击率不高但能培养长期兴趣"的物品
状态建模 独立预测每次请求 考虑用户状态序列和转移
探索机制 通常没有(或外挂EE模块) 内置(ε-Greedy / Boltzmann)
训练方式 监督学习 强化学习(需要环境交互或离线数据)
部署难度 简单,预测分→排序 复杂,需状态追踪、经验回放

关键洞察:DQN可以学会"牺牲短期收益换长期价值"。例如:给新用户推几次高质量但非最热门的内容 → 建立信任 → 长期留存。


题目6:Off-Policy Evaluation

Q: 什么是离线策略评估(OPE)?IPS估计的主要问题是什么?

A:

OPE定义:用行为策略 \(\pi_b\) 收集的历史数据,评估另一个策略 \(\pi_e\) 的期望表现,无需将 \(\pi_e\) 部署上线。

IPS

\[\hat{V}_{IPS}(\pi_e) = \frac{1}{n}\sum_{i=1}^n \frac{\pi_e(a_i|x_i)}{\pi_b(a_i|x_i)} r_i\]

主要问题

  1. 高方差:当 \(\pi_e\)\(\pi_b\) 差异大时,重要性权重 \(w = \pi_e / \pi_b\) 可能极大,导致少数样本主导估计
  2. 需要知道 \(\pi_b\):需要记录行为策略的概率,但工业系统中 \(\pi_b\) 可能很复杂且不精确不可知
  3. 支撑集问题:如果 \(\pi_b(a|x) = 0\)\(\pi_e(a|x) > 0\),权重无穷大

解决方案:权重裁剪 \(w = \min(w, M)\) 降低方差但引入偏差;自归一化IPS \(\hat{V}_{SNIPS} = \frac{\sum w_i r_i}{\sum w_i}\);Doubly Robust 结合DM和IPS更鲁棒。


题目7:Reward Shaping

Q: 为什么RL推荐需要Reward Shaping?怎么设计?

A:

需要的原因: - 真实长期奖励(如30日留存)稀疏且延迟,RL无法有效学习 - 直接用原始奖励信号(如购买=1),大量的非购买样本奖励为0,梯度信号太弱

设计原则: 1. 层次化奖励:曝光(0) < 点击(1) < 深度阅读(2) < 互动(3-5) < 转化(10) 2. 时间折扣:越快发生的正反馈奖励越高(快速点击 > 延迟点击) 3. 负反馈惩罚:明确的负信号(跳过、不感兴趣、举报)给负奖励 4. 多样性奖励:推荐非重复类别给额外奖励

潜在陷阱: - Reward Shaping 设计不当可能导致策略优化代理指标而非真实目标(Goodhart问题) - 需要定期验证 shaped reward 与长期业务指标的相关性 - 各信号的权重需要反复实验调优


题目8:Contextual Bandit vs Full RL

Q: Contextual Bandit和完整的MDP/RL在推荐中各自适合什么场景?

A:

维度 Contextual Bandit Full RL (MDP)
状态转移 不考虑 考虑
奖励 即时奖励 长期累积奖励
适用场景 单次推荐决策(如一次请求推什么) 序列推荐(优化session/长期)
计算复杂度
样本效率
可解释性 较好(线性模型) 较差(黑箱)
工业落地难度

选择建议: - 首选Contextual Bandit:大多数场景下,Bandit已经足够好,且更容易落地 - 使用Full RL:当需要优化长期指标(30天留存、生命周期价值)且有足够工程支持时 - 渐进路线:MAB → Contextual Bandit → Offline RL → Online RL


题目9:字节跳动推荐系统中的Bandit

Q: 字节跳动的推荐系统如何使用Bandit算法?在系统架构的哪个环节?

A:

核心应用环节

  1. 粗排策略选择
  2. 有多个排序策略(模型A、模型B、规则C),用Bandit动态选择对当前用户使用哪个策略
  3. Thompson Sampling 做策略采样,实时更新后验

  4. 冷启动探索

  5. 新内容(视频/文章)上传后缺乏数据,用Bandit快速测试其在不同用户群的表现
  6. 给新内容额外的探索流量配额

  7. 多目标权重调节

  8. 推荐打分 = w₁·CTR + w₂·时长 + w₃·互动
  9. 权重向量(w₁, w₂, w₃)用Bandit在不同时段/人群上寻优

  10. 请求级别的召回通道选择

  11. 有协同过滤召回、内容召回、热门召回等多路召回
  12. 用Bandit决定每路召回返回多少条候选

为什么选Bandit而非Full RL: - 工程复杂度远低于Full RL(无需状态追踪、经验回放) - 推理延迟几乎为零 - 效果在大多数场景已经足够好 - 容易做AB测试和回滚


题目10:Offline RL推荐

Q: 什么是Offline RL?它相比Online RL在推荐场景中有什么优势和问题?

A:

定义:Offline RL(也叫 Batch RL)完全从已有的历史交互日志中学习策略,不需要与环境进行新的交互。

优势: 1. 安全,所有学习离线完成;2. 充分利用海量历史日志;3. 成本低,无需在线RL框架;4. 可反复迭代尝试不同算法

问题:(1) 分布偏移\(\pi_e\) 可能选择数据中未出现的动作 → Q值过高估计,CQL通过惩罚OOD动作的Q值解决;(2) 评估困难:OPE本身也有误差;(3) 数据质量依赖:行为策略探索不足则学不到好策略。

工业实践:通常采用 Offline预训练 + Online微调,先从日志学到初始策略,再在线上小心做少量探索和适应。


总结

本章覆盖了推荐系统中RL/Bandit方法的完整知识体系:

Text Only
知识图谱:

探索与利用 (EE)
    ├── Multi-Armed Bandit
    │   ├── ε-Greedy
    │   ├── UCB
    │   └── Thompson Sampling ★工业首选
    ├── Contextual Bandit
    │   ├── LinUCB ★面试必考
    │   └── Hybrid LinUCB
    ├── Deep RL推荐
    │   ├── DQN → 离散动作
    │   ├── Actor-Critic → 大动作空间
    │   └── Policy Gradient → 列表推荐
    ├── 离线评估 (OPE)
    │   ├── IPS
    │   └── Doubly Robust ★
    ├── 工业实践
    │   ├── 字节跳动 Bandit ★
    │   ├── 阿里 RL 出价
    │   └── YouTube SlateQ
    └── 前沿
        ├── LLM Agent推荐
        ├── Offline RL (CQL)
        ├── 多目标RL
        └── 公平性RL

面试核心要点: 1. 理解 EE 问题的本质和工业解决方案 2. 能手写 UCB、Thompson Sampling、LinUCB 3. 理解 Contextual Bandit 如何做个性化推荐 4. 知道 DQN 推荐与传统 CTR 模型的区别 5. 了解 OPE(特别是 IPS 和 DR)的原理和局限 6. 熟悉字节跳动等公司的 Bandit 工程实践