1. 算法伪代码
首先是构建模型。构建并随机初始化估计网络 $Q(s,a|\theta^Q)$ 和 $\mu(s|\theta^\mu)$ 的权重 $\theta^Q$ 和 $\theta^\mu$。
构建目标网络 $Q\prime(s,a|\theta^{Q\prime})$ 和 $\mu\prime(s|\theta^{\mu\prime})$,并对权重赋值: $\theta^{Q\prime} \leftarrow \theta^Q$,$\theta^{\mu\prime} \leftarrow \theta^{\mu}$。(公式中的导数符号如果看不清楚放大一点就可以了。。。)
初始化经验回放池 $R$。
接下来进行训练。外层循环是进行M个episode的训练,每个episode是智能体从行动开始到任务结束(或任务超时)的过程。为了进行有效探索,对确定性的动作 $\mu(s_t)$ 加上噪声(随机过程)$\mathcal{N}$。
初始化随机过程 $\mathcal{N}$。
获取初始状态值 $s_1$。
内层循环的次数是每个episode的时间长度 $T$:
根据确定性策略选取动作,并对动作添加噪声:$a_t = \mu(s_t|\theta^\mu)$。
执行动作 $a_t$,获取奖励 $r_t$ 和新的状态值 $s_{t+1}$。
将一个transition($s_t$, $a_t$, $r_t$, $s_{t+1}$)加入经验回放池。
对经验回报池进行采样,随机抽取 $N$ 个transitions构成一个mini-batch。
通过最小化Q值均方损失函数 $L$ 更新 $\theta^Q$,通过计算策略梯度 $\bigtriangledown_{\theta^\mu}$ 更新 $\theta^\mu$
通过soft-update更新目标网络参数:
2. 代码实现(基于pytorch)
下面用代码实现简单的DDPG示例,用于gym中的小游戏Pendulum-v0(让摆锤倒立)。
2.1 构建网络
构建描述动作-值函数的网络,采用三层全连接神经网络,输入是3维状态向量和1维动作值,输出是Q值。隐藏层采用relu激活函数,输出层不需要激活函数。代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class QNet(nn.Module):
def __init__(self):
super(QNet, self).__init__()
self.fc_s = nn.Linear(3, 64)
self.fc_a = nn.Linear(1,64)
self.fc_q = nn.Linear(128, 32)
self.fc_3 = nn.Linear(32,1)
def forward(self, x, a):
h1 = F.relu(self.fc_s(x))
h2 = F.relu(self.fc_a(a))
cat = torch.cat([h1,h2], dim=1)
q = F.relu(self.fc_q(cat))
q = self.fc_3(q)
return q
构建描述动作策略的网络,采用三层全连接神经网络,输入是3维状态向量,输出是动作(连续的控制信号)。隐藏层采用relu激活函数,输出层采用tanh激活函数,动作值范围限定在[-1, 1]。代码如下:1
2
3
4
5
6
7
8
9
10
11
12class MuNet(nn.Module):
def __init__(self):
super(MuNet, self).__init__()
self.fc1 = nn.Linear(3, 128)
self.fc2 = nn.Linear(128, 64)
self.fc_mu = nn.Linear(64, 1)
def forward(self, x):
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
mu = torch.tanh(self.fc_mu(x))*2 # Multipled by 2 because the action space of the Pendulum-v0 is [-2,2]
return mu
2.2 构建经验回放池
经验回放池实际上是一个队列,当经验回放池满时,会抛弃旧的经验值,加入新采样的经验值。采样时,从经验回放池中随机抽取batch_size个经验值作为一个transition返回给训练机进行学习,代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class ReplayBuffer():
def __init__(self):
self.buffer = collections.deque(maxlen=buffer_limit)
def put(self, transition):
self.buffer.append(transition)
def sample(self, n):
mini_batch = random.sample(self.buffer, n)
s_lst, a_lst, r_lst, s_prime_lst, done_mask_lst = [], [], [], [], []
for transition in mini_batch:
s, a, r, s_prime, done_mask = transition
s_lst.append(s)
a_lst.append([a])
r_lst.append([r])
s_prime_lst.append(s_prime)
done_mask_lst.append([done_mask])
return torch.tensor(s_lst, dtype=torch.float), torch.tensor(a_lst), \
torch.tensor(r_lst), torch.tensor(s_prime_lst, dtype=torch.float), \
torch.tensor(done_mask_lst)
def size(self):
return len(self.buffer)
2.3 构建Ornstein Uhlenbeck(OU)噪声
OU过程是一种序贯相关过程,在DDPG中用于实现探索。OU过程满足下面的随机微分方程:
其中 $dW_t$ 是维纳过程(也称为布朗运动),满足:
$\triangle z$ 是变化量,$\epsilon$ 是标准正态分布。
OU过程的实现代码如下:1
2
3
4
5
6
7
8
9
10
11class OrnsteinUhlenbeckNoise:
def __init__(self, mu):
self.theta, self.dt, self.sigma = 0.1, 0.01, 0.1
self.mu = mu
self.x_prev = np.zeros_like(self.mu)
def __call__(self):
x = self.x_prev + self.theta * (self.mu - self.x_prev) * self.dt + \
self.sigma * np.sqrt(self.dt) * np.random.normal(size=self.mu.shape)
self.x_prev = x
return x
2.4 构建训练模型
估计网络的参数采用随机初始化,目标网络的参数复制估计网络的参数值。外层循环总共执行 $N$ 个episode,内层循环是每个episode的最大时间步长 $T$,超过 $T$ 之后重置状态,开启新的episode。每个episode结束之后,对估计网络和目标网络的参数进行训练和更新。需要注意的是,前期不会直接训练,直到经验回放池中的样本数量超过某个阈值才会开始训练策略。训练模型的代码如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48def train():
env = gym.make('Pendulum-v0')
memory = ReplayBuffer()
q, q_target = QNet(), QNet()
q_target.load_state_dict(q.state_dict())
mu, mu_target = MuNet(), MuNet()
mu_target.load_state_dict(mu.state_dict())
score = 0.0
print_interval = 20
mu_optimizer = optim.Adam(mu.parameters(), lr=lr_mu)
q_optimizer = optim.Adam(q.parameters(), lr=lr_q)
ou_noise = OrnsteinUhlenbeckNoise(mu=np.zeros(1))
for n_epi in range(N):
s = env.reset()
for t in range(T): # maximum length of episode is 200 for Pendulum-v0
a = mu(torch.from_numpy(s).float())
a = a.item() + ou_noise()[0]
s_prime, r, done, info = env.step([a])
memory.put((s,a,r/100.0,s_prime,done))
score +=r
s = s_prime
if done:
break
if memory.size()>train_threshold:
for i in range(update_epoch):
update(mu, mu_target, q, q_target, memory, q_optimizer, mu_optimizer)
soft_update(mu, mu_target)
soft_update(q, q_target)
if n_epi%print_interval==0 and n_epi!=0:
print("# of episode :{}, avg score : {:.1f}".format(n_epi, score/print_interval))
score = 0.0
torch.save({
'q_state_dict': q.state_dict(),
'q_target_state_dict': q_target.state_dict(),
'mu_state_dict': mu.state_dict(),
'mu_target_state_dict': mu_target.state_dict(),
'q_optimizer_state_dict': q_optimizer.state_dict(),
'mu_optimizer_state_dict': mu_optimizer.state_dict()
}, SAVE_PATH)
2.5 梯度下降更新参数
采用adam优化器,对估计网络的参数 $\theta^Q$ 和 $\theta^\mu$ 进行mini-batch梯度下降优化。
通过最小化均方损失函数,求 $\theta^Q$ 的梯度,利用adam优化器更新参数 $\theta^Q$。均方损失函数如下:
虽然实际上 $y_i$ 中也包含参数 $\theta^Q$,但是计算过程中通常忽略 $y_i$ 的梯度。
通过计算策略梯度,利用adam优化器更新参数 $\theta^\mu$
1 | def update(mu, mu_target, q, q_target, memory, q_optimizer, mu_optimizer): |
最后采用soft-update对目标网络 $Q\prime(s,a|\theta^{Q\prime})$ 和 $\mu\prime(s|\theta^{\mu\prime})$,代码如下:1
2
3def soft_update(net, net_target):
for param_target, param in zip(net_target.parameters(), net.parameters()):
param_target.data.copy_(param_target.data * (1.0 - tau) + param.data * tau)
2.6 运行DDPG
训练好模型之后,将网络的参数保存,然后在评测的时候重新载入模型参数。采用确定性的动作进行评测,代码如下所示:1
2
3
4
5
6
7
8
9
10
11
12
13
14def evaluate():
env = gym.make('Pendulum-v0')
checkpoint = torch.load(SAVE_PATH)
mu = MuNet()
mu.load_state_dict(checkpoint['mu_state_dict'])
mu.eval()
while 1:
s = env.reset()
done = False
while not done:
a = mu(torch.from_numpy(s).float())
s_prime, r, done, info = env.step([a.item()])
env.render()
s = s_prime
2.7 超参数的设置
超参数的设置很重要,但没有特定的方法指导,所以全凭经验。本实验设置的超参数如下所示:
1 | lr_mu = 0.0005 |