Python 实战人工智能数学基础:强化学习

91 阅读8分钟

1.背景介绍

什么是强化学习?强化学习是机器学习中的一个领域,是一种通过奖励和惩罚的方式让机器能够在不断学习中取得进步的一种方法。它可以看作是一种基于价值的动机机制来解决任务、选择行为和建立长期记忆的方法。简而言之,强化学习就是让机器具备学习能力的科技。

2.核心概念与联系

1) 环境状态(State)

状态可以定义为智能体处于某个特定的环境中,并具有一些特征或属性,如位置、速度、形状、颜色等。状态也可视作智能体感知到的环境信息。对于强化学习来说,状态可以被表示为向量或矩阵形式,其维度一般远远大于系统的实际参数数量,例如有海龟与狗两种智能体,状态可以包含它们两者的坐标、大小、朝向等所有潜在影响因素。

2) 行动选项(Action)

行动选项描述的是智能体的可用动作集合,对于强化学习来说,行动选项可以是动作或者指令,由智能体决定如何采取动作。例如,对于斗地主游戏,行动选项可以是出牌,包括同花顺、同花、同花不同号、同花顺不同刻、顺子、对子、散牌等。

3) 即时奖励(Reward)

即时奖励指的是智能体在完成当前动作后,所获得的奖励值。一般来说,即时奖励可能是正的或负的,具体取决于当前智能体的表现。

4) 折扣因子(Discount Factor)

折扣因子是一个重要的参数,用于控制随时间推移,即时奖励折现到将来的长期累积奖励的程度。其取值范围通常在[0,1]之间,取值越高,智能体越倾向于只考虑当前即时奖励,取值越低,智能体越趋向于考虑长期奖励。

5) 目标函数(Objective Function)

目标函数指的是智能体长期追求的目标,是评判智能体表现优劣的依据。目标函数一般是根据历史记录、当前状态、行动选项、即时奖励等综合计算得到的结果。强化学习的目标是找到能够最大化或最小化目标函数的策略,使得智能体始终以最大化长期奖励的方式去探索和学习。

6) 轨迹(Trajectory)

轨迹是指智能体从初始状态出发,经过一系列动作,到达最终状态的过程。轨迹上每个状态和奖励组成了一条路径。每条路径可能对应着一个不同的策略,而智能体可以通过模仿、比较、学习等方式找到最佳的策略,从而获取更好的回报。

7) 策略(Policy)

策略指的是智能体在给定状态下采取的动作,是为了达到某种目标而进行的动作选择。策略与环境之间的交互关系决定了智能体的表现。在强化学习中,策略可以用概率分布表示,概率分布给出了各个行动选项在当前状态下的概率,且满足一定条件(比如全局最优)。

8) 预测错误(Prediction Error)

预测错误是指智能体对环境未来状态的估计与真实情况之间的差距。预测错误会影响智能体的策略的更新,因为它直接影响智能体是否能从当前策略找到更好的策略。预测错误可能来自状态或奖励的噪声、未来状态的不可知性、动作序列与状态序列之间的关联性、多重影响因素等。预测误差会影响智能体的学习效率。

9) 模型(Model)

模型是指智能体用来估计环境状态、动作、奖励的数学模型。模型可以分为状态转移模型、奖励函数模型、动作价值函数模型等。强化学习研究的是如何开发有效的模型,模型的好坏将直接影响到智能体的学习效率。

3.核心算法原理和具体操作步骤以及数学模型公式详细讲解

本章节主要介绍强化学习算法的基本原理,算法的实现细节及相关数学公式。

1) 基于价值的方法(Q-learning)

Q-learning 是一种基于价值的方法,基于 Q 函数来确定智能体的动作。Q 函数是一个状态动作值函数,表示状态 action-state 对导致奖励 reward 的最大利益。具体来说,Q 函数的表达式如下:

其中,Q(s,a) 表示状态 s 下执行动作 a 时,智能体的 Q 值;Ns 表示下一个状态;r 表示即时奖励。当 Ns 不存在的时候,可以采用 Bellman 方程中的贝尔曼最优方程(Bellman equation)来递推更新 Q 值。

Q-learning 算法流程如下图所示:

从流程图中,可以看到,Q-learning 通过 Q 函数和实际的奖励来训练和更新智能体的策略。在每一步中,智能体都会根据 Q 函数选择一个动作,并利用新旧 Q 值之间的差异来更新 Q 函数。在更新完 Q 函数后,如果遇到局部最优情况,可能会导致策略收敛慢、不稳定。为了防止策略走偏离,还可以加入探索策略,即按照一定概率随机选取动作来探索新的可能性。

2) Sarsa 算法(Sarsa)

Sarsa 算法是 Q-learning 的变种,相比于 Q-learning 有几个显著的改进:首先,Sarsa 使用 state-action 而不是 state 来作为状态单元;其次,Sarsa 使用旧的 Q 函数来选择动作,而不是每次都直接选择最大 Q 函数对应的动作;最后,Sarsa 在更新 Q 函数时,同时考虑下一时刻的状态和奖励。Sarsa 算法流程如下图所示:

在 Sarsa 中,每一步都要保存完整的状态动作值函数 Q ,这就增加了存储开销,并且可能导致较大的方差。因此,Sarsa 更适用于较小规模的问题,比如 Atari 游戏。

3) DQN 算法(Deep Q Network)

DQN 算法是一种结合深度神经网络(DNN)与 Q-learning 的策略梯度算法。它的输入是环境的图像帧,输出是一个动作的值。与普通 DNN 不同的是,DQN 将图像帧输入到两个完全相同的 DNN 上,然后将两个输出值合并起来计算 Q 值,这样就可以引入图像帧的内容。它的结构如下图所示:

DQN 可以处理连续动作空间,但仍然无法处理离散动作空间,因此,它只能处理非常小的环境。

4) DDPG 算法(Deep Deterministic Policy Gradient)

DDPG 算法是一种基于 DQN 和策略梯度的策略算法。DDPG 算法与 DQN 类似,也是结合深度神经网络(DNN)与 Q-learning 的一种策略梯度算法,但是它可以解决连续动作空间,不需要特殊的编码器。DDPG 的结构如下图所示:

DDPG 可以处理连续动作空间,还可以学习高维的状态空间,因此,它可以处理复杂的环境。

4.具体代码实例和详细解释说明

本节将演示以上三个算法的具体实现和运行步骤。

1) Q-learning 示例

Q-learning 示例代码如下:

import gym
from collections import defaultdict

env = gym.make('FrozenLake-v0') # 初始化游戏环境

Q = defaultdict(lambda: np.zeros(env.action_space.n)) # 初始化 Q 表格
gamma = 0.9 # 设置折扣因子
alpha = 0.1 # 设定学习速率
epsilon = 0.1 # 设定探索因子
num_episodes = 2000 # 设置 episode 数量

for i in range(num_episodes):
    state = env.reset() # 开始新episode,初始化环境状态
    done = False
    
    while not done:
        if random.uniform(0, 1) < epsilon:
            action = env.action_space.sample() # 随机探索
        else:
            action = np.argmax(Q[state]) # 根据 Q 表格选取最优动作
        
        new_state, reward, done, _ = env.step(action) # 执行动作并接收反馈
        
        max_future_q = np.max(Q[new_state]) # 计算下一个状态的最优 q 值
        current_q = Q[state][action] # 获取当前状态 action 的 q 值
        
        new_q = (1 - alpha) * current_q + alpha * (reward + gamma * max_future_q) # 更新 q 值
        
        Q[state][action] = new_q # 更新 Q 表格
        state = new_state # 更新环境状态
        
        if done:
            print("Episode {}/{} finished with reward {}".format(i+1, num_episodes, reward))
            
print("\n\nQ-table:\n", Q) # 打印 Q 表格

代码首先初始化了游戏环境 Frozen Lake-v0 ,设置了折扣因子 gamma ,学习速率 alpha ,探索因子 epsilon ,episode 数量 num_episodes 。代码然后使用 defaultdict 实现 Q 表格,默认值为零。循环遍历 episode 数量,使用随机动作策略 epsilon 决定是否采用贪婪策略,否则根据 Q 表格选取最优动作。执行动作并接收反馈后,更新 Q 表格并更新环境状态。最后打印 Q 表格。

2) Sarsa 示例

Sarsa 示例代码如下:

import gym
from collections import defaultdict

env = gym.make('FrozenLake-v0') # 初始化游戏环境

Q = defaultdict(lambda: np.zeros(env.action_space.n)) # 初始化 Q 表格
gamma = 0.9 # 设置折扣因子
alpha = 0.1 # 设定学习速率
epsilon = 0.1 # 设定探索因子
num_episodes = 2000 # 设置 episode 数量

for i in range(num_episodes):
    state = env.reset() # 开始新episode,初始化环境状态
    action = env.action_space.sample() if random.uniform(0, 1) < epsilon else \
        np.argmax(Q[state]) # 随机动作策略 或 贪婪策略
    
    done = False
    total_reward = 0
        
    while not done:
        new_state, reward, done, _ = env.step(action) # 执行动作并接收反馈
        
        next_action = env.action_space.sample() if random.uniform(0, 1) < epsilon else \
            np.argmax(Q[new_state]) # 随机动作策略 或 贪婪策略
        
        max_future_q = np.max(Q[new_state]) # 计算下一个状态的最优 q 值
        current_q = Q[state][action] # 获取当前状态 action 的 q 值
        
        new_q = (1 - alpha) * current_q + alpha * (reward + gamma * Q[new_state][next_action]) # 更新 q 值
        
        Q[state][action] = new_q # 更新 Q 表格
        state = new_state # 更新环境状态
        action = next_action # 更新动作
        
        total_reward += reward # 计算总奖励
        
        if done:
            break
            
    if i % 100 == 0:
        print("Episode {}/{} finished with reward {}".format(i+1, num_episodes, total_reward))

print("\n\nQ-table:\n", Q) # 打印 Q 表格

代码首先初始化了游戏环境 Frozen Lake-v0 ,设置了折扣因子 gamma ,学习速率 alpha ,探索因子 epsilon ,episode 数量 num_episodes 。代码然后使用 defaultdict 实现 Q 表格,默认值为零。循环遍历 episode 数量,生成随机动作 action ,再执行 action 并接收反馈。如果下一个状态存在,则生成随机动作 next_action ,否则使用贪婪策略选取动作。计算下一个状态的最优 q 值和当前状态 action 的 q 值,使用 Sarsa 算法更新 Q 表格,更新环境状态和动作。最后打印 Q 表格。

3) DQN 示例

DQN 示例代码如下:

import gym
import torch
import numpy as np
import matplotlib.pyplot as plt

env = gym.make('CartPole-v1') # 初始化游戏环境

class DQN(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = torch.nn.Linear(in_features=4, out_features=128)
        self.relu1 = torch.nn.ReLU()
        self.fc2 = torch.nn.Linear(in_features=128, out_features=2)

    def forward(self, x):
        x = self.fc1(x)
        x = self.relu1(x)
        return self.fc2(x)

model = DQN().to('cuda') # 创建 DQN 模型,使用 GPU 加速
optimizer = torch.optim.Adam(params=model.parameters(), lr=1e-3) # 设置优化器
loss_fn = torch.nn.MSELoss() # 设置损失函数

def train():
    model.train()
    for e in range(1000):
        state = torch.FloatTensor(env.reset()).unsqueeze(0).to('cuda') # 开始新episode,初始化环境状态
        done = False
        total_reward = 0

        while not done:
            optimizer.zero_grad()

            action = torch.argmax(model(state)).item() # 根据模型预测执行的动作
            new_state, reward, done, _ = env.step(action) # 执行动作并接收反馈

            total_reward += reward
            new_state = torch.FloatTensor(new_state).unsqueeze(0).to('cuda')
            
            target_q = reward + (1-done)*gamma*torch.max(model(new_state)[0]).item() # 计算目标 Q 值
            curr_q = model(state).gather(dim=-1, index=torch.LongTensor([[action]]).to('cuda'))[0].item() # 计算当前 Q 值
            
            loss = loss_fn(target_q, curr_q) # 计算损失
            loss.backward()
            optimizer.step()
            
            state = new_state
            
        if e % 100 == 0:
            print("Epoch {:4d}/{} | Total Reward {:.3f}".format(
                e+1, 1000, total_reward))

def test():
    model.eval()
    state = torch.FloatTensor(env.reset()).unsqueeze(0).to('cuda')
    done = False
    total_reward = 0

    while not done:
        with torch.no_grad():
            action = torch.argmax(model(state)).item() # 根据模型预测执行的动作
            new_state, reward, done, _ = env.step(action) # 执行动作并接收反馈

            total_reward += reward
            new_state = torch.FloatTensor(new_state).unsqueeze(0).to('cuda')

            state = new_state
        
    return total_reward
    
if __name__ == '__main__':
    train() # 开始训练
    rewards = []

    for i in range(100):
        r = test() # 测试 100 次
        rewards.append(r)
    
    avg_rew = sum(rewards)/len(rewards)
    print("Average Reward over last 100 epochs is {:.3f}".format(avg_rew))
    plt.plot([i for i in range(1, len(rewards)+1)], rewards)
    plt.xlabel('Epoch')
    plt.ylabel('Total Reward')
    plt.show()

代码首先导入了必要的库,初始化了游戏环境 CartPole-v1 ,创建了一个 Deep Q Network 模型,使用 GPU 加速。然后定义了一个训练函数 train ,定义了一个测试函数 test ,分别用于训练模型和测试模型的性能。

训练函数 train 实现了以下逻辑:

  • 生成初始状态;
  • 使用当前模型预测执行的动作;
  • 执行动作并接收反馈;
  • 计算目标 Q 值;
  • 使用 MSE Loss 计算当前 Q 值和目标 Q 值之间的差异,并反向传播梯度;
  • 更新模型参数。

测试函数 test 实现了以下逻辑:

  • 生成初始状态;
  • 使用当前模型预测执行的动作;
  • 执行动作并接收反馈;
  • 返回总奖励。

最后调用训练函数开始训练模型,并使用测试函数测试模型的性能。测试结束后,绘制平均奖励曲线。

4) DDPG 示例

DDPG 示例代码如下:

import gym
import torch
import numpy as np
import matplotlib.pyplot as plt

env = gym.make('Pendulum-v0') # 初始化游戏环境

class Actor(torch.nn.Module):
    def __init__(self, input_size, output_size):
        super().__init__()
        self.linear1 = torch.nn.Linear(input_size, 64)
        self.linear2 = torch.nn.Linear(64, 64)
        self.mu_head = torch.nn.Linear(64, output_size)
        self.logstd_head = torch.nn.Linear(64, output_size)

    def forward(self, inputs):
        x = self.linear1(inputs)
        x = torch.tanh(x)
        x = self.linear2(x)
        x = torch.tanh(x)
        mu = self.mu_head(x)
        log_std = self.logstd_head(x)
        std = torch.exp(log_std)
        dist = torch.distributions.Normal(loc=mu, scale=std)
        return dist

class Critic(torch.nn.Module):
    def __init__(self, input_size, output_size):
        super().__init__()
        self.linear1 = torch.nn.Linear(input_size, 64)
        self.linear2 = torch.nn.Linear(64, 64)
        self.value_head = torch.nn.Linear(64, output_size)

    def forward(self, inputs):
        x = self.linear1(inputs)
        x = torch.tanh(x)
        x = self.linear2(x)
        x = torch.tanh(x)
        values = self.value_head(x)
        return values

actor = Actor(input_size=3, output_size=1).to('cuda') # 创建策略网络,输入大小为 3 ,输出大小为 1
critic = Critic(input_size=3, output_size=1).to('cuda') # 创建评价网络,输入大小为 3 ,输出大小为 1
actor_optimizer = torch.optim.Adam(params=actor.parameters(), lr=1e-4) # 设置策略网络优化器
critic_optimizer = torch.optim.Adam(params=critic.parameters(), lr=1e-3) # 设置评价网络优化器
mse_loss = torch.nn.MSELoss() # 设置损失函数

replay_buffer = []
batch_size = 64
gamma = 0.99
tau = 0.005
epoch = 100
eps = np.finfo(np.float32).eps.item() # 设置极小值

def select_action(state):
    global actor, replay_buffer
    state = torch.FloatTensor(state).unsqueeze(0).to('cuda')
    dist = actor(state)
    value = critic(state).detach()
    action = dist.sample().cpu().numpy()[0]
    buffer_entry = (state, value, action, None, False)
    replay_buffer.append(buffer_entry)
    return action

def update_parameters():
    global actor, critic, actor_optimizer, critic_optimizer, replay_buffer
    states, _, actions, rewards, dones = zip(*random.choices(replay_buffer, k=batch_size))
    states = torch.stack(states).squeeze().to('cuda')
    actions = torch.tensor(actions, dtype=torch.int64, device='cuda').view(-1, 1)
    rewards = torch.tensor(rewards, dtype=torch.float32, device='cuda').view(-1, 1)
    dones = torch.tensor(dones, dtype=torch.uint8, device='cuda').view(-1, 1)
    
    next_states = torch.cat([entry[0] for entry in replay_buffer[-100:] if entry[4]])
    with torch.no_grad():
        next_values = critic(next_states)
    targets = rewards + gamma*(1-dones)*(next_values)
    old_values = critic(states).gather(dim=-1, index=actions)
    
    critic_loss = mse_loss(targets, old_values)
    critic_optimizer.zero_grad()
    critic_loss.backward()
    critic_optimizer.step()
    
    predicted_actions = actor(states)
    expected_actions = predicted_actions.mean + torch.clamp(predicted_actions.scale, min=-1, max=1)**2
    policy_loss = -expected_actions.mean()
    actor_optimizer.zero_grad()
    policy_loss.backward()
    actor_optimizer.step()
    
    del replay_buffer[:]

def run_episode(render=False):
    state = env.reset()
    total_reward = 0
    steps = 0
    while True:
        if render:
            env.render()
        action = select_action(state)
        new_state, reward, done, _ = env.step(action)
        reward /= 100
        total_reward += reward
        buffer_entry = (
            torch.FloatTensor(state).unsqueeze(0),
            torch.FloatTensor([action]),
            torch.FloatTensor([reward]),
            torch.FloatTensor([done]),
            torch.FloatTensor([new_state]))
        replay_buffer.append(buffer_entry)
        if len(replay_buffer) > batch_size:
            update_parameters()
        if done or steps > 500:
            break
        state = new_state
        steps += 1
    return total_reward
        
if __name__ == "__main__":
    scores = []
    for e in range(epoch):
        score = run_episode(render=True)
        scores.append(score)
        print("Episode {}, Score: {:.3f}, Average Score: {:.3f}".format(
            e+1, score, np.array(scores).mean()))

代码首先导入了必要的库,初始化了游戏环境 Pendulum-v0 ,创建了策略网络 actor 和评价网络 critic, 策略网络和评价网络使用 GPU 加速。创建了优化器和损失函数。

代码定义了一个内存缓冲区 replay_buffer ,用于存放前面的数据,每批数据的大小为 batch_size ,定义了折扣因子 gamma 和目标网络参数更新参数 tau ,设置了迭代次数 epoch 。

定义了 select_action 函数,该函数接受环境状态 state ,根据当前策略网络 actor 生成动作,记录当前动作和新状态以及奖励,以及是否游戏结束的信息。该函数返回动作。

定义了 update_parameters 函数,该函数根据历史数据,批量更新策略网络和评价网络的参数。首先抽样 batch_size 条历史数据,然后计算目标 Q 值,包括 bellman error 和 greedy exploration。接着使用均方误差损失函数计算当前 Q 值和目标 Q 值之间的差异,并反向传播梯度,更新评价网络参数。然后生成动作分布并计算对数似然损失,并反向传播梯度,更新策略网络参数。最后清空缓冲区。

定义了 run_episode 函数,该函数接受渲染标识 render ,每完成一局游戏便渲染一次,接受环境状态 state ,使用策略网络生成动作,执行动作,接收奖励和新状态信息,记录在缓冲区中,判断游戏是否结束,或者游戏步数超过 500 。直至游戏结束或步数超过 500 ,返回奖励。

如果主程序运行,则每轮训练需要执行 run_episode 1 次,并显示当前局游戏的奖励和平均奖励,满 100 个局游戏才显示一次。