前几天买的新书《深度强化学习落地指南》,今天收到了,迫不及待的阅读起来。作者将深度强化学习应用落地分为七步,1、需求分析;2、动作空间设计;3、状态空间设计;4、汇报函数设计;5、算法选择;6、训练调试;7、性能冲刺。读罢,深受启发,总想找个例子实践一下。
思来想去,记得小时候在掌机上玩过一个打乒乓球的小游戏:从上面以一个角度落下来一个小球,遇到屏幕边界后会反弹,底部有一个由方格组成的球拍,玩家移动球拍,接住球得分,接不住则游戏结束。于是,我就想,能不能设计一个神经网络,通过训练使这个智能体学会玩这个游戏?查了一下,网上的素材,尤其是算法方面的素材非常多,典型的如:
Deep Reinforcement Learning超简单入门项目 Pytorch实现接水果游戏AI
实现起来难度不大,于是决定按照《深度强化学习落地指南》这本书的指导,一步一步的实践利用pytorch实现深度强化学习的过程。
一问是不是:打乒乓的小游戏,是一个典型的单智能体和环境交互的强化学习问题。乒乓球的状态随时间按一定规律运动,玩家每个时间间隔可以做出一个动作,每个动作有左、右两个选择。二问值不值:这个一个运算量要求不大的小例子,以学习和实践为主,主要是体验强化学习、深度学习落地的过程。三问能不能:场景固定、数据廉价。四问边界在哪里:这样一个简单的问题,不存在模块划分的问题,主要决策也只有控制动作这一个问题。
动作空间在事实上决定了任何算法所能达到的性能上限:对于打乒乓这个小游戏而言,动作就两个,移动底下的球拍,朝左运动或者朝右运动。这是一个离散的取值,分别定义为0和1。
状态信息代表了Agent所感知的环境信息及其动态变化:对于打乒乓这个小游戏而言,完整的状态信息就是以像素为代表的整个方格。在这里,我们采用留空式空间编码,可以参考用10*8的一个矩阵来表示,前面9层代表球落的空间,最后一层用3个连续的1代表球拍的位置。如下图所示:
回报信号是人与算法沟通的桥梁:对于打乒乓这个小游戏,回报就是要让底下的三个球拍接住落下来的小球,接住得1分,接不住扣1分。
读到这里,我们就可以开始动手编程了。结合前面写的面向对象编程,我们可以设计一个PingpongEnv类,在这个类里面,我们要首先定义reset方法,用来初始化状态空间和球拍的位置。接着,定义个step方法,用来定义当输入一个动作之后,对状态空间的变化。然后,通过判断球落地还是落到拍子上,确定本次移动的回报值。
class PingpongEnv:
def __init__(self):
self.reset()
def reset(self):
game = np.zeros((10,8))
game[9,3:6] = 1.
self.state = game
self.t = 0
self.done = False
return self.state[np.newaxis,:].copy()
def step(self, action):
reward = 0.
game = self.state
if self.done:
print('Call step after env is done')
if self.t==200:
self.done = True
return game[np.newaxis,:].copy(),10,self.done
# 根据action移动盘子
if action == 0 and game[9][0]!=1:
game[9][0:7] = game[9][1:8].copy()#向左移动
game[9][7] = 0
elif action == 1 and game[9][7]!=1:
game[9][1:8] = game[9][0:7].copy()#向右移动
game[9][0] = 0
# 判断球落地还是落到拍子上
if 1 in game[8]:
ball = np.where(game[8]==1)
if game[9][ball] != 1:
reward = -1.
self.done = True
else:
reward = 1.
game[8][ball] = 0.
#game[1:9] = game[0:8].copy()
game[0:9] = 0
if self.t%9==0:
self.idx = random.randint(a = 0, b = 7)#随机生成位置
self.idirect = random.randint(0,1) #随机生成方位
game[0][self.idx] = 1.
else:
if self.idirect == 0 and self.idx != 0: #方位向左,非最左边
self.idx -= 1
elif self.idirect == 0 and self.idx == 0: #方位向左,最左边
self.idx += 1
self.idirect = 1 #之后的方向变为右边
elif self.idirect == 1 and self.idx != 7: #方位向右,非最右边
self.idx += 1
elif self.idirect == 1 and self.idx == 7: #方位向右,最右边
self.idx -= 1
self.idirect = 0
game[self.t%9][self.idx] =1.
self.t += 1
return game[np.newaxis,:].copy(),reward,self.done
明确任务需求并初步完成问题定义之后,就可以为相关任务选择合适的深度强化学习算法了。:对于算法落地而言,还需要根据任务自身的特点从DRL本源出发进行由浅入深、粗中有细的筛选和迭代。
在本案例中,直接选用DQN算法即可,具体代码如下,其主要包括两个类,一个DQN,定义网络的层数和形状;一个DQNAgent,定义学习和采样方法。
class DQN(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(1, 8, 3, padding=1)
self.conv2 = nn.Conv2d(8, 16, 3, padding=1)
# 16*10*8
self.maxpool = nn.MaxPool2d(2,2)
# 16*5*4
self.fc = nn.Sequential(
nn.Linear(16*5*4, 32),
nn.ReLU(),
nn.Linear(32, 3),
)
def forward(self, x):
x = F.relu(self.conv1(x))
x = F.relu(self.conv2(x))
x = self.maxpool(x)
x = x.view(x.size(0), -1)
x = self.fc(x)
return x
class DQNAgent():
def __init__(self, network, eps, gamma, lr):
self.network = network
self.eps = eps
self.gamma = gamma
self.optimizer = optim.Adam(self.network.parameters(), lr=lr)
def learn(self, batch):
s0, a0, r1, s1, done = zip(*batch)
n = len(s0)
s0 = torch.FloatTensor(s0)
s1 = torch.FloatTensor(s1)
r1 = torch.FloatTensor(r1)
a0 = torch.LongTensor(a0)
done = torch.BoolTensor(done)
increment = self.gamma * torch.max(self.network(s1).detach(), dim=1)[0]
y_true = r1+increment
y_pred = self.network(s0)[range(n),a0]
loss = F.mse_loss(y_pred, y_true)
self.optimizer.zero_grad()
loss.backward()
self.optimizer.step()
return loss.item()
def sample(self, state):
'''
epsilon探索选择下一个action
'''
state = state[np.newaxis,:]
action_value = self.network(torch.FloatTensor(state))
if random.random()<self.eps:
return random.randint(0, 2)
else:
max_action = torch.argmax(action_value,dim=1)
return max_action.item()
在这之后,还需要定义个评估函数,用来评判训练效果:
def evaluate(agent,env,times):
eval_reward = []
for i in range(times):
obs = env.reset()
episode_reward = 0
while True:
action = agent.sample(obs)#选取最优动作
obs,reward,isOver = env.step(action)
episode_reward += reward
if isOver:
break
eval_reward.append(episode_reward)
return np.mean(eval_reward)
具体说来,就是设置不同的训练参数,尝试是否能够收敛,在DRL落地实践中,这种第一训练“心里没底”的忐忑体验无论对入门小白还是学术明星都是公平的。我们这个算例倒是不存在这个问题,直接进行训练即可。
if __name__ == "__main__":
gamma = 0.9
eps_high = 0.9
eps_low = 0.1
num_episodes = 500
LR = 0.001
batch_size = 5
decay = 200
net = DQN()
agent = DQNAgent(net,1,gamma,LR)
replay_lab = deque(maxlen=5000)
env = PingpongEnv()
state = env.reset()
for episode in range(num_episodes):
agent.eps = eps_low + (eps_high-eps_low) *\
(np.exp(-1.0 * episode/decay))
s0 = env.reset()
while True:
a0 = agent.sample(s0)
s1, r1, done = env.step(a0)
replay_lab.append((s0.copy(),a0,r1,s1.copy(),done))
if done:
break
s0 = s1
if replay_lab.__len__() >= batch_size:
batch = random.sample(replay_lab,k=batch_size)
loss = agent.learn(batch)
if (episode+1)%50==0:
print("Episode: %d, loss: %.3f"%(episode+1,loss))
score = evaluate(agent,env,10)
print("Score: %.1f"%(score))
训练完成后,利用以下代码,对训练的Agent运行效果进行观察:
import matplotlib.pyplot as plt
import torch
from matplotlib.colors import ListedColormap
from train import PingpongEnv,DQNAgent,DQN
cmap_light = ListedColormap(['white','red'])
from IPython import display
agent.eps = 0
env = PingpongEnv()
s = env.reset()
while True:
a = agent.sample(s)
s, r, done = env.step(a)
if done:
break
img = s.squeeze()
plt.imshow(img, cmap=cmap_light)
plt.show()
display.clear_output(wait=True)
通过这个打乒乓小游戏,结合深度强化学习落地指南,算是对如何将强化学习算法用到一个具体的事务上有了一个初步的认识。感觉pytorch确实是一个非常容易上手的神经网络建模工具。随着后面对《深度学习》认识的深入,将逐步细化理论方面的细节内容。