第4章:Q-Learning算法:Q表原理,Q-Learning更新公式,ε-贪婪策略,手写Q-Learning玩转简单交易环境
说实话,Q-Learning 是我个人最早接触的强化学习算法之一。那时候我刚从传统量化转过来,看到「Q表」这个词,第一反应是——这不就是个查表法吗?后来才发现,这玩意儿虽然简单,但背后的思想,几乎奠定了整个值函数方法的基石。
今天咱们就来聊聊 Q-Learning。我会尽量用我踩过的坑、犯过的错,帮你把这个算法吃透。
4.1 Q表原理:一张表搞定决策
Q-Learning 的核心,说白了就是一张表。这张表叫 Q 表,行是状态,列是动作。每个格子存一个数值,叫 Q 值,代表「在某个状态下,执行某个动作,能拿到的未来总回报的期望」。
举个例子。假设你交易一只股票,状态是「当前持仓量」和「价格趋势」,动作是「买入、卖出、持有」。Q 表大概长这样:
| 状态 | 买入 | 卖出 | 持有 |
|---|---|---|---|
| 空仓 + 上涨趋势 | 0.85 | 0.12 | 0.45 |
| 满仓 + 下跌趋势 | 0.10 | 0.92 | 0.33 |
| 半仓 + 震荡 | 0.50 | 0.48 | 0.60 |
你看,Q 值越高,说明这个动作在当前状态下越「值钱」。算法要做的,就是不断更新这张表,让 Q 值越来越准。
核心思想:Q-Learning 是一种 off-policy 的时序差分学习算法。它直接学习最优策略的 Q 值,而不依赖于当前策略。
我在项目中遇到过一个问题:状态空间太大,Q 表根本存不下。比如价格连续值,离散化后可能有几千个状态,再乘以几个动作,表就炸了。这时候就需要用神经网络近似 Q 函数——那就是 DQN 了,咱们后面会讲。
4.2 Q-Learning 更新公式:核心中的核心
Q-Learning 的更新公式,我建议你背下来。它长这样:
Q(s, a) ← Q(s, a) + α * [r + γ * max_a' Q(s', a') - Q(s, a)]
拆开来看:
- Q(s, a):当前状态 s 下,执行动作 a 的 Q 值
- α:学习率,控制更新步长。我一般设 0.1 到 0.5 之间
- r:执行动作后获得的即时奖励
- γ:折扣因子,代表未来奖励的权重。γ=0 表示只看眼前,γ=1 表示无限远视
- max_a' Q(s', a'):下一个状态 s' 中,所有动作里最大的 Q 值
这个公式的妙处在于:它用下一个状态的最优 Q 值来更新当前状态的 Q 值。这就是「自举」的思想——用自己估计的值来更新自己。
我的习惯:刚开始调参时,α 别设太大。我吃过亏,α=0.9 直接让 Q 值震荡到飞起,策略完全学不收敛。后来老老实实从 0.1 开始调。
4.3 ε-贪婪策略:探索与利用的平衡
你想想看,如果算法每次都选 Q 值最大的动作,那它永远不知道有没有更好的选择。这就是「利用」太多,「探索」太少。
ε-贪婪策略就是来解决这个问题的。它的逻辑很简单:
- 以概率 ε 随机选择一个动作(探索)
- 以概率 1-ε 选择 Q 值最大的动作(利用)
我一般这样设置 ε:
import numpy as np
def epsilon_greedy(Q, state, epsilon):
if np.random.random() < epsilon:
# 探索:随机选一个动作
return np.random.choice(len(Q[state]))
else:
# 利用:选 Q 值最大的动作
return np.argmax(Q[state])
嗯,这里要注意:ε 不能一直固定。训练初期需要多探索,后期要多利用。我常用的策略是让 ε 从 1.0 线性衰减到 0.01:
epsilon = max(0.01, 1.0 - episode / total_episodes)
我曾经踩过的坑:有一次我把 ε 设成固定值 0.5,结果训练了 10000 个 episode,策略还在随机乱走。后来发现,ε 不衰减,算法永远学不会稳定策略。记住:探索是为了更好的利用,不是为探索而探索。
4.4 手写 Q-Learning:玩转简单交易环境
光说不练假把式。咱们手写一个完整的 Q-Learning,跑一个简单的交易环境。
先定义环境。假设我们只有两种状态:持仓和空仓。动作有三种:买入、卖出、持有。奖励规则:
- 买入后价格上涨:+1
- 买入后价格下跌:-1
- 空仓不动:0
- 卖出:根据持仓盈亏给奖励
import numpy as np
class SimpleTradingEnv:
def __init__(self):
self.price = 100.0
self.position = 0 # 0: 空仓, 1: 持仓
self.step_count = 0
def reset(self):
self.price = 100.0
self.position = 0
self.step_count = 0
return self._get_state()
def _get_state(self):
# 状态 = (持仓状态, 价格趋势)
# 简化:只用持仓状态
return self.position
def step(self, action):
# 模拟价格随机波动
price_change = np.random.normal(0, 1)
self.price += price_change
self.step_count += 1
reward = 0
done = False
if action == 0: # 买入
if self.position == 0:
self.position = 1
reward = 0
else:
reward = -0.5 # 重复买入惩罚
elif action == 1: # 卖出
if self.position == 1:
reward = price_change # 赚差价
self.position = 0
else:
reward = 0
else: # 持有
if self.position == 1:
reward = price_change * 0.1 # 持仓波动
else:
reward = 0
if self.step_count >= 100:
done = True
return self._get_state(), reward, done
接下来是 Q-Learning 主体:
class QLearningAgent:
def __init__(self, n_states, n_actions, alpha=0.1, gamma=0.9, epsilon=1.0):
self.Q = np.zeros((n_states, n_actions))
self.alpha = alpha
self.gamma = gamma
self.epsilon = epsilon
self.n_actions = n_actions
def choose_action(self, state):
if np.random.random() < self.epsilon:
return np.random.randint(self.n_actions)
return np.argmax(self.Q[state])
def update(self, state, action, reward, next_state):
# Q-Learning 更新公式
best_next = np.max(self.Q[next_state])
td_target = reward + self.gamma * best_next
td_error = td_target - self.Q[state][action]
self.Q[state][action] += self.alpha * td_error
def decay_epsilon(self, episode, total_episodes):
self.epsilon = max(0.01, 1.0 - episode / total_episodes)
训练代码:
env = SimpleTradingEnv()
agent = QLearningAgent(n_states=2, n_actions=3)
total_episodes = 5000
rewards_history = []
for episode in range(total_episodes):
state = env.reset()
total_reward = 0
done = False
while not done:
action = agent.choose_action(state)
next_state, reward, done = env.step(action)
agent.update(state, action, reward, next_state)
state = next_state
total_reward += reward
agent.decay_epsilon(episode, total_episodes)
rewards_history.append(total_reward)
if episode % 500 == 0:
print(f"Episode {episode}, Total Reward: {total_reward:.2f}, Epsilon: {agent.epsilon:.3f}")
跑完之后,看看 Q 表长什么样:
print("Learned Q-table:")
print(agent.Q)
# 输出示例:
# [[ 0.23 0.15 0.08]
# [ 0.12 0.31 0.19]]
# 第一行:空仓状态下,买入最好
# 第二行:持仓状态下,卖出最好
我的经验:这个简单环境里,Q-Learning 大概 2000 个 episode 就能收敛。但如果你把价格趋势也加入状态,收敛速度会慢很多。这时候可以考虑用「经验回放」来加速——不过那是 DQN 的内容了。
4.5 知识体系总览
下面这张图,是我自己梳理的 Q-Learning 核心逻辑。你看一遍,应该就能把整个流程串起来:
这张图把 Q-Learning 的三个核心模块串起来了。你看,左边是 Q 表,中间是更新公式,右边是探索策略。三者缺一不可。
总结一下:Q-Learning 虽然简单,但它是理解深度强化学习的基石。你把这个算法吃透了,后面学 DQN、Double DQN、Dueling DQN 都会轻松很多。我当年就是靠手写 Q-Learning 入门,后来做高频交易策略时,很多思想都是从这儿来的。
公众号:蓝海资料掘金营,微信deep3321