强化学习:从网格世界到深度Q学习
1. 环境基础介绍
在强化学习中,环境是智能体进行交互和学习的场景。在开始时,我们接触到了一个终止标志(True或False)以及一个包含辅助信息的Python字典。以CartPole环境为例,env对象有一个render()方法,我们可以在每一步(或一系列步骤)后执行该方法,以可视化环境以及杆子和小车随时间的移动情况。当杆子相对于虚拟垂直轴的角度大于12度(任一侧),或者小车的位置距离中心位置超过2.4个单位时,该回合结束。此例中的奖励定义是最大化小车和杆子在有效区域内的稳定时间,即通过最大化回合长度来最大化总奖励。
2. 网格世界环境
在熟悉了CartPole环境后,我们转向一个简单的网格世界环境。这个环境有m行n列,当m = 4且n = 6时,有30种不同的可能状态。其中有4个终止状态:状态16处有一罐金子,状态10、15和22处有陷阱。进入这4个终止状态中的任何一个都会结束回合,但金子状态和陷阱状态有不同的奖励。落在金子状态会获得+1的正奖励,而落在陷阱状态会获得 -1的负奖励,其他所有状态的奖励为0。智能体总是从状态0开始,每次重置环境时,智能体都会回到状态0。动作空间包括四个方向:上、下、左、右。当智能体位于网格的外边界时,选择一个会使其离开网格的动作不会改变状态。
3. 在OpenAI Gym中实现网格世界环境
为了在OpenAI Gym中实验网格世界环境,建议使用脚本编辑器或IDE而不是交互式执行代码。具体步骤如下:
1.
创建脚本
:创建一个名为gridworld_env.py的新Python脚本。
2.
导入必要的包和辅助函数
:
import numpy as np
from gym.envs.toy_text import discrete
from collections import defaultdict
import time
import pickle
import os
from gym.envs.classic_control import rendering
CELL_SIZE = 100
MARGIN = 10
def get_coords(row, col, loc='center'):
xc = (col+1.5) * CELL_SIZE
yc = (row+1.5) * CELL_SIZE
if loc == 'center':
return xc, yc
elif loc == 'interior_corners':
half_size = CELL_SIZE//2 - MARGIN
xl, xr = xc - half_size, xc + half_size
yt, yb = xc - half_size, xc + half_size
return [(xl, yt), (xr, yt), (xr, yb), (xl, yb)]
elif loc == 'interior_triangle':
x1, y1 = xc, yc + CELL_SIZE//3
x2, y2 = xc + CELL_SIZE//3, yc - CELL_SIZE//3
x3, y3 = xc - CELL_SIZE//3, yc - CELL_SIZE//3
return [(x1, y1), (x2, y2), (x3, y3)]
def draw_object(coords_list):
if len(coords_list) == 1: # -> circle
obj = rendering.make_circle(int(0.45*CELL_SIZE))
obj_transform = rendering.Transform()
obj.add_attr(obj_transform)
obj_transform.set_translation(*coords_list[0])
obj.set_color(0.2, 0.2, 0.2) # -> black
elif len(coords_list) == 3: # -> triangle
obj = rendering.FilledPolygon(coords_list)
obj.set_color(0.9, 0.6, 0.2) # -> yellow
elif len(coords_list) > 3: # -> polygon
obj = rendering.FilledPolygon(coords_list)
obj.set_color(0.4, 0.4, 0.8) # -> blue
return obj
get_coords()函数返回用于标注网格世界环境的几何形状的坐标,draw_object()函数根据输入坐标列表的长度决定绘制圆形、三角形或多边形。
- 定义网格世界环境类 :
class GridWorldEnv(discrete.DiscreteEnv):
def __init__(self, num_rows=4, num_cols=6, delay=0.05):
self.num_rows = num_rows
self.num_cols = num_cols
self.delay = delay
move_up = lambda row, col: (max(row-1, 0), col)
move_down = lambda row, col: (min(row+1, num_rows-1), col)
move_left = lambda row, col: (row, max(col-1, 0))
move_right = lambda row, col: (
row, min(col+1, num_cols-1))
self.action_defs={0: move_up, 1: move_right,
2: move_down, 3: move_left}
nS = num_cols*num_rows
nA = len(self.action_defs)
self.grid2state_dict={(s//num_cols, s%num_cols):s
for s in range(nS)}
self.state2grid_dict={s:(s//num_cols, s%num_cols)
for s in range(nS)}
gold_cell = (num_rows//2, num_cols-2)
trap_cells = [((gold_cell[0]+1), gold_cell[1]),
(gold_cell[0], gold_cell[1]-1),
((gold_cell[0]-1), gold_cell[1])]
gold_state = self.grid2state_dict[gold_cell]
trap_states = [self.grid2state_dict[(r, c)]
for (r, c) in trap_cells]
self.terminal_states = [gold_state] + trap_states
print(self.terminal_states)
P = defaultdict(dict)
for s in range(nS):
row, col = self.state2grid_dict[s]
P[s] = defaultdict(list)
for a in range(nA):
action = self.action_defs[a]
next_s = self.grid2state_dict[action(row, col)]
if self.is_terminal(next_s):
r = (1.0 if next_s == self.terminal_states[0]
else -1.0)
else:
r = 0.0
if self.is_terminal(s):
done = True
next_s = s
else:
done = False
P[s][a] = [(1.0, next_s, r, done)]
isd = np.zeros(nS)
isd[0] = 1.0
super(GridWorldEnv, self).__init__(nS, nA, P, isd)
self.viewer = None
self._build_display(gold_cell, trap_cells)
def is_terminal(self, state):
return state in self.terminal_states
def _build_display(self, gold_cell, trap_cells):
screen_width = (self.num_cols+2) * CELL_SIZE
screen_height = (self.num_rows+2) * CELL_SIZE
self.viewer = rendering.Viewer(screen_width,
screen_height)
all_objects = []
bp_list = [
(CELL_SIZE-MARGIN, CELL_SIZE-MARGIN),
(screen_width-CELL_SIZE+MARGIN, CELL_SIZE-MARGIN),
(screen_width-CELL_SIZE+MARGIN,
screen_height-CELL_SIZE+MARGIN),
(CELL_SIZE-MARGIN, screen_height-CELL_SIZE+MARGIN)
]
border = rendering.PolyLine(bp_list, True)
border.set_linewidth(5)
all_objects.append(border)
for col in range(self.num_cols+1):
x1, y1 = (col+1)*CELL_SIZE, CELL_SIZE
x2, y2 = (col+1)*CELL_SIZE,\
(self.num_rows+1)*CELL_SIZE
line = rendering.PolyLine([(x1, y1), (x2, y2)], False)
all_objects.append(line)
for row in range(self.num_rows+1):
x1, y1 = CELL_SIZE, (row+1)*CELL_SIZE
x2, y2 = (self.num_cols+1)*CELL_SIZE,\
(row+1)*CELL_SIZE
line=rendering.PolyLine([(x1, y1), (x2, y2)], False)
all_objects.append(line)
for cell in trap_cells:
trap_coords = get_coords(*cell, loc='center')
all_objects.append(draw_object([trap_coords]))
gold_coords = get_coords(*gold_cell,
loc='interior_triangle')
all_objects.append(draw_object(gold_coords))
if (os.path.exists('robot-coordinates.pkl') and
CELL_SIZE==100):
agent_coords = pickle.load(
open('robot-coordinates.pkl', 'rb'))
starting_coords = get_coords(0, 0, loc='center')
agent_coords += np.array(starting_coords)
else:
agent_coords = get_coords(
0, 0, loc='interior_corners')
agent = draw_object(agent_coords)
self.agent_trans = rendering.Transform()
agent.add_attr(self.agent_trans)
all_objects.append(agent)
for obj in all_objects:
self.viewer.add_geom(obj)
def render(self, mode='human', done=False):
if done:
sleep_time = 1
else:
sleep_time = self.delay
x_coord = self.s % self.num_cols
y_coord = self.s // self.num_cols
x_coord = (x_coord+0) * CELL_SIZE
y_coord = (y_coord+0) * CELL_SIZE
self.agent_trans.set_translation(x_coord, y_coord)
rend = self.viewer.render(
return_rgb_array=(mode=='rgb_array'))
time.sleep(sleep_time)
return rend
def close(self):
if self.viewer:
self.viewer.close()
self.viewer = None
该类继承自OpenAI Gym的DiscreteEnv类,其构造函数定义了动作空间、每个动作的作用以及终止状态。
- 测试实现 :
if __name__ == '__main__':
env = GridWorldEnv(5, 6)
for i in range(1):
s = env.reset()
env.render(mode='human', done=False)
while True:
action = np.random.choice(env.nA)
res = env.step(action)
print('Action ', env.s, action, ' -> ', res)
env.render(mode='human', done=res[2])
if res[2]:
break
env.close()
执行此脚本后,将看到网格世界环境的可视化。
4. 用Q学习解决网格世界问题
在设置好环境后,我们使用当前最流行的强化学习算法——Q学习来解决网格世界问题。
4.1 实现Q学习算法
创建一个名为agent.py的新脚本,定义一个与环境交互的智能体:
from collections import defaultdict
import numpy as np
class Agent(object):
def __init__(
self, env,
learning_rate=0.01,
discount_factor=0.9,
epsilon_greedy=0.9,
epsilon_min=0.1,
epsilon_decay=0.95):
self.env = env
self.lr = learning_rate
self.gamma = discount_factor
self.epsilon = epsilon_greedy
self.epsilon_min = epsilon_min
self.epsilon_decay = epsilon_decay
self.q_table = defaultdict(lambda: np.zeros(self.env.nA))
def choose_action(self, state):
if np.random.uniform() < self.epsilon:
action = np.random.choice(self.env.nA)
else:
q_vals = self.q_table[state]
perm_actions = np.random.permutation(self.env.nA)
q_vals = [q_vals[a] for a in perm_actions]
perm_q_argmax = np.argmax(q_vals)
action = perm_actions[perm_q_argmax]
return action
def _learn(self, transition):
s, a, r, next_s, done = transition
q_val = self.q_table[s][a]
if done:
q_target = r
else:
q_target = r + self.gamma*np.max(self.q_table[next_s])
self.q_table[s][a] += self.lr * (q_target - q_val)
self._adjust_epsilon()
def _adjust_epsilon(self):
if self.epsilon > self.epsilon_min:
self.epsilon *= self.epsilon_decay
init ()构造函数设置了各种超参数,choose_action()方法根据ε - 贪心策略选择动作,_learn()方法实现了Q学习算法的更新规则。
4.2 训练智能体
创建一个名为qlearning.py的新脚本,将所有内容整合在一起并使用Q学习算法训练智能体:
from gridworld_env import GridWorldEnv
from agent import Agent
from collections import namedtuple
import matplotlib.pyplot as plt
import numpy as np
np.random.seed(1)
Transition = namedtuple(
'Transition', ('state', 'action', 'reward',
'next_state', 'done'))
def run_qlearning(agent, env, num_episodes=50):
history = []
for episode in range(num_episodes):
state = env.reset()
env.render(mode='human')
final_reward, n_moves = 0.0, 0
while True:
action = agent.choose_action(state)
next_s, reward, done, _ = env.step(action)
agent._learn(Transition(state, action, reward,
next_s, done))
env.render(mode='human', done=done)
state = next_s
n_moves += 1
if done:
break
final_reward = reward
history.append((n_moves, final_reward))
print('Episode %d: Reward %.1f #Moves %d'
% (episode, final_reward, n_moves))
return history
def plot_learning_history(history):
fig = plt.figure(1, figsize=(14, 10))
ax = fig.add_subplot(2, 1, 1)
episodes = np.arange(len(history))
moves = np.array([h[0] for h in history])
plt.plot(episodes, moves, lw=4,
marker='o', markersize=10)
ax.tick_params(axis='both', which='major', labelsize=15)
plt.xlabel('Episodes', size=20)
plt.ylabel('# moves', size=20)
ax = fig.add_subplot(2, 1, 2)
rewards = np.array([h[1] for h in history])
plt.step(episodes, rewards, lw=4)
ax.tick_params(axis='both', which='major', labelsize=15)
plt.xlabel('Episodes', size=20)
plt.ylabel('Final rewards', size=20)
plt.savefig('q-learning-history.png', dpi=300)
plt.show()
if __name__ == '__main__':
env = GridWorldEnv(num_rows=5, num_cols=6)
agent = Agent(env)
history = run_qlearning(agent, env)
env.close()
plot_learning_history(history)
执行此脚本将运行50个回合的Q学习程序,智能体的行为将被可视化。开始时,智能体大多会陷入陷阱状态,但随着时间推移,它会从失败中学习,最终找到金子状态。
5. 深度Q学习简介
之前的Q学习代码适用于离散状态空间较小(如30个状态)的网格世界示例,可将Q值存储在Python字典中。但在实际应用中,状态数量可能非常大,甚至几乎无限大,也可能是连续状态空间,且有些状态在训练期间可能从未被访问过,这会给智能体处理未见过的状态带来问题。
为解决这些问题,我们采用函数逼近方法,定义一个参数化函数$v_W(x_s)$来近似真实值函数,即$v_W(x_s) \approx v_{\pi}(s)$,其中$x_s$是一组输入特征(或“特征化”状态)。当逼近函数$q_W(x_s, a)$是深度神经网络(DNN)时,得到的模型称为深度Q网络(DQN)。训练DQN模型时,根据Q学习算法更新权重。总体而言,深度Q学习算法的主要方法与表格Q学习方法非常相似,主要区别在于使用多层神经网络来计算动作值。
mermaid流程图
graph TD;
A[开始] --> B[初始化环境和智能体];
B --> C[开始回合];
C --> D[重置环境状态];
D --> E[选择动作];
E --> F[执行动作并获取反馈];
F --> G[更新Q表];
G --> H{回合是否结束};
H -- 否 --> E;
H -- 是 --> I{是否完成所有回合};
I -- 否 --> C;
I -- 是 --> J[结束];
表格:环境状态与奖励
| 状态 | 奖励 | 说明 |
|---|---|---|
| 金子状态(如状态16) | +1 | 智能体到达该状态获得正奖励 |
| 陷阱状态(如状态10、15、22) | -1 | 智能体到达该状态获得负奖励 |
| 其他状态 | 0 | 无奖励 |
列表总结
- 环境基础:介绍了终止标志、辅助信息字典以及CartPole环境的回合结束条件和奖励定义。
- 网格世界环境:有30种状态,4个终止状态,智能体从状态0开始,动作空间为四个方向。
- 实现步骤:创建脚本、导入包和辅助函数、定义环境类、测试实现。
- Q学习:包括智能体的实现和训练,使用Q表更新动作值。
- 深度Q学习:针对状态空间大等问题,采用函数逼近和DNN。
强化学习:从网格世界到深度Q学习(下半部分)
6. 深度Q学习的具体优势与挑战
深度Q学习相较于传统的Q学习,有着显著的优势,但也面临一些挑战。
6.1 优势
- 处理大规模状态空间 :在传统Q学习中,当状态空间非常大时,存储Q表会变得极为困难,甚至不可行。而深度Q网络(DQN)通过使用深度神经网络作为函数逼近器,可以有效地处理大规模甚至连续的状态空间。例如,在复杂的游戏环境或机器人控制任务中,状态数量可能是天文数字,DQN能够通过学习状态特征之间的关系,对未见过的状态进行合理的泛化。
- 自动特征提取 :DNN具有强大的自动特征提取能力。在处理高维输入时,如图像、音频等,DQN可以自动从原始数据中提取出有意义的特征,而无需手动设计复杂的特征工程。这使得DQN在处理复杂感知任务时更加高效和灵活。
6.2 挑战
- 训练不稳定 :深度神经网络的训练过程本身就具有一定的不稳定性,而在DQN中,由于使用了经验回放和目标网络等技术,训练过程可能会更加复杂。例如,经验回放中的样本相关性和目标网络的更新频率等因素,都可能导致训练过程出现波动,甚至发散。
- 计算资源需求大 :训练深度神经网络需要大量的计算资源,包括高性能的GPU和大量的内存。对于一些资源受限的场景,如嵌入式设备或移动平台,实现和训练DQN可能会面临困难。
7. 深度Q学习的实现思路
为了实现深度Q学习,我们需要对之前的Q学习代码进行扩展,引入深度神经网络。以下是一个简化的实现思路:
7.1 定义深度Q网络模型
import torch
import torch.nn as nn
class DQN(nn.Module):
def __init__(self, input_dim, output_dim):
super(DQN, self).__init__()
self.fc1 = nn.Linear(input_dim, 64)
self.fc2 = nn.Linear(64, 64)
self.fc3 = nn.Linear(64, output_dim)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = torch.relu(self.fc2(x))
return self.fc3(x)
这里定义了一个简单的三层全连接神经网络作为DQN模型。输入维度为状态空间的维度,输出维度为动作空间的维度。
7.2 经验回放机制
为了减少样本之间的相关性,提高训练的稳定性,我们引入经验回放机制。
import random
from collections import deque
class ReplayBuffer:
def __init__(self, capacity):
self.buffer = deque(maxlen=capacity)
def add(self, state, action, reward, next_state, done):
self.buffer.append((state, action, reward, next_state, done))
def sample(self, batch_size):
batch = random.sample(self.buffer, batch_size)
states, actions, rewards, next_states, dones = zip(*batch)
return states, actions, rewards, next_states, dones
def __len__(self):
return len(self.buffer)
经验回放缓冲区用于存储智能体的经验,每次训练时从缓冲区中随机采样一批经验进行学习。
7.3 训练深度Q网络
import torch.optim as optim
def train_dqn(agent, env, num_episodes=1000, batch_size=32, gamma=0.99):
replay_buffer = ReplayBuffer(capacity=10000)
optimizer = optim.Adam(agent.parameters(), lr=0.001)
criterion = nn.MSELoss()
for episode in range(num_episodes):
state = env.reset()
done = False
while not done:
action = agent.choose_action(state)
next_state, reward, done, _ = env.step(action)
replay_buffer.add(state, action, reward, next_state, done)
state = next_state
if len(replay_buffer) > batch_size:
states, actions, rewards, next_states, dones = replay_buffer.sample(batch_size)
states = torch.FloatTensor(states)
actions = torch.LongTensor(actions)
rewards = torch.FloatTensor(rewards)
next_states = torch.FloatTensor(next_states)
dones = torch.FloatTensor(dones)
q_values = agent(states).gather(1, actions.unsqueeze(1)).squeeze(1)
next_q_values = agent(next_states).max(1)[0].detach()
target_q_values = rewards + gamma * next_q_values * (1 - dones)
loss = criterion(q_values, target_q_values)
optimizer.zero_grad()
loss.backward()
optimizer.step()
print(f'Episode {episode}: Reward {reward}')
在训练过程中,智能体与环境进行交互,将经验存储在经验回放缓冲区中。当缓冲区中的经验数量足够时,随机采样一批经验进行训练,更新DQN的参数。
8. 总结与展望
通过前面的介绍,我们从简单的网格世界环境入手,逐步了解了Q学习算法,并进一步探讨了深度Q学习。Q学习适用于离散状态空间较小的场景,通过维护Q表来学习最优策略;而深度Q学习则通过深度神经网络解决了大规模状态空间和连续状态空间的问题。
在未来的研究和应用中,强化学习有望在更多领域发挥重要作用。例如,在自动驾驶领域,深度强化学习可以帮助车辆学习最优的驾驶策略;在金融领域,可用于优化投资组合。然而,强化学习仍然面临许多挑战,如样本效率低、可解释性差等。未来的研究方向可能包括提高样本效率、增强模型的可解释性以及探索更有效的函数逼近方法等。
mermaid流程图:深度Q学习训练流程
graph TD;
A[开始] --> B[初始化DQN模型和经验回放缓冲区];
B --> C[开始回合];
C --> D[重置环境状态];
D --> E[选择动作];
E --> F[执行动作并获取反馈];
F --> G[将经验存入回放缓冲区];
G --> H{回放缓冲区是否足够};
H -- 否 --> E;
H -- 是 --> I[从缓冲区采样经验];
I --> J[计算Q值和目标Q值];
J --> K[计算损失并更新模型参数];
K --> L{回合是否结束};
L -- 否 --> E;
L -- 是 --> M{是否完成所有回合};
M -- 否 --> C;
M -- 是 --> N[结束];
表格:Q学习与深度Q学习对比
| 方法 | 适用状态空间 | 存储方式 | 处理未见过状态能力 | 计算资源需求 |
|---|---|---|---|---|
| Q学习 | 离散、小规模 | Q表(字典) | 有限 | 低 |
| 深度Q学习 | 大规模、连续 | 深度神经网络 | 强 | 高 |
列表总结
- 深度Q学习优势:处理大规模状态空间、自动特征提取。
- 深度Q学习挑战:训练不稳定、计算资源需求大。
- 实现思路:定义DQN模型、引入经验回放机制、训练模型。
- 未来展望:在多领域有应用前景,但面临样本效率低、可解释性差等挑战。
超级会员免费看
2830

被折叠的 条评论
为什么被折叠?



