現(xiàn)DQN:CartPole環(huán)境下的深度強(qiáng)化學(xué)習(xí)實(shí)戰(zhàn))
簡(jiǎn)介這份資源面向強(qiáng)化學(xué)習(xí)初學(xué)者與MATLAB使用者提供一套自主搭建DQN算法解決CartPole平衡桿問(wèn)題的完整代碼實(shí)現(xiàn)幫助讀者把Q-learning與神經(jīng)網(wǎng)絡(luò)結(jié)合的理論落到可運(yùn)行的程序中。壓縮包共3個(gè)文件均為m腳本整體約4KB分別承擔(dān)環(huán)境建模、DQN學(xué)習(xí)主流程與訓(xùn)練入口等職責(zé)結(jié)構(gòu)精簡(jiǎn)便于逐行閱讀與調(diào)試。內(nèi)容覆蓋狀態(tài)與動(dòng)作空間定義、Q網(wǎng)絡(luò)架構(gòu)設(shè)計(jì)、經(jīng)驗(yàn)回放緩沖區(qū)、目標(biāo)網(wǎng)絡(luò)更新、ε-greedy探索策略以及訓(xùn)練評(píng)估等關(guān)鍵環(huán)節(jié)讀者可據(jù)此理解價(jià)值函數(shù)近似、樣本隨機(jī)采樣與訓(xùn)練穩(wěn)定性之間的配合關(guān)系。目前已有2180人學(xué)習(xí)下載適合希望用MATLAB快速上手深度強(qiáng)化學(xué)習(xí)、并在此基礎(chǔ)上嘗試優(yōu)先回放等改進(jìn)技巧的讀者參考。1. 用 MATLAB 從零搭一個(gè) DQNCartPole 為什么是性?xún)r(jià)比最高的驗(yàn)證場(chǎng)很多人第一次接觸強(qiáng)化學(xué)習(xí)都是在 Python 里跑 Stable-Baselines3兩行代碼就把 CartPole 跑到了 500 分。但如果你手頭只有 MATLAB或者你的項(xiàng)目本身就跑在 MATLAB 生態(tài)里——比如做控制、做信號(hào)處理、做聯(lián)合仿真——那把 DQN 用 MATLAB 自主搭一遍價(jià)值就完全不一樣了。它逼你把經(jīng)驗(yàn)回放、目標(biāo)網(wǎng)絡(luò)、ε-貪心、貝爾曼更新這些環(huán)節(jié)全部手寫(xiě)出來(lái)而不是調(diào)一個(gè)model.learn()就完事。CartPole 這個(gè)環(huán)境狀態(tài)只有 4 維、動(dòng)作只有 2 個(gè)是驗(yàn)證 DQN 實(shí)現(xiàn)是否正確的最小閉環(huán)跑通了說(shuō)明你的梯度、采樣、目標(biāo)值計(jì)算邏輯沒(méi)寫(xiě)反跑不通問(wèn)題一定出在你自己的代碼里而不是環(huán)境太復(fù)雜。這篇筆記面向的是有 MATLAB 基礎(chǔ)、想真正搞懂 DQN 內(nèi)部機(jī)制、并且愿意用 MATLAB 做深度學(xué)習(xí)與控制聯(lián)合仿真的從業(yè)者。下面從網(wǎng)絡(luò)結(jié)構(gòu)、經(jīng)驗(yàn)回放、訓(xùn)練循環(huán)到調(diào)參排查一步步把可復(fù)現(xiàn)的代碼寫(xiě)出來(lái)。2. 先想清楚 DQN 在 MATLAB 里怎么落地網(wǎng)絡(luò)、回放、目標(biāo)網(wǎng)絡(luò)三件套2.1 為什么用 Deep Learning Toolbox 的 dlnetwork 而不是 trainNetworkMATLAB 做深度學(xué)習(xí)有兩條路一是trainNetwork適合監(jiān)督學(xué)習(xí)輸入輸出成對(duì)、一次喂一批二是dlnetwork 自定義訓(xùn)練循環(huán)適合 DQN 這種目標(biāo)值隨訓(xùn)練動(dòng)態(tài)變化、需要手動(dòng)控制每次梯度更新的場(chǎng)景。DQN 的 TD 目標(biāo) $y r \gamma \max_{a} Q_{target}(s, a)$ 每一輪都在變trainNetwork那套固定標(biāo)簽的流程根本套不進(jìn)去。所以必須用dlnetwork定義網(wǎng)絡(luò)用dlfevaldlgradient手動(dòng)求梯度用adamupdate手動(dòng)更新參數(shù)。這是整個(gè)方案的地基選錯(cuò)了后面全是坑。網(wǎng)絡(luò)結(jié)構(gòu)上CartPole 狀態(tài) 4 維、動(dòng)作 2 個(gè)不需要卷積。常見(jiàn)做法是兩層全連接每層 64 或 128 個(gè)神經(jīng)元激活用 ReLU輸出層 2 個(gè)節(jié)點(diǎn)對(duì)應(yīng)兩個(gè)動(dòng)作的 Q 值。注意輸出層不要加 softmax因?yàn)?Q 值是實(shí)數(shù)不是概率。下面是最小網(wǎng)絡(luò)定義% 定義 Q 網(wǎng)絡(luò)4 - 64 - 64 - 2 stateDim 4; actionDim 2; layers [ featureInputLayer(stateDim, Name, state) fullyConnectedLayer(64, Name, fc1) reluLayer(Name, relu1) fullyConnectedLayer(64, Name, fc2) reluLayer(Name, relu2) fullyConnectedLayer(actionDim, Name, qout) ]; qNet dlnetwork(layers);邏輯說(shuō)明featureInputLayer接收 4 維狀態(tài)向量?jī)蓪觙ullyConnectedLayer做特征提取最后輸出 2 維 Q 值。參數(shù)上64 是隱藏層寬度CartPole 用 64 足夠用 128 收斂略穩(wěn)但慢一點(diǎn)再大就過(guò)擬合且沒(méi)必要。dlnetwork會(huì)自動(dòng)初始化權(quán)重不用手動(dòng) assign。2.2 經(jīng)驗(yàn)回放池用循環(huán)緩沖區(qū)而不是動(dòng)態(tài)數(shù)組經(jīng)驗(yàn)回放的作用是打破樣本時(shí)間相關(guān)性讓訓(xùn)練穩(wěn)定。MATLAB 里如果每次用[buffer; newExp]拼接數(shù)據(jù)量一大內(nèi)存拷貝會(huì)拖垮訓(xùn)練速度。正確做法是預(yù)分配一個(gè)固定容量的結(jié)構(gòu)體數(shù)組用索引循環(huán)覆蓋% 經(jīng)驗(yàn)回放池預(yù)分配循環(huán)覆蓋 bufferCap 10000; buffer.state zeros(stateDim, bufferCap, single); buffer.action zeros(1, bufferCap, single); buffer.reward zeros(1, bufferCap, single); buffer.nextState zeros(stateDim, bufferCap, single); buffer.done zeros(1, bufferCap, single); buffer.count 0; buffer.ptr 1; function buffer pushExperience(buffer, s, a, r, sNext, done) idx buffer.ptr; buffer.state(:, idx) s; buffer.action(idx) a; buffer.reward(idx) r; buffer.nextState(:, idx) sNext; buffer.done(idx) done; buffer.ptr mod(buffer.ptr, size(buffer.state,2)) 1; buffer.count min(buffer.count 1, size(buffer.state,2)); end邏輯說(shuō)明ptr指向下一個(gè)寫(xiě)入位置寫(xiě)滿(mǎn)后回到 1 覆蓋最舊的經(jīng)驗(yàn)。count記錄當(dāng)前有效樣本數(shù)采樣時(shí)只在1:count范圍內(nèi)取避免采到未初始化的零。參數(shù)上bufferCap取 10000 是 CartPole 的常用值太小如 1000會(huì)導(dǎo)致樣本重復(fù)、訓(xùn)練震蕩太大如 100000在 CartPole 這種短回合任務(wù)里浪費(fèi)內(nèi)存且早期樣本長(zhǎng)期滯留。2.3 目標(biāo)網(wǎng)絡(luò)硬更新還是軟更新DQN 用目標(biāo)網(wǎng)絡(luò)計(jì)算 TD 目標(biāo)避免自舉導(dǎo)致的發(fā)散。目標(biāo)網(wǎng)絡(luò)參數(shù)不參與梯度更新每隔 C 步從在線(xiàn)網(wǎng)絡(luò)硬拷貝一次或者每步做軟更新 $\theta^- \leftarrow \tau\theta (1-\tau)\theta^-$。CartPole 上硬更新更常見(jiàn)C 取 100 到 500 都行。軟更新 τ 取 0.005 左右收斂更平滑但慢。我一般先用硬更新 C200 跑通再考慮換軟更新。% 目標(biāo)網(wǎng)絡(luò)初始化與硬更新 targetNet qNet; targetUpdateFreq 200; stepCount 0; % 訓(xùn)練循環(huán)內(nèi) stepCount stepCount 1; if mod(stepCount, targetUpdateFreq) 0 targetNet qNet; % 硬拷貝所有可學(xué)習(xí)參數(shù) end邏輯說(shuō)明targetNet qNet在 MATLAB 里會(huì)復(fù)制整個(gè) dlnetwork 對(duì)象包括可學(xué)習(xí)參數(shù)。注意不要寫(xiě)成targetNet.Learnables qNet.Learnables那樣只復(fù)制了引用后續(xù)更新會(huì)串。參數(shù)上targetUpdateFreq太小如 10目標(biāo)網(wǎng)絡(luò)追得太緊失去穩(wěn)定作用太大如 2000目標(biāo)值滯后嚴(yán)重學(xué)習(xí)變慢。3. 把訓(xùn)練循環(huán)寫(xiě)對(duì)采樣、計(jì)算 TD 目標(biāo)、梯度更新三步走3.1 ε-貪心采樣與動(dòng)作選擇訓(xùn)練初期網(wǎng)絡(luò)輸出接近隨機(jī)需要高探索后期要利用學(xué)到的 Q 值。ε 從 1.0 線(xiàn)性衰減到 0.05 是標(biāo)準(zhǔn)做法。動(dòng)作選擇時(shí)把狀態(tài)轉(zhuǎn)成dlarray前向傳播取 Q 值最大的動(dòng)作% ε-貪心動(dòng)作選擇 function [action, qValues] selectAction(qNet, state, epsilon, actionDim) if rand epsilon action randi(actionDim) - 1; % 0 或 1 qValues []; else dlState dlarray(single(state), CB); qValues predict(qNet, dlState); [~, idx] max(extractdata(qValues)); action idx - 1; end end邏輯說(shuō)明dlarray的CB格式表示通道×批次單樣本時(shí)批次為 1。predict只做前向不算梯度比f(wàn)orward快。extractdata把 dlarray 轉(zhuǎn)回普通數(shù)組才能用max。參數(shù)上ε 衰減步數(shù)一般取總訓(xùn)練步數(shù)的 10% 到 20%CartPole 總步數(shù)約 5 萬(wàn)到 10 萬(wàn)所以 ε 在 5000 到 10000 步內(nèi)從 1 降到 0.05。3.2 從回放池采樣并計(jì)算 TD 目標(biāo)這是 DQN 最容易寫(xiě)錯(cuò)的地方。TD 目標(biāo)里max要作用在目標(biāo)網(wǎng)絡(luò)的輸出上且終止?fàn)顟B(tài)的目標(biāo)值只等于即時(shí)獎(jiǎng)勵(lì)不加折扣的未來(lái)值% 從回放池采樣一個(gè)批次 batchSize 64; idx randperm(buffer.count, min(batchSize, buffer.count)); sBatch dlarray(buffer.state(:, idx), CB); aBatch buffer.action(idx); rBatch buffer.reward(idx); sNextBatch dlarray(buffer.nextState(:, idx), CB); doneBatch buffer.done(idx); % 目標(biāo)網(wǎng)絡(luò)前向取最大 Q qNext predict(targetNet, sNextBatch); maxQNext max(extractdata(qNext), [], 1); gamma 0.99; targetQ rBatch gamma * maxQNext .* (1 - doneBatch); % 在線(xiàn)網(wǎng)絡(luò)前向取出實(shí)際動(dòng)作對(duì)應(yīng)的 Q qOnline forward(qNet, sBatch); qOnlineData extractdata(qOnline); qTaken qOnlineData(sub2ind(size(qOnlineData), aBatch 1, 1:numel(aBatch)));邏輯說(shuō)明sub2ind把動(dòng)作索引和批次索引轉(zhuǎn)成線(xiàn)性索引取出每個(gè)樣本實(shí)際執(zhí)行動(dòng)作的 Q 值。doneBatch為 1 時(shí)(1-done)為 0終止?fàn)顟B(tài)目標(biāo)值只剩即時(shí)獎(jiǎng)勵(lì)。參數(shù)上gamma0.99是 CartPole 常用折扣太小如 0.9會(huì)讓智能體短視太大如 0.999在 CartPole 這種回合制任務(wù)里沒(méi)必要。3.3 用 dlfeval 和 adamupdate 完成一次梯度更新MATLAB 自定義訓(xùn)練循環(huán)的核心是dlfeval調(diào)用一個(gè)返回?fù)p失和梯度的函數(shù)再用adamupdate更新% 定義損失函數(shù) function [loss, gradients] dqnLoss(qNet, sBatch, aBatch, targetQ) qOut forward(qNet, sBatch); qOutData extractdata(qOut); qTaken qOutData(sub2ind(size(qOutData), aBatch 1, 1:numel(aBatch))); loss mean((qTaken - targetQ).^2); gradients dlgradient(loss, qNet.Learnables); end % 訓(xùn)練循環(huán)內(nèi)更新 [loss, gradients] dlfeval(dqnLoss, qNet, sBatch, aBatch, targetQ); [qNet, avgGrad, avgSqGrad] adamupdate(qNet, gradients, avgGrad, avgSqGrad, stepCount, learnRate);邏輯說(shuō)明dlfeval在自動(dòng)微分上下文中執(zhí)行dqnLossdlgradient對(duì)qNet.Learnables求梯度。adamupdate維護(hù)一階和二階動(dòng)量avgGrad和avgSqGrad初始化為空即可函數(shù)內(nèi)部會(huì)自動(dòng)初始化。參數(shù)上學(xué)習(xí)率learnRate取 1e-3 是起點(diǎn)CartPole 上 5e-4 到 1e-3 都常見(jiàn)太大如 1e-2會(huì)震蕩不收斂太小如 1e-5學(xué)得極慢。4. 避坑與排查CartPole 跑不收斂時(shí)先查這 5 個(gè)地方4.1 現(xiàn)象獎(jiǎng)勵(lì)一直卡在 10 到 20完全不漲原因最常見(jiàn)的是 TD 目標(biāo)里用了在線(xiàn)網(wǎng)絡(luò)的 Q 值而不是目標(biāo)網(wǎng)絡(luò)或者max維度搞錯(cuò)。另一個(gè)高頻原因是done標(biāo)志沒(méi)正確傳入終止?fàn)顟B(tài)還在加折扣未來(lái)值導(dǎo)致 Q 值被高估。解決檢查qNext predict(targetNet, sNextBatch)是否用的targetNet檢查max(extractdata(qNext), [], 1)的維度是否為 1因?yàn)镃B格式下第一維是通道即動(dòng)作維檢查doneBatch在回合結(jié)束時(shí)是否置 1。4.2 現(xiàn)象訓(xùn)練初期獎(jiǎng)勵(lì)上升幾百輪后突然崩掉原因經(jīng)驗(yàn)回放池里早期低質(zhì)量樣本被反復(fù)采樣或者學(xué)習(xí)率沒(méi)有衰減后期梯度太大把已學(xué)好的策略沖垮。解決把bufferCap適當(dāng)調(diào)小讓舊樣本更快被覆蓋或者在訓(xùn)練后期把學(xué)習(xí)率乘以 0.1。另一個(gè)辦法是增大targetUpdateFreq讓目標(biāo)網(wǎng)絡(luò)更穩(wěn)定。4.3 現(xiàn)象dlgradient報(bào)錯(cuò)說(shuō)找不到可微分參數(shù)原因qNet.Learnables在dlfeval外部被修改過(guò)或者網(wǎng)絡(luò)里混入了不支持自動(dòng)微分的層。MATLAB 的dlnetwork要求所有層都支持dlarray輸入。解決確保網(wǎng)絡(luò)只用featureInputLayer、fullyConnectedLayer、reluLayer這類(lèi)內(nèi)置層不要在dlfeval外面手動(dòng)改qNet.Learnables的值如果自定義了層檢查predict和forward是否都實(shí)現(xiàn)了。4.4 現(xiàn)象訓(xùn)練速度極慢一輪要好幾秒原因每次采樣都用randperm(buffer.count, batchSize)在幾萬(wàn)樣本里隨機(jī)取或者predict被反復(fù)調(diào)用在單樣本上。解決randperm本身不慢慢的是把dlarray反復(fù)創(chuàng)建。把批次數(shù)據(jù)一次性轉(zhuǎn)成dlarray不要在循環(huán)里逐個(gè)轉(zhuǎn)。另外predict用于目標(biāo)網(wǎng)絡(luò)時(shí)不需要梯度確認(rèn)沒(méi)有誤用forward。如果還是慢檢查 MATLAB 是否在用 CPU 而非 GPUgpuDevice確認(rèn)可用后把網(wǎng)絡(luò)和數(shù)據(jù)轉(zhuǎn)到 GPU。4.5 現(xiàn)象換了隨機(jī)種子結(jié)果差異巨大原因DQN 本身方差就大CartPole 雖然簡(jiǎn)單但不同初始化、不同采樣順序會(huì)導(dǎo)致收斂輪數(shù)差幾倍。這不是 bug是算法特性。解決跑 3 到 5 個(gè)種子取平均不要用單次結(jié)果判斷算法好壞。固定種子只用于調(diào)試復(fù)現(xiàn)評(píng)估時(shí)要用多個(gè)種子。如果多個(gè)種子都完全不收斂再回去查 4.1 到 4.4。5. 進(jìn)階技巧用獎(jiǎng)勵(lì)塑形和雙網(wǎng)絡(luò)結(jié)構(gòu)把 CartPole 推到穩(wěn)定 500 分CartPole 的原始獎(jiǎng)勵(lì)是每步 1回合上限 500 步。標(biāo)準(zhǔn) DQN 在調(diào)好參數(shù)后能穩(wěn)定到 475 以上但有時(shí)會(huì)在 300 到 400 徘徊。我一般會(huì)加兩個(gè)小改動(dòng)一是獎(jiǎng)勵(lì)塑形在桿子角度接近閾值時(shí)給一個(gè)小懲罰讓智能體更早學(xué)會(huì)修正二是把目標(biāo)值的max換成雙網(wǎng)絡(luò)結(jié)構(gòu)用在線(xiàn)網(wǎng)絡(luò)選動(dòng)作、目標(biāo)網(wǎng)絡(luò)算 Q 值緩解高估。獎(jiǎng)勵(lì)塑形可以這樣寫(xiě)在環(huán)境 step 之后加一層判斷% 獎(jiǎng)勵(lì)塑形角度越大懲罰越重 angleThreshold 0.15; % 弧度 if abs(nextState(3)) angleThreshold shapedReward reward - 0.1 * abs(nextState(3)); else shapedReward reward; end邏輯說(shuō)明nextState(3)是桿子角度超過(guò) 0.15 弧度時(shí)按比例扣分。參數(shù)上0.15 是經(jīng)驗(yàn)值CartPole 終止閾值是 0.2095 弧度取 0.15 留出緩沖懲罰系數(shù) 0.1 不要太大否則智能體會(huì)過(guò)度保守。雙網(wǎng)絡(luò)目標(biāo)值計(jì)算把原來(lái)的一行拆成兩步% 雙網(wǎng)絡(luò)在線(xiàn)網(wǎng)絡(luò)選動(dòng)作目標(biāo)網(wǎng)絡(luò)算 Q qNextOnline predict(qNet, sNextBatch); [~, bestAction] max(extractdata(qNextOnline), [], 1); qNextTarget predict(targetNet, sNextBatch); qNextTargetData extractdata(qNextTarget); maxQNext qNextTargetData(sub2ind(size(qNextTargetData), bestAction, 1:numel(bestAction))); targetQ rBatch gamma * maxQNext .* (1 - doneBatch);邏輯說(shuō)明bestAction由在線(xiàn)網(wǎng)絡(luò)決定但 Q 值從目標(biāo)網(wǎng)絡(luò)取這樣選動(dòng)作和估價(jià)值解耦減少樂(lè)觀偏差。參數(shù)上不需要額外調(diào)參只是多了一次前向傳播訓(xùn)練速度略降但穩(wěn)定性提升。驗(yàn)證方法上我習(xí)慣每 50 輪跑一次貪心策略ε0的評(píng)估記錄最近 100 輪的平均獎(jiǎng)勵(lì)。如果平均獎(jiǎng)勵(lì)連續(xù) 3 次評(píng)估都超過(guò) 450就認(rèn)為收斂。不要看單輪最高分那個(gè)沒(méi)意義。最后說(shuō)個(gè)血淚經(jīng)驗(yàn)MATLAB 的dlnetwork在保存和加載時(shí)如果版本不同可能丟參數(shù)訓(xùn)練完立刻用save(dqnCartPole.mat, qNet)存下來(lái)別等。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取