【深度强化学习】如何平衡cpu和gpu来加快训练速度(实录)


问题抛出

之前学习的时候,学的是动手学强化学习,里面的代码也一直都是用gpu来训练,我也一直以为gpu训练的会比cpu训练的快,直到我做完一个项目的时候,发现里面给的代码没有用gpu加速,好奇之下,我把代码改成了gpu加速的版本,发现速度反而比只cpu运行的慢了。

于是我开始研究其中的原因。
本文实验例子:
ppo算法

问题展示

原代码:默认gpu,耗时2min8s
在这里插入图片描述
cuda展示:45%左右
在这里插入图片描述
cpu展示: 大概27%占用。
在这里插入图片描述

改为cpu训练后:耗时47.4s (经常cpu满载后,cpu会变慢,后续测时为53s)
原代码改一处地方:

device = torch.device("cpu")    

在这里插入图片描述
满载展示:
在这里插入图片描述

问题探索

参考:

1:强化学习是不是主要吃cpu而不怎么吃gpu?
2:基于pytorch的代码在GPU和CPU上训练时,训练输出结果不同问题
3.Pytorch-GPU模型和CPU模型输出不同

参考1:很好的解释了cpu训练速度快,gpu训练速度慢的原因:模型从cpu 拷贝到gpu花了大量的时间,且环境交互时的少量运算,cpu比gpu运算速度更快。

对于参考3,我给出了答案:【深度强化学习】关于同一设备上cpu和gpu计算结果不一致问题

如何平衡cpu和gpu来加快训练速度呢?

参考1给出了答案:cpu采样+gpu训练模型

cpu采样+gpu训练更快的原因:
1、在环境交互方面:强化学习时要进行大量的环境交互,也就是进行文件的读取操作或者少量计算操作。cpu对于文件读取和少量计算往往比gpu更快,更准确,是因为cpu有少量且强大的核心,设计时是为了专注于处理不同的任务。
2、在模型训练方面:模型中有大量的参数,但模型更新时都是简单的矩阵运算。gpu有大量的小核心,专注于图形处理和矩阵运算,在这方面速度比cpu要快。

《参考1》解决方法总结如下:
首先单独check 只进行“一个batch的训练”(也就是把准备好的batch数据喂给模型,进行一次foward + backward计算)使用GPU是否能明显提速?如果不是,说明模型小、样本小,仅使用CPU反而更快。如果是,参考上述例子,rollout过程只用CPU计算,模型训练用GPU计算。但是,具体问题还是要具体分析。

我这里对对经典环境(CartPole-v0)测试时:使用ppo算法
模型为:128单隐层
样本数:0 (on-line)

发现即使在这种情况下,也要比只用cpu运行程序要稍快上一些。
且 只用cpu运行时:cpu满载100%,cpu发热严重,风速噪音大。
而 cpu+gpu的方法:cpu 24%,cuda 25%,训练时几乎无声(笔记本)。

综上:利用cpu 进行环境采样+ 利用gpu训练模型的方法 最佳。 (不是)

解决问题

上述参考1 虽然解释的很清楚,但是由于他是在pymarl库下操作的,和pytorch在实现方法上有一些不同,使在我实际修改时遇到了一些问题。

实现逻辑:

模型都在cpu上生成,由cpu与环境交互,由gpu训练模型。

先展示正确改法:

改法逻辑:(参考1中的图)
在这里插入图片描述
在这里插入图片描述
第1处逻辑:在while not done: 前 ,即在环境采样前将模型参数上传到(copy到)cpu上,此时在while循环里就是用cpu来采样。
注:要在while前,而不是在get_action里,while前的话只需要在整个序列开始前进行一次copy就行
第2处逻辑:在模型训练前(更新前)将模型copy到gpu上。
注:最好不要在update函数里加,而是像如图所示这样改,加到上面一行

PPO算法示例:

版本:gym 0.26.2 (关于gym0.26.2的版本问题
torch 2.2.2+cu121

偷懒改法:

在原代码上:

from tqdm import tqdm
def train_on_policy_agent(env, agent, num_episodes):
    return_list = []
    for i in range(10):
        with tqdm(total=int(num_episodes/10), desc='Iteration %d' % i) as pbar:
            for i_episode in range(int(num_episodes/10)):
                episode_return = 0
                transition_dict = {'states': [], 'actions': [], 'next_states': [], 'rewards': [], 'dones': []}
                state = env.reset(seed =0)[0] #1.改 gym 0.26.0版本后,env.reset()返回的是一个字典,所以需要加上[0]
                agent.actor.to('cpu')
                done = False
                while not done:
                    action = agent.take_action(state) 
                    #next_state, reward, done, _ = env.step(action)[0:4] #2.改
                    next_state, reward,terminated, truncated, _ = env.step(action) #2.改看gym版本0.26.2版本的 
                    done = terminated or truncated
                    transition_dict['states'].append(state)
                    transition_dict['actions'].append(action)
                    transition_dict['next_states'].append(next_state)
                    transition_dict['rewards'].append(reward)
                    transition_dict['dones'].append(done)
                    state = next_state
                    episode_return += reward
                return_list.append(episode_return)
                agent.actor.to('cuda') #
                agent.critic.to('cuda')
                agent.update(transition_dict)
                if (i_episode+1) % 10 == 0:
                    pbar.set_postfix({'episode': '%d' % (num_episodes/10 * i + i_episode+1), 'return': '%.3f' % np.mean(return_list[-10:])})
                pbar.update(1)
    return return_list

修改两处即可:
在这里插入图片描述
第1,第2处修改的逻辑对应上述所写。
第一处为什么没加critic网络,是因为在take_action中的函数中,没有用到critic网络。

第三处修改

这里一定要把这个 to device注释掉,原因:采样要用cpu采样,这里device是gpu。
在这里插入图片描述
如果不注释掉就会报如下所示的错:

RuntimeError: Expected all tensors to be on the same device, but found
at least two devices, cpu and cuda:0! (when checking argument for
argument mat1 in method wrapper_CUDA_addmm)

意思是所有的张量只能在相同的设备上训练,但现在发现了两个设备。
(我一开始没理解过来,以为是只能要么cpu训练,要么gpu训练,差点放弃)
其实就是在处理张量的同时,不能两个一起用。
这里报错是因为actor是转到cpu里了,但是state copy到gpu里了,所以报错。

其他不用改了:
理由:model.to(device) 这里点to 意思是模型先在cpu上生成,再copy至gpu。(见参考2)

在这里插入图片描述
也就是说都是模型还是从cpu上生成的,只不过,后面这个to.(device)没什么用了。

再次修改–24.5.22

发现了critic的网络其实一直都用cuda在训练,因为在采样时用不到critic网络,所以不妨直接在初始化的时候将网络to(‘cuda’),然后将update前的critic to (‘cuda’)去掉。
好处:省下了copy的时间

不偷懒改法

强迫证患者:(不偷懒改法,只将上述.actor的to(device)注释掉,删掉)

修改总结1

一般来说,对于ppo这种类在线策略的深度学习算法:
1、对于原代码全是gpu训练的(上面例子),需要改三处,注意第三处,可改可不改第4处。
2、全cpu训练的,前两处加的不变,第三处的修改有变化,:(这里也要特别注意,否则也会报同时用两个设备的错误)
即:在模型训练时,把所有要计算的tensor张量加到gpu中。如下所示:
(s,a,r,s,done,advantage)
在这里插入图片描述
顺口记法:对于在线的策略(无经验池的策略),大体框架改两处,对应设备改一处,否则会报错。

最终成绩(不是)

试了几次:
有43.4s也有45.1s,平均成绩比只用cpu的快上4-7s左右。
在这里插入图片描述
cpu 和cuda展示:
在这里插入图片描述
cpu占用甚至比只用gpu的占用还低5%左右,原因应该是单gpu训练时,采样时copy到gpu耗时多;比单cpu的低75%多
gpu占用也比单gpu训练低23%左右。

结论:几乎在运用到深度网络的场景下,平衡cpu和gpu的方法比只单用一个的方法好的多。

学习深度强化学习的购买主机电脑建议:
cpu 的核数越大 速度越快。
gpu入门 有就行,如果要训练到图像的,则显存越大越好。
内存 越大越好。(有经验池时)

附加赛 (离线版本的修改)

当我沾沾自喜的想把我这个方法运用到我的项目时,发现又有报错:报错还是用了两个设备。
我仔细研究了代码上的区别:发现主要的不同是 我项目中的ppo里写了个经验池(ppo+经验池),其主要逻辑和离线学习的经验池逻辑差不多。
(但ppo严格来说还是在线学习策略)

这里给出两者的区别: (这里只是总体逻辑上训练时的区别,其他要update的参数得根据实际情况具体分析)

在线学习(无经验池的版本)
在这里插入图片描述
注意看这里的1处,它是在1轮eposide结束后,再开始更新。

严格探讨,(参考1)处的博客,是在采样序列后,再进行在经验池的更新
(两者为并列关系,这里先归为这一类)
在这里插入图片描述离线学习(有经验池版本)在这里插入图片描述
注意看这里2处,是在这个序列eposide里进行更新的。(采样序列包含了更新的关系)

所以在在线版本只要两处修改:
在这里插入图片描述
而离线版本还需要再加一处:即更新完毕之后,还要再把要采样环境的模型放回cpu上。
在这里插入图片描述

当然,在无经验池版本上也能加这一句,不是加在这里的第三处,而是加在while not done 的下一句,take_action的上一句。这样,逻辑上就和这里的第三处一样了:在采样前将模型转到cpu上。不过,速度上的优势就荡然无存了,会比单cpu的慢上3-5s左右,因为每个序列加上了模型从gpu到cpu的copy的时间。如下所示:
在这里插入图片描述
(为什么我这里离线版本是写在第三处,第一点是可以在经验池未满时,不用进行采样时的copy,二是写在这里简单好记。大家也可以根据逻辑自己改位置,不报错都行,比如,改到在take_action的函数里的第一句加to(cpu))

同理:离线版本 因为加上了模型从gpu到cpu的copy的时间,也比单cpu的慢了,具体是差不多每10000次copy慢2s。
这样这个方法就不是最快的了,于是继续修改。

比方说,这里是对比于单cpu的,这里是粗看每10000次copy慢2s,细分来看,也就是说每10000次copy的时间慢上10s而模型的gpu训练又快上8s,而导致的慢上2s。解决方法是让copy的次数变少,比方说这里copy的次数变为8000次,而更新还是10000次,那么结果上就会平衡掉慢的时间,不快不慢了。

计算方法:
copy的总次数 = 采样每一步的总次数 = 序列的个数 x 单个序列的长度

这里可以根据自己运行一次(gpu+cpu)的版本 和单cpu的版本,看自己的数据得出来copy慢多少s 。比方说我这里得到
单cpu的时间如下:

在这里插入图片描述
gpu+cpu的时间如下:
在这里插入图片描述
我这里单条在eposide为300,一个eposide为1440长,则300*1440次copy慢了120s。
即432000次copy慢上了120s。即3600次copy慢了1s。
这里没测,故最坏假设copy一次需要1s,实际肯定没有1s
假设copy要3600s+模型快3599s。
那么,理论上copy的次数每3600次少上一次,即可平衡掉慢的速度。

继续探索

之前ddpg调参的时候,有个加快收敛稳定的方法:
如下所示:
逻辑表示为:从每轮更新一次->每50轮更新50次。(多次更新加快收敛)
在这里插入图片描述
这里时间不变,但可以在每50次更新只进行一次的copy到gpu,copy回cpu;而更新的次数不变。

需要注意的是,调节update_freq会影响收敛

理论存在,开始实践:
在这里插入图片描述
注:这里的critic to cuda 可以删去,改为初始化的时候to cuda; 由于agent初始化为cpu,所以偷懒改法中的第一处也可以删去,且这里第二个箭头相当于这个作用。

然而!!!

然而,事实并非如此,(ppo+经验池)的算法对此并不适用。出现了一次训练变快的情况,然后续再测时却一直卡在训练模型的程序上不动了,可以推测前面一次应该是代码每保存前导致。

ppo+经验池的算法和ddpg的算法在对经验池的操作上是有区别的。
前者会取出全部经验池里的数据,且训练完后会清空经验池,而后者只是训练时对经验池里的数据进行采样且不会清空数据,最多的经验池满了之后把最旧的剔除掉。

于是在ddpg上有用的收敛技巧在(ppo+经验池)上就无用了。因为后者在每次利用完经验池后会清空经验池,达不到每步都训练的条件。

不甘心的我尝试在ddpg上找到优势。然而在《动手学强化学习》的ddpg例子下:
单cpu:2min52s。单gpu:3min43s。gpu+cpu :4min23.9s。
甚至时间耗时最久,可以说明在这种情况下,
有两种可能:1、cpu模型训练比gpu模型还快 2、copy的时间过多导致gpu训练优势不明显。

最后还是试了刚刚的tips:在设置为每10步更新10次的参数后,gpu+cpu 来到了3min57s,说明刚刚的理论还是奏效的,只是只针对于off-online的经验池。
再试了每50步更新50次:成绩:3min43.7s,刚刚追平单gpu的速度。收敛的速度和稳定性却差了许多。这个超参数还是需要根据实际情况谨慎设置的。
可以得出结论在此小模型下,cpu的更新速度是快于gpu的。

探索

第一

为了在此小模型下,验证cpu确实比gpu训练快。
加入

import time

在这里插入图片描述
在gpu下:一次update 是0.005s
在cpu下:一次update 是0.004或0.003s。你无敌了cpu。
那么确实,参考1给出的解决方法十分有效,要先进行这一步,再抉择是否使用gpu+cpu的代码。
确实是实践出真知了。(–错误–)

— 24.5.24
再次验证到ppo的代码上,却又发现之前的验证方法不对。因为结果和上述一样,还是cpu快,但cpu+gpu的版本应该变快了才对。

于是一番探索之下,先固定住ppo的两个版本的take_action次数,将while not done 改为 for _ in range(200),(不考虑结果的情况下,改后,cpu仍然收敛,gpu+cpu不收敛了。)

不固定次数的情况下,可能会因为cpu和gpu的计算结果不一致导致的动作不一致从而导致take_action的次数不一致。
在这里插入图片描述
固定完,
之后运行cProfile性能分析器

import cProfile
# 运行性能分析器
cProfile.run('train_on_policy_agent(env, agent, num_episodes)')

又得到了令我吃惊的结果:
在这里插入图片描述
左cpu,右cpu+gpu,发现右边的探索时间(take_action)和训练时间(update)都比左边快。(forward暂时不用看,包含在take_action和update里)
模型会加快训练可以理解,为啥环境take_action也加快了。

回到ddpg,实验,方法同上,ddpg的cpu+gpu用了每50轮更新50次的加快方法。
在这里插入图片描述
左cpu,右cpu+gpu,发现take_action的时间还是右边快(猜测cpu空闲时可以较快响应),但是训练的时间确是左边快。
两方面原因:
1、update里包含了soft_update,软更新有数据拷贝的操作,所以cpu会比gpu快,快上面数据是快了10s。
2、模型太小了,ppo的模型是单隐层128层,ddpg是双层64层,有可能模型太小,导致模型更新速度慢。

设置ddpg模型为双层128层继续实验:(左:cpu,右:cpu+gpu)
在这里插入图片描述
果然,到了128层时,cpu运算的速度就没那么明显了,update的时间从38s缩减到16s。
总体的相差时间也从33s缩短到12s。

再仔细分析数据。
发现
1、

左:forward (23s+17s) 比 右:(30s+21s)要快
左:backward (38s) 比右:(48s)要快
左:optimizer.step(63s) 比右:(23s)要慢
左:optimizer.zero_grad(4.0s) 比右 (3.9s)要慢

一般来说模型训练还是backward,step一起运行的。

简单做了个测评,得出的结论仅供参考。

import torch
import torch.nn.functional as F
import time

# 设置随机种子
torch.manual_seed(0)

# 实际运算中critic的forwardy运算比actor的forward运算更耗时,这里以critic为例
# 定义单隐层模型
class Critic(torch.nn.Module):
    def __init__(self, state_dim, hidden_dim):
        super(Critic, self).__init__()
        self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
        self.fc2 = torch.nn.Linear(hidden_dim, 1)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        return self.fc2(x)
# 定义双隐层模型
class Critic_d(torch.nn.Module):
    def __init__(self, state_dim, hidden_dim):
        super(Critic_d, self).__init__()
        self.fc1 = torch.nn.Linear(state_dim, hidden_dim)
        self.fc2 = torch.nn.Linear(hidden_dim, hidden_dim) # 两层隐藏层
        self.fc3 = torch.nn.Linear(hidden_dim, 1)

    def forward(self, x):
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        return self.fc3(x)
    



# 设置模型和数据
state_dim = 1 #越大 gpu越占优势 快0.3s 可以忽略
hidden_dim = 200##256
critic = Critic(state_dim, hidden_dim)
input_data = torch.randn(100, state_dim, dtype=torch.float32)  # 假设有100个样本
critic_cpu = copy.deepcopy(critic)
critic_cuda = critic.to('cuda')
criterion = nn.MSELoss()
optimizer_cpu = optim.SGD(critic_cpu.parameters(), lr=0.01)
optimizer_cuda = optim.SGD(critic_cuda.parameters(), lr=0.01)
y = 3+torch.randn(100, state_dim, dtype=torch.float32)
# 在CPU上运行

input_data = input_data.cpu()
start_time = time.time()
for _ in range(5000):
    output = critic_cpu(input_data)
    loss = criterion(output, y)
    optimizer_cpu.zero_grad()
    loss.backward()
    optimizer_cpu.step()
cpu_time = time.time() - start_time
#print(f"CPU time: {cpu_time} seconds")
print('CPU time:','{:6f}'.format(cpu_time))

input_data = input_data.to('cuda')
start_time = time.time()
y =y.to('cuda')
for _ in range(5000):
    output = critic_cuda(input_data)
    loss = criterion(output,y)
    optimizer_cpu.zero_grad()
    loss.backward()
    optimizer_cpu.step()
gpu_time = time.time() - start_time
#print(f"GPU time: {gpu_time} seconds")
print('GPU time:','{:6f}'.format(gpu_time))


# 比较时间
if cpu_time and gpu_time:
    print(f"Speedup: {cpu_time / gpu_time}x")



## 结论1: 状态空间为1维度时
## 单隐层时Qnet 大于200维度的时候,GPU训练速度快于CPU训练速度,起码不会慢很多
# 200时
# CPU time: 3.903359
# GPU time: 3.752788
# Speedup: 1.0401225946109272x


## 结论2: 状态空间为1维度时
## 双隐层时Qnet 大于129维度的时候,GPU训练速度快于CPU训练速度
# 129时
# CPU time: 5.537388
# GPU time: 4.978731
# Speedup: 1.1122088025702137x

结论1: 状态空间为1维度时,单隐层时Qnet 大于200维度的时候,GPU训练速度快于CPU训练速度,起码不会慢很多。

结论2: 状态空间为1维度时,双隐层时Qnet 大于129维度的时候,GPU训练速度快于CPU训练速度。

给上面做个总结
结论3:状态的选取,对最终是否收敛影响很大,cpu在状态少一个的情况下依然能收敛。刚开始调参数的时候,使用单cpu时更容易收敛,使用cpu+gpu时调节的参数应该更具有鲁棒性。

2、

左:to方法({method ‘to’ of ‘torch._C.TensorBase’ objects})(0.34s)比右(13.6s)要快

这个应该就是 copy的时间,如何改进,看下面第二。

第二 --2024.5.28

由上方得出的,在离线的情况下,或者说在采样时包含了对经验池的训练的情况下,能否避免再次对环境采样时的模型拷贝。
答案是 可以的,还是感谢参考1给出的代码例子。

在这篇博客中,我找到了答案。(好文)

先采样,再训练 可以分开来做,且这样做更加高效。
即:
在这里插入图片描述
在这里插入图片描述
这样 ,就避免了每次update后,还要将模型继续copy回cpu的步骤。

那么上述的在线、离线策略的算法就可以合并成一类(都可以看作为先采样,后训练)。(–2024.5.28 不太是)

以下是,可以两者分开做的代码实例:
第一个是源自于PyMARL库的代码
在这里插入图片描述
第2个是小雅ElegentRL的代码
在这里插入图片描述

实践

简单在DDPG的原代码,将训练那行提前并不能得到想要的结果,(–24.5.30 可以,但要改经验池有关的超参数)
1、是要训练的参数从多条状态变为了多条轨迹。
2、是训练的次数也变了。

所以超参数也得改。
原来的代码:

# 原版
import collections
class ReplayBuffer:
    ''' 经验回放池 '''
    def __init__(self, capacity):
        self.buffer = collections.deque(maxlen=capacity)  # 队列,先进先出

    def add(self, state, action, reward, next_state, done):  # 将数据加入buffer
        self.buffer.append((state, action, reward, next_state, done))

    def sample(self, batch_size):  # 从buffer中采样数据,数量为batch_size
        transitions = random.sample(self.buffer, batch_size)
        state, action, reward, next_state, done = zip(*transitions)
        return np.array(state), action, reward, np.array(next_state), done

    def size(self):  # 目前buffer中数据的数量
        return len(self.buffer)
def train_off_policy_agent(env, agent, num_episodes, replay_buffer, minimal_size, batch_size):
    return_list = []
    #total_steps = 0
    for i in range(10):
        with tqdm(total=int(num_episodes/10), desc='Iteration %d' % i) as pbar:
            for i_episode in range(int(num_episodes/10)):
                episode_return = 0
                state = env.reset(seed =0)[0] #1.改 gym 0.26.0版本后,env.reset()返回的是一个字典,所以需要加上[0]
                done = False
                agent.actor.to('cpu')
                while not done:
                #for _ in range(200): 
                    action = agent.take_action(state)
                    #next_state, reward, done, _ = env.step(action)
                    next_state, reward,terminated, truncated, _ = env.step(action) #2.改看gym版本0.26.2版本的
                    done = terminated or truncated
                    replay_buffer.add(state, action, reward, next_state, done) ##!!
                    state = next_state
                    episode_return += reward
                    #total_steps += 1
                    if replay_buffer.size() > minimal_size:  ##!!
                        b_s, b_a, b_r, b_ns, b_d = replay_buffer.sample(batch_size)
                        transition_dict = {'states': b_s, 'actions': b_a, 'next_states': b_ns, 'rewards': b_r, 'dones': b_d}
                        agent.update(transition_dict)

                return_list.append(episode_return)
                if (i_episode+1) % 10 == 0:
                    pbar.set_postfix({'episode': '%d' % (num_episodes/10 * i + i_episode+1), 'return': '%.3f' % np.mean(return_list[-10:]), 'a_loss': '%.3f' % agent.a_loss, 'c_loss': '%.3f' % agent.c_loss})
                pbar.update(1)
    return return_list

actor_lr = 3e-4
critic_lr = 3e-3
num_episodes = 200#40000 #200经验池不变时 更新了约200*200次
hidden_dim = 64
gamma = 0.98
tau = 0.005  # 软更新参数
buffer_size = 10000
minimal_size = 1000
batch_size = 64#64
sigma = 0.01  # 高斯噪声标准差 #sigma越大,探索性越强
#device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")
device = torch.device("cpu") #cpu版
env_name = 'Pendulum-v1'
env = gym.make(env_name)
random.seed(0)
np.random.seed(0)
#env.seed(0)
torch.manual_seed(0)
replay_buffer = ReplayBuffer(buffer_size) # 经验回放池
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.shape[0]
action_bound = env.action_space.high[0]  # 动作最大值
agent = DDPG(state_dim, hidden_dim, action_dim, action_bound, sigma, actor_lr, critic_lr, tau, gamma, device)

return_list = train_off_policy_agent(env, agent, num_episodes, replay_buffer, minimal_size, batch_size)

结果为:(此时在同一电脑运行时间下,之前一次是开机了没多久,这次电脑运行了13:15:38:50 s)3min30s
在这里插入图片描述

可以看出,这里的eposide为200,假设每次序列都是完整的200帧,则会更新40000次,经验池大小为10000条状态信息,最小更新的经验池大小为1000条信息,每次更新为抽取64条状态信息更新一次。
我根据以上两条探索进行的修改如下:

## buffer 经验池后移时,传递整个序列
import collections
class ReplayBuffer:
    ''' 经验回放池 '''
    def __init__(self, capacity):
        self.buffer = collections.deque(maxlen=capacity)  # 队列,先进先出

    def add(self, trajecotry):  # 将数据加入buffer
        self.buffer.append(trajecotry)
    def size(self):  # 目前buffer中数据的数量
        return len(self.buffer)

    def sample(self, batch_size):  # 从buffer中采样数据,数量为batch_size
        #print('buffer:',self.buffer)
        transitions = random.sample(self.buffer, batch_size)
        #print('transitions:',transitions)
        # 初始化空的列表来存储转换后的数据
        states, actions, rewards, next_states, dones = [], [], [], [], []

        # 遍历轨迹并提取数据
        for trajectory in transitions:
            for experience in trajectory:
                state, action, reward, next_state, done = experience
                states.append(state)
                actions.append(action)
                rewards.append(reward)
                next_states.append(next_state)
                dones.append(done)
                # 将列表转换为numpy数组
        states = np.array(states)
        actions = np.array(actions)
        rewards = np.array(rewards)
        next_states = np.array(next_states)
        dones = np.array(dones)

        # 改v2
        # 构建结构化数据
        structured_data = {
            'states': states, 
            'actions': actions, 
            'rewards': rewards, 
            'next_states': next_states,
            'dones': dones
        }
        return structured_data


    
def train_off_policy_agent(env, agent, num_episodes, replay_buffer, minimal_size, batch_size):
    return_list = []
    total_steps = 0
    for i in range(10):
        with tqdm(total=int(num_episodes/10), desc='Iteration %d' % i) as pbar:
            for i_episode in range(int(num_episodes/10)):
                episode_return = 0
                state = env.reset(seed =0)[0] #1.改 gym 0.26.0版本后,env.reset()返回的是一个字典,所以需要加上[0]
                done = False
                agent.actor.to('cpu')
                new_trajecotry = []
                while not done:
                #for _ in range(200): 
                    action = agent.take_action(state)
                    #next_state, reward, done, _ = env.step(action)
                    next_state, reward,terminated, truncated, _ = env.step(action) #2.改看gym版本0.26.2版本的
                    done = terminated or truncated
                    new_trajecotry.append((state, action, reward, next_state, done)) ##!!
                    state = next_state
                    episode_return += reward
                total_steps += 1
                replay_buffer.add(new_trajecotry) ## 这里传递整个序列
                if replay_buffer.size() > minimal_size:  ##!!
                    # b_s, b_a, b_r, b_ns, b_d = replay_buffer.sample(batch_size)
                    # transition_dict = {'states': b_s, 'actions': b_a, 'next_states': b_ns, 'rewards': b_r, 'dones': b_d}
                    transition_dict = replay_buffer.sample(batch_size)
                    agent.actor.to('cuda')
                    for _ in range(10):
                        agent.update(transition_dict)
                        
                return_list.append(episode_return)
                if (i_episode+1) % 10 == 0:
                    pbar.set_postfix({'episode': '%d' % (num_episodes/10 * i + i_episode+1), 'return': '%.3f' % np.mean(return_list[-10:]), 'a_loss': '%.3f' % agent.a_loss, 'c_loss': '%.3f' % agent.c_loss})
                pbar.update(1)
    return return_list
# 改版
actor_lr = 3e-4
critic_lr = 3e-3
num_episodes = 2000 #200经验池不变时 更新了约200*200次 #4000*200状态信息 
hidden_dim = 64
gamma = 0.98
tau = 0.005  # 软更新参数
buffer_size = 1000  # 1000*200
minimal_size = 100
batch_size = 5#64
sigma = 0.01  # 高斯噪声标准差 #sigma越大,探索性越强
device = torch.device("cuda") if torch.cuda.is_available() else torch.device("cpu")

env_name = 'Pendulum-v1'
env = gym.make(env_name)
random.seed(0)
np.random.seed(0)
#env.seed(0)
torch.manual_seed(0)
replay_buffer = ReplayBuffer(buffer_size) # 经验回放池
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.shape[0]
action_bound = env.action_space.high[0]  # 动作最大值
agent = DDPG(state_dim, hidden_dim, action_dim, action_bound, sigma, actor_lr, critic_lr, tau, gamma, device)

return_list = train_off_policy_agent(env, agent, num_episodes, replay_buffer, minimal_size, batch_size)

此时每次经验池里增加是一整条轨迹,然后经验池里存多个轨迹,最后从多个轨迹里随机选取几条来训练,最终训练的还是状态信息。

注意:
小雅里面的的训练:经验池存储的是轨迹,但是和直觉上存储轨迹的方式不同,采样这一串轨迹后,状态s是一串轨迹,状态a是一串轨迹,状态r是一串轨迹,估计这种方式比下一种直觉上的存储轨迹会快一点,因为采样时已经将s,a,r,s,a分好类了,而下一种则是在sample里再分类。

pymarl里面的训练:经验池是存储的多个轨迹,也是随机选取几个轨迹来训练,不过和我写的代码有一点不同,就是他的经验池最大数量还是以状态信息为单位,如果此轨迹加入后,经验池会满,则此轨迹的前面的部分插入到经验池剩余的部分,后面部分则替代掉经验池的前面部分。
(浅看了下代码,要是有错误,可以评论)

将快速收敛的技巧改为每次经验池更新时,更新10次。

加入了,
超参数,这里我最终调试为如下:
改了四个:(差不多,经验池调为原来的20倍)
num_episodes = 2000 # 最终为更新20000次 # 原本约40000次
buffer_size = 1000 #状态信息约为1000x200 # 原本10000
minimal_size = 100 #状态信息约为100x200 # 原本1000
batch_size = 5 #状态信息约为5*200 #原本64

结果如下:运行时间为3min11.9s,比cpu版本的快了20s。(由于实际更新次数也不同,仅仅将差不多收敛的时间控制变量了,这里的速度也只能仅供参考了。)

在这里插入图片描述
这里基本实现了猜想。
后又想将此方法(cpu+gpu+先采样后训练)
改到ppo+经验池的算法,多次修改,收敛效果和时间均不如cpu版本,遂放弃。 结果如下:且相同时间内原版本收敛更快。
在这里插入图片描述
究其原因:应该是ppo的经验池是每次更新都要清空的。设置为较长的经验池,会增加训练时间,而设置较小的经验池又学不到参数,先采样后训练的模式应该对无经验池的ppo效果更好(小雅的代码就是这么做的,实际上原本的ppo就是无经验池+先采样后)。

补充:经验池的优化

之后又细看了下pymarl库和小雅的库对于经验池的写法以及训练过程,发现他们都只对off-online的算法进行经验池的构造,对于on-line的算法,前者的COMA算法采取的是直接拿取整个轨迹进行训练之后清空,后者是拿取整个状态信息进行训练后清空,两者做法效果和时间上基本一致。(因为还是要在这整个经验池随机抽样,满足独立分布。而off-online的经验池改成存储整条轨迹的方式比原方式更快了,效果也更加稳定。如下图所示)
在这里插入图片描述
这个是原来的方式,3min41s,下图是经验池改成存储轨迹的方式,(经验池的大小和每次训练的状态信息均一致。3min4s)
在这里插入图片描述

之后又做了下ppo+经验池的两者的对比,以及一些细琐的实验(改隐藏层看速度等。脑袋太晕,没做记录。)
得出如下结论。

大总结(省流版)

强化学习是不是主要吃cpu而不怎么吃gpu?
这个参考的结论基本对,这里做补充。

这里实验了ppo无经验池,ppo+经验池,ddpg三种算法,可以推广到所有on-line,off-online及先采样后更新的模式。
(在不考虑先采样后更新可以利用并行加速的情况下:)

1、单隐层的的数量为64时,及双隐层的数量为64x64时:
速度最快,收敛好的的off-online方法是:
(更新在采样函数里)+cpu版本

#伪代码
import collections
import random
import numpy as np
import torch
class ReplayBuffer:
    ''' 经验回放池 '''
    def __init__(self, capacity):
        self.buffer = collections.deque(maxlen=capacity)  # 队列,先进先出
    def add(self, state, action, reward, next_state, done):  # 将数据加入buffer
        self.buffer.append((state, action, reward, next_state, done))
    def sample(self, batch_size):  # 从buffer中采样数据,数量为batch_size
        transitions = random.sample(self.buffer, batch_size)
        state, action, reward, next_state, done = zip(*transitions)
        return np.array(state), action, reward, np.array(next_state), done
    def size(self):  # 目前buffer中数据的数量
        return len(self.buffer)
    
def train_off_policy_agent(env, agent, num_episodes, replay_buffer, minimal_size, batch_size):
    return_list = []
    for _ in range(num_episodes):
        episode_return = 0
        state = env.reset()
        done = False 
        while not done:  #1.第一种形式eposide不定长 # 2.第二种形式定长 for i in range(200): 
            action = agent.take_action(state)
            next_state, reward, done, _ = env.step(action)
            replay_buffer.add(state, action, reward, next_state, done)
            state = next_state
            episode_return += reward
            if replay_buffer.size() > minimal_size: 
                b_s, b_a, b_r, b_ns, b_d = replay_buffer.sample(batch_size)
                transition_dict = {'states': b_s, 'actions': b_a, 'next_states': b_ns, 'rewards': b_r, 'dones': b_d}
                agent.update(transition_dict)
        return_list.append(episode_return)
    return return_list
num_episodes = 200
hidden_dim = 64
buffer_size = 10000
minimal_size = 1000
batch_size = 64
device = torch.device("cpu")

速度快,收敛好的on-line算法是(有经验池,无经验池都是最好):
先采样后更新+cpu版本

#伪代码
def train_on_policy_agent(env, agent, num_episodes):
    return_list = []
    for i in range(num_episodes):
        episode_return = 0
        transition_dict = {'states': [], 'actions': [], 'next_states': [], 'rewards': [], 'dones': []}
        state = env.reset(seed =0)
        done = False
        while not done:#1.第一种形式eposide不定长 # 2.第二种形式定长 for i in range(200):
            action = agent.take_action(state) # forward 无2 这里是[-1,1]的动作
            next_state, reward, done, _ = env.step(action)
            transition_dict['states'].append(state)
            transition_dict['actions'].append(action)
            transition_dict['next_states'].append(next_state)
            transition_dict['rewards'].append(reward)
            transition_dict['dones'].append(done)
            state = next_state
            episode_return += reward
        return_list.append(episode_return)
        agent.update(transition_dict)  
    return return_list
device = torch.device("cpu")

2、单隐层的的数量为128时,及双隐层的数量为128x128时:
速度最快,收敛好的的off-online方法是:
先采样后更新+轨迹经验池+(cpu+gpu版本)(对应模型计算张量的地方要一致)

## buffer 经验池后移时,传递整个序列
import collections
class ReplayBuffer:
    ''' 经验回放池 '''
    def __init__(self, capacity):
        self.buffer = collections.deque(maxlen=capacity)  # 队列,先进先出

    def add(self, trajecotry):  # 将数据加入buffer
        self.buffer.append(trajecotry)
    def size(self):  # 目前buffer中数据的数量
        return len(self.buffer)

    def sample(self, batch_size):  # 从buffer中采样数据,数量为batch_size
        transitions = random.sample(self.buffer, batch_size)
        states, actions, rewards, next_states, dones = [], [], [], [], []
        # 遍历轨迹并提取数据
        for trajectory in transitions:
            for experience in trajectory:
                state, action, reward, next_state, done = experience
                states.append(state)
                actions.append(action)
                rewards.append(reward)
                next_states.append(next_state)
                dones.append(done)
                # 将列表转换为numpy数组
        states = np.array(states)
        actions = np.array(actions)
        rewards = np.array(rewards)
        next_states = np.array(next_states)
        dones = np.array(dones)

        # 改v2
        # 构建结构化数据
        structured_data = {
            'states': states, 
            'actions': actions, 
            'rewards': rewards, 
            'next_states': next_states,
            'dones': dones
        }
        return structured_data
def train_off_policy_agent(env, agent, num_episodes, replay_buffer, minimal_size, batch_size):
    return_list = []
    total_steps = 0
    for _ in range(num_episodes):
        episode_return = 0
        state = env.reset()
        done = False
        agent.actor.to('cpu')
        new_trajecotry = []
        while not done:
            action = agent.take_action(state)
            next_state, reward, done, _ = env.step(action)
            new_trajecotry.append((state, action, reward, next_state, done)) 
            state = next_state
            episode_return += reward
        total_steps += 1
        replay_buffer.add(new_trajecotry) ## 这里传递整个序列
        if replay_buffer.size() > minimal_size:  
            transition_dict = replay_buffer.sample(batch_size)
            agent.actor.to('cuda')
            for _ in range(10):
                agent.update(transition_dict)
        return_list.append(episode_return)
    return return_list
num_episodes = 2000 
hidden_dim = 128
buffer_size = 1000 
minimal_size = 100
batch_size = 5 

速度最快,收敛好的的on-online方法是:(有无经验池一致)
先采样后更新+(cpu+gpu版本)或者 先采样后更新+(cpu版本)
主要看转移时的状态信息量多少,如果转移的状态信息量多用后者,信息量少用前者。 例:状态信息200个用前者,1440用后者。因为可能转移的时间开销大于gpu更新快的时间开销。

3、单隐层的的数量为256时,及双隐层的数量为256x256时:
off-online:先采样后更新+轨迹经验池+(cpu+gpu)
on-online:先采样后更新+(cpu+gpu)
一般来说,隐层数量越多,收敛的越快。

cpu+gpu版本的改法

cpu+gpu版本的改法:主要就是环境采样时的模型和张量改为cpu;更新参数的模型和张量改为gpu。

cpu的地方:
在这里插入图片描述
在这里插入图片描述
gpu的地方:
如果是a-c的算法,actor初始化为cpu,critic初始化为gpu。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
这里是先采样后更新的改法,要是在采样里后更新的化,最后一张图update后,还要加to(‘cpu’)。

训练的加速方法,除了此,还有很多,比如multiprocessing和mpi的cpu并行方法,之后有空研究。

  • 38
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值