Carla 强化学习(一)

import glob
import os
import sys
import random
import time
import numpy as np
import cv2
import math
from collections import deque
import torch.cuda
from keras.applications.xception import Xception
from keras.layers import Dense, GlobalAveragePooling2D
from keras.optimizers import Adam
from keras.models import Model
from keras.callbacks import TensorBoard
import tensorflow as tf
import keras.backend.tensorflow_backend as backend
from threading import Thread
from tqdm import tqdm
try:
    sys.path.append(glob.glob('../carla/dist/carla-0.9.5-py3.7-win-amd64.egg' % (
        sys.version_info.major,
        sys.version_info.minor,
        'win-amd64' if os.name == 'nt' else 'linux-x86_64'))[0])
except IndexError:
    pass
##打开路径的引擎
import carla
SHOW_PREVIEW = True #打开摄像头
IM_WIDTH = 640
IM_HEIGHT = 480 #窗口大小
SECONDS_PER_EPISODE = 10 #每个 episode 的持续时间,以秒为单位。
REPLAY_MEMORY_SIZE = 5_000 #回放存储器的最大容量
MIN_REPLAY_MEMORY_SIZE = 1_000 #回放存储器的最小容量,达到这个容量后才开始训练模型
MINIBATCH_SIZE = 16 #每次从回放存储器中取出的样本数量
PREDICTION_BATCH_SIZE = 1 #模型进行预测时一次性输入的样本数量
TRAINING_BATCH_SIZE = MINIBATCH_SIZE // 4 #每次训练模型时一次性输入的样本数量
UPDATE_TARGET_EVERY = 5 #每经过多少个 episode 后更新模型的目标网络
MODEL_NAME = "Xception" #使用的模型的名称

MEMORY_FRACTION = 0.4 #GPU 可用内存所占比例
MIN_REWARD = -200 #最小奖励值,用于判断是否训练成功
EPISODES = 100 #总共玩的游戏 episode 的数量 在每个 episode 中,智能体从环境中接收一个状态,并根据该状态执行一个动作
DISCOUNT = 0.99 #折扣系数,用于计算未来的奖励值对当前奖励值的影响
epsilon = 1 #用于探索的参数,初始值为 1,随着训练不断降低
EPSILON_DECAY = 0.95 ## 0.9975 99975 epsilon 的下降速率,每个 episode 完成后乘以该值降低 epsilon
MIN_EPSILON = 0.001 #epsilon 的最小值,探索结束后不再降低 一种常用的强化学习策略,它在智能体进行动作选择时,以一定的概率epsilon随机选择一个动作,以1-epsilon的概率选择当前状态下估计值最大的动作
AGGREGATE_STATS_EVERY = 10 #每隔多少个 episode 统计一次训练状态信息和输出日志

class ModifiedTensorBoard(TensorBoard):

    # Overriding init to set initial step and writer (we want one log file for all .fit() calls)
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        self.step = 1
        self.writer = tf.summary.FileWriter(self.log_dir)
    def set_model(self, model):
        pass
    def on_epoch_end(self, epoch, logs=None):
        self.update_stats(**logs)
    def on_batch_end(self, batch, logs=None):
        pass
    def on_train_end(self, _):
        pass
    def update_stats(self, **stats):
        self._write_logs(stats, self.step)
'''
这段 Python 代码定义了一个自定义的 Tensorboard 类 ModifiedTensorBoard,
它继承自 TensorBoard 类,并覆盖了一些 TensorBoard 类的方法
'''
class CarEnv:
    SHOW_CAM = SHOW_PREVIEW #给相机打开参数初始化赋值
    STEEP_AMT = 1.0 #表示车辆转向量的大小
    im_width = IM_WIDTH #摄像头窗口初始化赋值
    im_height = IM_HEIGHT #摄像头窗口初始化赋值
    front_camera = None #前置摄像头数据初始化为0
    def __init__(self):
        self.client = carla.Client("localhost", 2000)
        self.client.set_timeout(4.0)
        self.world = self.client.get_world()
        self.world = self.client.load_world('Town06')
        self.blueprint_library = self.world.get_blueprint_library()
        self.module_3 = self.blueprint_library.filter("model3")[0]
##初始化连接,选地图,选车
    def collision_data(self, event):
        self.collision_hist.append(event)
##给出碰撞信息处理

    def process_img(self, image):
        i = np.array(image.raw_data)
        #print(i.shape)
        i2 = i.reshape((self.im_height, self.im_width, 4))
        i3 = i2[:, :, :3]
        if self.SHOW_CAM:
            cv2.imshow("", i3)
            cv2.waitKey(1)
        self.front_camera = i3
##处理摄像头图像函数

    def step(self, action):##可以选择的动作
        if action == 0:
            self.vehicle.apply_control(carla.VehicleControl(throttle=1.0, steer=-1*self.STEER_AMT))##油门为1,方向设置左拐一点
        elif action == 1:
            self.vehicle.apply_control(carla.VehicleControl(throttle=1.0, steer= 0))##油门为1,直走
        elif action == 2:
            self.vehicle.apply_control(carla.VehicleControl(throttle=1.0, steer=1*self.STEER_AMT))##油门为1,右拐一点,此时的转角单位为弧度

        v = self.vehicle.get_velocity()##得到当前小车速度
        kmh = int(3.6 * math.sqrt(v.x**2 + v.y**2 + v.z**2))##换成时速
        if len(self.collision_hist) != 0:
            done = True
            reward = -200##如果撞车,扣分
        elif kmh < 50:
            done = False
            reward = -1##如果很慢,减一点点分
        elif action != 1:
            done = False
            reward = -1##如果转一次方向盘
        else:
            done = False
            reward = 1

        if self.episode_start + SECONDS_PER_EPISODE < time.time():
            done = True##episode的开始时间 加上 每个episode应该运行的时间 ,预期该停止的时间和当前时间作比较,大于了就退出

        return self.front_camera, reward, done, None
##判断是否撞车,并且赋予数值

    def reset(self):
        self.collision_hist = []##撞击参数初始化
        self.actor_list = []#地图种的实物
        print('reset')##重置一次的提示
        self.transform = random.choice(self.world.get_map().get_spawn_points())##在地图选择一个随机生成位置
        self.vehicle = self.world.spawn_actor(self.model_3, self.transform)##生成汽车
        self.actor_list.append(self.vehicle)##在actor_list给出汽车的张量信息图像化

        self.rgb_cam = self.blueprint_library.find('sensor.camera.rgb')##选择摄像头蓝图
        self.rgb_cam.set_attribute("image_size_x", f"{self.im_width}")
        self.rgb_cam.set_attribute("image_size_y", f"{self.im_height}")##摄像头窗口尺寸
        self.rgb_cam.set_attribute("fov", f"110")##设置视场角为110

        transform = carla.Transform(carla.Location(x=2.5, z=0.7))##设置摄像头位置
        self.sensor = self.world.spawn_actor(self.rgb_cam, transform, attach_to=self.vehicle)##生成摄像头
        self.actor_list.append(self.sensor)##actor_list生成摄像头信息
        self.sensor.listen(lambda data: self.process_img(data))##处理摄像头信息

        self.vehicle.apply_control(carla.VehicleControl(throttle=0.0, brake=0.0))##将车辆的控制设置为油门和刹车都为零,这样就可以让车辆保持静止
        time.sleep(4)
        colsensor = self.blueprint_library.find("sensor.other.collision")##添加碰撞传感器蓝图
        self.colsensor = self.world.spawn_actor(colsensor, transform, attach_to=self.vehicle)##生成模型
        self.actor_list.append(self.colsensor)##将其添加至张量
        self.colsensor.listen(lambda event: self.collision_data(event))##监听过程

        while self.front_camera is None: #保证摄像头正确初始化
            time.sleep(0.01)

        self.episode_start = time.time() #记录训练开始时间
        self.vehicle.apply_control(carla.VehicleControl(throttle=0.0, brake=0.0))

        return self.front_camera ##返回摄像头信息


class DQNAgent:
    def __init__(self):
        self.model = self.create_model()#使用“self.create_model()”方法创建一个神经网络模型,并将其保存在“self.model”变量中
        self.target_model = self.create_model()#创建一个新的神经网络模型,并将其保存在“self.target_model”变量中
        self.target_model.set_weights(self.model.get_weights())#获取神经网络模型的权重,并使用“self.target_model.set_weights()”方法将这些权重设置到目标模型中。这样,目标模型就与神经网络模型完全相同

        self.replay_memory = deque(maxlen=REPLAY_MEMORY_SIZE)#创建一个双向队列对象“replay_memory”,用于存储训练数据。这个队列的最大长度被设置为“REPLAY_MEMORY_SIZE”,这是一个常量值

        self.tensorboard = ModifiedTensorBoard(log_dir=f"logs/{MODEL_NAME}-{int(time.time())}")#创建一个“ModifiedTensorBoard”对象,“tensorboard”,用于记录训练过程中的数据。
        # 这个对象会将数据写入一个名为“logs”的目录下,目录名包括了模型名称和当前时间戳
        self.target_update_counter = 0# '''代码初始化一些计数器和标志变量,用于控制训练过程的执行。这些变量包括“target_update_counter”(用于记录目标模型更新的次数)、
        self.graph = tf.get_default_graph()# “graph”(用于保存TensorFlow计算图)、
        self.terminate = False# “terminate”(用于标记是否结束训练)、
        self.last_logged_episode = 0# “last_logged_episode”(用于记录最后一次记录日志的训练轮数)
        self.training_initialized = False# 和“training_initialized”(用于标记训练是否已经初始化)
##强化学习初始化模型,self.training_initialized = False

    def create_model(self):
        base_model = Xception(weights=None, include_top=False, input_shape=(IM_HEIGHT, IM_WIDTH,3))
        # 创建一个 Xception 模型,Xception 模型是一种深度卷积神经网络模型该模型使用预先训练好的权重,
        # 并将顶部的全连接层(即分类器)去除,以便在其上添加自定义的分类器。输入形状为(IM_HEIGHT, IM_WIDTH, 3),即图像的高、宽和通道数。
        x = base_model.output
        #这行代码是在 Keras 中获取 base_model 的输出张量 output,并将其赋值给变量 x。变量 x 是一个 Keras 张量,
        # 具有形状 (batch_size, output_height, output_width, num_channels),代表经过 Xception 模型的特征图。
        # 在这个函数中,后续会在 x 上添加全局平均池化层和全连接层,以构建一个完整的卷积神经网络模型。
        x = GlobalAveragePooling2D()(x)
        #在模型的输出上添加一个全局平均池化层,以减少模型的参数数量
        predictions = Dense(3, activation="linear")(x)
        # 在全局平均池化层的输出上添加一个具有 3 个神经元的全连接层,用于输出模型的预测结果。
        # 因为本模型的目标是预测汽车的转向角度,因此输出层的激活函数为线性激活函数。
        model = Model(inputs=base_model.input, outputs=predictions)
        #这个模型由 Model 类构建而成,其输入和输出分别为 base_model.input 和 predictions。其中,base_model.input 表示输入层,
        # 即模型的输入数据,它对应于预训练的 Xception 模型的输入张量,具有形状 (batch_size, IM_HEIGHT, IM_WIDTH, 3),
        # 其中 batch_size 表示批大小,即一次训练的样本数。predictions 表示输出层,它是一个全连接层,其输入为 Xception 模型的输出,即特征向量,输出为一个长度为 3 的向量,分别代表三个操作的预测值
        model.compile(loss="mse", optimizer=Adam(lr=0.001), metrics=["accuracy"])
        #通过调用 model.compile() 函数,对模型进行编译,使用 MSE 作为损失函数,Adam 优化器进行训练,
        # 同时计算模型的准确率作为性能指标。编译之后,该模型可以用于训练和预测。
        return model

    def update_replay_memory(self, transition):
        # transition = (current_state, action, reward, new_state, done)
        self.replay_memory.append(transition)
        '''这段代码实现了智能体的经验回放(Experience Replay)功能,用于训练深度强化学习模型。update_replay_memory 函数的输入参数 transition 是一个元组,
    包含了智能体在环境中执行一次动作后的五个信息:当前状态 current_state、执行的动作 action、获得的奖励 reward、新的状态 new_state、以及智能体是否到达了
    终止状态 done。这五个信息被保存在一个叫做 replay_memory 的列表中。经验回放的基本思想是:将智能体在环境中的交互经验存储在一个经验池中,然后随机地从经验池中
    抽取一批数据,用于训练深度强化学习模型。这种方法可以解决许多深度强化学习模型训练中的问题,如解决数据相关性问题、加速收敛过程、提高训练稳定性等。在这段代码中,
    每次调用 update_replay_memory 函数时,都会将 transition 添加到 replay_memory 列表中。列表中存储的经验数据越多,就越有可能提取到具有代表性的数据,
    从而训练出更加鲁棒和通用的深度强化学习模型。 
        '''
    def train(self):
        '''
        在函数开始时加了一层判断,如果当前智能体的经验回放池中的数据不足 MIN_REPLAY_MEMORY_SIZE 条,则不进行训练,直接返回。
        '''
        if len(self.replay_memory) < MIN_REPLAY_MEMORY_SIZE:
            return

        minibatch = random.sample(self.replay_memory, MINIBATCH_SIZE)
        '''
        这段代码实现了从经验回放池中随机抽取一批数据进行训练的功能,被称为 minibatch 训练。这种方法在深度强化学习中非常常见,
        可以有效解决数据相关性、加速训练和提高泛化能力等问题。从replay_memory中抽取MINIBATCH_SIZE个作为训练
        '''
        current_states = np.array([transition[0] for transition in minibatch])/255
        '''transition[0],抽取每个序列的第一项 current_state
        将 minibatch 数据集中的所有当前状态取出,并将像素值除以255进行归一化处理。在深度强化学习中,输入的状态通常是图像或数值型向量,
        需要进行归一化处理,以提高模型的训练效果。最终,这个 current_states 数组被用作训练深度强化学习模型的输入。
        '''
        with self.graph.as_default():
            current_qs_list = self.model.predict(current_states, PREDICTION_BATCH_SIZE)
        '''
        这段代码实现了使用当前的状态输入,通过深度强化学习模型预测当前状态下各个可能行动的 Q 值的功能。
        这里的 self.graph.as_default() 是 TensorFlow 的一种上下文管理器,它可以确保代码中所有 TensorFlow操作都在同一个计算图中进行,
        避免不同计算图之间的操作发生冲突。self.model.predict(current_states, PREDICTION_BATCH_SIZE) 表示对当前状态 current_states
        输入到深度强化学习模型中进行预测,返回每个行动的 Q 值的预测结果。其中 PREDICTION_BATCH_SIZE 是一个常量,表示在进行预测时
        每次输入到模型中的样本数,通常为一个比较小的数,如32或64等。最终,这个 current_qs_list 数组表示当前状态下各个可能行动的 Q 值的预测结果。
        '''
        new_current_states = np.array([transition[3] for transition in minibatch])/255
        with self.graph.as_default():
            future_qs_list = self.target_model.predict(new_current_states, PREDICTION_BATCH_SIZE)
        '''transition[3],抽取每个序列的第一项 new_current_states
        使用经验回放池中随机抽取的一批数据,在模型中进行预测并计算目标 Q 值的功能。
        new_current_states = np.array([transition[3] for transition in minibatch])/255 表示将 minibatch 数据集中的所有新状态取出,
        并将像素值除以255进行归一化处理。self.target_model.predict(new_current_states, PREDICTION_BATCH_SIZE) 表示使用目标
         Q 网络对新状态输入到深度强化学习模型中进行预测,返回每个行动的 Q 值的预测结果。目标 Q 网络与训练 Q 网络的结构相同,但是
         目标 Q 网络的参数是从训练 Q 网络中定期更新得到的,目的是减少目标 Q 值的不稳定性,提高训练的效率和稳定性。最终,这个 future_qs_list
          数组表示新状态下各个可能行动的 Q 值的预测结果。
        '''
        X = []
        y = []
        '''
        初始化x,y
        初始化了两个空列表 X 和 y,它们将用于存储从经验回放池中随机抽取的一批数据的特征和目标值。
        在深度强化学习中,通常将当前状态作为输入,将目标 Q 值作为输出。因此,X 和 y 分别对应于输入特征和输出目标。在训练过程中,
        我们将从经验回放池中随机抽取一批数据,将其中的特征和目标值分别添加到 X 和 y 列表中,以用于更新深度强化学习模型的参数。
        具体地,X 列表中的每个元素都是当前状态,y 列表中的每个元素都是目标 Q 值。
        '''
        for index, (current_state, action, reward, new_state, done) in enumerate(minibatch):
            if not done:#如果没完成,并利用奖励和折扣因子计算出新的 Q 值
                max_future_q = np.max(future_qs_list[index])
                new_q = reward + DISCOUNT * max_future_q
            else:#如果完成了,新的Q值便是本身
                new_q = reward

            current_qs = current_qs_list[index]
            current_qs[action] = new_q
            '''
            将当前状态对应的 Q 值数组中的行动值(action)更新为新的 Q 值,这里用到了 numpy 数组的切片赋值操作。
            将当前状态和更新后的 Q 值分别添加到 X 和 y 列表中,以便用于训练模型的参数。
            '''
            X.append(current_state)
            y.append(current_qs)

        log_this_step = False
        if self.tensorboard.step > self.last_logged_episode:
            log_this_step = True
            self.last_log_episode = self.tensorboard.step
        '''
        这段代码主要是用来控制何时记录模型的训练日志到 TensorBoard 中。具体来说,当当前的训练步数(self.tensorboard.step)
        大于上一次记录日志时的步数(self.last_logged_episode)时,就将 log_this_step 标记为 True,表示需要记录当前步数的日志。
        然后将 self.tensorboard.step 赋值给 self.last_log_episode,更新上一次记录日志时的步数。这样做是为了避免在
        训练过程中记录过多的日志数据,导致 TensorBoard 的性能下降。只有当训练步数有一定增量时才记录日志,以达到较好的记录效果
        '''
        with self.graph.as_default():
            self.model.fit(np.array(X)/255, np.array(y), batch_size=TRAINING_BATCH_SIZE, verbose=0, shuffle=False, callbacks=[self.tensorboard] if log_this_step else None)
        '''
        这段代码是使用 self.model.fit() 方法来训练模型,其中的参数意义如下:
            np.array(X)/255:训练数据,即当前状态。X 是一个列表,其中每个元素是一组当前状态。将其转换为 Numpy 数组后,再将像素值缩放到 0~1 范围内。
            np.array(y):训练目标,即当前状态下每个动作的 Q 值。y 是一个列表,其中每个元素是一组当前状态下每个动作的 Q 值。将其转换为 Numpy 数组。
            batch_size:每次训练时使用的样本数。
            verbose:控制训练过程中的输出信息。verbose=0 表示不输出任何信息,verbose=1 表示输出进度条,verbose=2 表示输出每个 epoch 的训练结果。
            shuffle:是否在每个 epoch 开始时随机打乱数据。
            callbacks:训练过程中的回调函数列表。这里将 self.tensorboard 回调函数添加到回调列表中,表示训练过程中需要记录日志到 TensorBoard 中。如果 log_this_step 为 False,则回调函数列表为空,表示不记录日志。
            with self.graph.as_default(): 表示在训练模型之前使用默认图作为计算图,以确保在 TensorFlow 的多线程环境下正确运行模型训练过程。
        '''

        if log_this_step:##如果步数增加
            self.target_update_counter += 1

        if self.target_update_counter > UPDATE_TARGET_EVERY:
            #更新神经网络权重
            '''
            如果self.target_update_counter的值大于变量UPDATE_TARGET_EVERY的值,就执行下一行代码,否则跳过。
            在执行这一行代码时,会将self.model的权重复制到self.target_model中,并将self.target_update_counter的值重置为0。
            这个操作是为了保证每隔一段时间,目标模型的参数会与训练模型的参数同步,从而使得训练更加稳定。
            '''
            self.target_model.set_weights(self.model.get_weights())
            self.target_update_counter = 0

    def get_qs(self, state):
        return self.model.predict(np.array(state).reshape(-1, *state.shape)/255)[0]
    '''
    这个函数用于计算当前状态state下所有可能的行动的Q值。它首先将state转化为一个张量,然后通过self.model对这个张量进行前向传播,
    得到每个可能行动对应的Q值。最后,它返回一个长度为行动数目的一维向量,其中第i个元素是执行第i个行动的Q值。
    '''
    def train_in_loop(self):
        X = np.random.uniform(size=(1, IM_HEIGHT, IM_WIDTH, 3)).astype(np.float32)
        y = np.random.uniform(size=(1, 3)).astype(np.float32)
        '''
        函数定义了X和y两个变量,它们的作用是存储训练过程中生成的样本数据和对应的标签。其中X是一个四维张量,它的第一个维度表示样本数,
        这里设为1;后三个维度表示图像的高、宽和通道数。而y则是一个二维张量,第一个维度表示样本数,这里也设为1;第二个维度表示标签的维数,这里为3,
        因为模型的输出有三个元素(分别表示向左、向前和向右的Q值)。在这里,X和y的初始值都是随机生成的。这个函数并没有返回任何值,只是用来定义X和y。
        它的目的是为了在训练过程中逐渐填充X和y,直到它们的大小达到指定的MIN_REPLAY_MEMORY_SIZE。
        '''
        with self.graph.as_default():
            self.model.fit(X,y, verbose=False, batch_size=1)
        '''
        使用TensorFlow深度学习框架构建一个神经网络模型,并使用模型的fit()函数进行训练。
        更具体地说,代码中的self.model是一个已经构建好的神经网络模型,而X和y则是训练数据的输入和输出,分别表示特征矩阵和目标向量
        使用fit()函数进行训练时,verbose参数设为False表示不输出训练过程中的详细信息,batch_size参数设为1表示每次只训练一个样本
        代码中的with self.graph.as_default()语句是为了确保在构建和训练模型时使用的是当前默认的计算图,因为在TensorFlow中可以同时存在多个计算图。
        '''
        self.training_initialized = True
        '''
        这个变量用于记录模型是否已经被初始化过,从而避免重复初始化。
        在深度学习中,通常需要在训练和测试之间切换。当开始训练时,需要初始化模型的一些参数,如权重和偏置。
        如果每次进行训练时都重新初始化模型,那么将会浪费很多时间。因此,可以设置一个标志位来记录是否已经完成了初始化。在后续的训练中,
        只需要在第一次训练时进行初始化,之后就可以直接使用已经初始化好的参数进行训练。在这里,将self.training_initialized设置为True,
        表示已经完成了模型的初始化。在后续的训练中,就可以直接使用已经初始化好的模型参数进行训练,而无需再次进行初始化。
        '''
        while True:
            '''
        这段代码是一个无限循环,其中包含一个if语句和一个self.train()函数。循环会一直执行,直到self.terminate变量被设置为True,才会跳出循环并结束程序。
        在每次循环中,会先判断self.terminate是否为True,如果是则直接返回,否则会执行self.train()函数进行模型的训练。然后,使用time.sleep(0.01)语句暂停0.01秒,再次进入下一轮循环。
        这种循环结构通常用于在模型训练过程中不断地更新模型,直到满足一定的停止条件。例如,可以设置一个最大的训练轮数、达到一定的精度、损失函数等条件,当满足这些条件时,
        将self.terminate设置为True,退出循环并结束程序。需要注意的是,这种循环结构如果没有合适的停止条件,可能会一直执行下去,导致程序陷入死循环,因此需要特别小心。
            '''
            if self.terminate:
                return
            self.train()
            time.sleep(0.01)
##强化学习代码
if __name__ == '__main__':
    FPS = 60
    '''
    FPS = 60 - 这是一个常量,表示帧率(Frames Per Second,每秒帧数),通常用于动画或视频游戏等应用程序中。
    这个值被设置为60,表示应用程序将以每秒60帧的速度运行。
    '''
    ep_rewards = [-200] #初始化目标奖励值
    random.seed(1)#用于确保每次运行程序时得到的随机数序列是一致的。这是非常有用的,在测试和调试时可以确保得到可重复的结果。
    np.random.seed(1)#Numpy中的伪随机数生成器的种子
    tf.random.set_seed(1)#设置了TensorFlow中的随机数生成器的种子,与上面的两个语句类似。这个语句通常用于确保在使用TensorFlow训练模型时,得到可重复的结果。

    gpus = tf.config.list_physical_devices('GPU')
    '''
    通过 tf.config.list_physical_devices('GPU') 查找可用的物理GPU设备。如果有可用的GPU设备,将启动对GPU的内存增长进行配置,
    以避免在使用TensorFlow时内存占用过多而导致程序崩溃。
    '''
    if gpus:
        try:
            for gpu in gpus:
               tf.config.experimental.set_memory_growth(gpu, True)
            '''
            参数gpu是GPU设备对象,True表示允许TensorFlow动态申请并增长GPU内存,以适应不同大小的计算图或批次大小。
            如果该参数为False,则TensorFlow会在GPU上分配固定大小的内存,而不考虑实际的内存使用情况
            '''
            for gpu in gpus:
                tf.config.experimental.set_virtual_device_configuration(
                    gpu,
                    [tf.config.experimental.VirtualDeviceConfiguration(
                        memory_limit=int(1024 * MEMORY_FRACTION)
                        #其中MEMORY_FRACTION是一个浮点数,表示GPU内存的使用比例
                    )]
                )
            '''
            将GPU设备对象列表中的每个GPU设备分配一个虚拟设备,并设置每个虚拟设备的内存限制为指定的大小。
            '''
        except RuntimeError as e:
            print(e)
#使用try-except语句块可以捕获这些运行时错误,并输出错误信息,以便开发者进行调试和优化
    if not os.path.isdir('models'):
        os.makedirs('models')
        #检查当前目录下是否存在名为"models"的文件夹,如果不存在则创建该文件夹
    agent = DQNAgent()
    env = CarEnv()
    trainer_thread = Thread(target=agent.train_in_loop, daemon=True)
    trainer_thread.start()
    '''
    这段代码创建了一个线程对象,用于在后台运行智能体的训练函数agent.train_in_loop()。
    具体来说,代码首先调用Thread()函数创建一个新的线程对象trainer_thread,并指定线程的执行函数为agent.train_in_loop()。
    此外,代码还指定了线程的daemon属性为True,表示这个线程是一个后台线程,当主线程结束时,该线程也会自动结束。
    这段代码的作用是将智能体的训练过程放在一个独立的线程中执行,从而避免阻塞主线程的执行。这样可以让智能体的训练和其他任务并行执行,
    提高程序的运行效率。同时,由于线程的daemon属性被设置为True,所以当主线程结束时,该线程也会自动结束,不会继续运行造成资源浪费。
    '''
    while not agent.training_initialized:
    #等待智能体完成初始化,等到为 True
        time.sleep(0.01)
    agent.get_qs(np.ones((env.im_height, env.im_width, 3)))
    #调用智能体(agent)的get_qs()函数,用于获取给定状态下的动作值(Action Value)估计
    '''
    代码中的np.ones((env.im_height, env.im_width, 3))表示一个形状为(env.im_height, env.im_width, 3)的三维数组,这个三维数组
    可以被理解为一个游戏界面的截图,用于表示当前的游戏状态。代码会将这个截图作为参数传递给agent.get_qs()函数,并调用该函数来获取当前状态下的动作值估计。
    '''
    for episode in tqdm(range(1, EPISODES + 1), ascii=True, unit='episodes'):
            '''
            它遍历了一个由range函数生成的整数序列,其中的每个数字代表一个episode。这个序列的起始点是1,结束点是EPISODES + 1(不包括EPISODES + 1),
            步长是1。这个序列通过tqdm函数进行迭代,它会在终端上展示一个进度条,让用户知道当前代码运行到了哪个episode。ascii=True表示使用ASCII字符显示进度条,
            unit='episodes'表示进度条上的单位是“episodes”。在循环体内,episode被用来记录当前的episode数。
            '''
            '''
            这段代码是训练智能体的主循环,循环中的代码逐步完成每个回合的游戏过程,并更新智能体的策略和模型参数。
            具体来说,代码首先使用for循环遍历每个回合,每个回合都从环境的初始状态开始,执行一系列的游戏动作直至游戏结束。
            在每个回合中,代码记录并统计当前回合的游戏奖励(episode_reward),并将每个动作的(state, action, reward, new_state, done)
            元组保存到智能体的经验回放池中(replay memory),用于后续的训练和策略更新。在每个时间步中,代码根据当前状态和当前策略选择一个动作。
            具体来说,代码使用epsilon-greedy策略,即以epsilon的概率随机选择一个动作,以1-epsilon的概率选择当前最优的动作(即具有最高动作值的动作)。
            这个随机选择的过程有助于增加策略的探索性,从而更好地探索游戏的状态空间,避免陷入局部最优解。在每个回合结束时,代码记录当前回合的统计信息,
            例如回合游戏奖励的平均值、最小值和最大值等。代码还检查当前回合的游戏奖励是否达到了预设的最小阈值(MIN_REWARD),如果达到了则保存智能体的模型参数
            (model)到指定目录,以便后续测试和使用。在每个回合结束时,代码还更新epsilon的值,以逐步减小策略的探索性,使得智能体更倾向于选择已经学习到的最优策略。
            具体来说,代码使用一个指数衰减函数将epsilon的值逐步减小,直至达到最小值(MIN_EPSILON)。
            这个epsilon的逐渐减小过程有助于使得智能体在后续训练中更加聚焦于已经学习到的最优策略,从而加快学习速度和提高模型性能。
            '''
            env.collision_hist = []#清空环境对象env中的碰撞历史记录(collision_hist);
            agent.tensorboard.step = episode#将tensorboard对象agent.tensorboard的step属性设置为当前的episode数,用于在tensorboard上展示训练过程中的统计信息;
            episode_reward = 0#初始化一个变量episode_reward,用于记录当前episode的累计奖励(即从起点到终点所有动作获得的奖励总和)。
            step = 1#初始化变量step,用于记录当前episode中的步数;
            current_state = env.reset()##通过调用环境对象env的reset方法,初始化状态变量current_state为环境的初始状态;
            done = False#初始化一个布尔变量done,用于记录当前episode是否已经结束(即小车是否到达终点或碰到障碍物);
            episode_start = time.time()#初始化一个变量episode_start,记录当前episode的开始时间。
            while True:#进入一个无限循环,直到当前episode结束;
                '''
                通过与一个随机数比较(随机数的范围在0到1之间),确定是使用agent的当前Q网络得到的最优动作还是随机动作;
                '''
                if np.random.random() > epsilon:
                    #如果随机数大于epsilon,则选择最优动作;
                    action = np.argmax(agent.get_qs(current_state))
                    '''
                    如果随机数小于等于epsilon,则随机选择一个动作。其中epsilon是一个用于控制动作探索程度的超参数,随着训练次数的增加而逐渐减小,用于在早期阶段更多地探索不同的动作,
                    后期阶段更多地利用学习到的经验。agent.get_qs()方法用于计算当前状态的Q值,并返回Q值最大的动作。np.argmax()方法用于返回Q值最大的动作的索引。
                    '''
                else:
                    action = np.random.randint(0, 3)
                    time.sleep(1/FPS)
                    #time.sleep()方法用于在执行动作前等待一段时间,以保证每秒钟的帧数不超过预定的FPS值
                new_state, reward, done, _ = env.step(action)#函数获取新的状态 new_state,回报 reward,以及是否结束当前回合的标志 done。
                episode_reward += reward#累加奖励,以便计算整个episode的奖励总和。
                agent.update_replay_memory((current_state, action, reward, new_state, done))
                '''
                #agent.update_replay_memory((current_state, action, reward, new_state, done)):
                将这个交互过程中的状态转移信息(current_state, action, reward, new_state, done)存储到经验回放缓存中,以便后续训练时使用。
                '''
                current_state = new_state#current_state = new_state:将当前状态更新为新的状态,以便下一次交互。
                step += 1#step += 1:累加步数计数器,以便统计整个episode的步数。
                if done:
                    break
                #if done: break:如果完成标志位done为真,表示当前episode结束,跳出循环。

            for actor in env.actor_list:
                actor.destroy()
            ep_rewards.append(episode_reward)#将本轮episode的总奖励添加到一个列表中,用于后续统计分析。
            if not episode % AGGREGATE_STATS_EVERY or episode == 1:
                '''
                这段代码是用于更新并记录agent在训练过程中的表现的。
                首先判断当前训练轮数episode是否为一个汇总统计周期(AGGREGATE_STATS_EVERY),或者是第一轮(episode == 1),如果是,则进行以下操作:
                计算最近AGGREGATE_STATS_EVERY轮的平均奖励(average_reward)、最小奖励(min_reward)和最大奖励(max_reward)。
                调用agent.tensorboard.update_stats()方法,将上述三个统计指标和当前的探索率(epsilon)记录到tensorboard中,用于后续的可视化和分析。
                如果最小奖励(min_reward)大于等于设定的最小奖励(MIN_REWARD),则保存当前训练好的模型(agent.model.save()方法),以备后续测试或应用。
                保存的模型文件名包括了最大奖励、平均奖励、最小奖励以及当前时间戳的信息,方便后续查找和区分。
                '''
                '''
                如果当前训练轮数episode不是一个汇总统计周期,也不是第一轮,则不进行上述操作,继续进行后续的训练。这样可以避免过多记录不必要的统计信息,减少训练过程的开销。
                整个代码段展示了如何使用tensorboard记录训练过程中的统计信息和训练结果,并根据设定的最小奖励阈值自动保存训练好的模型。
                这样可以方便地记录训练过程中的表现和结果,以便进行分析、调试和后续应用。
                '''
                average_reward = sum(ep_rewards[-AGGREGATE_STATS_EVERY:])/len(ep_rewards[-AGGREGATE_STATS_EVERY:])
                min_reward = min(ep_rewards[-AGGREGATE_STATS_EVERY:])
                max_reward = max(ep_rewards[-AGGREGATE_STATS_EVERY:])
                #计算最近AGGREGATE_STATS_EVERY轮的平均奖励(average_reward)、最小奖励(min_reward)和最大奖励(max_reward)。
                agent.tensorboard.update_stats(reward_avg=average_reward, reward_min=min_reward, reward_max=max_reward, epsilon=epsilon)
                # 调用agent.tensorboard.update_stats()方法,将上述三个统计指标和当前的探索率(epsilon)记录到tensorboard中,用于后续的可视化和分析。
                if min_reward >= MIN_REWARD:
                    agent.model.save(f'models/{MODEL_NAME}__{max_reward:_>7.2f}max_{average_reward:_>7.2f}avg_{min_reward:_>7.2f}min__{int(time.time())}.model')
                 #如果最小奖励(min_reward)大于等于设定的最小奖励(MIN_REWARD),则保存当前训练好的模型(agent.model.save()方法),以备后续测试或应用。
            if epsilon > MIN_EPSILON:
                '''
                这段代码用于在每轮训练结束后逐渐减小epsilon值,以实现在训练初期较多地进行探索,逐渐减少探索次数,增加利用已有经验的比例。
                首先判断当前的探索率epsilon是否大于最小探索率(MIN_EPSILON),如果大于,则进行以下操作:
                将当前的epsilon乘以一个衰减因子(EPSILON_DECAY),减小epsilon的值。
                将上述计算结果和最小探索率(MIN_EPSILON)比较,取两者中的最大值,确保epsilon不会小于最小探索率。
                通过不断降低epsilon的值,逐渐减少探索次数,使得agent能够更多地利用已有经验进行学习,提高学习效率和性能。
                同时,由于epsilon衰减的速度是可调的,可以根据具体情况进行调整,以获得更好的训练效果。
                '''
                epsilon *= EPSILON_DECAY
                epsilon = max(MIN_EPSILON, epsilon)
    agent.terminate = True
    trainer_thread.join()
    agent.model.save(f'models/{MODEL_NAME}__{max_reward:_>7.2f}max_{average_reward:_>7.2f}avg_{min_reward:_>7.2f}min__{int(time.time())}.model')
    '''
    这段代码用于在训练结束时停止trainer线程,保存训练好的模型,并将agent的terminate标志置为True,以便其他线程能够安全退出。
    首先将agent的terminate标志置为True,通知其他线程需要退出。
    然后调用trainer_thread.join()方法等待trainer线程结束,确保训练线程已经安全退出。
    最后,保存训练好的模型(agent.model.save()方法),并将文件名中包含最大奖励、平均奖励、最小奖励和当前时间戳的信息,以便后续查找和区分不同的模型文件。
    '''


  • 4
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值