AI游戏玩法:深度强化学习实战
1. 强化学习简介
在之前,我们了解了监督学习(如回归和分类)和无监督学习(如GANs、自编码器和生成模型)。而现在,我们将聚焦于强化学习,特别是深度强化学习,即把深度神经网络应用到强化学习中。
强化学习源于行为心理学,通过对正确行为给予奖励、对错误行为进行惩罚来训练智能体。在深度强化学习里,网络接收输入后,会根据输出的正确性得到正或负的奖励。经过多次迭代,网络就能学会输出正确结果。
英国的DeepMind公司是深度强化学习领域的先驱。2013年,他们发表论文,阐述了如何通过向卷积神经网络(CNN)展示屏幕像素并在得分增加时给予奖励,让其学会玩Atari 2600电子游戏。该架构用于学习7种不同的Atari 2600游戏,在其中6种游戏中超越了以往所有方法,在3种游戏中胜过人类专家。
与之前的学习策略不同,强化学习似乎是一种通用的学习算法,可应用于多种环境,甚至可能是通用人工智能的第一步。此后,DeepMind被谷歌收购,该团队一直处于人工智能研究的前沿。2015年,他们在著名的《自然》杂志上发表论文,将同一模型应用于49种不同的游戏。
2. 核心概念
我们将学习强化学习的以下核心概念:
- Q学习
- 探索与利用的平衡
- 经验回放
3. 强化学习与接球游戏
我们的目标是构建一个神经网络来玩接球游戏。游戏开始时,球从屏幕顶部随机位置落下,玩家需使用左右箭头键移动屏幕底部的挡板,在球到达底部时接住它。
这个游戏的状态可以用球和挡板的(x, y)坐标表示。对于大多数街机游戏,通常将当前整个游戏屏幕图像作为状态。
我们可以将这个问题建模为分类问题,网络输入是游戏屏幕图像,输出是三个动作之一:向左移动、静止或向右移动。但这需要为网络提供训练示例,比如专家游戏的记录。更简单的方法是构建一个网络,让它反复玩游戏,并根据是否成功接球给予反馈,这种方法更直观,也更接近人类和动物的学习方式。
最常见的表示此类问题的方法是马尔可夫决策过程(MDP)。游戏是智能体学习的环境,时间步t的环境状态由st表示(包含球和挡板的位置)。智能体可以执行某些动作(如向左或向右移动挡板),这些动作有时会带来奖励rt(正或负)。动作会改变环境,导致新状态st+1,智能体可以在新状态下执行另一个动作at+1,依此类推。状态、动作和奖励的集合,以及状态转换规则构成了马尔可夫决策过程。
4. 最大化未来奖励
作为智能体,我们的目标是最大化每场游戏的总奖励。为了实现这一目标,智能体应尝试最大化游戏中任意时间点t的总奖励。然而,预测未来奖励的值越来越困难,因此我们尝试最大化时间t的总折扣未来奖励。通过将每个未来时间步的奖励乘以折扣因子γ来实现。γ为0时,网络完全不考虑未来奖励;γ为1时,网络完全确定。γ取0.9左右是一个不错的值。
5. Q学习
深度强化学习采用一种无模型的强化学习技术——Q学习。Q学习可用于在有限马尔可夫决策过程中为任何给定状态找到最优动作。Q学习试图最大化Q函数的值,该函数表示在状态s中执行动作a时的最大折扣未来奖励。
一旦我们知道了Q函数,状态s下的最优动作a就是具有最高Q值的动作。我们可以定义一个策略π(s),为任何状态提供最优动作。
Q函数可以通过贝尔曼方程进行近似。我们可以将Q函数看作一个查找表(Q表),其中状态(用s表示)是行,动作(用a表示)是列,元素(用Q(s, a)表示)是在该行给定状态下执行该列给定动作所获得的奖励。在任何状态下,应采取的最佳动作是具有最高奖励的动作。
我们通过以下算法迭代更新Q表:
initialize Q-table Q
observe initial state s
repeat
select and carry out action a
observe reward r and move to new state s'
Q(s, a) = Q(s, a) + α(r + γ max_a' Q(s', a') - Q(s, a))
s = s'
until game over
这里α是学习率,决定了应纳入多少前一个Q值与折扣后的新最大Q值之间的差异。
6. 作为Q函数的深度Q网络
我们的Q函数将是一个神经网络,对于接球游戏,每个状态由四个连续的80x80黑白屏幕图像表示,可能的状态总数非常大。由于卷积神经网络具有局部连接性,能够避免不可能或极不可能的像素组合,并且擅长为图像等结构化数据提取良好特征,因此可以有效地建模Q函数。
DeepMind的论文中使用了三层卷积层和两层全连接层,与传统用于图像分类或识别的CNN不同,这里没有池化层,因为池化层会使网络对图像中特定对象的位置不敏感,而在游戏中,这些信息可能是计算奖励所必需的。
我们的深度Q网络输入形状为(80, 80, 4),输出形状为(3),对应三个可能动作(向左移动、静止、向右移动)的Q值。这是一个回归任务,我们可以通过最小化当前Q(s, a)值与根据奖励和折扣后的未来Q值计算得到的值之间的平方误差来优化网络。
7. 探索与利用的平衡
深度强化学习是在线学习的一个例子,训练和预测步骤相互穿插。在训练的初始阶段,深度Q网络会给出随机预测,导致Q学习性能不佳。为了缓解这个问题,我们可以使用ε - 贪心探索方法。在ε - 贪心探索中,智能体以1 - ε的概率选择网络建议的动作,否则随机选择一个动作。
随着训练轮数的增加,Q函数收敛,开始返回更一致的Q值。此时,可以逐渐减小ε的值,使智能体更多地利用网络返回的值,而不是随机选择动作。在DeepMind的例子中,ε的值从1逐渐减小到0.1,在我们的例子中,从0.1减小到0.001。
8. 经验回放
基于表示状态动作对(st, at)的Q值的方程,我们的策略是训练网络根据当前状态(s, a, r)预测最佳下一个状态s’。但这往往会使网络陷入局部最小值,原因是连续的训练样本往往非常相似。
为了解决这个问题,在游戏过程中,我们将所有先前的移动(s, a, r, s’)收集到一个固定大小的队列(经验回放内存)中。训练网络时,我们从经验回放内存中随机生成批次,而不是使用最新的交易批次。由于批次由无序的随机经验元组(s, a, r, s’)组成,网络可以更好地训练,避免陷入局部最小值。
经验也可以从人类游戏玩法中收集,或者在开始时让网络以观察模式运行一段时间,生成完全随机的动作,提取奖励和下一个状态,并将它们收集到经验回放队列中。
9. 代码实现:Keras深度Q网络玩接球游戏
以下是使用Keras构建深度Q网络玩接球游戏的详细步骤和代码:
9.1 安装Pygame
Pygame是一个用于构建游戏的免费开源库,运行在Python之上,支持多种操作系统。安装方法如下:
- 对于有预构建版本的平台(32位和64位的Linux和Windows,64位的macOS),可以使用
pip install pygame
命令进行安装。
- 如果没有预构建版本,可以从源代码进行构建,具体说明可参考:http://www.pygame.org/wiki/GettingStarted 。
- Anaconda用户可以在conda-forge上找到预构建的Pygame版本:
conda install binstar
conda install anaconda-client
conda install -c https://conda.binstar.org/tlatorre pygame # Linux
conda install -c https://conda.binstar.org/quasiben pygame # Mac
9.2 封装游戏
为了让神经网络能够玩游戏,我们需要对原始游戏进行封装,让网络通过API与游戏进行通信,而不是使用键盘的左右箭头键。以下是封装游戏的代码:
from __future__ import division, print_function
import collections
import numpy as np
import pygame
import random
import os
class MyWrappedGame(object):
def __init__(self):
# run pygame in headless mode
os.environ["SDL_VIDEODRIVER"] = "dummy"
pygame.init()
# set constants
self.COLOR_WHITE = (255, 255, 255)
self.COLOR_BLACK = (0, 0, 0)
self.GAME_WIDTH = 400
self.GAME_HEIGHT = 400
self.BALL_WIDTH = 20
self.BALL_HEIGHT = 20
self.PADDLE_WIDTH = 50
self.PADDLE_HEIGHT = 10
self.GAME_FLOOR = 350
self.GAME_CEILING = 10
self.BALL_VELOCITY = 10
self.PADDLE_VELOCITY = 20
self.FONT_SIZE = 30
self.MAX_TRIES_PER_GAME = 1
self.CUSTOM_EVENT = pygame.USEREVENT + 1
self.font = pygame.font.SysFont("Comic Sans MS", self.FONT_SIZE)
def reset(self):
self.frames = collections.deque(maxlen=4)
self.game_over = False
# initialize positions
self.paddle_x = self.GAME_WIDTH // 2
self.game_score = 0
self.reward = 0
self.ball_x = random.randint(0, self.GAME_WIDTH)
self.ball_y = self.GAME_CEILING
self.num_tries = 0
# set up display, clock, etc
self.screen = pygame.display.set_mode((self.GAME_WIDTH, self.GAME_HEIGHT))
self.clock = pygame.time.Clock()
def step(self, action):
pygame.event.pump()
if action == 0: # move paddle left
self.paddle_x -= self.PADDLE_VELOCITY
if self.paddle_x < 0:
# bounce off the wall, go right
self.paddle_x = self.PADDLE_VELOCITY
elif action == 2: # move paddle right
self.paddle_x += self.PADDLE_VELOCITY
if self.paddle_x > self.GAME_WIDTH - self.PADDLE_WIDTH:
# bounce off the wall, go left
self.paddle_x = self.GAME_WIDTH - self.PADDLE_WIDTH - self.PADDLE_VELOCITY
else: # don't move paddle
pass
self.screen.fill(self.COLOR_BLACK)
score_text = self.font.render("Score: {:d}/{:d}, Ball: {:d}"
.format(self.game_score, self.MAX_TRIES_PER_GAME,
self.num_tries), True, self.COLOR_WHITE)
self.screen.blit(score_text,
((self.GAME_WIDTH - score_text.get_width()) // 2,
(self.GAME_FLOOR + self.FONT_SIZE // 2)))
# update ball position
self.ball_y += self.BALL_VELOCITY
ball = pygame.draw.rect(self.screen, self.COLOR_WHITE,
pygame.Rect(self.ball_x, self.ball_y, self.BALL_WIDTH,
self.BALL_HEIGHT))
# update paddle position
paddle = pygame.draw.rect(self.screen, self.COLOR_WHITE,
pygame.Rect(self.paddle_x, self.GAME_FLOOR,
self.PADDLE_WIDTH, self.PADDLE_HEIGHT))
# check for collision and update reward
self.reward = 0
if self.ball_y >= self.GAME_FLOOR - self.BALL_WIDTH // 2:
if ball.colliderect(paddle):
self.reward = 1
else:
self.reward = -1
self.game_score += self.reward
self.ball_x = random.randint(0, self.GAME_WIDTH)
self.ball_y = self.GAME_CEILING
self.num_tries += 1
pygame.display.flip()
# save last 4 frames
self.frames.append(pygame.surfarray.array2d(self.screen))
if self.num_tries >= self.MAX_TRIES_PER_GAME:
self.game_over = True
self.clock.tick(30)
return np.array(list(self.frames)), self.reward, self.game_over
9.3 训练网络
以下是训练网络的代码:
from __future__ import division, print_function
from keras.models import Sequential
from keras.layers.core import Activation, Dense, Flatten
from keras.layers.convolutional import Conv2D
from keras.optimizers import Adam
from scipy.misc import imresize
import collections
import numpy as np
import os
import wrapped_game
def preprocess_images(images):
if images.shape[0] < 4:
# single image
x_t = images[0]
x_t = imresize(x_t, (80, 80))
x_t = x_t.astype("float")
x_t /= 255.0
s_t = np.stack((x_t, x_t, x_t, x_t), axis=2)
else:
# 4 images
xt_list = []
for i in range(images.shape[0]):
x_t = imresize(images[i], (80, 80))
x_t = x_t.astype("float")
x_t /= 255.0
xt_list.append(x_t)
s_t = np.stack((xt_list[0], xt_list[1], xt_list[2], xt_list[3]),
axis=2)
s_t = np.expand_dims(s_t, axis=0)
return s_t
def get_next_batch(experience, model, num_actions, gamma, batch_size):
batch_indices = np.random.randint(low=0, high=len(experience),
size=batch_size)
batch = [experience[i] for i in batch_indices]
X = np.zeros((batch_size, 80, 80, 4))
Y = np.zeros((batch_size, num_actions))
for i in range(len(batch)):
s_t, a_t, r_t, s_tp1, game_over = batch[i]
X[i] = s_t
Y[i] = model.predict(s_t)[0]
Q_sa = np.max(model.predict(s_tp1)[0])
if game_over:
Y[i, a_t] = r_t
else:
Y[i, a_t] = r_t + gamma * Q_sa
return X, Y
# build the model
model = Sequential()
model.add(Conv2D(32, kernel_size=8, strides=4,
kernel_initializer="normal",
padding="same",
input_shape=(80, 80, 4)))
model.add(Activation("relu"))
model.add(Conv2D(64, kernel_size=4, strides=2,
kernel_initializer="normal",
padding="same"))
model.add(Activation("relu"))
model.add(Conv2D(64, kernel_size=3, strides=1,
kernel_initializer="normal",
padding="same"))
model.add(Activation("relu"))
model.add(Flatten())
model.add(Dense(512, kernel_initializer="normal"))
model.add(Activation("relu"))
model.add(Dense(3, kernel_initializer="normal"))
model.compile(optimizer=Adam(lr=1e-6), loss="mse")
# initialize parameters
DATA_DIR = "../data"
NUM_ACTIONS = 3 # number of valid actions (left, stay, right)
GAMMA = 0.99 # decay rate of past observations
INITIAL_EPSILON = 0.1 # starting value of epsilon
FINAL_EPSILON = 0.0001 # final value of epsilon
MEMORY_SIZE = 50000 # number of previous transitions to remember
NUM_EPOCHS_OBSERVE = 100
NUM_EPOCHS_TRAIN = 2000
BATCH_SIZE = 32
NUM_EPOCHS = NUM_EPOCHS_OBSERVE + NUM_EPOCHS_TRAIN
game = wrapped_game.MyWrappedGame()
experience = collections.deque(maxlen=MEMORY_SIZE)
fout = open(os.path.join(DATA_DIR, "rl-network-results.tsv"), "wb")
num_games, num_wins = 0, 0
epsilon = INITIAL_EPSILON
for e in range(NUM_EPOCHS):
game.reset()
loss = 0.0
# get first state
a_0 = 1 # (0 = left, 1 = stay, 2 = right)
x_t, r_0, game_over = game.step(a_0)
s_t = preprocess_images(x_t)
while not game_over:
s_tm1 = s_t
# next action
if e <= NUM_EPOCHS_OBSERVE:
a_t = np.random.randint(low=0, high=NUM_ACTIONS, size=1)[0]
else:
if np.random.rand() <= epsilon:
a_t = np.random.randint(low=0, high=NUM_ACTIONS, size=1)[0]
else:
q = model.predict(s_t)[0]
a_t = np.argmax(q)
# apply action, get reward
x_t, r_t, game_over = game.step(a_t)
s_t = preprocess_images(x_t)
# if reward, increment num_wins
if r_t == 1:
num_wins += 1
# store experience
experience.append((s_tm1, a_t, r_t, s_t, game_over))
if e > NUM_EPOCHS_OBSERVE:
# finished observing, now start training
# get next batch
X, Y = get_next_batch(experience, model, NUM_ACTIONS, GAMMA, BATCH_SIZE)
loss += model.train_on_batch(X, Y)
# reduce epsilon gradually
if epsilon > FINAL_EPSILON:
epsilon -= (INITIAL_EPSILON - FINAL_EPSILON) / NUM_EPOCHS
print("Epoch {:04d}/{:d} | Loss {:.5f} | Win Count {:d}"
.format(e + 1, NUM_EPOCHS, loss, num_wins))
fout.write("{:04d}\t{:.5f}\t{:d}\n".format(e + 1, loss, num_wins))
if e % 100 == 0:
model.save(os.path.join(DATA_DIR, "rl-network.h5"), overwrite=True)
fout.close()
model.save(os.path.join(DATA_DIR, "rl-network.h5"), overwrite=True)
9.4 评估模型
以下是评估模型的代码:
from __future__ import division, print_function
from keras.models import load_model
from keras.optimizers import Adam
from scipy.misc import imresize
import numpy as np
import os
import wrapped_game
DATA_DIR = "../data"
model = load_model(os.path.join(DATA_DIR, "rl-network.h5"))
model.compile(optimizer=Adam(lr=1e-6), loss="mse")
game = wrapped_game.MyWrappedGame()
num_games, num_wins = 0, 0
for e in range(100):
game.reset()
# get first state
a_0 = 1 # (0 = left, 1 = stay, 2 = right)
x_t, r_0, game_over = game.step(a_0)
s_t = preprocess_images(x_t)
while not game_over:
s_tm1 = s_t
# next action
q = model.predict(s_t)[0]
a_t = np.argmax(q)
# apply action, get reward
x_t, r_t, game_over = game.step(a_t)
s_t = preprocess_images(x_t)
# if reward, increment num_wins
if r_t == 1:
num_wins += 1
num_games += 1
print("Game: {:03d}, Wins: {:03d}".format(num_games, num_wins), end="\r")
print("")
10. 实验结果
我们让模型分别观察100场游戏,然后依次玩1000、2000和5000场游戏进行训练。训练5000场游戏的日志文件最后几行显示,训练后期网络玩游戏的技能相当熟练。损失和获胜次数随训练轮数的变化图也表明,随着训练轮数的增加,损失从0.6降至约0.1,获胜次数曲线上升,网络学习速度加快。
评估模型时,训练1000场游戏的模型在100场测试游戏中获胜42场,训练2000场游戏的模型获胜74场,训练5000场游戏的模型获胜87场,这清楚地表明网络通过训练不断提高。
11. 其他改进方法
自DeepMind提出原始方案以来,还出现了其他改进方法,如:
- 双Q学习:使用两个网络,主网络选择动作,目标网络选择动作的目标Q值,减少单个网络对Q值的高估,使网络训练更快更好。
- 优先经验回放:增加采样具有更高预期学习进度的经验元组的概率。
- 对决网络架构:将Q函数分解为状态和动作组件,并分别组合。
以上就是深度强化学习在接球游戏中的应用,这个简单的例子展示了深度强化学习模型的工作过程,希望能帮助你构建更复杂的实现。例如Ben Lau使用Keras实现的FlappyBird游戏,以及Keras-RL项目(https://github.com/matthiasplappert/keras-rl )中也有很多很好的例子。所有讨论的代码,包括人类玩家可以玩的基础游戏,都可以在相关代码包中找到。
AI游戏玩法:深度强化学习实战
12. 总结与展望
通过上述接球游戏的例子,我们深入了解了深度强化学习的核心概念和实现过程。下面我们对整个过程进行总结,并展望未来可能的发展方向。
12.1 核心要点总结
- 强化学习基础 :强化学习通过奖励和惩罚机制训练智能体,使其在环境中学习最优策略。在接球游戏中,智能体根据球和挡板的位置信息,通过不断尝试不同动作,学习如何接球以获得最大奖励。
- Q学习 :作为深度强化学习的重要技术,Q学习通过贝尔曼方程近似Q函数,帮助智能体找到最优动作。我们使用Q表来存储和更新状态-动作对的Q值,指导智能体决策。
- 深度Q网络 :利用卷积神经网络(CNN)来近似Q函数,充分发挥了CNN在处理图像数据方面的优势。网络的输入是游戏屏幕图像,输出是不同动作的Q值,通过最小化损失函数进行训练。
- 探索与利用平衡 :采用ε - 贪心探索方法,在训练初期让智能体更多地探索状态空间,随着训练的进行逐渐增加利用已有知识的比例,提高学习效率。
- 经验回放 :将智能体的历史经验存储在经验回放内存中,训练时随机采样批次数据,避免连续样本的相关性,使网络更好地学习和泛化。
12.2 未来发展方向
- 更复杂的游戏和环境 :当前的接球游戏相对简单,未来可以将深度强化学习应用于更复杂的游戏,如3D游戏、策略游戏等,以及现实世界中的各种问题,如机器人控制、自动驾驶等。
- 算法改进 :除了前面提到的双Q学习、优先经验回放和对决网络架构等改进方法,还可以探索更多的算法优化策略,提高学习速度和性能。
- 多智能体强化学习 :研究多个智能体在同一环境中相互协作或竞争的学习机制,解决更复杂的多智能体系统问题。
- 结合其他技术 :将深度强化学习与其他人工智能技术,如自然语言处理、计算机视觉等相结合,实现更强大的智能系统。
13. 流程图与表格总结
为了更清晰地展示深度强化学习的流程和关键参数,我们提供以下流程图和表格。
13.1 深度强化学习流程
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px;
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px;
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px;
A([开始]):::startend --> B(初始化游戏环境和网络):::process
B --> C(获取初始状态):::process
C --> D{是否游戏结束}:::decision
D -- 否 --> E(选择动作):::process
E --> F(执行动作,获取奖励和新状态):::process
F --> G(存储经验到回放内存):::process
G --> H{是否开始训练}:::decision
H -- 是 --> I(从回放内存中采样批次数据):::process
I --> J(计算Q值和损失):::process
J --> K(更新网络参数):::process
K --> L(更新ε值):::process
L --> M(更新当前状态):::process
M --> D
D -- 是 --> N(结束游戏,记录结果):::process
N --> O([结束]):::startend
H -- 否 --> M
13.2 关键参数表格
| 参数 | 含义 | 值 |
|---|---|---|
NUM_ACTIONS
| 网络可执行的有效动作数量 | 3(左移、静止、右移) |
GAMMA
| 未来奖励的折扣因子 | 0.99 |
INITIAL_EPSILON
| ε - 贪心探索的初始值 | 0.1 |
FINAL_EPSILON
| ε - 贪心探索的最终值 | 0.0001 |
MEMORY_SIZE
| 经验回放内存的大小 | 50000 |
NUM_EPOCHS_OBSERVE
| 观察阶段的训练轮数 | 100 |
NUM_EPOCHS_TRAIN
| 训练阶段的训练轮数 | 2000 |
BATCH_SIZE
| 训练批次的大小 | 32 |
14. 常见问题解答
在学习和实践深度强化学习的过程中,可能会遇到一些常见问题,下面我们提供一些解答。
14.1 为什么要使用经验回放?
经验回放可以打破连续训练样本之间的相关性,使网络能够更有效地学习。在游戏中,连续的状态和动作往往具有很强的关联性,如果直接使用连续样本进行训练,网络容易陷入局部最优解。通过经验回放,我们可以随机采样不同时间的经验,增加样本的多样性,提高网络的泛化能力。
14.2 ε - 贪心探索的作用是什么?
在训练初期,网络的预测能力较差,随机探索可以让智能体尝试不同的动作,充分探索状态空间,发现更多可能的策略。随着训练的进行,网络的性能逐渐提高,我们逐渐减小ε的值,让智能体更多地利用已学习到的知识,选择具有更高Q值的动作,提高学习效率。
14.3 如何选择合适的折扣因子γ?
折扣因子γ控制了未来奖励的重要性。γ值越接近1,智能体越关注长远的奖励;γ值越接近0,智能体越关注即时奖励。在实际应用中,需要根据具体问题进行调整。对于接球游戏,我们选择γ = 0.99,这意味着智能体在决策时会考虑未来多步的奖励。
15. 实践建议
如果你想进一步探索深度强化学习,可以按照以下步骤进行实践:
- 环境搭建 :确保你已经安装了所需的库,如Keras、Pygame等。可以参考前面提到的安装方法进行安装。
- 代码复现 :将本文中的代码复制到本地环境中运行,观察网络的训练过程和性能表现。尝试调整参数,如训练轮数、折扣因子等,观察对结果的影响。
- 扩展实验 :对现有代码进行扩展,例如改变游戏的规则、增加游戏的复杂度,或者尝试不同的网络架构和优化算法,探索深度强化学习的更多可能性。
- 参考其他实现 :参考其他开源项目和论文,学习更多的深度强化学习技巧和方法,如前面提到的FlappyBird实现和Keras-RL项目。
通过不断实践和探索,你将逐渐掌握深度强化学习的核心技术,为解决更复杂的问题打下坚实的基础。希望本文能够帮助你开启深度强化学习的旅程,在AI游戏玩法领域取得更多的成果。
超级会员免费看
10万+

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



