AI

基于Deep Q-Network算法和highway-env仿真环境的车道变更策略

Deep Q-Network

强化学习中的策略可以按照目标策略和行为策略进行分类:

  • 目标策略 target policy: 智能体要学习的策略
  • 行为策略 behavior policy: 智能体与环境交互的策略, 即用于生成行为的策略

Q-learning 是一种off-policy TD方法. 所谓off-policy就是指行为策略和目标策略不是同一个策略, 智能体可以通过离线学习自己或别人的策略来指导自己的行为; 与之相反, on-policy的行为策略和目标策略是同一个策略

Q-learning算法: 该算法中存在一张表格, 记录每个状态下执行每个动作所得到的Q值, 在选取动作时会进行表格的查阅, 然后选取Q值最大的动作

DQN算法: 当状态和动作为连续的, 无限的, 通过表格的方式记录就不合理了, 采用神经网络来替代表格, 输入为状态, 输出为动作

原始论文:Playing Atari with Deep Reinforcement Learning

算法流程:

对于每个状态, 我们会给出一个策略, 智能体会执行一个动作, 并得到相应的奖励. 我们训练的目的是使得累计的奖励最大

1. 经验池的储存: 输入状态, 输出动作, 我们将每一次的[当前状态, 动作, 奖励, 下一状态]存储进经验池, 当经验池满容量时, 进行训练, 此后有新的样本将不断替换经验池中的原有样本, 反复训练

2. 双网络:在DQN中存在两个神经网络,一个是Q-target,一个是Q-eval。其中Q-eval网络会不断进行训练和Q值的更新,而Q-target则是相对固定,只有经过固定训练轮次后才会与Q-eval同步。损失函数就是两个网络的Q值之差,我们需要使其差值越来越小。如果两个网络都在改变,很有可能会“错过”,当固定一个网络时,网络的训练会更有目标性,即更容易收敛。

3. e-greedy算法:上述我们在选择动作时,一般是选择Q值最大的那个动作。但如果有的执行动作不被选择过,它的Q值将始终是0,因此可能永远都不会被选择。所以我们引入动作选择时的随机性,使其具有一定概率去随机选择动作而不是完全依靠Q值的大小选择最优动作。

我尝试了从头开始构建DQN算法,最后代码可以跑通,但看起来小车在每一步的交互中没有为其后面的策略带来优化;后面我发现,在stable_baselines3这个库中已经集成了DQN算法,于是我直接调用再次尝试。

highway-env

在用Carla进行仿真之前, 打算先用highway-env简单上手一下RL, 了解到一个自动驾驶模拟环境highway-env, 由Edouard Leurent开发和维护, 其中包含6个场景

– 高速公路”highway-v0″

– 汇入”merge-v0″

– 环岛”parking-v0″

– 十字路口”intersection-v0″

– 赛车道”racetrack-v0″

[官方文档](highway-env Documentation)

总共分为四个步骤

– 安装环境

– 配置环境

– 训练模型

– 总结

安装环境

问题记录

1. gym版本变更导致参数报错(https://github.com/boyu-ai/Hands-on-RL/issues/30)

2. torchvision已经安装好了但是无法import

解决: 因为我安装了多个python版本, 所以要先check一下torchvision安装到哪个版本下然后切换到对应的版本

3.

4. 环境不存在

解决: 不要直接import gym而是import gymnasium as gym

Code-v1

import torch
import torch.nn as nn
from torch.autograd import Variable
import torch.nn.functional as F
import torch.optim as optim
import torchvision.transforms as T
from torch import FloatTensor, LongTensor, ByteTensor
from collections import namedtuple
import random 

# DQN网络模型基本参数
Tensor = FloatTensor

EPSILON = 0    # epsilon used for epsilon greedy approach
GAMMA = 0.9
TARGET_NETWORK_REPLACE_FREQ = 40       # target网络更新频率
MEMORY_CAPACITY = 100 # 经验库容量
BATCH_SIZE = 80 # 批量训练
LR = 0.01         # 学习率

# 实现DQN网络类
class DQNNet(nn.Module):
    def __init__(self):
        super(DQNNet,self).__init__()       
        
        # 定义两个线性层, 用于处理输入数据           
        self.linear1 = nn.Linear(35,35)
        self.linear2 = nn.Linear(35,5)               
    def forward(self,s):
        s=torch.FloatTensor(s)        
        s = s.view(s.size(0),1,35)        
        s = self.linear1(s)
        s = self.linear2(s)
        return s           

# 实现DQN算法主要逻辑
class DQN(object):
    def __init__(self):
        self.net,self.target_net = DQNNet(),DQNNet()        
        self.learn_step_counter = 0      
        self.memory = []
        self.position = 0 
        self.capacity = MEMORY_CAPACITY       
        self.optimizer = torch.optim.Adam(self.net.parameters(), lr=LR)
        self.loss_func = nn.MSELoss()

    # 动作选择, 遵循e-greedy算法
    def choose_action(self,s,e):
        x=np.expand_dims(s, axis=0) # 拓展数组的形状和维度, 使其满足输入格式
        if np.random.uniform() < 1-e:  # 从(0, 1)范围内随机取值
            actions_value = self.net.forward(x)  # 向前传播          
            action = torch.max(actions_value,-1)[1].data.numpy()
            action = action.max()    # 选取Q值最大的一个动作       
        else: 
            action = np.random.randint(0, 5) # 随机选取动作
        return action

    # 放入经验库
    def push_memory(self, s, a, r, s_):
        if len(self.memory) < self.capacity:
            self.memory.append(None)
        self.memory[self.position] = Transition(torch.unsqueeze(torch.FloatTensor(s), 0),torch.unsqueeze(torch.FloatTensor(s_), 0),\
                                                torch.from_numpy(np.array([a])),torch.from_numpy(np.array([r],dtype='float32')))#
        self.position = (self.position + 1) % self.capacity
        # 如果有超过经验库容量的样本,则从头开始替换经验库里的样本
    # 随机选取batch个样本进行训练
    def get_sample(self,batch_size):
        sample = random.sample(self.memory,batch_size)
        return sample

    # 训练
    def learn(self):
        if self.learn_step_counter % TARGET_NETWORK_REPLACE_FREQ == 0:
            self.target_net.load_state_dict(self.net.state_dict())
        self.learn_step_counter += 1
        
        transitions = self.get_sample(BATCH_SIZE)
        batch = Transition(*zip(*transitions))

        b_s = Variable(torch.cat(batch.state))
        b_s_ = Variable(torch.cat(batch.next_state))
        b_a = Variable(torch.cat(batch.action))
        b_r = Variable(torch.cat(batch.reward))    
             
        q_eval = self.net.forward(b_s).squeeze(1).gather(1,b_a.unsqueeze(1).to(torch.int64)) 
        q_next = self.target_net.forward(b_s_).detach() #
        q_target = b_r + GAMMA * q_next.squeeze(1).max(1)[0].view(BATCH_SIZE, 1).t()           
        loss = self.loss_func(q_eval, q_target.t())        
        self.optimizer.zero_grad() # reset the gradient to zero        
        loss.backward()
        self.optimizer.step() # execute back propagation for one step       
        return loss
Transition = namedtuple('Transition',('state', 'next_state','action', 'reward'))


import gymnasium as gym
import highway_env
from matplotlib import pyplot as plt
import numpy as np
import time
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'

# 配置环境参数
config = \
    {
    "observation": 
         {
        "type": "Kinematics",
        "vehicles_count": 5,
        "features": ["presence", "x", "y", "vx", "vy", "cos_h", "sin_h"],
        "features_range": 
            {
            "x": [-100, 100],
            "y": [-100, 100],
            "vx": [-20, 20],
            "vy": [-20, 20]
            },
        "absolute": False,
        "order": "sorted"
        },
    "simulation_frequency": 8,  # [Hz]
    "policy_frequency": 2,  # [Hz]
    }
    
env = gym.make("highway-v0")
env.configure(config)

# 初始化DQN对象, 设置count用于追踪训练过程
dqn=DQN()
count=0

reward=[]
avg_reward=0
all_reward=[]

time_=[]
all_time=[]

collision_his=[]
all_collision=[]

'''
训练循环

- reset环境并开始新的回合
- 每个回合中, 根据当前状态选择动作, 执行动作并接收新状态和奖励
- 存储转换到记忆库
- 每当记忆库中有足够的数据时, 执行学习步骤来更新网络
- 记录并可视化训练过程中的奖励, 时间和碰撞率
'''
while True:
    done = False    
    start_time=time.time()
    s = env.reset()[0]
    
    while not done:
        e = np.exp(-count/300)  #随机选择action的概率,随着训练次数增多逐渐降低
        a = dqn.choose_action(s,e)
        s_, r, done, truncated, info = env.step(a)
        env.render()
        
        dqn.push_memory(s, a, r, s_)
        
        if ((dqn.position !=0)&(dqn.position % 99==0)):
            loss_=dqn.learn()
            count+=1
            print('trained times:',count)
            # 每训练40次统计一次平均值
            if (count%40==0):
                avg_reward=np.mean(reward)
                avg_time=np.mean(time_)
                collision_rate=np.mean(collision_his)
                                
                all_reward.append(avg_reward)
                all_time.append(avg_time)
                all_collision.append(collision_rate)
                                
                plt.plot(all_reward)
                plt.show()
                plt.plot(all_time)
                plt.show()
                plt.plot(all_collision)
                plt.show()
                
                reward=[]
                time_=[]
                collision_his=[]
                
        s = s_
        reward.append(r)      
    
    end_time=time.time()
    episode_time=end_time-start_time
    time_.append(episode_time)
        
    is_collision=1 if info['crashed']==True else 0
    collision_his.append(is_collision)

把代码跑通了, 但得到的训练结果不理想(平均碰撞率恒为1), 并且env.render()这个函数并没有显示画面.

解决: render_mode=”rbg_array”

然后我看这个小车一直撞车, 感觉它并没有从之前的经验中学习到(也有可能是训练次数的问题), 于是我想先看看它的奖励函数是什么样的

没啥头绪…只能看着它一遍又一遍地跑, 但不知道怎么改. 一边等一边找些资料来看

Code-v2

参考了一下其他代码

Colin.Fang:基于DQN强化学习的高速路决策控制4 赞同 · 2 评论文章

这个代码只给出了训练100次得到的模型结果,并且模型性能不算好,输出不够直观

Code-v3

自己改写了一下代码,调整了一些参数,加了一些输出数据和图像的代码进去,方便实验记录,最后的完整代码上传至github

https://github.com/Yifu-Tian/Highway-env-DQN.git​github.com/Yifu-Tian/Highway-env-DQN.git

模型训练好之后就是评估模型,以下是一些关键的评估指标

1. 累积奖励

计算在单次回合或多个回合中模型获得的总奖励. 一个高效的变道模型应该能够在保持高速的同时避免碰撞,因此累积奖励会较高。

2. 平均奖励

将累积奖励除以回合数或时间步数,得到平均奖励。这反映了模型在每个时间步骤或每回合的平均表现。

3. 成功率

4. 碰撞率

实验结果记录与模型评估

我一共做了四组实验(不算完整),分别是训练100次,200次,500次和1000次得到的模型性能比较,把验证模型的步骤设为10回合,其他参数也同样保证一致。(这只是一个初步的实验,后续可以补充更详细的实验结果)

训练100次得到的模型结果表现一般,模型不稳定;训练200次得到的结果表现最优,大部分都是0.9或1.0的成功率;500次和1000次得到的模型最差,碰撞率在90%情况下都为1.0

总结

这一块内容给我最大的感受就是RL训练出的模型的不稳定性,因为自动驾驶不可能永远停留于纸面分析,哪怕碰撞率是0.1也不能接受。再者就是调参的技巧,后面接触到各种算法(比如PPO算法)也是需要的

AI相关的一切

留言

您的邮箱地址不会被公开。 必填项已用 * 标注