初识强化学习

232 阅读15分钟

本文首发于:行者AI

引言

强化学习(Reinforcement Learning)也被称为增强学习,它在模型实时更新、用户行为快速反馈等方向上拥有巨大的优势。自从2013年以来,强化学习大显身手,给人印象极为深刻。强化学习在游戏中的应用非常自然:比如用DQN玩Atari游戏,AlphaGo打败围棋高手,AlphaZero自学成才成为围棋、国际象棋高手,游戏可以与强化学习相互成就。

1. 强化学习基本概念

强化学习的基本原理就是一个智能体通过与环境进行交互,不断学习强化自己的智力,来指导自己的下一步行动,以取得最大化的预期利益。比如说,婴儿学走路就是通过与环境交互,不断从失败中学习,来改进自己的下一步的动作才最终成功的。再比如说,在机器人领域,一个智能机器人控制机械臂来完成一个指定的任务,或者协调全身的动作来学习跑步,本质上都符合强化学习的过程。

举个不恰当的例子,有点像爸爸训练儿子一样,比如训练儿子好好写作业,不许打游戏,儿子就是训练对象。如果儿子完成了写作业的动作,就会获得一定的奖励(给零花钱、允许打游戏),如果没有完成或者完成的不对,就没有奖励甚至还会打屁股。时间久了,儿子自然而然地就知道要先写完作业,才能打游戏,因为这个动作是当前环境下获得收益最大的动作,其他动作就不会有奖励,甚至还要被打屁股。

对于一个通用的强化学习框架来说,有六个元素是必须有的:

  • 智能体(Agent):强化学习的主体也就是作出决定的“大脑”;
  • 环境(Environment):智能体所在的环境,智能体交互的对象;
  • 行动(Action):由智能体做出的行动;
  • 奖励(Reward):智能体作出行动后,该行动带来的奖励;
  • 状态(State):智能体自身当前所处的状态;
  • 目标(Objective):指智能体希望达成的目标。

为了方便大家记忆,简单一句话概括起来就是:一个智能体身处在不断变化的环境之中,为了达成某个目标,它需要不断作出行动,行动会带来好与不好的奖励,智能体收集这些奖励反馈并进行自我学习,改变自己所处的状态,再进行下一步的行动,然后智能体会持续这个“行动 - 奖励 - 更新状态”的循环,不断优化自身,直到达成设定的目标。

2. Q-learning

目前强化学习的算法很多,诸如一些基础的算法Q_learning, Sarsa, DQN。 今天我们主要讲述 Q_learning和DQN的实现,Sarsa和Q-learning算法很相近,这里就不展开介绍。

2.1 介绍

Q-learing是强化学习中的入门算法(Value-Based),Q-Learning假设可能出现的动作a和状态s是有限多,这时a和s的全部组合也是有限多个,并且引入价值量Q(s,a)表示智能体认为做出某个a时所能够获得的利益。在这种假设下,智能体收到s,应该做出怎样的a,取决于选择哪一个a可以产生最大的Q值。举个例子,下面的表格显示了动物在面对环境的不同状态时做出的行为对应着怎样的一个Q值,这里为了简单说明只分别列举了2种s和a:

s\a前进(a1)后退(a2)
前方是美食(s1)10
前方是天敌(s2)-10

显然,如果此时动物处于s1的状态时,有两种行为a1,a2可以被它选择,选择a1得到的奖励是”1” ,显然比选择a2的奖励”0”要大,所以在s1的状态下,选择a1作为下一个动作;现在状态更新成s2,还是有两个同样的选择,重复上面的过程,在行为准则Q 表中寻找 Q(s2, a1) Q(s2, a2) 的值, 并比较他们的大小, 选取较大的一个。这种表格在Q-Learning中被称为Q表,表中的S和a需要事先确定,表格主体的数据在初始化的时候被随机设置(一般初始化为0),在后续通过训练得到更新。

2.2 算法描述

Q-learning的训练过程就是Q表中的Q值逐渐更新的一个过程,其核心是根据当前状态下S选择的某个行动a的Q值和当前选择的行动a作用于环境获得的回报r以及下一轮的状态S’对应所有可选择动作中可以获得的最大奖励Q,来更新Q表:

Q(s,a)=Q(s,a)+α[r+γmaxQ(s,a)Q(s,a)]Q(s,a)=Q(s,a)+α[r+γmaxQ(s',a')−Q(s,a)]

其中 Q(s',a')是下一时刻的状态和采取的行动(并没有在实际中采取任何行为, 而是在想象自己在 s‘ 上采取了每一种行为, 分别看看哪种行为的 Q 值大)对应的 Q 值,Q(s,a)是当前时刻的状态和当前状态下实际采取的行动对应的Q值。折扣因子γ的取值范围是 [ 0,1 ],其本质是一个衰减值,如果γ更接近0,agent趋向于只考虑瞬时奖励值,反之如果更接近1,则agent为延迟奖励赋予更大的权重,更侧重于延迟奖励;也就是对获取过的奖励为了防止再次获取后得到的分数一样,于是对reward进行一个衰减,这样就会有长远的眼光,机器人就不只专注于眼前的奖励了;r为当前时刻状态s执行行动a得到的奖励值。α为学习率,取值范围(0,1]。

一般情况下,可以对上式进行简化:

(1)当α等于1时,Q表的更新公式如下:

Q(s,a)=r+γmaxQ(s,a)Q(s,a)=r+γmaxQ(s',a')

(2)当α等于1, γ=0时,公式如下:

Q(s,a)=rQ(s,a)=r

表示只看眼前利益,这时Q矩阵等于R矩阵,并没有什么意义。

(3)当α等于1, γ=1时,公式如下:

Q(s,a)=r+maxQ(s,a)Q(s,a)=r+maxQ(s',a')

表示不仅考虑眼前的利益r,还考虑未来动作的最大收益。(α正常情况下不为1,一般都比较小,本文Q-learning和DQN算法模型中,α都设置为0.01)

2.3 算法流程

Q-learning的伪代码如下:

图1. Q-learning算法流程图

其中ε-greedy是用在决策上的一种策略, 比如 epsilon = 0.9 时, 就说明有90% 的情况会按照 Q 表的最优值选择行为, 10% 的情况随机选取行为。

算法流程如下:

  • 初始化Q表,构建全为0的m(状态)×n(行动)矩阵。

  • for each episode:

    ​ a)随机选择一个s作为初始状态.

    ​ b)while True:

    ​ c) 首先在初始状态s的情况下,基于ε-greedy策略选择动作a。

    ​ d) 利用选定的行为a,得到下一个状态s'和奖励r

    ​ e) 此时对状态s进行更新,得到新的Q(s,a)的值,更新Q表:

Q(s,a)=Q(s,a)+α[r+γmaxQ(s,a)Q(s,a)]Q(s,a)=Q(s,a)+α[r+γmaxQ(s',a')−Q(s,a)]

​ f)令s = s'

​ g)if done,break

2.4 具体案例

(1)迷宫游戏地图

图2. 迷宫游戏地图
  • 黑色的是陷阱(reward -1)

  • 黄色的是宝藏 (reward 1)

  • 蓝色的是探险者

迷宫中每个正方形方格代表一个状态,每个状态有四种行动(上,下,左,右),每个方格都用一个二维矩阵坐标唯一表示,如下表:

0,01,02,03,0
0,11,12,13,1
0,21,22,23,2
0,31,32,33,3

我们设置learning_rate=0.01, reward_decay=0.9, e_greedy=0.9。

(2)不同阶段的Q表对比

初始化Q表,首先每个状态可以有4个行动,向上走,向下走,向左走,向右走,分别记为0,1,2,3;新的状态s‘在之后不断添加在Q表中。

s\a0123
s10.00.00.00.0
...0.00.00.00.0

将episode设为1,输出包括探险者的行为轨迹和更新后的Q表,如下表所示:

行为轨迹:(在环境中对行为有做出限制,但在action的输出上,不会对上下左右动作做限制)

stateaction_spacerewardQaction
0[0,0]u00.00
1[0,0]r00.03
2[1,0]r00.03
3[2,0]u00.00
4[2,0]r00.03
5[3,0]u00.00
6[3,0]d-1-0.011

Q-table(经过一轮训练后,更新后的Q表):

s\a0123
[0,0]0.00.00.00.0
[1,0]0.00.00.00.0
[2,0]0.00.00.00.0
[3,0]0.0-0.010.00.0
terminal0.00.00.00.0

在本轮实验中,为了解释方便,我们设[0,0]为s1,[1,0]为s2,[2,0]为s3,[3,0]为s4,[3,1]为s5(terminal)。

根据本轮Q表的更新结果,可以看出探险者的行动轨迹为S1->S2->S3->S4->S5(陷阱)。Q(s4,1)=-0.01说明探险者在本轮实验中最后踩到了陷阱导致回合结束,在s5状态下踩到了陷阱。探险者每行走一部,Q表的值都会相应的进行更新,S1->S2,S2->S3,S3->S4因为Q值都为0,所以更新后的Q表还是全为0(这里不展开解释,注意此时S4还没有进行更新)。那么从S4->S5,根据算法流程,首先在状态S4下随机选择了动作1,因为踩到了陷阱(S5),所以Q(s4,1)对应的R= -1,maxQ(s5,a)四个行动(上,下,左,右)的对应Q值全为0。所以根据更新公式得出S4状态下选择1(向下)的动作的Q值为Q(s4,1)=-0.01。

将episode设为100,最终探险者行动轨迹和更新后的Q表:

输出行为轨迹(由于轨迹太多,截取部分):

stateaction_spacerewardQaction
0[0,0]r00.03
1[1,0]d00.01
2[1,1]r00.03
3[2,1]l00.02
4[1,1]r00.03
5[2,1]r-1-0.013
6[0,0]u00.00

Q-table(经过100轮训练后,更新后的Q表,保留两位或三位小数):

s\a0123
[0,0]1.390.00.012.98
[0,1]9.470.00.00.0
[0,2]0.00.00.0-0.02
[0,3]0.00.00.00.0
[1,0]0.013.820.063.57
[1,1]0.001-0.020.010.51
[2,0]0.0010.223.510.0
[2,1]0.0043.600.67-0.02
[2,3]0.00.00.00.0
[3,0]0.0-0.020.00.001
[3,2]0.00.00.00.0
[3,3]0.00.00.00.0
terminal0.00.00.00.0

取每行状态对应动作的最大值,由Q表可以得出最后探险者获取宝藏的轨迹为:[0,0]->[1,0]->[1,1]->[2,1]->[2,2] (终点)

图3. 探险者路线图

2.5 Q-learing的缺点

虽然Q-learning很好的解决了迷宫问题,且迷宫的状态和行动数量并不高,当状态和行动空间是高纬度并且连续时,那么Q-learning将不再适用。

3. DQN

3.1 介绍

Q-learning不能适用于高维度的状态和行动,但我们可以引入价值函数近似(Value Function Approximation)将Q表更新问题变成一个函数拟合问题,相近的状态得到相近的输出动作。如下式,通过更新参数w使得函数f逼近最优Q值。

[公式]

深度学习能自动地从数据中获得有效的特征表示和不同数据形态的适应性,因此强化学习(RL)需要与深度学习(DL)相结合,直接从原始高维度的数据中进行模型的有效学习。

深度学习与强化学习结合时面临的问题[1]:

  • 深度学习需要大量带标签的样本来计算损失函数;强化学习只有reward作为返回值,而且伴随着噪声,稀疏(很多State的reward是0)等问题;

  • 深度学习的样本彼此之间独立,且来自相同分布;强化学习与环境交互的序列存在一定的相关性,且分布不断变化,比如玩一个游戏,一个关卡和下一个关卡的状态分布是不同的,所以训练好了前一个关卡,下一个关卡又要重新训练;

  • 在网络的更新过程中,需要计算Q-目标值和Q-估计值,网络需要同时计算这两个Q值,由于数据本身存在不稳定性,每轮的迭代都可能会产生一些波动,这些波动会立刻返回下一个迭代中,这样很难得到平稳的模型。

用DQN解决以上问题(一一对应):

  • 通过Q-learning使用reward来构造标签,并确定损失函数,使Q-目标值和Q-估计值相差越小越好。

  • 通过Experience Replay(经验池)的方法来解决相关性及非静态分布问题。

  • 采用双网络结构,且两个网络结构完全相同。使用一个估计网络(MainNet)产生当前Q-估计值,使用另外一个目标网络(Target)产生Q-目标值。在每一步更新参数时,只更新估计网络的参数,不更新目标网络的参数。每迭代一定步数,再将估计网络的参数赋值给目标网络,然后进行训练,这样就阻止了波动的传递,在一定程度上降低了当前Q值和目标Q值的相关性,提高了算法的稳定性。

3.2 算法流程

DQN的伪代码如下:(假设DQN的输入为图像)

图4. DQN算法流程图
  • 初始化Memory D(经验池),容量为N;

  • 随机初始化Q估计网络的参数θ;

  • 随机初始化Q目标网络的参数θ‘=θ

  • 循环遍历episode:

    ​ a)获取环境初始状态s1,通过Φ函数对s1进行图像处理得到Φ1;

    ​ b)循环遍历step:

    ​ c)用ϵ−greedy策略选择action;

    ​ d)执行上一步被选择的action,得到即时的reward及新的state(图像);

    ​ e)将当前状态 Φt,选择的动作at,得到的即时奖励rt和下一个状态 Φt+1放入经验池D中;

    ​ f)从经验池D中随机抽取一个minibatch的( Φt,at,rt, Φt+1);

    ​ g)通过上一步选出的minibatch,计算目标Q值;

    ​ h)损失函数是Q目标值和Q估计值的均方误差,采用SGD优化器对参数θ进行更新;

    ​ i)Φ = Φ’;

    ​ j)每隔C步,θ‘=θ;

    ​ k)End For;

  • End For;

3.3 代码实现

案例:AI需要决定向左移动还是向右移动,来使得木棍保持稳定。测试环境:CartPole-v0

我们采用的测试环境的状态空间相对简单,因此网络结构设计也相对简单。采用两层全连接并以 ReLU 作为激活函数,当遇到更复杂的诸如以图像作为输入的环境时,可以考虑采用卷积神经网络。

定义网络结构:

class Net(nn.Module):
    def __init__(self):                                               # 定义Net的一系列属性
        # nn.Module的子类函数必须在构造函数中执行父类的构造函数
        super(Net, self).__init__()                                   # 等价与nn.Module.__init__()
		# 设置第一个全连接层(输入层到隐藏层): 状态数个神经元到50个神经元
        self.fc1 = nn.Linear(N_STATES, 50)        
        # 权重初始化 (均值为0,方差为0.1的正态分布)
        self.fc1.weight.data.normal_(0, 0.1)                          
        # 设置第二个全连接层(隐藏层到输出层): 50个神经元到动作数个神经元
        self.out = nn.Linear(50, N_ACTIONS)  
        # 权重初始化 (均值为0,方差为0.1的正态分布)
        self.out.weight.data.normal_(0, 0.1)                          

        # 定义forward函数 (x为状态)
    def forward(self, x):    
        # 连接输入层到隐藏层,且使用激励函数ReLU来处理经过隐藏层后的值
        x = F.relu(self.fc1(x))  
        # 连接隐藏层到输出层,获得最终的输出值 (即动作值)
        actions_value = self.out(x)      
        # 返回动作值
        return actions_value                                                    

定义DQN类(两个网络):

class DQN(object):
    def __init__(self):    
        # 定义DQN的一系列属性
        # 利用Net创建两个神经网络: 评估网络和目标网络
        self.eval_net, self.target_net = Net(), Net()                          
        self.learn_step_counter = 0                                             # for target updating
        self.memory_counter = 0                                                 # for storing memory
         # 初始化记忆库,一行代表一个transition
        self.memory = np.zeros((MEMORY_CAPACITY, N_STATES * 2 + 2))  
        # 使用Adam优化器 (输入为评估网络的参数和学习率)
        self.optimizer = torch.optim.Adam(self.eval_net.parameters(), lr=LR)    
        # 使用均方损失函数 (loss(xi, yi)=(xi-yi)^2)
        self.loss_func = nn.MSELoss()                                           

    def choose_action(self, x):   
        # 定义动作选择函数 (x为状态)
        # 将x转换成32-bit floating point形式,并在dim=0增加维数为1的维度
        x = torch.unsqueeze(torch.FloatTensor(x), 0)   
        # 生成一个在[0, 1)内的随机数,如果小于EPSILON,选择最优动作
        if np.random.uniform() < EPSILON:      
            # 通过对评估网络输入状态x,前向传播获得动作值
            actions_value = self.eval_net.forward(x)  
            # 输出每一行最大值的索引,并转化为numpy ndarray形式
            action = torch.max(actions_value, 1)[1].data.numpy()   
            # 输出action的第一个数
            action = action[0]                                                  
        else:   
            # 随机选择动作
            # 这里action随机等于0或1 (N_ACTIONS = 2)
            action = np.random.randint(0, N_ACTIONS)  
            # 返回选择的动作 (0或1)
        return action         
    
	# 定义记忆存储函数 (这里输入为一个transition)
    def store_transition(self, s, a, r, s_): 
        # 在水平方向上拼接数组
        transition = np.hstack((s, [a, r], s_))                                
        # 如果记忆库满了,便覆盖旧的数据
        # 获取transition要置入的行数
        index = self.memory_counter % MEMORY_CAPACITY    
        # 置入transition
        self.memory[index, :] = transition   
        # memory_counter自加1
        self.memory_counter += 1   
        
	# 定义学习函数(记忆库已满后便开始学习)
    def learn(self):                                                            
        # 目标网络参数更新
        # 一开始触发,然后每100步触发
        if self.learn_step_counter % TARGET_REPLACE_ITER == 0:   
            # 将评估网络的参数赋给目标网络
            self.target_net.load_state_dict(self.eval_net.state_dict()) 
        # 学习步数自加1
        self.learn_step_counter += 1                                            

        # 抽取记忆库中的批数据
        # 在[0, 2000)内随机抽取32个数,可能会重复
        sample_index = np.random.choice(MEMORY_CAPACITY, BATCH_SIZE)  
        
        # 抽取32个索引对应的32个transition,存入b_memory
        b_memory = self.memory[sample_index, :]  
        
        # 将32个s抽出,转为32-bit floating point形式,并存储到b_s中,b_s为32行4列
        b_s = torch.FloatTensor(b_memory[:, :N_STATES])
        
        # 将32个a抽出,转为64-bit integer (signed)形式,并存储到b_a中 (之所以为LongTensor类型,是为了方便后面torch.gather的使用),b_a为32行1列
        b_a = torch.LongTensor(b_memory[:, N_STATES:N_STATES+1].astype(int))
        
        # 将32个r抽出,转为32-bit floating point形式,并存储到b_s中,b_r为32行1列
        b_r = torch.FloatTensor(b_memory[:, N_STATES+1:N_STATES+2])
        
        # 将32个s_抽出,转为32-bit floating point形式,并存储到b_s中,b_s_为32行4列
        b_s_ = torch.FloatTensor(b_memory[:, -N_STATES:])
        
        # 获取32个transition的评估值和目标值,并利用损失函数和优化器进行评估网络参数更新
        # eval_net(b_s)通过评估网络输出32行每个b_s对应的一系列动作值,然后.gather(1, b_a)代表对每行对应索引b_a的Q值提取进行聚合
        q_eval = self.eval_net(b_s).gather(1, b_a)
        
        # q_next不进行反向传递误差,所以detach;q_next表示通过目标网络输出32行每个b_s_对应的一系列动作值
        q_next = self.target_net(b_s_).detach()
       
     	# q_next.max(1)[0]表示只返回每一行的最大值,不返回索引(长度为32的一维张量);.view()表示把前面所得到的一维张量变成(BATCH_SIZE, 1)的形状;最终通过公式得到目标值
        q_target = b_r + GAMMA * q_next.max(1)[0].view(BATCH_SIZE, 1)
       
    	# 输入32个评估值和32个目标值,使用均方损失函数
        loss = self.loss_func(q_eval, q_target)
        
        self.optimizer.zero_grad()                                      # 清空上一步的残余更新参数值
        loss.backward()                                                 # 误差反向传播, 计算参数更新值
        self.optimizer.step()                                           # 更新评估网络的所有参数

训练DQN:

for i in range(400):                                                    # 400个episode循环
    print('<<<<<<<<<Episode: %s' % i)
    s = env.reset()                                                     # 重置环境
    episode_reward_sum = 0                                              # 初始化该循环对应的episode的总奖励

    while True:                                                         # 开始一个episode (每一个循环代表一步)
        env.render()                                                    # 显示实验动画
        a = dqn.choose_action(s)                                        # 输入该步对应的状态s,选择动作
        s_, r, done, info = env.step(a)                                 # 执行动作,获得反馈

        # 修改奖励 (不修改也可以,修改奖励只是为了更快地得到训练好的摆杆)
        x, x_dot, theta, theta_dot = s_
        r1 = (env.x_threshold - abs(x)) / env.x_threshold - 0.8
        r2 = (env.theta_threshold_radians - abs(theta)) / env.theta_threshold_radians - 0.5
        new_r = r1 + r2

        dqn.store_transition(s, a, new_r, s_)                 # 存储样本
        episode_reward_sum += new_r                           # 逐步加上一个episode内每个step的reward

        s = s_                                                # 更新状态

        if dqn.memory_counter > MEMORY_CAPACITY:              # 如果累计的transition数量超过了记忆库的固定容量2000
            # 开始学习 (抽取记忆,即32个transition,并对评估网络参数进行更新,并在开始学习后每隔100次将评估网络的参数赋给目标网络)
            dqn.learn()

        if done:       # 如果done为True
            # round()方法返回episode_reward_sum的小数点四舍五入到2个数字
            print('episode%s---reward_sum: %s' % (i, round(episode_reward_sum, 2)))
            break                                             # 该episode结束

4. 总结

在本章内容中,我们对Q-learning和DQN 算法有了初步的了解,其中DQN 作为深度强化学习的基础,掌握该算法才算是真正入门了深度强化学习,接下来还有会更多的深度强化学习算法等待我们去探索。

5. 参考文献

[1]Playing Atari with Deep Reinforcement Learning

[2]zhuanlan.zhihu.com/p/260703124

关注微信公众号【潜在科技】,了解相关信息

行者AI(成都潜在人工智能科技有限公司,xingzhe.ai)致力于使用人工智能和机器学习技术提高游戏和文娱行业的生产力,并持续改善行业的用户体验。我们有内容安全团队、游戏机器人团队、数据平台团队、智能音乐团队和自动化测试团队。> >如果您对世界拥有强烈的好奇心,不畏惧挑战性问题;能够容忍摸索过程中的各种不确定性、并且坚持下去;能够寻找创新的方式来应对挑战,并同时拥有事无巨细的责任心以确保解决方案的有效执行。那么请将您的个人简历、相关的工作成果及您具体感兴趣的职位提交给我们。我们欢迎拥抱挑战、并具有创新思维的人才加入我们的团队。请联系:hr@xingzhe.ai

如果您有任何关于内容安全、游戏机器人、数据平台、智能音乐和自动化测试方面的需求,我们也非常荣幸能为您服务。可以联系:contact@xingzhe.ai