强化学习代码实操
写在最前面
本人本科生,为了大创项目在老师的帮助下自学强化学习和深度学习等知识,目前听过了David Silver和周博磊等大牛的课程,对于强化学习的基础知识有了一定的了解,但是上升到打代码上却依然一头雾水,不知道从何写起,因而我从GitHub上面找到了一些感觉很好的练习示例项目,大多数源自《Reinforcement Learning: An introduction》一书,来进行赏析学习。这是原项目地址。里面基本分了章节进行了排布,部分代码有注释,但是没有注释的一部分我对着书研究了一段时间。因而这一系列的文章既是学习过程的记录,也是对自己的一种鞭策。本博客跳过了原书第一章的导论部分,从第二章的多臂赌博机开始。
总体思路
我在接下来的文章中主要用以下思路进行排布:
1.对代码所在的章节背景进行一个简介
2.对重点代码进行赏析解释
3.结合理论对代码进行复盘
4.对学习内容进行总结
希望我能够坚持到底,希望各位看到这篇文章的有缘人也能指出我的不成熟看法并且给些建议。
那接下来让我们开始吧!
背景介绍
k臂赌博机是本书中十分经典的一个模型,在此模型之下没有状态(state)的差异,我们仅仅需要关心我们拉哪个臂的动作(action)。另外,k臂赌博机有动作的真实价值,动作的真实价值从一个均值为0,方差为1的标准正态分布中选择,而实际选择某个动作时,我们所收获的实际收益(return)又是以每个动作的真实价值为均值的方差为1的标准正态分布(整体结构如图所示)。我一开始不明白这个环境的不确定性在哪里,后来才发现实际收益是真实价值基础上的一个分布,并不是确定值(不然也不能算赌博机)。
def figure_2_1(): #绘制下图回报分布的代码
plt.violinplot(dataset=np.random.randn(200, 10) + np.random.randn(10))
plt.xlabel("Action")
plt.ylabel("Reward distribution")
plt.savefig('figure_2_1.png')
plt.close()
接下来我们将根据书上的描述对一个赌博机进行1000时刻的交互,以评估性能和动作,这将构成一轮的试验。我们再用2000个不同的赌博机(价值、收益不同)完成2000次独立重复实验,从而获得对一个学习算法平均表现的评估。
重点代码解析
我们将在k臂赌博机的环境下实验 ε ε ε-贪心方法再不同 ε ε ε下的训练情况,赌博机简单的增量式实现,乐观初始值的应用,基于置信度上界(UCB)的动作选择以及梯度赌博机算法。基本涵盖了第二章所有有关实操的知识,让我们来逐步分析。
环境设置
import matplotlib.pyplot as plt
import matplotlib
import numpy as np
from tqdm import trange
matplotlib.use('TkAgg')
class Bandit:
def __init__(self,k_arm=10,epsilon=0.,initial=0.,step_size=0.1,sample_average=False,UCB_parameter=None,gradient=False,gradient_baseline=False,true_reward=0.):
self.k=k_arm #设定赌博机臂数
self.step_size=step_size #设定更新步长
self.sample_average=sample_average #bool变量,表示是否使用简单增量式算法
self.indices=np.arange(self.k) #创建动作索引(和赌博机臂数长度相同)
self.time=0 #计算所有选取动作的数量,为UCB做准备
self.UCB_parameter=UCB_parameter #如果设定了UCB的参数c,就会在下面使用UCB算法
self.gradient=gradient #bool变量,表示是否使用梯度赌博机算法
self.gradient_baseline=gradient_baseline #设定梯度赌博机算法中的基准项(baseline),通常都用时刻t内的平均收益表示
self.average_reward=0 #用于存储baseline所需的平均收益
self.true_reward=true_reward #存储一个赌博机的真实收益均值,为下面制作一个赌博机的真实价值函数做准备
self.epsilon=epsilon #设定epsilon-贪婪算法的参数
self.initial=initial #设定初始价值估计,如果调高就是乐观初始值
首先肯定是引入一些环境,主要是matplotlib和numpy包,下面的tqdm主要是用来在实际训练过程中打印进度条的。
接下来就是创建bandit类并且初始化一些变量。所有步骤的含义我都写在了代码块里,要特别注意的是本类涵盖了第二章里的多个任务,所以需要添加一些变量来指示接下来要完成哪个任务,这块代码里的主要难点就是许多不同任务中需要的参量混杂在一起,给理解上带来困难。
接下来就是agent与环境交互和学习的主要部分,大概由以下三个部分组成:
1.reset:在一幕(episode)结束后重置状态以及各项参数;
2.act:agent根据自己的策略选择行动的部分
3.step:环境接受agent的动作并返回return,同时在内部维护一个Q函数,为agent的下一步选择做好准备
之所以要在这里强调这三个部分,是因为这是强化学习编程中的一个很好的分块思想,之后的代码实操很多都是按照这样的思路进行的。
reset函数设置
def reset(self): #初始化训练状态,设定真实收益和最佳行动状态
self.q_true=np.random.randn(self.k)+self.true_reward #设定真实价值函数,在一个标准高斯分布上抬高一个外部设定的true_reward,true_reward设置为非0数字以和不使用baseline的方法相区分
self.q_estimation=np.zeros(self.k)+self.initial #设定初始估计值,在全0的基础上用initial垫高,表现乐观初始值
self.action_count=np.zeros(self.k) #计算每个动作被选取的数量,为UCB做准备
self.best_action=np.argmax(self.q_true) #根据真实价值函数选择最佳策略,为之后做准备
self.time=0 #设定时间步t为0
我把每一步的具体含义也打在代码中了,值得注意的是其中true_reward的用意,我是根据上下文才推测出来的,由于之后要对比使用和不使用baseline的训练效果,而真实价值是一个标准正态分布,这就导致它回报的平均值会接近0,即baseline接近0,这和不使用baseline的效果是类似的,所以在对比baseline效果的实验中我们通过true_reward把真实价值垫高。
act函数设置
def act(self):
if np.random.rand()<self.epsilon:
return np.random.choice(self.indices) #以epsilon的几率随机选择动作
if self.UCB_parameter is not None: #当有设置UCB的参数时,使用基于UCB的动作选择
UCB_estimation=self.q_estimation+self.UCB_parameter*np.sqrt(np.log(self.time+1)/(self.action_count+1e-5)) #预测值更新函数
q_best=np.max(UCB_estimation) #选择不同动作导致的预测值中最大的
return np.random.choice(np.where(UCB_estimation==q_best)[0]) #返回基于UCB的动作选择下值最大的动作
if self.gradient: #如果使用梯度赌博机算法
exp_est=np.exp(self.q_estimation)
self.action_prob=exp_est/np.sum(exp_est) #以上两步按照softmax分布确定动作概率
return np.random.choice(self.indices,p=self.action_prob) #按概率选择执行动作
q_best=np.max(self.q_estimation) #如果不使用以上的其他方法,则使用贪心算法,与第一步构成epsilon-贪心算法
return np.random.choice(np.where(self.q_estimation==q_best)[0]) #执行估计值最大的动作
基本上每一步的用意我也写在了代码中,这一段完成了三个任务,第一是 ε ε ε-greedy算法,第二是基于UCB的动作选择,第三是梯度赌博机算法。每一步都比较严格地按照树上的流程来编写,如果对照书本应该可以很快看懂。
step函数
def step(self,action):
reward=np.random.rand()+self.q_true[action] #奖励是以真实值为平均的正态分布
self.time+=1 #总行动数量+1
self.action_count[action]+=1 #某个行动的行动数量+1
self.average_reward+=(reward-self.average_reward)/self.time #计算平均回报(return),为baseline做好准备
if self.sample_average: #如果使用增量式实现
self.q_estimation[action]+=(reward-self.q_estimation[action])/self.action_count[action] #用采样数据来更新行动价值(赌博机实验中只有一个状态,所以只更新行动价值)
elif self.gradient: #如果使用梯度赌博机
one_hot=np.zeros(self.k)
one_hot[action]=1 #把应该采取的行动概率置为1
if self.gradient_baseline: #在梯度赌博机上使用baseline
baseline=self.average_reward #平均收益天生就是一个良好的baseline
else:
baseline=0 #这里不使用baseline的话baseline就设置为0,如果上面的q_true不用true_reward抬高的话就没办法区分有没有使用baseline了
self.q_estimation+=self.step_size*(reward-baseline)*(one_hot-self.action_prob) #梯度赌博机更新状态价值估计
else:
self.q_estimation