Week 13: 商业案例分析与模拟:动态定价
导言
欢迎来到第13周!在过去的几周里,我们已经从强化学习的基础(MDP、Bellman方程)逐步深入到核心算法(Q-Learning)以及如何使用函数逼近(特别是深度神经网络)来解决更复杂的问题 (DQN, A2C)。我们已经接触了强大的 Stable Baselines3
库,并用它解决了一些经典的控制问题,如 CartPole
。
本周,我们将把所有这些知识串联起来,聚焦于一个在经济管理领域至关重要的商业问题:动态定价 (Dynamic Pricing)。我们将:
- 深入分析如何将动态定价问题构建为一个强化学习问题 (MDP)。
- 讨论数据需求以及如何利用真实世界数据(或其模式)来构建模拟环境。
- 亲自动手使用
Stable Baselines3
在一个简化的动态定价模拟环境中训练一个智能体。 - 分析和讨论模拟结果,并思考其对商业决策的启示和实际部署中的挑战。
这一周的内容将为你完成课程的最终大作业提供坚实的基础和直接的启发。许多同学可能会选择与动态定价、资源优化或个性化推荐相关的课题,本周的案例和模拟将是一个很好的起点。
本周的动态定价案例是一个典型的RL应用场景。理解如何将其建模为MDP,如何设计状态、动作和奖励,以及如何使用SB3进行模拟训练,这些技能将直接应用于你的期末项目。
- 选题方向1 (模拟实验与分析): 你可以基于本周的框架,扩展状态空间、动作空间,或尝试不同的奖励函数,并分析其对定价策略和利润的影响。
- 选题方向2 (应用方案设计): 你可以借鉴本周的MDP定义,为一个更具体的商业场景(如特定行业的动态定价、共享经济的资源分配)设计更完善的RL解决方案。
动态定价问题深度剖析
1. 什么是动态定价?
动态定价 (Dynamic Pricing) 是一种根据市场需求、供应情况、竞争对手价格、顾客特征、时间因素等多种变量,实时或近实时调整商品或服务价格的策略。
常见应用场景:
- 航空公司: 根据航班的剩余座位、预订时间、季节性需求等调整票价。
- 酒店行业: 根据客房入住率、预订提前期、当地活动等因素调整房价。
- 电商平台: 根据用户浏览历史、库存水平、促销活动、竞争对手价格调整商品价格。
- 网约车服务: 在高峰期或需求旺盛区域提高价格(溢价),以平衡供需。
- 广告投放: 实时竞价广告位,价格根据目标受众、广告效果预估等变动。
动态定价的目标: 通常是最大化总收入或总利润,但也可能包括提高市场份额、管理库存、提升顾客满意度(通过确保可用性)等。
2. 为什么使用强化学习 (RL) 进行动态定价?
传统的定价方法可能依赖于固定的规则、经济学模型(如需求弹性曲线)或基于历史数据的监督学习预测。然而,动态定价具有以下特点,使其非常适合用RL来解决:
- 序贯决策 (Sequential Decision Making): 当前的定价决策不仅影响即时收益,还会影响未来的市场状态(如顾客期望、品牌认知、库存水平),进而影响未来的决策和收益。RL擅长处理这类需要考虑长期回报的序贯决策问题。
- 不确定性与动态性 (Uncertainty and Dynamics): 市场需求往往是随机的,并受到多种未完全可知因素的影响。竞争对手的行为也是动态变化的。RL智能体可以通过与环境的交互(试错)来学习适应这种不确定性和动态性。
- 探索与利用 (Exploration vs. Exploitation): RL智能体需要在已知的”最优”价格(利用)和尝试新价格以发现潜在更高收益(探索)之间进行权衡。例如,在什么情况下应该尝试一个更高的价格,即使这可能暂时减少销量?
- 复杂的状态空间 (Complex State Space): 影响定价决策的因素众多(时间、库存、需求模式、竞争等),构成了一个复杂的状态空间。深度强化学习 (DRL) 利用神经网络作为函数逼近器,能够处理高维甚至连续的状态和动作空间。
3. 将动态定价问题构建为马尔可夫决策过程 (MDP)
为了用RL解决动态定价问题,我们首先需要将其形式化为一个MDP,即定义其状态 (State)、动作 (Action)、奖励 (Reward) 和状态转移概率 (Transition Probability)。
3.1. 状态 (S, State)
状态应该包含所有与定价决策相关且智能体可以观测到的信息。理想情况下,状态应满足马尔可夫性。以下是一些可能的状态特征:
- 时间相关特征:
- 一天中的时段 (e.g., 高峰、平峰、低谷)
- 一周中的天 (e.g., 工作日、周末)
- 一年中的季节或特定时期 (e.g., 节假日、促销季)
- 距离某个重要事件的时间 (e.g., 航班起飞前天数)
- 库存/容量特征:
- 当前库存水平 (对于实体商品)
- 剩余可用容量 (对于服务,如酒店房间、网约车座位)
- 需求相关特征 (历史/预测):
- 近期历史平均需求/销量
- 基于某种预测模型的需求预测值
- 衡量需求趋势的指标
- 竞争对手信息 (如果可获取):
- 竞争对手的当前价格或价格范围
- 自身行为历史:
- 上一个时间步的价格或采取的促销活动
简化案例中的状态: 为了本周的模拟,我们可以从一个简化的状态空间开始,例如:
s_t = (current_time_step, demand_at_previous_step)
current_time_step
: 表示在一个销售周期(比如一个月中的第几天)中的位置。demand_at_previous_step
: 上一个时间步的实际需求量,可以作为市场反应的一个简单信号。
3.2. 动作 (A, Action)
动作是智能体在每个状态下可以采取的决策。在动态定价问题中,动作通常是:
- 设定一个具体的价格: 例如,从一个预定义的离散价格集合中选择一个价格 {\(p_1, p_2, ..., p_k\)}。
- 设定一个价格调整幅度: 例如,在当前价格基础上提价x%,或降价y%。
- 选择一种促销策略: 例如,打折、买一送一等(这可能使动作空间更复杂)。
简化案例中的动作: 我们可以定义一组离散的价格选项,例如:
a_t ∈ {低价, 中价, 高价}
3.3. 奖励 (R, Reward)
奖励是智能体在采取一个动作后从环境中获得的即时反馈,用于评估该动作的好坏。奖励函数的设计至关重要,它直接引导智能体的学习方向。
- 主要目标 - 利润最大化:
R_t = (price_t - cost_t) * demand_t
price_t
: 当前设定的价格cost_t
: 单位产品/服务的成本demand_t
: 在价格price_t
下产生的实际需求/销量
- 考虑其他因素:
- 惩罚库存积压或缺货: 如果库存过高或因缺货导致销售损失,可以给予负奖励。
- 考虑市场份额: 可以将市场份额的变化作为奖励的一部分。
- 考虑长期客户价值 (LTV): 避免因追求短期高利润而设定过高价格,损害客户关系和长期价值(这通常较难直接量化为即时奖励,但可以通过合理的折扣因子 \(\gamma\) 体现长期视角)。
简化案例中的奖励: 假设单位成本 cost
固定,奖励可以是:
R_t = (chosen_price_t - cost) * actual_demand_t
3.4. 状态转移概率 (P, Transition Probability)
\(P(s_{t+1} | s_t, a_t)\) 描述了在状态 \(s_t\) 下采取动作 \(a_t\) 后,转移到下一个状态 \(s_{t+1}\) 的概率。在动态定价中,这通常由市场需求模型决定。
- 需求模型: 需求量 \(D\) 通常是价格 \(p\) 的函数,还可能受到其他状态特征(如时间、促销)和随机性的影响。
- \(D = f(p, \text{other\_state\_features}, \epsilon)\),其中 \(\epsilon\) 是随机噪声。
- 这个需求函数 \(f(...)\) 是环境的核心。它可以基于经济学理论(如价格弹性)、历史数据拟合,或者是一个更复杂的模拟器。
- 下一个状态:
next_time_step
通常是current_time_step + 1
。demand_at_next_step
(如果作为状态的一部分) 就是根据需求模型在 \(a_t\) 下产生的 \(D_t\)。- 库存状态会根据 \(D_t\) 更新:
inventory_{t+1} = inventory_t - D_t
。
在强化学习中,无模型(Model-Free)方法的关键优势在于:
- 无需显式建模:智能体不需要知道状态转移概率 \(P(s_{t+1} | s_t, a_t)\) 的数学形式
- 通过交互学习:智能体通过”试错”方式与环境交互:
- 执行动作(如设定价格)
- 观察结果(实际需求和下一状态)
- 从这些经验中隐式学习环境动态
但注意:当我们构建模拟环境用于训练时,仍需要明确定义需求生成的逻辑规则(如需求函数),这是创建训练环境的基础。
模拟环境的优势:
- 安全探索:可以在虚拟环境中测试各种定价策略,避免在真实市场中造成经济损失
- 快速迭代:可以加速时间模拟,快速获取大量训练数据
- 可控实验:可以固定随机种子进行重复实验,便于策略比较
- 极端测试:可以模拟罕见但重要的市场情况(如经济危机)
模拟环境的局限:
- 真实性差距:模拟的需求函数可能与真实市场存在偏差
- 过度拟合风险:智能体可能学会利用模拟环境的特定规则而非学习通用策略
- 数据依赖性:模拟质量高度依赖历史数据的质量和覆盖范围
无模型方法的优势:
- 无需精确建模:不需要完全了解复杂市场动态的数学模型
- 适应性强:可以通过持续学习适应市场变化
- 端到端优化:直接从原始数据学习最优策略
无模型方法的挑战:
- 样本效率低:通常需要大量交互数据
- 探索风险:在真实环境中直接探索可能代价高昂,例如:
- 过低定价可能导致短期利润损失
- 过高定价可能造成客户流失和品牌损害
- 频繁价格变动可能影响消费者信任
- 在金融等敏感领域,错误定价可能导致重大经济损失
- 可解释性差:学习到的策略可能难以用商业逻辑解释
3.5. 折扣因子 (γ, Gamma)
折扣因子 \(\gamma \in [0, 1]\) 平衡了即时奖励和未来奖励的重要性。
- \(\gamma\) 接近 0:智能体更关注短期收益(短视)。
- \(\gamma\) 接近 1:智能体更关注长期累计收益(远见)。
在动态定价中,通常希望智能体有长远眼光,避免杀鸡取卵,所以 \(\gamma\) 值通常设置得比较高(例如 0.95-0.99)。
4. 数据需求与挑战
要成功应用RL进行动态定价,数据是关键:
- 历史销售数据: 包含时间、价格、销量、促销活动等信息。用于理解基本的需求模式、价格弹性,以及参数化模拟环境中的需求模型。
- 成本数据: 单位产品/服务的成本,用于计算利润。
- 库存/容量数据: 如果库存/容量是决策的重要因素。
- 竞争对手数据 (可选,但有价值): 竞争对手的价格和策略。获取这类数据可能有难度。
- 顾客特征数据 (用于个性化定价): 用户画像、购买历史等。
挑战:
- 数据质量和量: 需要足够多、覆盖不同市场条件和价格水平的高质量数据。
- 冷启动问题: 在没有足够历史数据或新产品上市时,如何初始化RL智能体?可能需要先采用基于规则的策略或进行更多的探索。
- 环境构建的准确性 (Sim-to-Real Gap): 模拟环境与真实市场的差异。如果模拟环境不能很好地反映真实市场动态,在模拟环境中训练出的优秀策略在真实世界中可能表现不佳。
- 奖励函数设计的复杂性: 如何设计一个能准确反映商业目标的奖励函数?短期利润 vs. 长期价值的权衡。
- 探索的成本: 在真实市场中进行价格探索可能会有实际的经济成本(例如,过低价格导致利润损失,过高价格导致顾客流失)。
虽然我们可能不会直接用一个原始的 Kaggle CSV 文件来实时驱动我们的 RL 环境(因为环境需要对智能体的动作做出即时响应),但 Kaggle 上的许多数据集(如电商交易数据、零售数据、航班票价数据等)可以用来:
- 理解需求模式: 通过EDA(探索性数据分析)分析价格、时间、促销等因素如何影响需求量。
- 参数化需求模型: 基于历史数据拟合一个需求函数 \(D = f(price, features)\),这个函数将成为我们模拟环境的核心组成部分。例如,我们可以估计价格弹性系数、基准需求量等。
- 评估策略: 在部分历史数据上回测 (backtest) RL 智能体学习到的策略(尽管回测有其局限性,因为它不能完全反映智能体动作对市场产生的反作用)。
对于期末项目,如果你选择与动态定价相关的课题,你可以寻找相关的 Kaggle 数据集,进行上述分析,并用分析结果来指导你的模拟环境设计。
5. 算法选择
对于具有离散动作空间(如选择预定义的价格水平)的动态定价问题,以下算法是常用的选择:
- Q-Learning / Deep Q-Network (DQN): 当状态空间较大但仍可管理,或者可以使用神经网络逼近Q值时。DQN特别适合处理高维状态输入(如图像,或此处编码后的多种市场特征)。
- Policy Gradient (PG) 方法 (如 REINFORCE, A2C, PPO): 当动作空间是连续的(例如直接输出一个价格数值),或者当希望直接学习一个随机策略时。A2C (Advantage Actor-Critic) 和 PPO (Proximal Policy Optimization) 是目前非常流行且表现稳健的算法。
对于我们本周的模拟,由于我们会定义一个离散的价格选项集合,DQN 会是一个不错的选择。Stable Baselines3
提供了DQN的优秀实现。
模拟与讨论 - 基于Kaggle数据启发的动态定价仿真
在本讲中,我们将动手实践,使用 Python
和 Stable Baselines3
库来模拟一个简化的动态定价场景。我们将重点关注环境的构建(尤其是需求模型部分,这部分可以从Kaggle数据分析中获得启发)以及如何训练和评估RL智能体。
1. 模拟环境设计 (DynamicPricingEnv
)
我们将创建一个自定义的 Gymnasium
环境。
核心组件:
- 销售周期 (Episode Length): 例如,一个销售周期为30天。
- 状态空间 (Observation Space):
time_step
: 当前是销售周期中的第几天 (0到29)。last_demand_level
: 上一天需求量的离散级别 (例如,0:低, 1:中, 2:高)。这给智能体提供了关于当前市场状况的反馈。- 因此,状态是一个包含两个整数的向量。
- 动作空间 (Action Space):
- 一组离散的价格选项。在我们的实现中,使用5个价格等级:0(低价)到4(高价)。
self.price_options = [7.0, 8.5, 10.0, 11.5, 13.0]
(假设成本是5.0)
- 需求模型 (
_calculate_demand
): 这是环境的核心。需求是价格的函数,并受多种因素影响:- 价格弹性: 价格越高,需求越低,不同需求水平有不同弹性系数。
- 基准需求: 根据需求水平设置不同的基准需求值。
- 时间效应: 模拟周期性的需求变化,如周末或节假日效应。
- 市场冲击: 随机发生的市场事件,可能增加或减少需求。
- 随机噪声: 模拟真实市场的不确定性。
- 奖励函数 (
reward
):reward = (chosen_price - self.cost_price) * actual_demand
- 即利润=单位利润×销量
假设我们分析了一个类似的 Kaggle 零售数据集,我们可能会发现:
- 当价格在 $10 左右时,平均每日销量约为 50 件。
- 价格每上涨 $1,销量大约下降 10 件 (价格弹性)。
- 周末的基准需求比工作日高 20%。
- 有明显的季节性波动和促销活动影响。
- 市场偶尔会出现突发事件,如竞争对手活动、社交媒体热议等。
在我们的模拟中,我们将这些观察结果抽象为需求模型中的参数,如基准需求、价格弹性、周期性波动和随机市场冲击。
2. Python 环境实现
import gymnasium as gym
from gymnasium import spaces
import numpy as np
import random
import math
class DynamicPricingEnv(gym.Env):
= {'render_modes': ['human'], 'render_fps': 30}
metadata
def __init__(self, episode_length=30, seed=None):
super(DynamicPricingEnv, self).__init__()
if seed is not None:
np.random.seed(seed)
random.seed(seed)
self.episode_length = episode_length
self.current_step = 0
# 价格选项增加更多选择
self.action_space = spaces.Discrete(5) # 增加到5个价格等级
self.price_options = np.array([7.0, 8.5, 10.0, 11.5, 13.0]) # 美元
self.cost_price = 5.0 # 美元
# 状态空间: [current_time_step, last_demand_level]
# current_time_step: 0 to episode_length - 1
# last_demand_level: 0 (low), 1 (medium), 2 (high)
self.observation_space = spaces.Box(
=np.array([0, 0]),
low=np.array([self.episode_length - 1, 2]),
high=np.int32
dtype
)
# 用于需求模型的参数,增加差异化
self.base_demands = [20, 50, 80] # 更大的基准需求差异
# 价格弹性根据需求水平变化
self.price_elasticity_by_level = [5, 10, 15] # 不同需求水平对应不同价格弹性
self.base_price_for_elasticity = 10.0
# 增加周期性需求变化
self.seasonal_amplitude = 10 # 季节性波动振幅
# 增加需求噪声
self.demand_noise_std = 8 # 增加随机性
# 增加突发事件概率
self.shock_probability = 0.1 # 10%概率出现需求冲击
self.shock_effect = [-15, 15] # 冲击可能减少或增加需求
self.last_demand_level = 1 # 初始假设为中等需求水平
self.state = None
def _calculate_demand(self, price_chosen_idx):
= self.price_options[price_chosen_idx]
price
# 根据上期需求水平选择基准需求和价格弹性
= self.base_demands[self.last_demand_level]
current_base_demand = self.price_elasticity_by_level[self.last_demand_level]
current_elasticity
# 基本价格弹性效应
= current_base_demand - current_elasticity * (price - self.base_price_for_elasticity)
demand
# 添加时间效应(周期性波动)- 模拟节假日或周末效应
= math.sin(2 * math.pi * self.current_step / self.episode_length)
time_factor = self.seasonal_amplitude * time_factor
seasonal_effect += seasonal_effect
demand
# 随机突发事件(市场冲击)
if random.random() < self.shock_probability:
= random.uniform(self.shock_effect[0], self.shock_effect[1])
shock += shock
demand
# 增加随机噪声
= np.random.normal(0, self.demand_noise_std)
noise += noise
demand
# 需求不能为负,且为整数
= max(0, int(round(demand)))
demand
return demand
def _get_obs(self):
return np.array([self.current_step, self.last_demand_level], dtype=np.int32)
def _update_demand_level(self, current_demand):
# 根据当前需求量,更新下一期的需求水平
if current_demand < (self.base_demands[0] + self.base_demands[1]) / 2:
self.last_demand_level = 0 # low
elif current_demand < (self.base_demands[1] + self.base_demands[2]) / 2:
self.last_demand_level = 1 # medium
else:
self.last_demand_level = 2 # high
# 加入一点随机性,模拟市场需求的不确定性
if random.random() < 0.2: # 20%概率随机改变需求水平
self.last_demand_level = random.choice([0, 1, 2])
def reset(self, seed=None, options=None):
super().reset(seed=seed)
self.current_step = 0
# 随机初始化上期需求水平
self.last_demand_level = random.choice([0, 1, 2])
self.state = self._get_obs()
= {}
info return self.state, info
def step(self, action_idx):
if not self.action_space.contains(action_idx):
raise ValueError(f"Invalid action: {action_idx}")
= self.price_options[action_idx]
chosen_price
# 计算需求
= self._calculate_demand(action_idx)
actual_demand
# 计算奖励 (利润)
= chosen_price * actual_demand
revenue = self.cost_price * actual_demand
cost = revenue - cost
reward
# 更新状态
self.current_step += 1
self._update_demand_level(actual_demand)
self.state = self._get_obs()
# 判断是否结束
= (self.current_step >= self.episode_length)
terminated = False
truncated
= {'price_chosen': chosen_price, 'actual_demand': actual_demand}
info
return self.state, reward, terminated, truncated, info
def render(self):
# 简单的文本渲染
if self.state is not None:
print(f"Step: {self.state[0]}, Last Demand Lvl: {self.state[1]}")
def close(self):
pass
3. 使用 Stable Baselines3 训练 DQN 智能体
现在我们有了环境,可以使用 Stable Baselines3
来训练一个DQN智能体。
from stable_baselines3 import DQN
from stable_baselines3.common.env_checker import check_env
from stable_baselines3.common.evaluation import evaluate_policy
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from dynamic_pricing_env import DynamicPricingEnv
# 创建环境
= DynamicPricingEnv(seed=None) # 去掉固定种子,增加随机性
env print("检查环境...")
# 检查环境是否符合gymnasium标准
check_env(env)
# 创建DQN模型
print("创建DQN模型...")
= DQN("MlpPolicy", env, verbose=1,
model =5e-4, # 提高学习率
learning_rate=100000, # 增大经验回放缓冲区
buffer_size=2000, # 增加初始探索步数
learning_starts=128, # 增大批量大小
batch_size=0.005, # 使用软更新
tau=0.99, # 折扣因子保持不变
gamma=1, # 每步都训练
train_freq=2, # 每次训练更多次梯度
gradient_steps=500, # 目标网络更新频率
target_update_interval=0.3, # 增加探索比例
exploration_fraction=1.0, # 从完全随机开始
exploration_initial_eps=0.1, # 保持较高的最终探索率
exploration_final_eps="./dqn_dynamic_pricing_tensorboard/")
tensorboard_log
print("开始训练DQN智能体...")
# 增加训练步数以获得更好的结果
=50000, progress_bar=True)
model.learn(total_timestepsprint("训练完成!")
# 保存模型
= "dqn_dynamic_pricing_agent"
model_path
model.save(model_path)print(f"模型已保存为 {model_path}")
在训练动态定价智能体时,我们对默认参数进行了多项优化:
- 提高学习率:从 1e-4 提高到 5e-4,加速学习过程
- 增大回放缓冲区:从 50,000 增加到 100,000,存储更多样本
- 增加初始探索:从 1,000 步增加到 2,000 步,收集更多多样化经验
- 增大批量大小:从 64 增加到 128,提高每次更新的稳定性
- 使用软更新:从硬更新(tau=1.0)改为软更新(tau=0.005),使训练更平稳
- 增加探索率:更多的探索帮助智能体发现更优策略
- 更频繁的训练:每步都进行训练而不是每4步一次
这些调整使智能体能够学习到更动态的定价策略,对不同市场状态做出更明智的反应。
4. 评估训练好的智能体并分析结果
训练完成后,我们需要全面评估智能体表现,包括与固定价格策略比较、观察智能体的动态定价行为。
4.1. 评估不同定价策略的表现
# 评估训练好的智能体
print("\n开始评估智能体...")
= evaluate_policy(model, env, n_eval_episodes=10, deterministic=True)
mean_reward, std_reward print(f"评估结果 (10个episodes, 确定性策略):")
print(f"平均奖励: {mean_reward:.2f} +/- {std_reward:.2f}")
print(f"每个episode平均天数: {env.episode_length}")
print(f"单日平均利润: {mean_reward / env.episode_length:.2f}")
# 与随机策略比较
print("\n与随机策略比较...")
= []
random_rewards for _ in range(10):
= env.reset()
obs, info = False
done = 0
episode_reward while not done:
= env.action_space.sample()
action = env.step(action)
obs, reward, terminated, truncated, info = terminated or truncated
done += reward
episode_reward
random_rewards.append(episode_reward)print(f"随机策略平均奖励 (10个episodes): {np.mean(random_rewards):.2f} +/- {np.std(random_rewards):.2f}")
print(f"随机策略单日平均利润: {np.mean(random_rewards) / env.episode_length:.2f}")
# 定义固定价格策略的评估函数
def evaluate_fixed_price_strategy(env, fixed_price_idx, n_episodes=10):
= []
total_rewards for _ in range(n_episodes):
= env.reset()
obs, _ = False
done = 0
episode_reward while not done:
= env.step(fixed_price_idx)
obs, reward, terminated, truncated, _ = terminated or truncated
done += reward
episode_reward
total_rewards.append(episode_reward)return np.mean(total_rewards), np.std(total_rewards)
# 评估每个固定价格策略
print("\n各固定价格策略评估 (10个episodes/策略):")
for price_idx in range(env.action_space.n):
= env.price_options[price_idx]
price_value = evaluate_fixed_price_strategy(env, price_idx)
mean_reward, std_reward print(f"固定价格 ${price_value:.2f}: 平均奖励 = {mean_reward:.2f} +/- {std_reward:.2f}, 日均利润 = {mean_reward/env.episode_length:.2f}")
4.2. 可视化智能体的动态定价行为
# 观察智能体在多个episodes的动态定价行为
print("\n观察智能体在不同初始条件下的动态定价行为 (3个episodes):")
# 准备图表
=(15, 20))
plt.figure(figsize
for episode in range(3):
# 重置环境,不设置种子以获得不同的初始条件
= env.reset(seed=episode*100) # 不同的种子创造不同的初始条件
obs, info = False
done = 0
total_episode_reward print(f"\n--- Episode {episode + 1} ---")
= []
episode_data
# 记录整个episode的数据
while not done:
= model.predict(obs, deterministic=True)
action, _states = obs[0]
current_day = obs[1]
last_demand_lvl
# 采取行动前记录状态
= {'Day': current_day + 1, 'DemandLevel': last_demand_lvl}
pre_action_state
# 执行动作
= env.step(action)
obs, reward, terminated, truncated, info = terminated or truncated
done
= info['price_chosen']
chosen_price = info['actual_demand']
actual_demand
# 打印详细信息
print(f"Day {current_day+1:2d}/{env.episode_length} | DemandLvl: {last_demand_lvl} | Action: Opt {action} (${chosen_price:5.2f}) | Demand: {actual_demand:3d} | Reward: ${reward:7.2f}")
# 保存数据用于可视化
episode_data.append({'Day': current_day + 1,
'DemandLevel': last_demand_lvl,
'PriceOption': action,
'Price': chosen_price,
'Demand': actual_demand,
'Profit': reward
})+= reward
total_episode_reward
print(f"Episode {episode + 1} 总利润: ${total_episode_reward:.2f}")
# 转换为DataFrame便于绘图
= pd.DataFrame(episode_data)
df_episode
# 创建子图
3, 1, episode + 1)
plt.subplot(
# 设置两个Y轴
= plt.gca()
ax1 = ax1.twinx()
ax2
# 绘制价格曲线
'Day'], df_episode['Price'], 'b-o', label='价格')
ax1.plot(df_episode['价格 ($)', color='b')
ax1.set_ylabel(='y', labelcolor='b')
ax1.tick_params(axis
ax1.set_yticks(env.price_options)
# 绘制需求和利润曲线
= ax2.plot(df_episode['Day'], df_episode['Demand'], 'g--x', label='需求量')
demand_line = ax2.plot(df_episode['Day'], df_episode['Profit'], 'r:s', label='利润')
profit_line '需求量 / 利润 ($)', color='r')
ax2.set_ylabel(='y', labelcolor='r')
ax2.tick_params(axis
# 添加需求水平标注(背景色)
for i, row in df_episode.iterrows():
= row['Day']
day = row['DemandLevel']
lvl = {0: 'lightblue', 1: 'lightgreen', 2: 'salmon'}
color_map -0.5, day+0.5, alpha=0.2, color=color_map[lvl])
ax1.axvspan(day
# 设置标题和图例
f'Episode {episode+1}: 动态定价策略 (总利润: ${total_episode_reward:.2f})')
plt.title(
# 组合两个轴的图例
= ax1.get_legend_handles_labels()
lines1, labels1 = ax2.get_legend_handles_labels()
lines2, labels2
# 添加需求水平图例
= [
custom_patches 0, 0), 1, 1, color='lightblue', alpha=0.2, label='低需求'),
plt.Rectangle((0, 0), 1, 1, color='lightgreen', alpha=0.2, label='中需求'),
plt.Rectangle((0, 0), 1, 1, color='salmon', alpha=0.2, label='高需求')
plt.Rectangle((
]=custom_patches + lines1 + lines2,
ax1.legend(handles=['低需求', '中需求', '高需求'] + labels1 + labels2,
labels='upper right')
loc
# 设置X轴标签
'天数')
ax1.set_xlabel(0.5, env.episode_length + 0.5)
ax1.set_xlim(
=3.0)
plt.tight_layout(pad'dynamic_pricing_strategy.png', dpi=150)
plt.savefig(print(f"已保存多个episode的策略图表到 dynamic_pricing_strategy.png")
env.close()
5 实际应用启示
从这个模拟实验中,我们可以得到一些对实际业务的启示:
- 数据驱动决策:强化学习提供了一种数据驱动的方法来优化定价策略,超越传统的基于规则的方法
- 状态设计重要性:选择合适的状态表示(如需求水平、时间信息)是成功应用RL的关键
- 平衡探索与利用:设置适当的探索参数帮助智能体发现更优策略
- 奖励函数设计:直接使用利润作为奖励函数简单有效,但在复杂场景中可能需要加入更多商业因素
6. 优化建议
要进一步改进动态定价模型,可以考虑以下几个方向:
6.1. 环境增强
- 扩展状态空间:
- 添加库存约束(有限库存情况下的定价策略)
- 引入竞争对手价格作为状态的一部分
- 加入客户忠诚度或品牌影响作为长期影响因素
- 需求模型优化:
- 实现更复杂的价格弹性模型(非线性响应)
- 加入跨期需求影响(今天的高价可能导致明天需求下降)
- 模拟不同客户群体的价格敏感度差异
- 奖励函数设计:
- 在利润基础上考虑市场份额目标
- 对价格波动过大进行惩罚,鼓励价格稳定性
- 添加长期客户价值(LTV)因素
6.2. 算法改进
- 尝试其他RL算法:
- 使用PPO (Proximal Policy Optimization),它通常比DQN更稳定
- 实现Double DQN或Dueling DQN架构,减少Q值过估计
- 尝试SAC (Soft Actor-Critic)处理连续价格空间
- 网络架构优化:
- 设计更深或更宽的神经网络
- 实验不同的激活函数和初始化方法
- 添加注意力机制处理时间序列特征
6.3. 实际应用拓展
- 基于真实数据:
- 使用Kaggle或其他公开数据集拟合需求模型
- 实现数据导入接口,允许用实际销售数据训练
- 多产品定价:
- 扩展到多产品定价问题,考虑产品间的关联性
- 实现产品组合优化和交叉销售策略
7. 对最终大作业的启示
本周的练习为你提供了期末项目的一个缩影:
问题定义与MDP构建: 你需要为你选择的商业问题清晰地定义状态、动作、奖励。这是最关键的一步。思考哪些信息对决策是重要的(状态),智能体可以做什么(动作),以及如何衡量决策的好坏(奖励)。
环境模拟:
- 如果你的问题可以创建一个合理的模拟环境(如动态定价、库存管理、资源分配),那么本周的
DynamicPricingEnv
就是一个很好的模板。 - 关键在于核心逻辑的模拟 (例如,动态定价中的需求函数,库存管理中的订单到达和处理逻辑)。
- 数据启发: 如果你有相关的真实数据 (来自Kaggle或你自己的项目),用它来指导你的模拟环境参数设置,使其更真实。
- 如果你的问题可以创建一个合理的模拟环境(如动态定价、库存管理、资源分配),那么本周的
选择并应用SB3算法:
- 根据你的问题特性(状态/动作空间的离散/连续,问题复杂度)选择合适的SB3算法 (DQN, PPO, A2C, SAC等)。
- 学习调整核心超参数,理解它们对训练过程和结果的影响。
结果分析与商业洞察:
- 不仅仅是运行代码得到一个数字,更重要的是分析智能体学到了什么策略。
- 它的行为是否符合商业直觉?是否有出乎意料的发现?
- 这些结果对实际商业决策有什么启示?
- 讨论模型的局限性、潜在的改进方向以及实际部署中可能遇到的挑战。
思考一个你感兴趣的经济管理问题,尝试用本周学到的框架去初步构思:
- 它能被看作一个序贯决策问题吗?
- 状态(S)可能是什么? (需要哪些信息来做决策?)
- 动作(A)有哪些选项? (智能体能做什么?)
- 奖励(R)如何定义才能反映商业目标? (如何衡量一个动作的好坏?)
- 是否可以创建一个简化的模拟环境? (如果不能,也许更适合文献综述或方案设计类型的项目)
- 可以使用SB3中的哪个算法?
总结与展望
本周我们深入探讨了动态定价这一重要的商业应用,并实践了如何将其建模为RL问题,利用Stable Baselines3
构建模拟环境并训练智能体。这个过程不仅巩固了我们对DRL算法的理解,更为重要的是,它展示了如何将理论应用于解决实际(或接近实际)的商业挑战。
在接下来的几周,我们将继续探讨RL在其他商业领域的应用,讨论RL落地的更多实践挑战和伦理考量,并为你的期末项目提供进一步的指导。
请务必动手运行本周的代码,尝试修改参数,观察结果的变化。这将是准备期末项目非常有价值的练习!