0.杂谈:
前文发了几篇阅读笔记,大家可能只是觉得我只读了论文,就是个理论学派。
怕被大家瞧不起,所以跟大家分享一下我的学习过程,实际项目做了啥就不透露了:
- 先参考了Pytorch的教学自己改成项目所需的任务(DDQN之类的方法)。
- 觉得不行,到处都是一知半解的,所以开始查找了将近20多篇相关论文,把整体脉络,原理都梳理了一遍,发现好像DDPG方法好像挺行的。
- 把论文学完了就去搞了个DDPG,好像有点用,但好像项目有点急,然后就不做了,本着积累经验,把代码放这供自己参考。
正题开始:
1.强化学习
就我个人来看,强化学习只需要两件事:环境和智能体。
笔者所用的编程语言Python,也不是只会这一门语言,理由就不说了,主要是方便。
那么从Python编程的角度来说,我们所需要的就是建立两个类,一个就是环境class,一个就是智能体class,可以扔到两个包里面导入,这样写起来比较清晰。
接下来就是清楚地知道要在这两个类里面写些什么东西:
环境Class:
class Env:
#必须的函数
def __init__():#初始化
self.info="write something which let someone else think you are professional"
#定义环境状态
self.state=None
def step(action)#进行一步动作,如果是自定义的环境,就需要和智能体商量一下,怎么动作。
def reset()#重置环境,便于重启一轮新的训练过程
def reward()#定义奖励函数,设定自己的奖励方法,多步的,单步的,随机的,咋奖励都行,只是会影响训练结果罢了,炼丹,啥都得试试。
#非必须的
def display()#展示一下环境
def update()#更新环境,实际上step函数里面就可以更新
智能体Class:
class AI_Player():
def __init__():
self.info="same thing"
def select_action()#选动作。
def memory()#记忆,便于自己训练。
def train_self()#训练自己。
所谓DQN、DDQN、DDPG等方法,实际上就是智能体的内核不同,这一内核体现在网络结构、训练方法以及动作方法的不同,这几种方法我都尝试过,现在来给大家讲解一下。
2.DQN方法
Deep Q learning(DQN)方法是一种基于Q表的方法,即状态-动作-价值表,即在不同状态下采取不同动作所带来的价值。由于实际问题中的状态一般都是连续的,非离散的,所以常规的Q学习无法有效解决这类问题。但使用神经网络代替Q表却可以很好地解决这个问题,DQN方法的输入就是连续的状态,输出就是离散动作空间中的各动作的价值。
因此,我们可以首先构建一个DQN神经网络。
class DQN(nn.Module):
def __init__(self,state_dim,num_actions):#初始化网络
super(DQN,self).__init__()
self.net=nn.Sequential(
nn.Linear(state_dim,200),
nn.ReLU(),
nn.Linear(200,300),
nn.ReLU(),
nn.Linear(300,200),
nn.ReLU(),
nn.Linear(200,num_actions),
)
def forward(self,x):#前向传播
result=self.net(x)
return result
构建了神经网络还需要训练样本,而这一样本的产生过程就是强化学习了,即使用智能体与环境不断交互,产生样本,然后进行学习,下面写一下强化学习过程。
#实例化一个我们的环境类和AI类,其中包含了各种方法。
env=Env()
AI=AI_player()
#重置一下环境
state,info=env.reset()
#定义AI强化学习过程的次数
num_episodes=2000
for i_episode in range(num_episodes):
state,info=env.reset()
for t in count():
#AI给出动作
action_id=AI.select_action(state)
action=AI.actions[int(action_id)]
#这一动作作用给环境,给出动作奖励。
observation,reward,done=env.step(action)
reward=torch.tensor([reward],device=device)
#记录后续动作
if done:
next_state=None
else:
next_state=observation
#记忆
AI.cache(state,action_id,next_state,reward)
state=next_state
AI.optimize_model()
#用旧网络参数预测下一步的价值,可以起到很好的防止乐观估计的作用。
target_net_state_dict=AI.target_net.state_dict()
policy_net_state_dict=AI.policy_net.state_dict()
for key in policy_net_state_dict:
target_net_state_dict[key] = policy_net_state_dict[key]*AI.TAU + target_net_state_dict[key]*(1-AI.TAU)
AI.target_net.load_state_dict(target_net_state_dict)
#环境给出终止条件。
if done:
break
然后就是我们具体AI的代码内容了:
#命名数组
Transition = namedtuple('Transition',('state', 'action', 'next_state', 'reward'))
#定义记忆类
class ReplayMemory(object):
def __init__(self, capacity):#初始化
self.memory = deque([], maxlen=capacity)
def push(self, *args):#增加一个样本
"""Save a transition"""
self.memory.append(Transition(*args))
def sample(self, batch_size):#随机选取案例
return random.sample(self.memory, batch_size)
def __len__(self):#输出长度
return len(self.memory)
#定义AI智能体类:
class AI_player():
def __init__(self,state_dims=500,memory_length=10000,Train=True):#策略网络、价值网络和记忆容量
self.info="something make no sense"
self.device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.state_dims=state_dims#定义卷积输出的数量
self.conv2d=[]
self.actions={
0:[0.1,0,0],
1:[-0.1,0,0],
2:[0,0.1,0],
3:[0,-0.1,0],
4:[0,0,0.1],
5:[0,0,-0.1],}#定义动作空间,表示三维空间的移动比例。
#policy网络用于直接给出动作价值,而target网络则是预测后一步的动作价值。
self.policy_net=DQN(self.state_dims,len(self.actions)).to(self.device)#初始化策略网络
self.target_net=DQN(self.state_dims,len(self.actions)).to(self.device)#初始化价值网络
self.target_net.load_state_dict(self.policy_net.state_dict())
self.steps_done=0 #已进行的步骤数
self.BATCH_SIZE = 256 #训练时所使用的样本规模
self.GAMMA = 0.99 #未来价值权重值
self.EPS_START = 0.5 #开始时随机尝试概率
self.EPS_END = 0.05 #最低随机尝试概率
self.EPS_DECAY = 10 #降低速率
self.TAU = 0.005 #价值网络更新速率
self.LR = 1e-4 #学习率
if Train!=True:
self.EPS_START = 0
self.EPS_END = 0
#定义AI的记忆和大脑容量
self.memory_length=memory_length
self.memory=ReplayMemory(self.memory_length)
self.optimizer = optim.AdamW(self.policy_net.parameters(), lr=self.LR, amsgrad=True)#定义优化器
上面是记忆结构体定义和智能体初始化的代码内容,下面则是智能体的动作选择函数和训练自己的各种方法:
#选择动作,有随机选择的概率,不过随着步数增多,随机性减弱
def select_action(self,state):
state=self.pretreat_data(state)
sample=random.random()
eps_threshold=self.EPS_END+(self.EPS_START-self.EPS_END)*\
math.exp(-1*self.steps_done/self.EPS_DECAY)
self.steps_done+=1
if sample>eps_threshold:
with torch.no_grad():
return torch.tensor([[torch.argmax(self.policy_net(state))]])
else:
return torch.tensor([[random.randint(0,len(self.actions)-1)]])
def pretreat_data(self,state):
t=[]
for i in range(len(state)):
if len(state[i])<self.out_facets:
tmp=torch.tensor(state[i],dtype=torch.float)
zero=torch.zeros([self.out_facets-len(state[i]),3,3],dtype=torch.float)
t+=[torch.cat([tmp,zero]).view(-1)]
else:
tmp=torch.tensor(state[i],dtype=torch.float)[torch.randperm(self.out_facets)]
t+=[tmp.view(-1)]
data=torch.cat(t).to(self.device)
data=(data-torch.min(data))/(torch.max(data)-torch.min(data))
return data
#存储记忆
def cache(self,state,action_id,next_state,reward):
if next_state==None:
self.memory.push(self.pretreat_data(state),action_id,next_state,reward)
else:
self.memory.push(self.pretreat_data(state),action_id,self.pretreat_data(next_state),reward)
def optimize_model(self):
if len(self.memory)<self.BATCH_SIZE:
return
#提取记忆进行训练
transitions=self.memory.sample(self.BATCH_SIZE)
batch=Transition(*zip(*transitions))
non_final_mask = torch.tensor(tuple(map(lambda s: s is not None,
batch.next_state)), device=self.device, dtype=torch.bool)
non_final_next_states = torch.cat([s for s in batch.next_state
if s is not None]).reshape(-1,self.state_dims)
state_batch = torch.cat(batch.state).reshape(-1,self.state_dims)
action_batch = torch.cat(batch.action).to(self.device)
reward_batch = torch.cat(batch.reward).to(self.device)
state_action_values=self.policy_net(state_batch).gather(1,action_batch)
next_state_values=torch.zeros(self.BATCH_SIZE,device=self.device)
#计算价值,包括了后续价值的计算,具体原理可以查贝尔曼方程。
with torch.no_grad():
next_state_values[non_final_mask] = self.target_net(non_final_next_states).max(1)[0]
expected_state_action_values = (next_state_values * self.GAMMA) + reward_batch
#损失函数
#criterion = nn.SmoothL1Loss()
criterion = nn.MSELoss()
loss = criterion(state_action_values, expected_state_action_values.unsqueeze(1))
self.optimizer.zero_grad()
loss.backward()
#梯度裁剪
#torch.nn.utils.clip_grad_value_(self.policy_net.parameters(), 100)
self.optimizer.step()
然后,就让智能体和环境进行交互,再让环境给出奖励即可完成强化学习的过程,如你所见,强化学习就是这么简单。
至于本文的环境部分,为了避免惹上不必要的麻烦,就暂不贴出了,如有疑惑可以私下解答。
3.DDQN方法
所谓DDQN方法与DQN方法的不同,就是DDQN全程叫做Double Deep Q learning 方法,而我当时学习的时候就有这样的疑惑,DQN方法也有两个Q网络啊,这啥意思啊。
后来在论文里才明白,人家两个网络的使用方式是不同的。DDQN和DQN的区别就在于Target Net的使用方法,DQN是直接让Target网络给出最大q值,而DDQN则是让Policy网络选出下一步动作,然后让Target网络评估这个动作的价值作为后一阶段的价值,这么一来就有效降低了乐观估计的方法,在论文里提到,这个改动非常的小,也非常的简单,只需要将后续价值的计算方式一改就可以了,不再取最大值,而是预测一下下一步的动作,然后再让Target网络评估一下,懒得写了,对这个问题也没啥用,所以就给大家当参考了。
4.至于为啥DQN这类Value-based方法对我们这个问题严重不适用呢,是因为我们这个问题根本就不是一个离散动作空间能解决的问题,而是一个连续的动作空间问题,我一直以为强化学习解决不了连续空间的问题,直到阅读了大量文献之后,才发现了终极大杀器,DPG,Deterministic Policy Gradient 确定型策略梯度。这就解决了连续空间的强化学习问题,通过这一策略梯度法,我们就可以训练神经网络了,即诞生了Deep DPG (DDPG) ,它还有另一个响当当的名字,Actor-Critic(AC)算法,还有多个版本,分为了AC、A2C、A3C。
这个东西有多好玩呢,它可以直接输出连续的动作值,比如让AI玩游戏,DQN的AI只会一次走一步、两步、三步,这些步数都是写好的,不会边,因此需要n个动作输出。但是DDPG的AI就可以只有一个输出,这个输出直接就是走1.234235235米,走3.1413412312米,看到没有,直接输出的就是连续值,这相当离谱。
然后就有人问了,你给它个奖励,它也没法训练啊,这个时候就可以看看DDPG算法的原理了,AI实际上分为了两个,一个叫做Critic,一个叫做Actor,Actor负责根据现状给出连续的动作价值,而Critic负责根据现在的状态和Actor的动作进行打分,而这个打分就和奖励值有关了。
Critic根据环境给出的奖励优化自身,从而变得越来越专业,然后它就可以告诉Actor如何改进自己的动作,即给出Critic网络的梯度上升方向。
大概原理就是这样,废话不多说了,直接上代码:
首先是Actor和Critic两个网络的定义:
#Actor网络
class Actor(nn.Module):
def __init__(self,state_dims,action_dims,max_value):
self.max_value=max_value
super(Actor,self).__init__()
self.policy_net=nn.Sequential(
nn.Linear(state_dims,500),
nn.ReLU(),
nn.Linear(500,500),
nn.ReLU(),
nn.Linear(500,500),
nn.ReLU(),
nn.Linear(500,action_dims),
nn.Tanh(),
)
def forward(self,x):
result=self.max_value*self.policy_net(x)
return result
#Critic网络
class Critic(nn.Module):
def __init__(self,state_dims,action_dims):
super(Critic,self).__init__()
self.value_net=nn.Sequential(
nn.Linear(state_dims+action_dims,500),
nn.ReLU(),
nn.Linear(500,500),
nn.ReLU(),
nn.Linear(500,500),
nn.ReLU(),
nn.Linear(500,1),
#nn.Tanh(),
#nn.Sigmoid(),
)
def forward(self,x):
return self.value_net(x)
然后给大家加个餐,还可以再加一个网络,观测网络,如果大家了解过Transformer模型的话,就知道,这俩基本没啥关系,而是和里面的Emebedding层有关系,本文这个问题包含了一个大型的三维01矩阵,所以使用一个观测网络降个维度不过分吧。
#embedding 网络
class Eye(nn.Module):
def __init__(self,voxel_scale,out_features):
super(Eye,self).__init__()
self.net=nn.Sequential(
nn.Conv3d(1, 64, kernel_size=(4,4,4), stride=4, padding=4),
nn.MaxPool3d((4, 4, 4), stride=(4, 4, 4)),
nn.Conv3d(64, 128, kernel_size=(4,4,4), stride=4, padding=4),
#nn.MaxPool3d((4, 4, 4), stride=(4, 4, 4)),
nn.Flatten(),
nn.Linear(1024,1000),
nn.ReLU(),
nn.Linear(1000,1000),
nn.ReLU(),
nn.Linear(1000,1000),
nn.ReLU(),
nn.Linear(1000,out_features),
)
def forward(self,x):
return self.net(x)
然后就是智能体了,智能体的动作要改改了,这次直接输出了动作的具体值,直接跟环境进行交互就得了,写得多了,有点懒了,还有其他事情要忙,我猜也没什么人看,也猜不出来我们是在干啥。我就直接贴代码了。
#AI_Player
class AI_Player():
def __init__(self,out_features=500,memory_length=10000,voxel_scale=40,Train=True):#感知特征数量,记忆容量、体素网格大小、是否训练
self.info="something useless"
self.device=torch.device("cuda" if torch.cuda.is_available() else "cpu")
self.out_features = out_features#感知层输出特征数量
self.voxel_scale = voxel_scale#体素网格大小
self.move_size=0
self.actions={
#移动Policy_gradient
0:"左右移",
1:"上下移",
2:"前后移",
#3:"旋转value_method"
}#定义动作空间。
self.eye=Eye(self.voxel_scale,self.out_features).to(self.device)
self.actor=Actor(self.out_features,len(self.actions),self.voxel_scale).to(self.device)
self.critic=Critic(self.out_features,len(self.actions)).to(self.device)
self.steps_done=0 #已进行的步骤数
self.MIN_BATCH=40
self.MAX_BATCH=512
self.BATCH_SIZE = 40 #训练时所使用的样本规模
self.GAMMA = 0.99 #未来价值权重值
self.EPS_START = 0.95 #开始时随机尝试概率
self.EPS_END = 0.05 #最低随机尝试概率
self.EPS_DECAY = 100 #降低速率
self.TAU = 0.005 #价值网络更新速率
self.LR = 1e-4 #学习率
if Train!=True:
self.EPS_START = 0
self.EPS_END = 0
#定义AI的记忆和大脑容量
self.memory_length=memory_length
self.memory=ReplayMemory(self.memory_length)
#定义优化器,优化两个网络同时优化感知网络,但后来发现,实际上两个网络的评价方法不同,还是让Critic网络来反向传播优化得了,毕竟在实际问题中,比如跳水比赛、体操比赛啥的,管你选手自己咋看,评委打分。
#self.actor_optimizer = optim.AdamW([{'params': self.eye.parameters()},{'params': self.actor.parameters()}], lr=self.LR, amsgrad=True)
self.actor_optimizer = optim.AdamW([{'params': self.actor.parameters()}], lr=self.LR, amsgrad=True)
self.critic_optimizer= optim.AdamW([{'params': self.eye.parameters()},{'params': self.critic.parameters()}], lr=self.LR, amsgrad=True)
def reset():
self.steps_done=0
def select_action(self,state):
state=self.pretreat_data(state).to(self.device)#预处理体素化
state=self.eye(state)#进入网络进行感知
sample=random.random()#生成随机数判定是否需要随机产生策略
eps_threshold=self.EPS_END+(self.EPS_START-self.EPS_END)*\
math.exp(-1*self.steps_done/self.EPS_DECAY)#随机策略发生率递减
self.steps_done+=1
if sample>eps_threshold:
with torch.no_grad():
#print(state)
return self.actor(state).view(-1)
else:
return torch.tensor([self.voxel_scale*random.randint(-100,100)/100 for i in range(3)])
def pretreat_data(self,state):
#在此处进行体素化
data=norm_voxelized(state,self.voxel_scale)
#print(data)
t=torch.tensor(np.array(data[0])).view(1,1,self.voxel_scale,self.voxel_scale,self.voxel_scale).to(torch.float32)
self.move_size=data[1]
return t
def cache(self,state,action_id,next_state,reward):
if reward<1e-8:
return
if next_state==None:
self.memory.push(self.pretreat_data(state),action_id,next_state,reward)
else:
self.memory.push(self.pretreat_data(state),action_id,self.pretreat_data(next_state),reward)
def optimize_model(self):
if len(self.memory)<self.MIN_BATCH:
return 0
transitions=self.memory.sample(min(len(self.memory),self.MAX_BATCH))
batch=Transition(*zip(*transitions))
#print(batch)
non_final_mask = torch.tensor(tuple(map(lambda s: s is not None,
batch.next_state)), device=self.device, dtype=torch.bool)
non_final_next_states = torch.cat([s for s in batch.next_state if s is not None])\
.view(-1,1,self.voxel_scale,self.voxel_scale,self.voxel_scale).to(self.device)
state_batch=torch.cat(batch.state).view(-1,1,self.voxel_scale,self.voxel_scale,self.voxel_scale).to(self.device)
#action_batch=torch.cat(batch.action).view(-1,len(self.actions)).to(self.device)
reward_batch=torch.cat(batch.reward).to(self.device)
#首先使用数据在观测网络里面跑一遍,把这个观测结果放给Critic网络,然后让Critic网络跑一遍,这个时候Critic网络的损失函数使用-Mean(q)就可以给出梯度上升方向,所以就是Actor网络的优化方向,由于神经网络的数据计算过程中,梯度都已经保存在网络参数里面了,可以理解为数据流过网络一次,就会留下一次权重,给出损失函数直接优化即可。
observ=self.eye(state_batch)
action_batch=self.actor(observ)
action_state_batch=torch.cat([action_batch,observ],dim=1)
actor_loss=-torch.mean(self.critic(action_state_batch))
self.actor_optimizer.zero_grad()
actor_loss.backward()
#torch.nn.utils.clip_grad_value_(self.actor.parameters(), self.move_size)
self.actor_optimizer.step()
#优化Critic网络。
observ=self.eye(state_batch)
action_batch=torch.cat(batch.action).view(-1,len(self.actions)).to(self.device)
action_state_batch=torch.cat([action_batch,observ],dim=1)
state_action_values=self.critic(action_state_batch)
criterion = nn.SmoothL1Loss()
critic_loss=criterion(state_action_values,reward_batch.unsqueeze(1))
self.critic_optimizer.zero_grad()
critic_loss.backward()
#torch.nn.utils.clip_grad_value_(self.critic.parameters(), 10)
self.critic_optimizer.step()
#print(f"actor_loss={actor_loss.item()} , critic_loss={critic_loss.item()}")
return critic_loss.item()
最后,受算力限制和种种条件限制,我们的AC算法没有取得应有的效果,不过这个技术路线基本上是行得通的,本文所提出两种方法也是基本方法,如果环境和智能体训练方法和网络结构合理的话,基本上可以解决80%以上的需要强化学习的问题。