大家好,今天和各位講解一下深度強化學習中的基礎(chǔ)模型 DQN,配合 OpenAI 的 gym 環(huán)境,訓練模型完成一個小游戲,完整代碼可以從我的 GitHub 中獲得:
https://github.com/LiSir-HIT/Reinforcement-Learning/tree/main/Model
1. 算法原理
1.1 基本原理
DQN(Deep Q Network)算法由 DeepMind 團隊提出,是深度神經(jīng)網(wǎng)絡和 Q-Learning 算法相結(jié)合的一種基于價值的深度強化學習算法。
Q-Learning 算法構(gòu)建了一個狀態(tài)-動作值的 Q 表,其維度為 (s,a),其中 s 是狀態(tài)的數(shù)量,a 是動作的數(shù)量,根本上是 Q 表將狀態(tài)和動作映射到 Q 值。此算法適用于狀態(tài)數(shù)量能夠計算的場景。但是在實際場景中,狀態(tài)的數(shù)量可能很大,這使得構(gòu)建 Q 表難以解決。為破除這一限制,我們使用 Q 函數(shù)來代替 Q 表的作用,后者將狀態(tài)和動作映射到 Q 值的結(jié)果相同。
由于神經(jīng)網(wǎng)絡擅長對復雜函數(shù)進行建模,因此我們用其當作函數(shù)近似器來估計此 Q 函數(shù),這就是 Deep Q Networks。此網(wǎng)絡將狀態(tài)映射到可從該狀態(tài)執(zhí)行的所有動作的 Q 值。即只要輸入一個狀態(tài),網(wǎng)絡就會輸出當前可執(zhí)行的所有動作分別對應的 Q 值。如下圖所示,它學習網(wǎng)絡的權(quán)重,以此輸出最佳 Q 值。
1.2 模型結(jié)構(gòu)
DQN 體系結(jié)構(gòu)主要包含:Q 網(wǎng)絡、目標網(wǎng)絡,以及經(jīng)驗回放組件。.Q 網(wǎng)絡是經(jīng)過訓練以生成最佳狀態(tài)-動作值的 agent。經(jīng)驗回放單元的作用是與環(huán)境交互,生成數(shù)據(jù)以訓練 Q 網(wǎng)絡。目標網(wǎng)絡與 Q 網(wǎng)絡在初始時是完全相同的。DQN 工作流程圖如下
1.2.1? 經(jīng)驗回放
經(jīng)驗回放從當前狀態(tài)中以貪婪策略??選擇一個動作,執(zhí)行后從環(huán)境中獲得獎勵和下一步的狀態(tài),如下圖所示。
然后將此觀測值另存為用于訓練數(shù)據(jù)的樣本,如下圖所示。
與 Q Learning 算法不同,經(jīng)驗回放組件的存在有其必須性。神經(jīng)網(wǎng)絡通常接受一批數(shù)據(jù),如果我們用單個樣本去訓練它,每個樣本和相應的梯度將具有很大的方差,并且會導致網(wǎng)絡權(quán)重永遠不會收斂。
當我們訓練神經(jīng)網(wǎng)絡時,最好的做法是在隨機打亂的訓練數(shù)據(jù)中選擇一批樣本。這確保了訓練數(shù)據(jù)有足夠的多樣性,使網(wǎng)絡能夠?qū)W習有意義的權(quán)重,這些權(quán)重可以很好地泛化并且可以處理一系列數(shù)據(jù)值。如果我們以順序動作傳遞一批數(shù)據(jù),則不會達到此效果。
所以可得出結(jié)論:順序操作彼此高度相關(guān),并且不會像網(wǎng)絡所希望的那樣隨機洗牌。這導致了一個 “災難性遺忘” 的問題,網(wǎng)絡忘記了它不久前學到的東西。
以上是引入經(jīng)驗回放組件的原因。智能體在內(nèi)存容量范圍內(nèi)從一開始就執(zhí)行的所有動作和觀察都將被存儲。然后從此存儲器中隨機選擇一批樣本。這確保了批次是經(jīng)過打亂,并且包含來自舊樣品和較新樣品的足夠多樣性,這樣能保證訓練過的網(wǎng)絡具有能處理所有場景的權(quán)重。
# --------------------------------------- #
# 經(jīng)驗回放池
# --------------------------------------- #
class ReplayBuffer():
def __init__(self, capacity):
# 創(chuàng)建一個先進先出的隊列,最大長度為capacity,保證經(jīng)驗池的樣本量不變
self.buffer = collections.deque(maxlen=capacity)
# 將數(shù)據(jù)以元組形式添加進經(jīng)驗池
def add(self, state, action, reward, next_state, done):
self.buffer.append((state, action, reward, next_state, done))
# 隨機采樣batch_size行數(shù)據(jù)
def sample(self, batch_size):
transitions = random.sample(self.buffer, batch_size) # list, len=32
# *transitions代表取出列表中的值,即32項
state, action, reward, next_state, done = zip(*transitions)
return np.array(state), action, reward, np.array(next_state), done
# 目前隊列長度
def size(self):
return len(self.buffer)
1.2.2 Q 網(wǎng)絡預測 Q 值
所有之前的經(jīng)驗回放都將保存為訓練數(shù)據(jù)?,F(xiàn)在從此訓練數(shù)據(jù)中隨機抽取一批樣本,以便它包含較舊樣本和較新樣本的混合。隨后將這批訓練數(shù)據(jù)輸入到兩個網(wǎng)絡。Q 網(wǎng)絡從每個數(shù)據(jù)樣本中獲取當前狀態(tài)和操作,并預測該特定操作的 Q 值,這是“預測 Q 值”。如下圖所示。
1.2.3 目標網(wǎng)絡預測目標 Q 值
目標網(wǎng)絡從每個數(shù)據(jù)樣本中獲取下一個狀態(tài),并可以從該狀態(tài)執(zhí)行的所有操作中預測最佳 Q 值,這是“目標 Q 值”。如下圖所示。
DQN 同時用到兩個結(jié)構(gòu)相同參數(shù)不同的神經(jīng)網(wǎng)絡,區(qū)別是一個用于訓練,另一個不會在短期內(nèi)得到訓練,這樣設(shè)置是從考慮實際效果出發(fā)的必然需求。
如果構(gòu)建具有單個 Q 網(wǎng)絡且不存在目標網(wǎng)絡的 DQN,假設(shè)此網(wǎng)絡應該如下工作:通過 Q 網(wǎng)絡執(zhí)行兩次傳遞,首先輸出 “預測 Q 值”,然后輸出 “目標 Q 值”。這可能會產(chǎn)生一個潛在的問題:Q 網(wǎng)絡的權(quán)重在每個時間步長都會更新,從而改進了對“預測 Q 值”的預測。但是,由于網(wǎng)絡及其權(quán)重相同,因此它也改變了我們預測的“目標 Q 值”的方向。它們不會保持穩(wěn)定,在每次更新后可能會波動,類似一直追逐一個移動著的目標。
通過采用第二個未經(jīng)訓練的網(wǎng)絡,可以確保 “目標 Q 值” 至少在短時間內(nèi)保持穩(wěn)定。但這些“目標 Q 值”畢竟只是預測值,這是為改善它們的數(shù)值做出的妥協(xié)。所以在經(jīng)過預先配置的時間步長后,需將 Q 網(wǎng)絡中更新的權(quán)重復制到目標網(wǎng)絡。
可以得出,使用目標網(wǎng)絡可以帶來更穩(wěn)定的訓練。
1.2.2 和 1.2.3 代碼對應如下:
# -------------------------------------- #
# 構(gòu)造深度學習網(wǎng)絡,輸入狀態(tài)s,得到各個動作的reward
# -------------------------------------- #
class Net(nn.Module):
# 構(gòu)造只有一個隱含層的網(wǎng)絡
def __init__(self, n_states, n_hidden, n_actions):
super(Net, self).__init__()
# [b,n_states]-->[b,n_hidden]
self.fc1 = nn.Linear(n_states, n_hidden)
# [b,n_hidden]-->[b,n_actions]
self.fc2 = nn.Linear(n_hidden, n_actions)
# 前傳
def forward(self, x): # [b,n_states]
x = self.fc1(x)
x = self.fc2(x)
return x
# -------------------------------------- #
# 構(gòu)造深度強化學習模型
# -------------------------------------- #
class DQN:
#(1)初始化
def __init__(self, n_states, n_hidden, n_actions,
learning_rate, gamma, epsilon,
target_update, device):
# 屬性分配
self.n_states = n_states # 狀態(tài)的特征數(shù)
self.n_hidden = n_hidden # 隱含層個數(shù)
self.n_actions = n_actions # 動作數(shù)
self.learning_rate = learning_rate # 訓練時的學習率
self.gamma = gamma # 折扣因子,對下一狀態(tài)的回報的縮放
self.epsilon = epsilon # 貪婪策略,有1-epsilon的概率探索
self.target_update = target_update # 目標網(wǎng)絡的參數(shù)的更新頻率
self.device = device # 在GPU計算
# 計數(shù)器,記錄迭代次數(shù)
self.count = 0
# 構(gòu)建2個神經(jīng)網(wǎng)絡,相同的結(jié)構(gòu),不同的參數(shù)
# 實例化訓練網(wǎng)絡 [b,4]-->[b,2] 輸出動作對應的獎勵
self.q_net = Net(self.n_states, self.n_hidden, self.n_actions)
# 實例化目標網(wǎng)絡
self.target_q_net = Net(self.n_states, self.n_hidden, self.n_actions)
# 優(yōu)化器,更新訓練網(wǎng)絡的參數(shù)
self.optimizer = torch.optim.Adam(self.q_net.parameters(), lr=self.learning_rate)
#(3)網(wǎng)絡訓練
def update(self, transition_dict): # 傳入經(jīng)驗池中的batch個樣本
# 獲取當前時刻的狀態(tài) array_shape=[b,4]
states = torch.tensor(transition_dict['states'], dtype=torch.float)
# 獲取當前時刻采取的動作 tuple_shape=[b],維度擴充 [b,1]
actions = torch.tensor(transition_dict['actions']).view(-1,1)
# 當前狀態(tài)下采取動作后得到的獎勵 tuple=[b],維度擴充 [b,1]
rewards = torch.tensor(transition_dict['rewards'], dtype=torch.float).view(-1,1)
# 下一時刻的狀態(tài) array_shape=[b,4]
next_states = torch.tensor(transition_dict['next_states'], dtype=torch.float)
# 是否到達目標 tuple_shape=[b],維度變換[b,1]
dones = torch.tensor(transition_dict['dones'], dtype=torch.float).view(-1,1)
# 輸入當前狀態(tài),得到采取各運動得到的獎勵 [b,4]==>[b,2]==>[b,1]
# 根據(jù)actions索引在訓練網(wǎng)絡的輸出的第1維度上獲取對應索引的q值(state_value)
q_values = self.q_net(states).gather(1, actions) # [b,1]
# 下一時刻的狀態(tài)[b,4]-->目標網(wǎng)絡輸出下一時刻對應的動作q值[b,2]-->
# 選出下個狀態(tài)采取的動作中最大的q值[b]-->維度調(diào)整[b,1]
max_next_q_values = self.target_q_net(next_states).max(1)[0].view(-1,1)
# 目標網(wǎng)絡輸出的當前狀態(tài)的q(state_value):即時獎勵+折扣因子*下個時刻的最大回報
q_targets = rewards + self.gamma * max_next_q_values * (1-dones)
# 目標網(wǎng)絡和訓練網(wǎng)絡之間的均方誤差損失
dqn_loss = torch.mean(F.mse_loss(q_values, q_targets))
# PyTorch中默認梯度會累積,這里需要顯式將梯度置為0
self.optimizer.zero_grad()
# 反向傳播參數(shù)更新
dqn_loss.backward()
# 對訓練網(wǎng)絡更新
self.optimizer.step()
# 在一段時間后更新目標網(wǎng)絡的參數(shù)
if self.count % self.target_update == 0:
# 將目標網(wǎng)絡的參數(shù)替換成訓練網(wǎng)絡的參數(shù)
self.target_q_net.load_state_dict(
self.q_net.state_dict())
self.count += 1
DQN 模型偽代碼:
2. 實例演示
接下來我們用 GYM 庫中的車桿穩(wěn)定小游戲來驗證一下我們構(gòu)建好的 DQN 模型,導入最基本的庫,設(shè)置參數(shù)。有關(guān) GYM 強化學習環(huán)境的內(nèi)容可以查看官方文檔:
https://www.gymlibrary.dev/#
環(huán)境的狀態(tài) state 包含四個:位置、速度、角度、角速度;動作 action 包含 2 個:小車左移和右移;目的是保證桿子豎直。環(huán)境交互與模型訓練如下:
import gym
from RL_DQN import DQN, ReplayBuffer
import torch
from tqdm import tqdm
import matplotlib.pyplot as plt
# GPU運算
device = torch.device("cuda") if torch.cuda.is_available() \
else torch.device("cpu")
# ------------------------------- #
# 全局變量
# ------------------------------- #
capacity = 500 # 經(jīng)驗池容量
lr = 2e-3 # 學習率
gamma = 0.9 # 折扣因子
epsilon = 0.9 # 貪心系數(shù)
target_update = 200 # 目標網(wǎng)絡的參數(shù)的更新頻率
batch_size = 32
n_hidden = 128 # 隱含層神經(jīng)元個數(shù)
min_size = 200 # 經(jīng)驗池超過200后再訓練
return_list = [] # 記錄每個回合的回報
# 加載環(huán)境
env = gym.make("CartPole-v1", render_mode="human")
n_states = env.observation_space.shape[0] # 4
n_actions = env.action_space.n # 2
# 實例化經(jīng)驗池
replay_buffer = ReplayBuffer(capacity)
# 實例化DQN
agent = DQN(n_states=n_states,
n_hidden=n_hidden,
n_actions=n_actions,
learning_rate=lr,
gamma=gamma,
epsilon=epsilon,
target_update=target_update,
device=device,
)
# 訓練模型
for i in range(500): # 100回合
# 每個回合開始前重置環(huán)境
state = env.reset()[0] # len=4
# 記錄每個回合的回報
episode_return = 0
done = False
# 打印訓練進度,一共10回合
with tqdm(total=10, desc='Iteration %d' % i) as pbar:
while True:
# 獲取當前狀態(tài)下需要采取的動作
action = agent.take_action(state)
# 更新環(huán)境
next_state, reward, done, _, _ = env.step(action)
# 添加經(jīng)驗池
replay_buffer.add(state, action, reward, next_state, done)
# 更新當前狀態(tài)
state = next_state
# 更新回合回報
episode_return += reward
# 當經(jīng)驗池超過一定數(shù)量后,訓練網(wǎng)絡
if replay_buffer.size() > min_size:
# 從經(jīng)驗池中隨機抽樣作為訓練集
s, a, r, ns, d = replay_buffer.sample(batch_size)
# 構(gòu)造訓練集
transition_dict = {
'states': s,
'actions': a,
'next_states': ns,
'rewards': r,
'dones': d,
}
# 網(wǎng)絡更新
agent.update(transition_dict)
# 找到目標就結(jié)束
if done: break
# 記錄每個回合的回報
return_list.append(episode_return)
# 更新進度條信息
pbar.set_postfix({
'return': '%.3f' % return_list[-1]
})
pbar.update(1)
# 繪圖
episodes_list = list(range(len(return_list)))
plt.plot(episodes_list, return_list)
plt.xlabel('Episodes')
plt.ylabel('Returns')
plt.title('DQN Returns')
plt.show()
我簡單訓練了100輪,每回合的回報 returns 繪圖如下。若各位發(fā)現(xiàn)代碼有誤,請及時反饋。文章來源:http://www.zghlxwxcb.cn/news/detail-402307.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-402307.html
到了這里,關(guān)于【深度強化學習】(1) DQN 模型解析,附Pytorch完整代碼的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!