第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(s,a) ← Q(s,a) + α[ r + γ·maxQ(s',a') - Q(s,a)] 时序差分 + 自举 概率 ε:随机探索 概率 1-ε:贪婪利用 ε 需随训练衰减 初期探索,后期利用 完整训练流程 1. 初始化 Q 表 → 2. 观察状态 s → 3. ε-贪婪选动作 a 4. 执行动作,获得奖励 r 和下一状态 s' 5. 用更新公式更新 Q(s,a) → 6. s = s',重复直到收敛

这张图把 Q-Learning 的三个核心模块串起来了。你看,左边是 Q 表,中间是更新公式,右边是探索策略。三者缺一不可。

总结一下:Q-Learning 虽然简单,但它是理解深度强化学习的基石。你把这个算法吃透了,后面学 DQN、Double DQN、Dueling DQN 都会轻松很多。我当年就是靠手写 Q-Learning 入门,后来做高频交易策略时,很多思想都是从这儿来的。


公众号:蓝海资料掘金营,微信deep3321