强化学习十分依赖奖励函数的设置,有时奖励函数上的一些微小改动都会使得训练出的策略天差地别。然而在很多现实场景中,奖励函数并未给定,或者奖励信号极其稀疏,那么此时随机设计奖励函数将无法保证训练出来的策略满足实际需要。比如无人驾驶,如果只规定正常行驶而不发生碰撞的奖励为+1,发生碰撞的奖励为-100,那么智能体学习的结果很可能是找个地方停滞不前。具体能帮助无人驾驶小车规控的奖励函数往往需要经过专家的精心设计和调试。
假设存在一个专家智能体,其策略可以看成最优策略,我们就可以直接模仿这个专家在环境中交互的状态动作数据来训练一个策略,并且不需要用到环境提供的奖励信号。模仿学习研究的便是这一类问题,专家能够提供一系列状态动作对,表示专家在s(t)环境下做出了a(t)的动作,而模仿者的任务是利用这些专家数据进行训练,无需奖励信号就可以达到一个接近专家的策略。
目前学术界模仿学习的方法基本上可以分为 3 类:
- 行为克隆(behavior cloning,BC)
- 逆强化学习(inverse RL)
- 生成式对抗模仿学习(generative adversarial imitation learning,GAIL)
在本章将主要介绍行为克隆方法和生成式对抗模仿学习方法。尽管逆强化学习有良好的学术贡献,但由于其计算复杂度较高,实际应用的价值较小。
行为克隆就是直接使用监督学习方法,将专家数据中(s(t),a(t))的s(t)看作样本输入,a(t)看作标签,学习的目标为:
其中,B是专家的数据集,L是对应监督学习框架下的损失函数。若动作是离散的,该损失函数可以是最大似然估计得到的。若动作是连续的,该损失函数可以是均方误差函数
在训练数据量比较大的时候,BC 能够很快地学习到一个不错的策略。例如,围棋人工智能 AlphaGo 就是首先在 16 万盘棋局的 3000 万次落子数据中学习人类选手是如何下棋的,仅仅凭这个行为克隆方法,AlphaGo 的棋力就已经超过了很多业余围棋爱好者。由于 BC 的实现十分简单,因此在很多实际场景下它都可以作为策略预训练的方法。BC 能使得策略无须在较差时仍然低效地通过和环境交互来探索较好的动作,而是通过模仿专家智能体的行为数据来快速达到较高水平,为接下来的强化学习创造一个高起点。
BC 也存在很大的局限性,该局限在数据量比较小的时候犹为明显。具体来说,由于通过 BC 学习得到的策略只是拿小部分专家数据进行训练,因此 BC 只能在专家数据的状态分布下预测得比较准。然而,强化学习面对的是一个序贯决策问题,通过 BC 学习得到的策略在和环境交互过程中不可能完全学成最优,只要存在一点偏差,就有可能导致下一个遇到的状态是在专家数据中没有见过的。此时,由于没有在此状态(或者比较相近的状态)下训练过,策略可能就会随机选择一个动作,这会导致下一个状态进一步偏离专家策略遇到的的数据分布。最终,该策略在真实环境下不能得到比较好的效果,这被称为行为克隆的复合误差(compounding error)问题。
生成对抗模仿学习(GAIL)实质上是模仿了专家策略的占用度量,为了达成这个目标,策略和环境需要进行交互,收集下一个状态的信息并进一步做出动作,因为有如下定理,给定一合法占用度量ρ,可生成该占用度量的唯一策略是:
,因此需要和环境进行交互。这一点和BC不同,BC完全不需要和环境交互。GAIL算法中有一个判别器和一个策略,策略π相当于生成对抗网络中的而生成器,给定一个状态,策略会输出这个状态下采取的动作,而判别器D将状态动作对(s,a)作为输入,输出一个0到1的实数,表示判别器认为该状态动作对(s,a)是来智能体策略而非专家的概率。判别器D的目标是尽量将专家数据的输出靠近0,将模仿者策略的输出靠近1,这样就可以尽量将两组数据分辨开来。于是,判别器D的损失函数为:
而模仿者策略的目标就是要使得其交互产生的轨迹尽可能地被判别器误认为是专家轨迹,也就是说判别器的输出越靠近0越好,因此,我们可以将奖励函数设置为r(s,a)=-logD(s,a),因为我们需要最大化奖励,而当判别器的输出趋向于0时,logD(s,a)趋向于负无穷,因此要加负号构造奖励函数,事实上,我们应该也可以利用神经网络,将损失函数设置为logD(s,a),通过最小化损失函数,从而使得判别器的输出靠近0。
下面进行实践,首先,我们需要一定量的专家数据,为此,我们预先通过PPO算法训练出一个表现良好的专家模型,再利用专家模型生成专家数据。本次代码实践的环境是车杆环境。
导入库
import gym
import torch
import torch.nn.functional as F
import torch.nn as nn
import numpy as np
import matplotlib.pyplot as plt
from tqdm import tqdm
import random
import rl_utils
定义PPO算法的策略网络和价值网络
class PolicyNet(torch.nn.Module):
def __init__(self,state_dim,hidden_dim,action_dim):
super(PolicyNet,self).__init__()
self.fc1=torch.nn.Linear(state_dim,hidden_dim)
self.fc2=torch.nn.Linear(hidden_dim,action_dim)
def forward(self,x):
x=F.relu(self.fc1(x))
return F.softmax(self.fc2(x),dim=1)
class ValueNet(torch.nn.Module):
def __init__(self,state_dim,hidden_dim):
super(ValueNet,self).__init__()
self.fc1=torch.nn.Linear(state_dim,hidden_dim)
self.fc2=torch.nn.Linear(hidden_dim,1)
def forward(self,x):
x=F.relu(self.fc1(x))
return self.fc2(x)
定义PPO算法
class PPO:
"""采用PPO截断,预先训练出一个表现良好的专家模型,再利用专家模型生成专家数据"""
def __init__(self,state_dim,hidden_dim,action_dim,actor_lr,critic_lr,lmbda,epochs,eps,gamma,device):
self.actor=PolicyNet(state_dim,hidden_dim,action_dim).to(device)
self.critic=ValueNet(state_dim,hidden_dim).to(device)
self.actor_optimizer=torch.optim.Adam(self.actor.parameters(),lr=actor_lr)
self.critic_optimizer=torch.optim.Adam(self.critic.parameters(),lr=critic_lr)
self.gamma=gamma
self.lmbda=lmbda
self.epochs=epochs #一条序列的数据用于训练轮数
self.eps=eps #PPO阶段中截断范围的参数
self.device=device
def take_action(self,state):
state=torch.tensor([state],dtype=torch.float).to(self.device)
probs=self.actor(state)
action_dist=torch.distributions.Categorical(probs)
action=action_dist.sample()
return action.item()
def update(self,transition_dict):
states = torch.tensor(transition_dict['states'],
dtype=torch.float).to(self.device)
actions = torch.tensor(transition_dict['actions']).view(-1, 1).to(
self.device)
rewards = torch.tensor(transition_dict['rewards'],
dtype=torch.float).view(-1, 1).to(self.device)
next_states = torch.tensor(transition_dict['next_states'],
dtype=torch.float).to(self.device)
dones = torch.tensor(transition_dict['dones'],
dtype=torch.float).view(-1, 1).to(self.device)
td_target = rewards + self.gamma * self.critic(next_states) * (1-dones)
td_delta = td_target - self.critic(states)
advantage = rl_utils.compute_advantage(self.gamma,self.lmbda,td_delta.cpu()).to(self.device)
old_log_probs = torch.log(self.actor(states).gather(1,actions)).detach()
for _ in range(self.epochs):
log_probs = torch.log(self.actor(states).gather(1,actions))
ratio = torch.exp(log_probs - old_log_probs)
surr1 = ratio * advantage
surr2 = torch.clamp(ratio,1-self.eps,1+self.eps) * advantage #截断
actor_loss = torch.mean(-torch.min(surr1,surr2)) #PPO损失函数
critic_loss = torch.mean(F.mse_loss(self.critic(states),td_target.detach()))
self.actor_optimizer.zero_grad()
self.critic_optimizer.zero_grad()
actor_loss.backward()
critic_loss.backward()
self.actor_optimizer.step()
self.critic_optimizer.step()
设置超参数,进行训练
actor_lr = 1e-3
critic_lr = 1e-2
num_episodes = 250
hidden_dim = 128
gamma = 0.98
lmbda = 0.95
epochs = 10
eps = 0.2
device = torch.device("cuda") if torch.cuda.is_available() else torch.device(
"cpu")
env_name = 'CartPole-v0'
env = gym.make(env_name)
env.reset(seed=0)
torch.manual_seed(0)
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n
ppo_agent = PPO(state_dim, hidden_dim, action_dim, actor_lr, critic_lr, lmbda,
epochs, eps, gamma, device)
return_list = rl_utils.train_on_policy_agent(env, ppo_agent, num_episodes)
接下来生成专家数据。因为车杆环境比较简单,我们只生成一条轨迹,并从中采样30个状态动作对样本(s,a),利用这30个专家数据样本来训练模仿策略。
#生成专家数据
def sample_expert_data(n_episode):
states = []
actions = []
for episode in range(n_episode):
state = env.reset()
done = False
while not done:
action = ppo_agent.take_action(state)
states.append(state)
actions.append(action)
next_state, reward, done, _ = env.step(action)
state = next_state
return np.array(states), np.array(actions)
#采样专家数据
env.reset(seed=0)
torch.manual_seed(0)
random.seed(0)
n_episode = 1
expert_s, expert_a = sample_expert_data(n_episode)
n_samples = 30 # 采样30个数据
random_index = random.sample(range(expert_s.shape[0]), n_samples)
expert_s = expert_s[random_index]
expert_a = expert_a[random_index]
定义行为克隆
class BehaviorClone:
def __init__(self, state_dim, hidden_dim, action_dim, lr):
self.policy = PolicyNet(state_dim, hidden_dim, action_dim).to(device)
self.optimizer = torch.optim.Adam(self.policy.parameters(), lr=lr)
def learn(self, states, actions):
states = torch.tensor(states, dtype=torch.float).to(device)
#gather()函数的index索引应为int64,而后面gather的索引是actions,所以应把actions转为torch.int64
#书中代码好像并没有如此做,可能是版本问题?
actions = torch.tensor(actions,dtype=torch.int64).view(-1, 1).to(device)
log_probs = torch.log(self.policy(states).gather(1, actions))
#模仿学习,状态动作对数据来自专家数据,应该使状态动作对的概率分布尽可能趋向于1,即最大化log_probs,所以加负号
bc_loss = torch.mean(-log_probs) # 最大似然估计
self.optimizer.zero_grad()
bc_loss.backward()
self.optimizer.step()
def take_action(self, state):
state = torch.tensor([state], dtype=torch.float).to(device)
probs = self.policy(state)
action_dist = torch.distributions.Categorical(probs)
action = action_dist.sample()
return action.item()
训练函数
def test_agent(agent, env, n_episode):
return_list = []
for episode in range(n_episode):
episode_return = 0
state = env.reset()
done = False
while not done:
action = agent.take_action(state)
next_state, reward, done, _ = env.step(action)
state = next_state
episode_return += reward
return_list.append(episode_return)
return np.mean(return_list)
设置超参数
env.reset(seed=0)
torch.manual_seed(0)
np.random.seed(0)
lr = 1e-3
bc_agent = BehaviorClone(state_dim, hidden_dim, action_dim, lr)
n_iterations = 1000
batch_size = 64
test_returns = []
with tqdm(total=n_iterations, desc="进度条") as pbar:
for i in range(n_iterations):
sample_indices = np.random.randint(low=0,
high=expert_s.shape[0],
size=batch_size)
bc_agent.learn(expert_s[sample_indices], expert_a[sample_indices])
current_return = test_agent(bc_agent, env, 5)
test_returns.append(current_return)
if (i + 1) % 10 == 0:
pbar.set_postfix({'return': '%.3f' % np.mean(test_returns[-10:])})
pbar.update(1)
绘图
iteration_list = list(range(len(test_returns)))
plt.plot(iteration_list, test_returns)
plt.xlabel('Iterations')
plt.ylabel('Returns')
plt.title('BC on {}'.format(env_name))
plt.show()
接下来我们实现GAIL的代码
首先实现判别器模型,模型输入为一个状态动作对,输出一个概率标量。
class Discriminator(nn.Module):
"""判别器模型"""
def __init__(self,state_dim,hidden_dim,action_dim):
super(Discriminator,self).__init__()
self.fc1 = torch.nn.Linear(state_dim + action_dim, hidden_dim)
self.fc2 = torch.nn.Linear(hidden_dim, 1)
#sigmoid函数经常用于二分类问题,将输入映射到一个0到1的概率值,表示某个样本属于某个类别的概率。在代码中,判别器的任务是判别给定的状态和动作是否来自真实数据分布,因此是一个二分类问题。
#而softmax函数则常用于多分类问题,将输入转化为多个类别的概率分布。对于多分类任务,softmax函数通常适用于输出层,将模型的输出转化为每个类别的概率。
#两个函数值域均为0到1,但softmax还能保证所有值加和为1
def forward(self,x,a):
cat = torch.cat([x,a],dim=1)
x = F.relu(self.fc1(cat))
return torch.sigmoid(self.fc2(x))
定义生成对抗模仿学习算法
class GAIL:
def __init__(self,agent,state_dim,action_dim,hidden_dim,lr_d):
self.discriminator = Discriminator(state_dim,hidden_dim,action_dim).to(device)
self.discriminator_optimizer = torch.optim.Adam(self.discriminator.parameters(),lr=lr_d)
self.agent = agent
def learn(self,expert_s,expert_a,agent_s,agent_a,next_s,dones):
expert_states = torch.tensor(expert_s,dtype=torch.float).to(device)
#actions应转化为int64,否则报错了,书中代码好像并没有如此做,可能是版本问题?
expert_actions = torch.tensor(expert_a,dtype=torch.int64).to(device)
agent_states = torch.tensor(agent_s,dtype=torch.float).to(device)
#这里也是如此
agent_actions = torch.tensor(agent_a,dtype=torch.int64).to(device)
#转化为独热向量,独热向量常用于多类别分类,逻辑回归,神经网络输入,聚类分析中
expert_actions = F.one_hot(expert_actions,num_classes=2).float()
agent_actions = F.one_hot(agent_actions,num_classes=2).float()
#样本属于专家类别的概率
expert_prob = self.discriminator(expert_states,expert_actions)
#样本属于模仿者的概率
agent_prob = self.discriminator(agent_states,agent_actions)
#神经网络输出状态动作对来自智能体策略的概率
#通过nn.BCELoss()来定义二元交叉熵损失函数,并使用它来计算判别器的损失
#第一个损失项计算了生成样本属于模仿者类别时与目标值1的损失,即模仿者损失
#第二个损失项计算了生成样本属于专家类别时与目标值0的损失,即专家损失
#通过最小化损失函数,使得判别器能够能够准确地区分专家样本和模仿者样本
discriminator_loss = nn.BCELoss()(agent_prob,torch.ones_like(agent_prob)) + nn.BCELoss()(expert_prob,torch.zeros_like(expert_prob))
self.discriminator_optimizer.zero_grad()
discriminator_loss.backward()
self.discriminator_optimizer.step()
#模仿学习就是要使模仿者数据尽可能地被判别器误判为专家轨迹,所以也就是说模仿者的概率越接近0越好
#那么奖励函数就设置成这个
#那么可不可以利用神经网络,将损失函数设置为torch.log(agent_prob).detach().cpu(),利用最小化损失函数使得agent_prob趋向于0呢?
rewards = -torch.log(agent_prob).detach().cpu().numpy()
transition_dict = {'states':agent_s,'actions':agent_a,'rewards':rewards,'next_states':next_s,'dones':dones}
self.agent.update(transition_dict)
设置超参数,进行训练
env.seed(0)
torch.manual_seed(0)
lr_d = 1e-3
agent = PPO(state_dim, hidden_dim, action_dim, actor_lr, critic_lr, lmbda,
epochs, eps, gamma, device)
gail = GAIL(agent, state_dim, action_dim, hidden_dim, lr_d)
n_episode = 500
return_list = []
with tqdm(total=n_episode, desc="进度条") as pbar:
for i in range(n_episode):
episode_return = 0
state = env.reset()
done = False
state_list = []
action_list = []
next_state_list = []
done_list = []
while not done:
action = agent.take_action(state)
next_state, reward, done, _ = env.step(action)
state_list.append(state)
action_list.append(action)
next_state_list.append(next_state)
done_list.append(done)
state = next_state
episode_return += reward
return_list.append(episode_return)
gail.learn(expert_s, expert_a, state_list, action_list,
next_state_list, done_list)
if (i + 1) % 10 == 0:
pbar.set_postfix({'return': '%.3f' % np.mean(return_list[-10:])})
pbar.update(1)
绘图
iteration_list = list(range(len(return_list)))
plt.plot(iteration_list, return_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('GAIL on {}'.format(env_name))
plt.show()
通过上面两个实验的对比我们可以直观地感受到,在数据样本有限的情况下,BC 不能学习到最优策略,但是 GAIL 在相同的专家数据下可以取得非常好的结果。这一方面归因于 GAIL 的训练目标(拉近策略和专家的占用度量)十分贴合模仿学习任务的目标,避免了 BC 中的复合误差问题;另一方面得益于 GAIL 训练中,策略可以和环境交互出更多的数据,以此训练判别器,进而生成对基于策略“量身定做”的指导奖励信号。