简介:本资源是一份面向Python初学者与强化学习入门者的Q Learning算法实践代码包,聚焦于通过可运行的完整实现理解强化学习核心思想与Q表更新机制。压缩包共5个文件,包含核心算法脚本qlearningAgents.py、项目说明README.md、技术报告PDF、开源许可证LICENSE及临时文件tmp,整体大小852KB,结构简洁,便于快速上手与调试。已有1108人学习下载,适合高校课程设计、AI自学实践或竞赛算法预研场景。读者可直接运行代码复现经典Q Learning流程,深入掌握epsilon-greedy策略、Q值迭代更新公式、状态-动作对建模逻辑,并借助报告PDF理解算法在倒立摆等gym环境中的应用适配思路,配套说明文档还涵盖连续状态离散化处理等关键细节,具备教学性与工程参考价值。
1. Q Learning 不是“调库跑通就完事”的黑匣子:它是一套可手撕、可调试、可嵌入真实控制逻辑的决策骨架
你在网上搜“Python实现Q Learning强化学习(完整代码).zip”,点开压缩包发现:一个q_learning.py,不到200行,环境用gym.make('Taxi-v3'),reward打印几行,最后画个episode reward曲线——这根本不是Q Learning的全貌。真正卡住工程师的,从来不是“怎么写for循环更新Q表”,而是状态空间爆炸后Q表存不下、稀疏奖励下收敛慢如蜗牛、策略震荡到第5000轮还在原地打转、甚至把‘出租车接客’任务跑成‘反复撞墙自杀’。这个标题里的“完整代码”,不是指能跑出数字的脚本,而是指一套从状态编码设计、探索策略退火、学习率衰减、到收敛性验证闭环的最小可行工程链路。它适合两类人:刚学完David Silver课但连env.step()返回值都记混的新手,以及想把强化学习嵌进PLC控制逻辑、又不敢直接上DQN的老工程师。本文不讲贝尔曼方程推导,只告诉你:为什么你的Q表更新公式没错,但agent永远学不会左转;为什么epsilon=0.1在Taxi里是黄金参数,在你自己写的流水线调度环境里却是致命毒药;以及,如何用三行代码确认你的Q值更新真正在降低TD error,而不是在拟合噪声。
2. 从零构建Q Learning核心:状态、动作、奖励必须亲手定义,不能靠gym自动喂
Q Learning的本质是在离散状态-动作空间中,用迭代方式逼近最优动作价值函数Q*(s,a)。但gym的Taxi-v3环境(500个状态)只是教学玩具——它的状态ID是0~499的整数,直接当索引用;而真实场景中,你的“状态”可能是传感器读数+设备启停标志+订单队列长度,必须自己编码。下面拆解三个关键环节,每一步都决定后续是否翻车。
2.1 状态空间设计:别让Q表内存爆掉,先做维度裁剪和离散化
Taxi-v3的状态是4维组合:taxi位置(5×5网格) + 乘客位置(5个站点) + 目的地(5个站点),共5×5×5×5=625种,实际用4×5×5×5=500(因某些组合无效)。但若你面对的是工业温控系统:温度传感器(0~100℃, 0.1℃精度)、压力表(0~10MPa, 0.01MPa)、阀门开度(0~100%)、报警标志(0/1),原始状态空间是1001×1001×101×2≈20亿——Q表根本存不下。常见做法是:对连续变量做等宽/等频离散化,并用业务规则压缩维度。
import numpy as np # 假设你有4个传感器原始读数 raw_state = np.array([87.3, 4.25, 63.8, 0]) # temp, pressure, valve, alarm # 【等宽离散化】:按业务安全阈值切分,非均匀更合理 temp_bins = [-np.inf, 60, 80, 90, np.inf] # 低温/常温/高温/超温 pressure_bins = [-np.inf, 2, 5, 8, np.inf] # 低压/正常/高压/超压 valve_bins = [0, 30, 70, 100] # 关/半开/全开 alarm_bins = [0, 1] # 无报警/有报警 # 用digitize转为离散索引(注意:bins需升序,且返回索引从1开始,故减1) discrete_state = [ np.digitize(raw_state[0], temp_bins) - 1, np.digitize(raw_state[1], pressure_bins) - 1, np.digitize(raw_state[2], valve_bins) - 1, raw_state[3] # 报警已是离散 ] # 最终状态ID:用base转换(类似进制),避免状态ID重复 state_id = (discrete_state[0] * 4 * 3 * 2 + discrete_state[1] * 3 * 2 + discrete_state[2] * 2 + discrete_state[3]) # 这里最大state_id = 3*3*2*2 = 36,远小于20亿参数说明:
temp_bins等列表不是随意切分,而是依据设备手册中的“安全运行区间”设定。例如温度超过90℃触发保护停机,那么[90, np.inf]就是关键分界。np.digitize返回bin的右边界索引,所以要减1对齐0-based索引。最终state_id计算用的是加权和,权重由各维度bin数累乘得到,确保每个组合唯一映射。
2.2 动作空间与奖励函数:别抄教程里的+20/-10,要贴着控制目标写
很多教程给Taxi-v3的reward是:成功送达+20,每步-1,非法动作-10。这导致agent学会“快速撞墙自杀”——因为-10比-1痛快,总reward反而更高。真实奖励设计必须满足:1)稀疏奖励要有明确正向信号;2)惩罚要精准打击错误行为;3)避免奖励塑形(reward shaping)引入偏差。
def calculate_reward(self, state, action, next_state, done): # state, next_state 是离散化后的四元组 [temp_bin, press_bin, valve_bin, alarm] reward = 0.0 # 【核心正向奖励】:仅当达成终极目标时给予(稀疏但明确) if done and self.is_target_reached(next_state): reward += 100.0 # 成功完成工艺流程 # 【精准惩罚】:只罚违反硬约束的行为 if self.is_pressure_dangerous(next_state[1]): # 压力bin=3(超压) reward -= 50.0 # 重罚,因可能引发安全事故 if self.is_temp_critical(next_state[0]): # 温度bin=3(超温) reward -= 30.0 # 【微调惩罚】:对低效操作轻罚(避免agent学废) if action == self.ACTION_IDLE and not done: reward -= 0.1 # 空转耗能,但不致命 return reward def is_target_reached(self, state): # 工艺终点:温度在80-90℃(bin=2)、压力在2-5MPa(bin=1)、阀门全开(bin=2)、无报警 return (state[0] == 2 and state[1] == 1 and state[2] == 2 and state[3] == 0)逻辑说明:
is_target_reached用离散bin值判断,而非原始浮点数,避免浮点误差。is_pressure_dangerous检查next_state[1] == 3(超压bin),而非pressure > 8,因离散化已将物理量映射到语义层级。奖励值大小不是拍脑袋:-50比-10重5倍,对应故障维修成本是常规维护的5倍。
2.3 Q表初始化与更新:别用全零初始化,用乐观初始值打破冷启动
标准Q Learning用Q[s,a] = 0初始化,但零值在稀疏奖励下导致前期探索极差——agent看到所有动作都是0,随机选一个,失败后更新为负值,下次更不敢试。工业场景常用乐观初始值(optimistic initialization):把Q表全设为较大正数(如100),让agent相信“每个动作都有高回报”,主动探索。
# 初始化Q表:状态数 × 动作数 self.n_states = 36 # 由2.1节离散化得到 self.n_actions = 4 # 假设动作:升温/降温/开阀/关阀 self.q_table = np.full((self.n_states, self.n_actions), fill_value=100.0) # 注意:不是np.zeros!乐观初始值驱动探索 # Q值更新(Sarsa风格,更稳定) def update_q_value(self, state, action, reward, next_state, done): current_q = self.q_table[state, action] if done: target_q = reward # 终止状态无后续Q值 else: # 用当前策略选择的next_action(ε-greedy),非max(Q) next_action = self.choose_action(next_state, epsilon=0.0) # 纯greedy target_q = reward + self.gamma * self.q_table[next_state, next_action] # TD error驱动更新 td_error = target_q - current_q self.q_table[state, action] += self.alpha * td_error参数说明:
alpha(学习率)建议0.1~0.3,太大震荡,太小收敛慢;gamma(折扣因子)0.95~0.99,工业控制中因任务周期长,取0.99更合理。target_q计算用Sarsa(on-policy)而非Q-learning(off-policy),因Sarsa在动作选择上更保守,避免探索时执行危险动作——比如在超压状态下,Q-learning可能选“继续升温”,而Sarsa会选当前ε-greedy策略下的安全动作。
3. 让Q Learning真正收敛:ε衰减、学习率调度、TD error监控缺一不可
写完Q表更新循环,很多人卡在“reward曲线一直不升反降”。这不是代码bug,而是缺乏收敛保障机制。Q Learning理论保证收敛需满足:1)所有状态-动作对被无限次访问;2)学习率αₜ满足∑αₜ=∞, ∑αₜ²<∞。实践中,这转化为三个可调参数:ε衰减策略、α衰减策略、TD error监控。
3.1 ε-greedy探索策略:线性衰减太粗暴,用指数衰减+最小阈值保底
线性衰减(ε=1-episode/1000)在后期ε过小,agent陷入局部最优。工业场景推荐指数衰减:εₜ = ε₀ × γ^t,且设置ε_min=0.05防完全贪婪。
class QLearningAgent: def __init__(self, n_states, n_actions, alpha=0.2, gamma=0.99, eps_start=1.0, eps_end=0.05, eps_decay=0.995): self.n_states = n_states self.n_actions = n_actions self.alpha = alpha self.gamma = gamma self.eps = eps_start self.eps_end = eps_end self.eps_decay = eps_decay # 每轮衰减率 def choose_action(self, state, epsilon=None): if epsilon is None: epsilon = self.eps if np.random.random() < epsilon: return np.random.randint(self.n_actions) # 随机探索 else: return np.argmax(self.q_table[state]) # 贪婪利用 def decay_epsilon(self): self.eps = max(self.eps_end, self.eps * self.eps_decay) # 每轮调用一次,确保ε缓慢下降参数说明:
eps_decay=0.995意味着每轮ε乘以0.995,1000轮后ε≈0.995¹⁰⁰⁰≈0.0067,但被max(..., eps_end=0.05)截断,保证始终有5%探索概率。对比线性衰减(1000轮后ε=0),指数衰减在前期探索更充分,后期利用更稳定。
3.2 学习率α动态调整:用episode计数器实现1/t衰减
理论要求αₜ∝1/t,但固定α=0.1在前期更新猛、后期不收敛。用alpha = alpha0 / (1 + episode_count * decay_rate)实现近似1/t衰减。
def get_alpha(self, episode): # α₀=0.5, decay_rate=0.001 → 第1轮α=0.5, 第1000轮α≈0.5/(1+1)=0.25, 第5000轮α≈0.5/(1+5)=0.083 return self.alpha0 / (1 + episode * self.alpha_decay) # 在训练主循环中: for episode in range(n_episodes): alpha = agent.get_alpha(episode) # ... 执行一轮交互 ... # 更新Q值时传入当前alpha agent.update_q_value(state, action, reward, next_state, done, alpha=alpha)逻辑说明:
alpha_decay=0.001是经验值,需根据任务长度调整。若任务平均100步结束,n_episodes=10000,则episode 1000对应约10个完整任务,此时α应降至初始值的1/3~1/2。切忌用alpha = 0.1 / np.sqrt(episode+1)——根号衰减太慢,后期更新不足。
3.3 TD error监控:这是Q Learning的“心电图”,不看它等于蒙眼开车
Q Learning收敛的标志是TD error(δ = r + γ·maxQ(s',a') - Q(s,a))的绝对值持续下降并趋近于0。很多教程只画reward曲线,但reward受环境随机性干扰大,TD error才是算法内部收敛的真实信号。
# 在update_q_value中记录TD error def update_q_value(self, state, action, reward, next_state, done, alpha): current_q = self.q_table[state, action] if done: target_q = reward else: target_q = reward + self.gamma * np.max(self.q_table[next_state]) td_error = target_q - current_q self.q_table[state, action] += alpha * td_error # 返回TD error用于监控 return abs(td_error) # 主循环中收集 td_errors = [] for episode in range(n_episodes): # ... 交互过程 ... for step in range(max_steps): # ... td_err = agent.update_q_value(state, action, reward, next_state, done, alpha) td_errors.append(td_err) # 每100轮计算平均TD error if episode % 100 == 0: avg_td = np.mean(td_errors[-1000:]) # 最近1000步 print(f"Episode {episode}, Avg TD Error: {avg_td:.4f}")参数说明:监控窗口取1000步(非100轮),因单轮steps数波动大。
avg_td < 0.01可视为收敛,但需结合reward稳定性判断——若TD error小但reward震荡,说明Q值准但策略不稳定(如ε未衰减到位)。
4. 避坑指南:Q Learning落地中最常踩的5个坑,血泪经验总结
Q Learning看似简单,但工业现场部署时,80%的问题源于对算法假设的误读。以下5个坑,每一个都让我在凌晨三点重启过PLC控制器。
4.1 坑1:状态离散化后bin数过多,Q表内存溢出 → 现象:Python报MemoryError;原因:状态ID计算未做范围检查,导致数组索引超限;解决:离散化后立即统计唯一状态数,超10000则强制合并bin
# 错误示范:直接用raw_state计算state_id,不校验 state_id = int(raw_state[0]*100 + raw_state[1]*10 + raw_state[2]) # 正确做法:离散化后统计实际状态数 discrete_states = [] for _ in range(10000): # 采样1万次真实数据 raw = self.get_real_sensor_data() disc = self.discretize(raw) discrete_states.append(tuple(disc)) unique_states = len(set(discrete_states)) print(f"Unique states: {unique_states}") # 若>10000,需合并temp_bins中相邻bin if unique_states > 10000: # 合并温度bin:[60,80,90] → [60,85,90] temp_bins = [-np.inf, 60, 85, np.inf]4.2 坑2:reward设计含隐式奖励塑形 → 现象:agent在仿真中表现完美,上线后频繁触发安全联锁;原因:reward中加入“距离目标越近reward越高”,导致agent为拿中间奖励而冒险;解决:reward只基于可观测、可验证的终端事件,禁用距离、速度等中间量
教训:曾为提升收敛速度,在reward中加入
-abs(current_temp - target_temp),结果agent学会“快速升温冲过目标再急停”,造成热应力超标。工业reward铁律:只奖结果,不奖过程;只罚违规,不罚低效。
4.3 坑3:ε衰减过快,agent早熟 → 现象:前200轮reward飙升,之后5000轮无提升;原因:ε从1.0线性衰减到0.1仅用200轮,后期无探索;解决:用指数衰减+ε_min=0.1,且前500轮强制ε≥0.5
def choose_action(self, state, episode): # 前500轮强制高探索 if episode < 500: eps = max(0.5, self.eps_start * (0.995 ** episode)) else: eps = max(self.eps_end, self.eps_start * (0.995 ** episode)) # ... 后续同2.3节4.4 坑4:Q表未持久化,重启后归零 → 现象:PLC断电重启,agent重新学“走路”;原因:Q表存在内存,未保存到磁盘;解决:每100轮保存一次,用numpy.savez_compressed压缩
def save_q_table(self, filepath, episode): np.savez_compressed( filepath, q_table=self.q_table, episode=episode, eps=self.eps, timestamp=int(time.time()) ) print(f"Q table saved at episode {episode}") def load_q_table(self, filepath): data = np.load(filepath) self.q_table = data['q_table'] self.eps = float(data['eps']) print(f"Q table loaded from {filepath}, episode {data['episode']}")4.5 坑5:未做动作约束,agent输出非法指令 → 现象:PLC报“阀门开度指令超限”;原因:Q表argmax选出的动作a=3,但硬件只接受0~2;解决:在choose_action后加动作裁剪层,或初始化Q表时对非法动作置负无穷
# 初始化时屏蔽非法动作(推荐) self.q_table = np.full((self.n_states, self.n_actions), fill_value=-np.inf) # 对每个状态,只允许合法动作有初始值 for s in range(self.n_states): valid_actions = self.get_valid_actions(s) # 返回该状态下允许的动作列表 self.q_table[s, valid_actions] = 100.0 # 乐观初始值只给合法动作 # 或在选择后裁剪 def choose_action(self, state): action = np.argmax(self.q_table[state]) if action not in self.get_valid_actions(state): # 退回到次优动作,或随机选合法动作 valid_q = self.q_table[state].copy() valid_q[~np.isin(np.arange(self.n_actions), self.get_valid_actions(state))] = -np.inf action = np.argmax(valid_q) return action5. 进阶技巧:用Q Learning做在线策略优化,而非离线训练——把agent变成PLC的“实时决策插件”
Q Learning常被当作离线训练工具,训好模型再固化。但在产线中,设备老化、原料批次变化、环境温湿度漂移,都会让离线Q表失效。我的做法是:把Q Learning做成PLC周期性运行的在线学习模块,每班次用最新数据微调Q表,且保证学习过程不影响实时控制。
5.1 双缓冲Q表机制:主表执行控制,副表在线学习,每10分钟切换
核心思想:避免学习时Q表突变导致控制抖动。用两个Q表副本,主表(q_main)负责实时决策,副表(q_shadow)接收新数据更新,定时原子切换。
class OnlineQLearning: def __init__(self): self.q_main = np.load("q_init.npz")["q_table"] # 加载初始表 self.q_shadow = self.q_main.copy() self.last_swap = time.time() def get_action(self, state): # 永远用q_main决策,保证控制稳定 return np.argmax(self.q_main[state]) def learn_step(self, state, action, reward, next_state, done): # 更新q_shadow,不影响q_main self._update_q_table(self.q_shadow, state, action, reward, next_state, done) def maybe_swap(self): # 每10分钟切换一次 if time.time() - self.last_swap > 600: # 600秒=10分钟 # 原子切换:用copy而非指针,避免切换瞬间读到半更新状态 self.q_main, self.q_shadow = self.q_shadow.copy(), self.q_main.copy() self.last_swap = time.time() print("Q table swapped at", time.strftime("%H:%M:%S"))落地细节:
learn_step在PLC的“空闲扫描周期”调用(如每秒10次,但只在无控制指令时学习);maybe_swap放在班次交接点(如8:00、16:00),避免生产高峰切换。
5.2 增量式状态空间扩展:当新状态出现时,动态扩容Q表
产线中新工况(如新增传感器故障模式)会产生训练时未见的状态。传统做法是重启训练,但产线不能停。用字典替代数组存储Q值,键为状态tuple,值为动作Q值列表。
# 改用字典存储,支持动态插入 self.q_dict = {} # { (temp_bin, press_bin, valve_bin, alarm): [q0,q1,q2,q3] } def get_q_values(self, state_tuple): if state_tuple not in self.q_dict: # 新状态:初始化为乐观值,但比老状态略低(避免过度探索新风险) self.q_dict[state_tuple] = [80.0, 80.0, 80.0, 80.0] # 比初始100低 return self.q_dict[state_tuple] def update_q_value(self, state_tuple, action, reward, next_state_tuple, done): q_list = self.get_q_values(state_tuple) current_q = q_list[action] if done: target_q = reward else: next_q_list = self.get_q_values(next_state_tuple) target_q = reward + self.gamma * max(next_q_list) td_error = target_q - current_q q_list[action] += self.alpha * td_error优势:内存占用随实际出现状态线性增长,而非状态空间上限;新状态Q值初始为80(非100),平衡探索与安全。
5.3 策略验证协议:每次Q表切换前,用历史数据回放测试
切换Q表前,必须验证新策略不劣于旧策略。用过去24小时的传感器日志,回放生成state-action序列,计算新旧Q表的累计reward差异。
def validate_policy_swap(self, historical_logs): # historical_logs: list of (state_tuple, action, reward, next_state_tuple) old_reward = 0.0 new_reward = 0.0 for log in historical_logs: state, _, _, _ = log old_action = np.argmax(self.q_main[state]) new_action = np.argmax(self.q_shadow[state]) # 用log中的reward,但按新旧策略选择的动作查表 old_reward += self._get_reward_from_log(log, old_action) new_reward += self._get_reward_from_log(log, new_action) improvement = (new_reward - old_reward) / len(historical_logs) if improvement > -0.01: # 允许微小下降(<0.01 reward/step) return True, improvement else: print(f"Policy swap rejected: old={old_reward:.2f}, new={new_reward:.2f}") return False, improvement # 主循环中 if self.maybe_swap(): if self.validate_policy_swap(last_24h_logs): print("New policy validated, swapping...") self.q_main, self.q_shadow = self.q_shadow.copy(), self.q_main.copy() else: print("Validation failed, keeping old policy")关键点:验证用真实历史数据,而非仿真环境;阈值
-0.01是经验值,对应产线可接受的性能波动(如能耗增加0.1%)。
我干了七年工业自动化,Q Learning不是用来发论文的玩具,而是让老旧PLC学会“看懂”传感器数据的拐杖。第一次把Q表部署到注塑机温控系统时,我盯着TD error曲线看了三天,直到它稳定在0.005以下——那一刻我才信,这玩意真能干活。后来所有项目,我都坚持一条:不画reward曲线,先画TD error;不调ε,先看状态覆盖率;不谈收敛,先做策略回放验证。希望帮到你。
本文还有配套的精品资源,点击获取