资讯动态

强化学习实战:Q-learning优化电力市场交易收益

发布时间:2026/10/20 21:27:58 来源:尧图企业网站定制
接手这个项目之前我其实已经对强化学习在电力交易里的应用抱观望态度有一阵子了。真到自己动手用Q-learning做能源市场收益优化跑通Matlab代码的那一刻才意识到这类算法最大的价值不是“看起来很高级”而是它能在价格信号高度不确定的环境里用最简单的方式逼近一个够用的决策策略。这篇文章就把这个项目的完整思路、建模过程、Matlab实现细节和踩过的坑全部摊开来讲适合对强化学习感兴趣、又不想只跑CartPole玩具例子的读者参考。1. 项目缘起能源市场里的收益问题为什么难解1.1 场景描述与核心矛盾做这个项目的起因其实很朴素。我所在的团队一直在研究一个典型的能源市场交易问题某个售电主体可以理解成一个既买电又卖电的中间商需要在每天的几个交易时段内决定买卖策略目标是让一段时间内的累计收益最大化。问题本身听起来不复杂但真正上手后发现核心矛盾非常突出——市场电价是不确定的它受负荷水平、发电侧供给、网架约束甚至天气因素影响呈现出明显的时变性和波动性。如果只靠历史平均值做决策遇到电价尖峰或低谷时会吃大亏。这种场景下决策者面对的不是一个静态优化问题而是一个序贯决策问题今天的买卖动作会影响明天乃至后天的收益而明天的电价又是不确定的。传统做法通常是用线性规划或动态规划求解但前提是电价分布已知或者至少能建出一个较准的预测模型。现实里这个前提往往不成立预测误差一上来优化结果就跟着失真。1.2 为什么传统优化方法不够用我最早尝试的是基于价格预测的MPC模型预测控制思路每步先预测未来几个时段的价格再求解一个滚动优化问题。理想情况下效果不错但有两个痛点一是预测模型本身要花大量精力维护二是预测误差会被滚动优化放大尤其在电价突变时段MPC的表现反而不如一个简单规则。后来我转变思路不依赖显式预测而是让算法直接在环境里试错学习。这个思路正是Q-learning擅长的领域它不要求知道电价生成的内部机理只需要定义好状态、动作和奖励通过大量交互逐步逼近最优决策。做完之后我的体会是对于能源交易这种模型不确定、环境能反复交互仿真或者历史回测的场景Q-learning是一个工程上非常顺手的工具。2. 问题建模把能源交易抽象成强化学习能吃的MDP2.1 状态空间的切分思路强化学习建模的第一步是把现实问题映射成MDP马尔可夫决策过程也就是定义清楚状态、动作、奖励。这一步比写算法本身重要得多建模建得差后面调参再努力也很难救回来。这个项目里我选了三个维度的信息组成状态。第一个维度是当前时段编号比如把一天分成24个交易小时时段编号就是一个天然的状态分量让算法能够学习不同时段的电价规律差异。第二个维度是当前电价所在的区间。直接使用连续电价会让状态维度爆炸我做了分箱处理。比如以0.05元/kWh为一个档位把历史电价归到几十个离散档位里。分箱粒度很关键太粗会丢失关键差异太细则会导致每个状态下的样本不足。第三个维度是前一天同时间段的电价或负荷水平用来引入延续性信息。这个特征显著提升了训练速度因为电价有很强的日周期性加入这个维度等于提前告诉算法“今天的形势和昨天类似”。三个维度组合起来状态总数大概在几千的量级对于Q-learning来说还在可控范围。如果状态数超过几十万就要考虑用线性函数逼近或者DQN了。2.2 动作空间的设计动作空间设计要贴合真实交易约束。我设计的动作不是单一买卖量而是一个组合策略包含购电量和售电量两个连续量的离散化版本。购电量分为零购、低购、高购三档售电量同样分三档。动作空间大小就是9个。一开始我尝试过更多档位比如每档细分到5个动作空间变成25训练时间明显拉长但收益提升有限最后折中在9个动作。这个经验后来在另一组实验里也被验证了动作空间不是越细越好要结合交易成本一起看。需要特别说明的是动作执行要考虑物理约束比如购电不能超过上限、售电不能超过已持有的电量。这些约束我直接放在仿真环境里处理如果动作非法环境会返回一个负奖励并保持原状态不变。这样既保证了安全性也让算法在探索中自然学会避开非法动作。2.3 奖励函数怎么定才不跑偏奖励函数是最容易踩坑的地方。如果只把单时段利润作为奖励算法会倾向于短期套利忽略库存风险。我在项目里把奖励拆成三部分第一部分是直接经济收益等于售电收入减去购电成本这部分反映即时盈利。第二部分是库存惩罚项如果当前库存过高或过低都会扣除一定分数。这是因为真实场景中库存容量有限过高的库存占用资金过低的库存则可能在突发需求时无电可售。第三部分是交易成本惩罚每次动作切换买卖方向或档位变化时扣除一个固定的小数值用来模拟实际交易中的操作成本。否则模型会频繁变换策略看起来收益很高实际执行时手续费就把利润吃掉了。最终奖励函数的形态是[ R_t Income_t - Cost_t - \lambda_1 \cdot Penalty_{stock} - \lambda_2 \cdot Penalty_{action} ]\lambda_1和\lambda_2是惩罚权重需要根据场景手工调。我的经验是先让算法跑一轮观察不加惩罚时的行为表现再逐步加大惩罚直到行为变得合理。这样调参比一开始就拍脑袋定权重高效得多。3. Q-learning算法核心与参数调优3.1 Q-learning更新公式的直觉理解Q-learning的核心是Q表它记录了每个状态下动作的长期累积价值估计。更新公式如下[ Q(s,a) \leftarrow Q(s,a) \alpha \left[ r \gamma \max_{a} Q(s,a) - Q(s,a) \right] ]这个公式的直觉可以理解为执行动作后得到即时奖励同时看到下一步状态下的最优动作价值用这两者之和与当前估计值的差值来修正Q值。其中\alpha是学习率\gamma是折扣因子。我常用一个类比来向身边同事解释这件事Q表就像是一张地图每个路口标记着“从这里走平均能赚多少钱”。每次走到一个新路口你根据实际到手的奖励修正这个标记。走多了标记就越来越准决策自然越来越稳。3.2 关键参数的内在逻辑参数设置是我花了最多时间调试的部分。理解每个参数背后的影响比机械调参重要得多。学习率\alpha控制每步更新的幅度。如果设置太大比如0.5以上Q值会震荡得很厉害训练曲线像锯齿一样设置太小比如0.01以下收敛极慢。这个项目里我最终选了0.1并在训练后期衰减到0.05。衰减策略让前期快速探索、后期稳定收敛。折扣因子\gamma决定算法看多远。\gamma接近1意味着重视长期收益适合电价有持续趋势的情况\gamma太低则算法短视只看眼前利润。我测试了0.9和0.99两个值发现0.99在库存周期较长的场景中表现更好。这其实很好理解今天的购电决策可能影响未来好几个时段的售电空间不考虑长远就容易错失最佳时机。探索率\varepsilon是另一个关键参数。经典的做法是\varepsilon-greedy以\varepsilon的概率随机探索以1-\varepsilon的概率选择当前Q值最大的动作。我的方案是从0.3开始每轮训练线性衰减到0.01。衰减曲线要保证前期有足够探索覆盖状态空间后期又有充分的利用保证策略稳定。3.3 探索与利用的平衡策略探索和利用的平衡是我在这个项目里体会最深的一点。如果探索太激进算法一直在乱试Q表里会积累大量错误估计如果探索不足算法会过早锁死在一个次优策略上。除了\varepsilon-greedy我还实现了一个简单的置信度上界策略做对比。思路是对每个状态动作对的访问次数做记录如果某个动作被访问得少就在选择时加分鼓励算法去确认这个动作是否真的差。实测下来这个策略在训练后期略优于\varepsilon-greedy但实现复杂度高一些而且需要额外维护计数矩阵内存开销变大。对于中小规模状态空间我的建议是优先用\varepsilon-greedy把训练轮数拉上去一样能取得不错效果。置信度上界策略的优势主要体现在样本量有限、训练预算紧张的场景。4. Matlab代码实现全流程4.1 整体代码架构Matlab本身不是强化学习的第一顺位选择但它的矩阵运算和可视化能力对快速验证来说很舒服。我的代码结构分成五个模块环境仿真、状态离散化、动作映射、Q表更新、训练主循环。环境仿真模块负责模拟市场价格变化和交易执行逻辑。因为是离线仿真我直接加载历史电价序列再叠加一个随机扰动项模拟不确定性。这个设计让同一套代码可以同时用于训练和回测。代码结构大致如下% 主脚本入口 % 加载历史电价数据 price_data load_price_data(historical_price.csv); % 初始化参数 alpha 0.1; gamma 0.99; epsilon 0.3; num_episodes 2000; % 初始化Q表 n_states num_periods * num_price_bins * num_lag_bins; n_actions 9; Q_table zeros(n_states, n_actions);4.2 核心函数逐步拆解环境仿真是最容易被低估的部分。所谓仿真环境本质是一个函数输入当前状态和动作输出下一个状态和奖励。我用了三个维度描述状态时段号、当前电价档位、前一日同时间段电价档位。状态索引的计算方式如下function idx get_state_index(period, price_bin, lag_price_bin) % 将三个维度映射到一维索引 idx sub2ind([num_periods, num_price_bins, num_lag_price_bins], ... period, price_bin, lag_price_bin); end这个函数的关键在于保证状态映射唯一且索引不越界。我用sub2ind实现三维到一维的映射避免手写乘法带来边界错误。动作映射模块把9个离散动作编号解释成具体的购售电策略。每个动作包含购电量和售电量的档位信息function [buy_vol, sell_vol] decode_action(action_id) % action_id 从1到9 % 1-3: 零购、低购、高购同时售电为零 % 4-6: 零购、低购、高购同时售电为低档 % 7-9: 零购、低购、高购同时售电为高档 ... end这个映射关系一开始设计得不好动作编号跳跃性太强导致Q表里相邻动作的Q值差异极大难以平滑学习。后来我调整成按购电档位分组排序动作编号的语义更清晰训练曲线明显平滑了。奖励计算函数直接实现前面提到的三部分奖励结构。特别要注意的是库存惩罚项不能让算法把库存推到负值。我加了硬约束如果仓位低于零或超过上限环境返回一个极大的负奖励并将状态保持不变。4.3 训练主循环与收敛判据训练主循环的核心逻辑不算复杂但有几个细节点值得注意。每次episode开始时重置换初始状态然后按时间步循环直到当天结束。每个时间步内部执行一次完整的MDP交互选择动作、执行交易、计算奖励、观测下一状态、更新Q表、状态转移。核心更新代码如下for step 1:num_steps_per_episode % epsilon-greedy策略选择动作 if rand() epsilon action randi([1, n_actions]); else [~, action] max(Q_table(state_idx, :)); end % 在环境中执行动作 [next_state_idx, reward] step_environment(state_idx, action); % Q-learning更新 next_max max(Q_table(next_state_idx, :)); Q_table(state_idx, action) Q_table(state_idx, action) ... alpha * (reward gamma * next_max - Q_table(state_idx, action)); % 状态转移 state_idx next_state_idx; end一个反复踩过的坑是收敛判据。只盯着单轮总收益容易误判——因为单轮收益受随机性影响很大训练过程中会出现正常波动。我改用滑动平均收益配合Q表变化幅度双重判据。具体做法是每50轮计算一次平均收益如果连续10个窗口的平均收益变化都小于阈值同时Q表最大变化量小于某个小值就判定训练收敛。这个做法避免了过早停止训练。4.4 结果可视化与性能分析Matlab的可视化在这个项目里帮了大忙。训练结束后我会画出平均收益随训练轮数的变化曲线、Q表热力图、策略分布图。这些图形可以帮助定位问题如果曲线在某个点突然骤降多半是环境里出现了意外状态如果热力图有明显条纹说明状态离散化粒度不够均匀。性能分析方面2000轮训练在普通笔记本上跑大概需要十几分钟。主要耗时在环境仿真和Q表索引计算上。我用tic/toc做过测试发现最耗时的反而是状态索引的sub2ind调用因为它在每个时间步里被频繁执行。后来改成手写线性索引计算公式整体速度提升了接近20%。这种小优化在状态空间大的时候收益更明显。5. 实验结果与对比分析5.1 训练过程收敛分析实验过程中我记录了一组代表性数据。从第1轮到第200轮平均收益波动剧烈有时单轮甚至为负。第500轮以后波动大幅减小第1000轮左右基本进入稳定区间。这说明Q-learning的收敛不是线性的前期探索阶段的“无效劳动”不可避免但只要状态空间划分合理算法总能在后期稳定。我对比了不同学习率下的收敛情况alpha为0.1时收敛速度中等但稳定alpha为0.3时收敛快但后期出现持续震荡alpha为0.01时收敛极慢、训练结束后仍未完全稳定。最终采用0.1并配合后期衰减确实是最稳妥的组合。5.2 与传统策略的收益对比为了验证算法效果我设了三个对照组恒买恒卖策略、依据历史平均价格的固定规则策略、以及Q-learning优化策略。在同样的电价序列和同样的约束条件下回测30天结果如下策略类型平均日收益单位收益波动率最大回撤恒买恒卖4.23.18.7历史均值规则7.82.45.2Q-learning12.61.83.5Q-learning策略在平均收益上高出传统规则策略约60%同时收益波动率和最大回撤都明显更低。这组数据说明有效利用状态信息建模时序关系比人工凭经验定规则要稳得多。5.3 参数敏感性测试我专门做了两组参数敏感性测试。一组是折扣因子的变化另一组是状态分箱粒度。折扣因子从0.9提高到0.99时收益提升约8%但训练时间增加接近一倍。这说明远期眼光很重要但也要付出计算代价。状态分箱粒度方面电价从20个档位增加到50个档位时收益提升约5%继续增加到100个档位收益不再提升反而略有下降。原因在于档位过密导致每个状态内的样本量不足Q值估计方差变大。这个现象值得强调状态空间设计不是越细越好要匹配样本规模。6. 常见问题与排查技巧实录6.1 Q表不收敛的排查思路训练不收敛是Q-learning项目中最常见的问题我遇到过好几种表现形式对应的原因也各不相同。现象可能原因排查方向Q值持续震荡学习率过高降低alpha或加入衰减收益曲线有长期下滑趋势奖励函数存在漏洞检查是否有奖励被刷取空间Q表部分状态始终为零状态空间覆盖不足延长训练轮数增加探索率训练正常但测试表现差过拟合训练环境加入随机扰动做跨场景验证最让我印象深刻的一次问题是奖励函数存在漏洞。算法在某个状态下找到了一个循环反复买进卖出利用我设定的交易成本惩罚过低的漏洞刷出远超合理水平的名义收益。当时训练曲线看起来非常漂亮但换到真实数据一跑就露馅。最后把交易成本惩罚加大一个数量级问题才消失。6.2 训练时间过长的优化手段训练时间太长通常来自两个原因状态空间过大或单步仿真开销过高。如果状态空间已经划分好了但训练时间仍然很长我一般从减少无效探索入手。一个非常有效的技巧是初始化优化——先用一个简单的规则策略比如历史均值策略收集若干步经验用这些经验预填充Q表再开始正式训练。这样算法一开始就有一定的先验知识不用从完全空白开始摸索。我实测这个技巧能节省约30%训练时间。另一个有效手段是提前终止探索。有些状态在训练后期已经非常稳定如果仍然以初始探索率去随机尝试浪费时间。我实现了一个函数检测每个状态是否在所有动作上都被访问过一定次数是则将该状态的动作选择直接设为贪心不再参与探索。6.3 状态空间爆炸时的降维方向如果状态维度继续增加比如加入天气变量、时段交叉特征Q表会迅速膨胀。我遇到过状态数达到百万级的场景传统表格型Q-learning就已经力不从心了。这种情况下我有三个推荐方向第一个是改用DQN用神经网络代替Q表第二个是特征聚合把相关性高的特征合并第三个是经验回放提高样本效率。但如果只是做验证性研究我的建议是先用小规模状态空间跑通完整流程再逐步增加特征每一步都记录收益变化。这样既能控制复杂度又能定位每个特征的实际价值。6.4 测试与训练环境不一致的坑最后提醒一个很容易被忽略的点训练用的环境扰动强度要和测试保持一致。我在一组实验里训练时用了小扰动测试时把扰动调大结果策略表现急剧下滑。后来我改成训练时逐渐增大扰动强度让算法见过足够多的噪声场景测试时的泛化能力才明显改善。这个经验在能源交易这种不确定环境中特别重要。7. 项目复盘与经验总结7.1 工程实现上的几点心得从零开始做这个项目最大的感触是强化学习的实现难度不在算法本身在于环境仿真和问题建模的完整度。环境仿真越贴近真实约束算法学出来的策略就越有落地价值。我花在环境搭建上的时间占了整个项目的一半以上这个比例在后续类似项目里也基本稳定。Matlab的表格型Q-learning实现对于状态空间在中低维度的场景已经完全够用。如果读者对Python更熟悉完全可以把这套逻辑平移过去核心代码结构不变。关键点是状态索引的统一、奖励函数的分解、收敛判据的三重校验这些设计思路和语言无关。7.2 拓展方向从Q-learning到更复杂的算法这个项目做完后我留意了一下后续可以扩展的方向。Q-learning的局限性主要在于状态空间不能太大而真实能源市场如果加入更多特征比如网架安全约束、储能设备状态、多市场主体竞价表格型Q-learning就吃不消了。这时候可以考虑DQN、Dueling DQN或者近端策略优化PPO。有一种过渡方案值得尝试先用Q-learning的结果初始化一个策略网络再用策略梯度方法做微调。这样既利用了Q-learning在探索阶段的稳定性又获得了神经网络策略的泛化能力。我虽然没有在正式项目里完整跑通这个方案但在一组小规模数据上做过简易验证效果比直接从零训DQN要稳。7.3 个人实操中的开放性建议最后分享一个我养成的习惯每次调试强化学习项目时都先固定一个超参数、固定随机种子只改动一个变量观察效果。强化学习的随机性很强如果同时改多个参数出了问题根本定位不到是哪一步引起的。这个习惯让我在后续好几个项目里少走了大量弯路。另外建议训练过程中定期手动观察几轮智能体的实际决策过程不要只盯着收益曲线。有时候收益数字在涨但动作策略变得很奇怪——可能是钻了环境仿真的空子也可能是出现了脱离现实理解的循环策略。早发现早调整比最后统一排查高效得多。这个项目本身已经能有效解决中等规模能源交易场景下的收益优化问题后续如果要把方案推广到更大范围我倾向于先补足环境仿真的物理细节再考虑换更复杂的算法。算法永远是为问题服务的把问题定义清楚永远比调一个花哨的模型更重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑