67、强化学习中的Q学习算法及相关技术

强化学习中的Q学习算法及相关技术

1. Q值迭代算法

首先,我们运行Q值迭代算法。该算法会对每个状态和每个可能的动作的所有Q值重复应用特定公式:

gamma = 0.90  # 折扣因子
for iteration in range(50):
    Q_prev = Q_values.copy()
    for s in range(3):
        for a in possible_actions[s]:
            Q_values[s, a] = np.sum([
                    transition_probabilities[s][a][sp]
                    * (rewards[s][a][sp] + gamma * Q_prev[sp].max())
                for sp in range(3)])

运行该算法后得到的Q值如下:

>>> Q_values
array([[18.91891892, 17.02702702, 13.62162162],
       [ 0.        ,        -inf, -4.87971488],
       [       -inf, 50.13365013,        -inf]])

例如,当智能体处于状态s0并选择动作a1时,预期的折扣未来奖励总和约为17.0。对于每个状态,我们可以找到具有最高Q值的动作:

>>> Q_values.argmax(axis=1)  # 每个状态的最优动作
array([0, 0, 1])

这就给出了在折扣因子为0.90时该马尔可夫决策过程(MDP)的最优策略:在状态s0选择动作a0,在状态s1选择动作a0(即保持不动),在状态s2选择动作a1(唯一可能的动作)。有趣的是,如果将折扣因子增加到0.95,最优策略会发生变化:在状态s1,最佳动作变为a2(穿过火焰)。这是因为越看重未来奖励,就越愿意忍受当前的痛苦以换取未来的幸福。

2. 时间差分学习

具有离散动作的强化学习问题通常可以建模为马尔可夫决策过程,但智能体最初并不知道转移概率(即不知道T(s, a, s′)),也不知道奖励情况(即不知道R(s, a, s′))。它必须至少经历每个状态和每个转移一次才能了解奖励,并且如果要合理估计转移概率,则需要多次经历这些状态和转移。

时间差分(TD)学习算法与Q值迭代算法非常相似,但进行了调整以考虑智能体对MDP只有部分了解的事实。一般来说,我们假设智能体最初只知道可能的状态和动作,其他一无所知。智能体使用探索策略(例如,纯随机策略)来探索MDP,随着过程的推进,TD学习算法会根据实际观察到的转移和奖励更新状态值的估计(见公式18 - 4)。

公式18 - 4:TD学习算法
[V_{k + 1}(s) = (1 - \alpha)V_{k}(s) + \alpha (r + \gamma \cdot V_{k}(s’))]
或者等价地:
[V_{k + 1}(s) = V_{k}(s) + \alpha \cdot \delta_{k}(s, r, s’)]
其中(\delta_{k}(s, r, s’) = r + \gamma \cdot V_{k}(s’) - V_{k}(s))

在这个公式中:
- (\alpha)是学习率(例如,0.01)。
- (r + \gamma \cdot V_{k}(s’)) 被称为TD目标。
- (\delta_{k}(s, r, s’)) 被称为TD误差。

用更简洁的方式表示第一个公式,可以使用符号 (a \leftarrow_{\alpha} b),这意味着 (a_{k + 1} \leftarrow (1 - \alpha) \cdot a_{k} + \alpha \cdot b_{k})。所以,公式18 - 4的第一行可以重写为:(V(s) \leftarrow_{\alpha} r + \gamma \cdot V(s’))。

TD学习与随机梯度下降有很多相似之处,包括每次处理一个样本的事实。此外,就像随机梯度下降一样,只有逐渐降低学习率,它才能真正收敛;否则,它会在最优Q值附近波动。对于每个状态s,该算法会跟踪智能体离开该状态时获得的即时奖励的运行平均值,以及假设其最优行动时预期后续获得的奖励。

3. Q学习算法

同样,Q学习算法是Q值迭代算法在转移概率和奖励最初未知情况下的一种适应(见公式18 - 5)。Q学习通过观察智能体的行动(例如,随机行动)并逐渐改进其Q值估计来工作。一旦它有了准确(或足够接近)的Q值估计,那么最优策略就是选择具有最高Q值的动作(即贪婪策略)。

公式18 - 5:Q学习算法
[Q(s, a) \leftarrow_{\alpha} r + \gamma \cdot \max_{a’} Q(s’, a’)]
对于每个状态 - 动作对 (s, a),该算法会跟踪智能体以动作a离开状态s时获得的奖励r的运行平均值,以及预期获得的折扣未来奖励总和。为了估计这个总和,我们取下一状态s′的Q值估计的最大值,因为我们假设目标策略从那时起将最优行动。

下面我们来实现Q学习算法。首先,我们需要让智能体探索环境。为此,我们需要一个step函数,以便智能体可以执行一个动作并获得结果状态和奖励:

def step(state, action):
    probas = transition_probabilities[state][action]
    next_state = np.random.choice([0, 1, 2], p=probas)
    reward = rewards[state][action][next_state]
    return next_state, reward

接下来,实现智能体的探索策略。由于状态空间相当小,简单的随机策略就足够了。如果我们运行算法足够长的时间,智能体将多次访问每个状态,并且也会多次尝试每个可能的动作:

def exploration_policy(state):
    return np.random.choice(possible_actions[state])

然后,在像之前一样初始化Q值后,我们准备运行带有学习率衰减的Q学习算法(使用第11章介绍的幂调度):

alpha0 = 0.05  # 初始学习率
decay = 0.005  # 学习率衰减
gamma = 0.90  # 折扣因子
state = 0  # 初始状态
for iteration in range(10_000):
    action = exploration_policy(state)
    next_state, reward = step(state, action)
    next_value = Q_values[next_state].max()  # 下一步的贪婪策略
    alpha = alpha0 / (1 + iteration * decay)
    Q_values[state, action] *= 1 - alpha
    Q_values[state, action] += alpha * (reward + gamma * next_value)
    state = next_state

这个算法会收敛到最优Q值,但需要很多次迭代,并且可能需要大量的超参数调整。如图所示,Q值迭代算法(左)收敛非常快,在少于20次迭代内就收敛了,而Q学习算法(右)大约需要8000次迭代才能收敛。显然,不知道转移概率或奖励会使找到最优策略变得困难得多!

Q学习算法被称为离策略算法,因为正在训练的策略不一定是训练期间使用的策略。例如,在我们刚刚运行的代码中,正在执行的策略(探索策略)是完全随机的,而正在训练的策略从未被使用。训练后,最优策略对应于系统地选择具有最高Q值的动作。相反,策略梯度算法是在线策略算法:它使用正在训练的策略来探索世界。令人惊讶的是,Q学习仅通过观察智能体随机行动就能学习到最优策略。

3. 探索策略

3.1 ε - 贪婪策略

Q学习只有在探索策略能够充分探索MDP时才能有效工作。虽然纯随机策略最终保证会多次访问每个状态和每个转移,但可能需要极长的时间才能做到这一点。因此,更好的选择是使用ε - 贪婪策略(ε是epsilon):在每一步,它以概率ε随机行动,或以概率1 - ε贪婪行动(即选择具有最高Q值的动作)。与完全随机策略相比,ε - 贪婪策略的优势在于,随着Q值估计越来越好,它会花越来越多的时间探索环境中有趣的部分,同时仍会花一些时间访问MDP的未知区域。通常的做法是从一个较高的ε值(例如,1.0)开始,然后逐渐降低它(例如,降至0.05)。

3.2 基于探索函数的Q学习

另一种方法是鼓励探索策略尝试之前很少尝试的动作。这可以通过在Q值估计中添加一个奖励来实现,如公式18 - 6所示。

公式18 - 6:使用探索函数的Q学习
[Q(s, a) \leftarrow_{\alpha} r + \gamma \cdot \max_{a’} f(Q(s’, a’), N(s’, a’))]
在这个公式中:
- (N(s’, a’)) 统计在状态s′中选择动作a′的次数。
- (f(Q, N)) 是一个探索函数,例如 (f(Q, N) = Q + \kappa / (1 + N)),其中κ是一个好奇心超参数,用于衡量智能体对未知的吸引力。

4. 近似Q学习和深度Q学习

4.1 Q学习的扩展性问题

Q学习的主要问题是,它在具有许多状态和动作的大型(甚至中型)MDP中扩展性不佳。例如,假设要使用Q学习训练一个智能体来玩吃豆人游戏。吃豆人可以吃大约150个豆子,每个豆子可能存在或不存在(即已被吃掉)。因此,可能的状态数量大于 (2^{150} \approx 10^{45})。如果再加上所有幽灵和吃豆人的所有可能位置组合,可能的状态数量会变得比地球上的原子数量还多,所以根本无法跟踪每个Q值的估计。

4.2 近似Q学习

解决方案是找到一个函数 (Q_{\theta}(s, a)),它使用可管理数量的参数(由参数向量θ给出)来近似任何状态 - 动作对 (s, a) 的Q值。这就是近似Q学习。多年来,建议使用从状态中提取的手工特征的线性组合(例如,最近幽灵的距离、它们的方向等)来估计Q值,但在2013年,DeepMind表明使用深度神经网络可以工作得更好,特别是对于复杂问题,而且不需要任何特征工程。用于估计Q值的深度神经网络称为深度Q网络(DQN),使用DQN进行近似Q学习称为深度Q学习。

4.3 训练深度Q网络

考虑DQN为给定的状态 - 动作对 (s, a) 计算的近似Q值。根据贝尔曼方程,我们知道希望这个近似Q值尽可能接近在状态s中执行动作a后实际观察到的奖励r,加上从那时起最优行动的折扣值。为了估计未来折扣奖励的总和,我们可以在下一步状态s′上执行DQN,针对所有可能的动作a′。我们得到每个可能动作的近似未来Q值。然后我们选择最高值(因为我们假设会最优行动)并进行折扣,这就得到了未来折扣奖励总和的估计。通过将奖励r和未来折扣值估计相加,我们得到了状态 - 动作对 (s, a) 的目标Q值y(s, a),如公式18 - 7所示。

公式18 - 7:目标Q值
[y(s, a) = r + \gamma \cdot \max_{a’} Q_{\theta}(s’, a’)]
有了这个目标Q值,我们可以使用任何梯度下降算法进行训练步骤。具体来说,我们通常尝试最小化估计Q值 (Q_{\theta}(s, a)) 和目标Q值y(s, a) 之间的平方误差,或者使用Huber损失来减少算法对大误差的敏感性。这就是深度Q学习算法!

下面我们来看看如何实现深度Q学习算法来解决CartPole环境问题。

4.3.1 创建深度Q网络

首先,我们需要一个深度Q网络。理论上,我们需要一个以状态 - 动作对为输入并输出近似Q值的神经网络。但在实践中,使用仅以状态为输入并为每个可能动作输出一个近似Q值的神经网络效率更高。为了解决CartPole环境问题,我们不需要非常复杂的神经网络,几个隐藏层就足够了:

input_shape = [4]  # == env.observation_space.shape
n_outputs = 2  # == env.action_space.n
model = tf.keras.Sequential([
    tf.keras.layers.Dense(32, activation="elu", input_shape=input_shape),
    tf.keras.layers.Dense(32, activation="elu"),
    tf.keras.layers.Dense(n_outputs)
])
4.3.2 实现ε - 贪婪策略

为了选择动作,我们选择具有最大预测Q值的动作。为了确保智能体探索环境,我们将使用ε - 贪婪策略(即以概率ε选择随机动作):

def epsilon_greedy_policy(state, epsilon=0):
    if np.random.rand() < epsilon:
        return np.random.randint(n_outputs)  # 随机动作
    else:
        Q_values = model.predict(state[np.newaxis], verbose=0)[0]
        return Q_values.argmax()  # 根据DQN的最优动作
4.3.3 经验回放

为了减少训练批次中经验的相关性,我们将所有经验存储在一个回放缓冲区(或回放内存)中,并在每次训练迭代时从中随机采样一个训练批次。我们使用双端队列(deque)来实现:

from collections import deque
replay_buffer = deque(maxlen=2000)

每个经验由六个元素组成:一个状态s、智能体采取的动作a、得到的奖励r、到达的下一个状态s′、一个布尔值表示该回合是否在该点结束(done),以及另一个布尔值表示该回合是否在该点被截断。我们需要一个小函数来从回放缓冲区中随机采样一批经验,它将返回六个对应于六个经验元素的NumPy数组:

def sample_experiences(batch_size):
    indices = np.random.randint(len(replay_buffer), size=batch_size)
    batch = [replay_buffer[index] for index in indices]
    return [
        np.array([experience[field_index] for experience in batch])
        for field_index in range(6)
    ]  # [states, actions, rewards, next_states, dones, truncateds]
4.3.4 执行一步并存储经验

我们创建一个函数,使用ε - 贪婪策略执行一步,然后将得到的经验存储在回放缓冲区中:

def play_one_step(env, state, epsilon):
    action = epsilon_greedy_policy(state, epsilon)
    next_state, reward, done, truncated, info = env.step(action)
    replay_buffer.append((state, action, reward, next_state, done, truncated))
    return next_state, reward, done, truncated, info
4.3.5 训练步骤

最后,我们创建一个函数,从回放缓冲区中采样一批经验,并通过对这批经验执行一次梯度下降步骤来训练DQN:

batch_size = 32
discount_factor = 0.95
optimizer = tf.keras.optimizers.Nadam(learning_rate=1e-2)
loss_fn = tf.keras.losses.mean_squared_error
def training_step(batch_size):
    experiences = sample_experiences(batch_size)
    states, actions, rewards, next_states, dones, truncateds = experiences
    next_Q_values = model.predict(next_states, verbose=0)
    max_next_Q_values = next_Q_values.max(axis=1)
    runs = 1.0 - (dones | truncateds)  # 回合未结束或未被截断
    target_Q_values = rewards + runs * discount_factor * max_next_Q_values
    target_Q_values = target_Q_values.reshape(-1, 1)
    mask = tf.one_hot(actions, n_outputs)
    with tf.GradientTape() as tape:
        all_Q_values = model(states)
        Q_values = tf.reduce_sum(all_Q_values * mask, axis=1, keepdims=True)
        loss = tf.reduce_mean(loss_fn(target_Q_values, Q_values))
    grads = tape.gradient(loss, model.trainable_variables)
    optimizer.apply_gradients(zip(grads, model.trainable_variables))

代码解释如下:
1. 首先定义一些超参数,创建优化器和损失函数。
2. 创建 training_step() 函数,它首先从回放缓冲区中采样一批经验,然后使用DQN预测每个经验的下一个状态中每个可能动作的Q值。由于我们假设智能体将最优行动,所以只保留每个下一个状态的最大Q值。接着,使用公式18 - 7计算每个经验的状态 - 动作对的目标Q值。
3. 我们希望使用DQN计算每个经历过的状态 - 动作对的Q值,但DQN也会输出其他可能动作的Q值,所以需要屏蔽掉不需要的Q值。 tf.one_hot() 函数可以将动作索引数组转换为这样的掩码。例如,如果前三个经验分别包含动作1、1、0,那么掩码将以 [[0, 1], [0, 1], [1, 0], ...] 开头。然后将DQN的输出与这个掩码相乘,将不需要的Q值置为零。接着对轴1求和以去除所有零,只保留经历过的状态 - 动作对的Q值,得到 Q_values 张量,其中包含批次中每个经验的一个预测Q值。
4. 计算损失:它是经历过的状态 - 动作对的目标Q值和预测Q值之间的均方误差。
5. 最后,执行一次梯度下降步骤以最小化损失相对于模型可训练变量的值。

4.3.6 训练模型

现在训练模型就很简单了:

for episode in range(600):
    obs, info = env.reset()
    for step in range(200):
        epsilon = max(1 - episode / 500, 0.01)
        obs, reward, done, truncated, info = play_one_step(env, obs, epsilon)
        if done or truncated:
            break
    if episode > 50:
        training_step(batch_size)

我们运行600个回合,每个回合最多200步。在每一步,我们首先计算ε - 贪婪策略的epsilon值:它将在不到500个回合内从1线性下降到0.01。然后调用 play_one_step() 函数,它将使用ε - 贪婪策略选择一个动作,然后执行该动作并将经验记录在回放缓冲区中。如果回合结束或被截断,我们退出循环。最后,如果回合数超过50,我们调用 training_step() 函数在从回放缓冲区中采样的一批经验上训练模型。我们在不训练的情况下进行多个回合的原因是让回放缓冲区有时间填充(如果等待时间不够,回放缓冲区中的多样性将不足)。这样,我们就实现了深度Q学习算法!

5. 强化学习的挑战

5.1 灾难性遗忘

从学习曲线可以看出,该算法一开始需要一段时间才能开始学习,部分原因是开始时ε非常高。然后其进展不稳定:它在大约第220个回合首次达到最大奖励,但随后立即下降,然后上下波动几次,在大约第320个回合似乎终于稳定在最大奖励附近,但不久后其分数又急剧下降。这被称为灾难性遗忘,是几乎所有强化学习算法面临的重大问题之一:随着智能体探索环境,它会更新其策略,但在环境的一部分中学到的东西可能会破坏在其他部分之前学到的东西。经验之间的相关性很强,并且学习环境不断变化,这对于梯度下降来说并不理想!如果增大回放缓冲区的大小,算法受此问题的影响会较小。调整学习率也可能有所帮助。

5.2 训练不稳定

强化学习以困难著称,这主要是因为训练不稳定,并且对超参数值和随机种子的选择非常敏感。正如研究人员Andrej Karpathy所说:“[监督学习]想工作。……强化学习必须被强迫工作”。训练通常不稳定,可能需要尝试许多超参数值和随机种子才能找到一个有效的组合。例如,如果将激活函数从 "elu" 更改为 "relu" ,性能会低得多。

5.3 损失不是性能的良好指标

我们可能会想为什么不绘制损失曲线。事实证明,损失并不是模型性能的良好指标。损失可能会下降,但智能体的表现可能会更差(例如,当智能体被困在环境的一个小区域中,而DQN开始过拟合该区域时就会发生这种情况)。相反,损失可能会上升,但智能体的表现可能会更好(例如,如果DQN之前低估了Q值,而现在开始正确增加其预测值,智能体可能会表现得更好,获得更多奖励,但损失可能会增加,因为DQN也设置了目标,目标值也会更大)。因此,绘制奖励曲线更合适。

5.4 实际应用

尽管强化学习面临诸多挑战,但它仍有一些实际应用,除了AlphaGo和Atari游戏之外,例如谷歌使用强化学习来优化其数据中心成本,它还用于一些机器人应用、超参数调整和推荐系统中。基本的深度Q学习算法在学习玩Atari游戏时过于不稳定,DeepMind通过调整算法解决了这个问题。

6. 深度Q学习算法的优化思路

6.1 回放缓冲区的优化

在之前的实现中,我们使用了双端队列 deque 作为回放缓冲区。当需要非常大的回放缓冲区时, deque 的随机访问可能会变慢。此时可以考虑使用循环缓冲区,或者使用DeepMind的Reverb库。以下是一个简单的循环缓冲区的实现思路:

class CircularBuffer:
    def __init__(self, maxlen):
        self.maxlen = maxlen
        self.buffer = [None] * maxlen
        self.index = 0
        self.length = 0

    def append(self, item):
        self.buffer[self.index] = item
        self.index = (self.index + 1) % self.maxlen
        if self.length < self.maxlen:
            self.length += 1

    def __len__(self):
        return self.length

    def __getitem__(self, index):
        return self.buffer[index]

    def sample(self, batch_size):
        indices = np.random.randint(len(self), size=batch_size)
        return [self.buffer[i] for i in indices]

replay_buffer = CircularBuffer(maxlen=2000)

6.2 超参数调优

强化学习对超参数值和随机种子非常敏感,因此超参数调优至关重要。以下是一些常见超参数及其可能的调整范围:
| 超参数 | 含义 | 建议调整范围 |
| ---- | ---- | ---- |
| alpha0 | 初始学习率 | 0.001 - 0.1 |
| decay | 学习率衰减 | 0.001 - 0.01 |
| gamma | 折扣因子 | 0.9 - 0.99 |
| epsilon | ε - 贪婪策略中的随机概率 | 0.01 - 1.0 |
| batch_size | 训练批次大小 | 16 - 128 |

可以使用网格搜索、随机搜索或更高级的调优算法(如贝叶斯优化)来找到最佳的超参数组合。

6.3 网络结构调整

对于深度Q网络(DQN)的结构,也可以进行调整以提高性能。例如,可以增加隐藏层的数量或神经元的数量,或者尝试不同的激活函数。以下是一个增加了一层隐藏层的DQN示例:

input_shape = [4]  # == env.observation_space.shape
n_outputs = 2  # == env.action_space.n
model = tf.keras.Sequential([
    tf.keras.layers.Dense(64, activation="elu", input_shape=input_shape),
    tf.keras.layers.Dense(64, activation="elu"),
    tf.keras.layers.Dense(32, activation="elu"),
    tf.keras.layers.Dense(n_outputs)
])

7. 深度Q学习算法的流程图

graph TD;
    A[初始化DQN、回放缓冲区、超参数] --> B[开始回合];
    B --> C[重置环境状态];
    C --> D[计算epsilon值];
    D --> E[使用ε - 贪婪策略选择动作];
    E --> F[执行动作并获取奖励和下一个状态];
    F --> G[将经验存储在回放缓冲区];
    G --> H{回合是否结束或截断};
    H -- 否 --> D;
    H -- 是 --> I{回合数是否大于50};
    I -- 是 --> J[从回放缓冲区采样一批经验];
    J --> K[计算目标Q值];
    K --> L[使用DQN计算预测Q值];
    L --> M[计算损失];
    M --> N[执行梯度下降步骤更新DQN];
    N --> B;
    I -- 否 --> B;

8. 总结

强化学习中的Q学习算法及其相关技术,如近似Q学习和深度Q学习,为解决复杂的决策问题提供了强大的工具。然而,这些算法也面临着诸多挑战,如灾难性遗忘、训练不稳定等。通过合理选择探索策略、优化回放缓冲区、调整超参数和网络结构等方法,可以在一定程度上缓解这些问题。但要成功应用强化学习,仍需要时间、耐心和不断的尝试。尽管如此,强化学习在数据中心优化、机器人、超参数调优和推荐系统等领域已经有了实际应用,未来随着技术的不断发展,其应用前景将更加广阔。在实际应用中,我们需要根据具体问题的特点,灵活选择和调整算法,以达到最佳的性能。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值