網(wǎng)資源分配實(shí)戰(zhàn):MADDPG多智能體強(qiáng)化學(xué)習(xí)源碼解析與避坑指南)
簡(jiǎn)介這份資源是面向計(jì)算機(jī)相關(guān)專業(yè)學(xué)生與從業(yè)者的車聯(lián)網(wǎng)通信資源分配優(yōu)化項(xiàng)目源碼基于多智能體深度強(qiáng)化學(xué)習(xí)實(shí)現(xiàn)可作為畢業(yè)設(shè)計(jì)、期末課程設(shè)計(jì)或課程大作業(yè)的完整參考方案。項(xiàng)目圍繞車聯(lián)網(wǎng)場(chǎng)景下的通信資源分配問題整合了MADDPG、MADQN、DDPG等多種強(qiáng)化學(xué)習(xí)算法并配套環(huán)境建模、經(jīng)驗(yàn)回放、隨機(jī)基線等模塊便于讀者理解多智能體協(xié)作與資源調(diào)度的實(shí)現(xiàn)思路。壓縮包共20個(gè)文件以13個(gè)Python源碼為主另含6個(gè)編譯緩存文件與1份使用說明文檔整體約72KB結(jié)構(gòu)緊湊、便于快速定位核心代碼。該資源為個(gè)人畢設(shè)成果評(píng)審分達(dá)97分代碼經(jīng)過嚴(yán)格調(diào)試可穩(wěn)定運(yùn)行。目前已有328人學(xué)習(xí)下載適合希望深入強(qiáng)化學(xué)習(xí)與車聯(lián)網(wǎng)交叉方向、需要可運(yùn)行工程范例的讀者參考借鑒。1. 車聯(lián)網(wǎng)資源分配跑不通先看清這套 MADDPG 源碼的真實(shí)骨架車聯(lián)網(wǎng)通信資源分配優(yōu)化這個(gè)方向很多人卡在第一步仿真環(huán)境搭不起來或者多智能體一訓(xùn)練就發(fā)散。這套基于多智能體深度強(qiáng)化學(xué)習(xí)的 Python 源代碼核心是把 V2V車與車和 V2I車與基礎(chǔ)設(shè)施鏈路的功率與頻譜分配建模成多智能體博弈問題再用 MADDPG 及其變體去解。它包含 VN-MADDPG、SAMADDPG、MADDPG、MADQN、DDPG 五套算法實(shí)現(xiàn)外加 Random 基線環(huán)境文件統(tǒng)一叫Environment_marl.py。適合正在做車聯(lián)網(wǎng)資源分配畢業(yè)設(shè)計(jì)、課程大作業(yè)或者想拿一個(gè)能跑通的多智能體 DRL 框架改自己場(chǎng)景的人。下面我按實(shí)際拆包順序把環(huán)境、算法、訓(xùn)練、排錯(cuò)一條線講透。2. 環(huán)境與算法選型為什么是 MADDPG 而不是獨(dú)立 DDPG2.1 車聯(lián)網(wǎng)資源分配的馬爾可夫建模這套代碼把每個(gè) V2V 鏈路看作一個(gè)智能體狀態(tài)包括鏈路信道增益、干擾功率、剩余隊(duì)列等動(dòng)作是發(fā)射功率和頻譜選擇獎(jiǎng)勵(lì)與 V2I 鏈路容量和 V2V 鏈路可靠性掛鉤。Environment_marl.py里定義了step()和reset()這是所有算法的公共接口。常見做法是先跑 Random 基線拿到一個(gè)下界再對(duì)比 MADDPG 系列否則你無(wú)法判斷訓(xùn)練出來的策略到底有沒有學(xué)到東西。環(huán)境的關(guān)鍵參數(shù)集中在文件開頭的常量區(qū)比如車輛數(shù)量、信道數(shù)量、噪聲功率、最大發(fā)射功率。改場(chǎng)景時(shí)優(yōu)先動(dòng)這幾個(gè)不要一上來就改獎(jiǎng)勵(lì)函數(shù)否則后面排查會(huì)變成黑匣子。2.2 五套算法各自的定位算法文件適用場(chǎng)景特點(diǎn)Randomrandom.py基線對(duì)照不學(xué)習(xí)用于驗(yàn)證環(huán)境獎(jiǎng)勵(lì)量級(jí)DDPGDDPG_method.py單智能體對(duì)照把多智能體當(dāng)獨(dú)立個(gè)體忽略非平穩(wěn)性MADDPGmaddpg.py/model_agent_maddpg.py主算法集中訓(xùn)練分散執(zhí)行評(píng)論家看全局狀態(tài)MADQNmadqn.py離散動(dòng)作對(duì)照動(dòng)作空間離散時(shí)用和連續(xù)動(dòng)作對(duì)比VN-MADDPG / SAMADDPG對(duì)應(yīng)目錄改進(jìn)變體在 MADDPG 基礎(chǔ)上做價(jià)值分解或注意力機(jī)制選型理由很直接車聯(lián)網(wǎng)里每個(gè)鏈路的決策會(huì)互相影響?yīng)毩?DDPG 把其他智能體當(dāng)環(huán)境的一部分訓(xùn)練時(shí)非平穩(wěn)性會(huì)導(dǎo)致策略震蕩。MADDPG 的集中評(píng)論家能看到所有智能體的狀態(tài)和動(dòng)作緩解了這個(gè)問題。如果你只是做課程設(shè)計(jì)先把 MADDPG 跑通再拿 MADQN 做離散動(dòng)作的對(duì)比實(shí)驗(yàn)論文里的對(duì)比表格就夠用了。2.3 環(huán)境初始化的可復(fù)現(xiàn)步驟拿到包后先確認(rèn) Python 版本和依賴。代碼基于 PyTorch常見做法是建一個(gè)干凈虛擬環(huán)境python -m venv venv_marl source venv_marl/bin/activate # Windows 用 venv_marl\Scripts\activate pip install torch numpy matplotlib然后進(jìn)到Environment_marl.py所在目錄先單獨(dú)跑一次環(huán)境自檢# 環(huán)境自檢確認(rèn) reset 和 step 返回維度正確 from Environment_marl import Environment env Environment() state env.reset() print(state dim:, len(state)) next_state, reward, done, info env.step([0.5] * env.n_agents) print(reward:, reward, done:, done)邏輯說明reset()返回初始狀態(tài)向量長(zhǎng)度應(yīng)等于智能體數(shù)量乘以每個(gè)智能體的狀態(tài)維度。step()接收一個(gè)動(dòng)作列表返回下一狀態(tài)、獎(jiǎng)勵(lì)、終止標(biāo)志和信息字典。參數(shù)說明動(dòng)作值一般歸一化到 0 到 1 之間對(duì)應(yīng)發(fā)射功率比例如果傳入越界值環(huán)境內(nèi)部通常會(huì)裁剪但最好自己先確認(rèn)邊界。這一步跑通說明環(huán)境本身沒問題后面算法報(bào)錯(cuò)就集中在訓(xùn)練邏輯上。3. MADDPG 訓(xùn)練主循環(huán)從 replay buffer 到集中評(píng)論家3.1 經(jīng)驗(yàn)回放與 segment_tree 的作用replay_buffer.py和replay_memory.py負(fù)責(zé)存(state, action, reward, next_state, done)五元組。MADDPG 目錄下還有segment_tree.py這是優(yōu)先經(jīng)驗(yàn)回放用的數(shù)據(jù)結(jié)構(gòu)按 TD 誤差采樣讓訓(xùn)練更聚焦在難樣本上。如果你發(fā)現(xiàn)訓(xùn)練前期獎(jiǎng)勵(lì)上升很慢可以先關(guān)掉優(yōu)先回放用均勻采樣跑一遍對(duì)比確認(rèn)不是采樣邏輯寫錯(cuò)。# 優(yōu)先經(jīng)驗(yàn)回放采樣核心邏輯簡(jiǎn)化示意 import numpy as np class PrioritizedBuffer: def __init__(self, capacity, alpha0.6): self.capacity capacity self.alpha alpha self.buffer [] self.priorities np.zeros(capacity, dtypenp.float32) self.pos 0 def add(self, transition, td_error): max_prio self.priorities.max() if self.buffer else 1.0 if len(self.buffer) self.capacity: self.buffer.append(transition) else: self.buffer[self.pos] transition self.priorities[self.pos] (abs(td_error) 1e-5) ** self.alpha self.pos (self.pos 1) % self.capacity def sample(self, batch_size, beta0.4): prios self.priorities[:len(self.buffer)] probs prios / prios.sum() indices np.random.choice(len(self.buffer), batch_size, pprobs) samples [self.buffer[i] for i in indices] weights (len(self.buffer) * probs[indices]) ** (-beta) weights / weights.max() return samples, indices, weights邏輯說明add()時(shí)用 TD 誤差的絕對(duì)值加一個(gè)小常數(shù)作為優(yōu)先級(jí)避免零概率。sample()按優(yōu)先級(jí)概率采樣并用重要性采樣權(quán)重修正偏差。參數(shù)說明alpha控制優(yōu)先級(jí)程度0 就是均勻采樣beta控制重要性采樣修正強(qiáng)度訓(xùn)練后期應(yīng)逐漸退火到 1。常見坑是beta一直設(shè) 0.4導(dǎo)致后期更新方差偏大。3.2 集中評(píng)論家的輸入拼接model_agent_maddpg.py里評(píng)論家網(wǎng)絡(luò)接收的是所有智能體的狀態(tài)和動(dòng)作拼接向量。實(shí)現(xiàn)時(shí)要注意每個(gè)智能體的動(dòng)作維度可能不同拼接前必須按固定順序排列否則訓(xùn)練時(shí)輸入維度對(duì)不上會(huì)直接報(bào)錯(cuò)。我一般會(huì)在初始化時(shí)打印一次評(píng)論家輸入維度和n_agents * (state_dim action_dim)對(duì)一遍。# 評(píng)論家輸入拼接檢查 import torch import torch.nn as nn class Critic(nn.Module): def __init__(self, total_state_dim, total_action_dim, hidden64): super().__init__() self.net nn.Sequential( nn.Linear(total_state_dim total_action_dim, hidden), nn.ReLU(), nn.Linear(hidden, hidden), nn.ReLU(), nn.Linear(hidden, 1) ) def forward(self, all_states, all_actions): x torch.cat([all_states, all_actions], dim-1) return self.net(x) # 假設(shè) 3 個(gè)智能體每個(gè)狀態(tài) 4 維動(dòng)作 2 維 n_agents, state_dim, action_dim 3, 4, 2 critic Critic(n_agents * state_dim, n_agents * action_dim) dummy_s torch.randn(1, n_agents * state_dim) dummy_a torch.randn(1, n_agents * action_dim) print(critic output:, critic(dummy_s, dummy_a).shape)邏輯說明torch.cat在最后一維拼接狀態(tài)和動(dòng)作輸出一個(gè)標(biāo)量 Q 值。參數(shù)說明total_state_dim和total_action_dim必須是所有智能體維度之和不是單個(gè)智能體的維度。如果這里寫錯(cuò)訓(xùn)練時(shí) loss 會(huì)異常大或者直接 NaN。3.3 訓(xùn)練循環(huán)與超參數(shù)設(shè)置主訓(xùn)練循環(huán)一般在maddpg.py里每步先讓每個(gè)智能體根據(jù)當(dāng)前策略選動(dòng)作加探索噪聲執(zhí)行后存回放再?gòu)幕胤挪蓸痈?。關(guān)鍵超參數(shù)包括學(xué)習(xí)率、折扣因子、軟更新系數(shù)、探索噪聲衰減。# 訓(xùn)練主循環(huán)骨架 for episode in range(max_episodes): state env.reset() episode_reward 0 for step in range(max_steps): actions [] for agent in agents: action agent.select_action(state[agent.id], noise_scale) actions.append(action) next_state, reward, done, _ env.step(actions) buffer.add((state, actions, reward, next_state, done)) if len(buffer) batch_size: for agent in agents: agent.update(buffer, agents, gamma0.95, tau0.01) state next_state episode_reward reward if done: break noise_scale max(0.05, noise_scale * 0.995) print(fepisode {episode}, reward {episode_reward:.2f}, noise {noise_scale:.3f})邏輯說明每個(gè) episode 重置環(huán)境逐步選動(dòng)作、存經(jīng)驗(yàn)、更新網(wǎng)絡(luò)。參數(shù)說明gamma是折扣因子車聯(lián)網(wǎng)場(chǎng)景常用 0.9 到 0.99tau是目標(biāo)網(wǎng)絡(luò)軟更新系數(shù)0.01 比較穩(wěn)noise_scale初始 0.3 到 0.5按 0.995 衰減最低保留 0.05 保證持續(xù)探索。如果獎(jiǎng)勵(lì)曲線一直不漲先檢查噪聲是不是衰減太快智能體過早停止探索。4. 避坑與排查訓(xùn)練不收斂時(shí)先看這五條4.1 獎(jiǎng)勵(lì)曲線震蕩不上升現(xiàn)象episode reward 在某個(gè)值附近來回跳幾百輪沒有趨勢(shì)。原因通常是學(xué)習(xí)率偏大或者評(píng)論家過擬合。解決把 actor 和 critic 學(xué)習(xí)率都降到 1e-4 或 5e-5加梯度裁剪torch.nn.utils.clip_grad_norm_(params, 0.5)再跑 500 輪看趨勢(shì)。4.2 評(píng)論家 loss 變成 NaN現(xiàn)象訓(xùn)練幾十步后 loss 打印 nan。原因一般是輸入里有 inf 或者獎(jiǎng)勵(lì)量級(jí)過大。解決在step()返回前檢查 reward 是否有限對(duì)獎(jiǎng)勵(lì)做縮放比如除以 100同時(shí)確認(rèn)狀態(tài)歸一化不要讓某個(gè)維度數(shù)值到幾千。4.3 多智能體動(dòng)作維度對(duì)不上現(xiàn)象報(bào)錯(cuò)size mismatch或cat維度不一致。原因不同智能體的動(dòng)作空間定義不同但拼接時(shí)按統(tǒng)一維度處理了。解決在環(huán)境里統(tǒng)一每個(gè)智能體的動(dòng)作維度或者在評(píng)論家拼接前對(duì)每個(gè)動(dòng)作做 padding保證總維度固定。4.4 優(yōu)先回放導(dǎo)致訓(xùn)練不穩(wěn)定現(xiàn)象開了 segment_tree 后獎(jiǎng)勵(lì)波動(dòng)比均勻回放還大。原因優(yōu)先級(jí)更新太激進(jìn)或者重要性采樣權(quán)重沒退火。解決把a(bǔ)lpha從 0.6 降到 0.4beta從 0.4 線性升到 1.0觀察是否改善。如果還不行先退回均勻回放把主流程跑通。4.5 環(huán)境隨機(jī)種子沒固定現(xiàn)象每次跑結(jié)果差異很大無(wú)法復(fù)現(xiàn)。原因numpy 和 torch 的隨機(jī)種子沒設(shè)。解決在訓(xùn)練腳本開頭加import numpy as np import torch import random seed 42 np.random.seed(seed) torch.manual_seed(seed) random.seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)邏輯說明固定種子后同一份代碼在同一環(huán)境下結(jié)果可復(fù)現(xiàn)。參數(shù)說明seed選 42 只是習(xí)慣換成其他整數(shù)也行但論文里要寫清楚用的哪個(gè)。5. 進(jìn)階技巧用 Random 基線驗(yàn)證環(huán)境再拿 MADDPG 做對(duì)比實(shí)驗(yàn)跑通訓(xùn)練只是第一步真正讓這套源碼在畢業(yè)設(shè)計(jì)里站住腳是學(xué)會(huì)用它做對(duì)照實(shí)驗(yàn)。我一般會(huì)強(qiáng)制走一遍這個(gè)流程先跑 Random再跑獨(dú)立 DDPG最后跑 MADDPG 和它的變體把四條曲線畫在同一張圖里。Random 的獎(jiǎng)勵(lì)應(yīng)該是一條水平線如果它波動(dòng)很大說明環(huán)境本身隨機(jī)性過強(qiáng)或者獎(jiǎng)勵(lì)設(shè)計(jì)有問題這時(shí)候調(diào)算法是白費(fèi)力氣。驗(yàn)證環(huán)境是否合理可以看 Random 基線的平均獎(jiǎng)勵(lì)和方差。如果方差比均值還大常見做法是增加每個(gè) episode 的步數(shù)或者對(duì)獎(jiǎng)勵(lì)做滑動(dòng)平均。下面這個(gè)畫圖腳本可以直接抄import matplotlib.pyplot as plt import numpy as np def moving_average(x, window50): return np.convolve(x, np.ones(window)/window, modevalid) # 假設(shè) random_rewards, ddpg_rewards, maddpg_rewards 是三個(gè)列表 plt.figure(figsize(10, 5)) plt.plot(moving_average(random_rewards), labelRandom) plt.plot(moving_average(ddpg_rewards), labelIndependent DDPG) plt.plot(moving_average(maddpg_rewards), labelMADDPG) plt.xlabel(Episode) plt.ylabel(Average Reward) plt.legend() plt.grid(True) plt.savefig(comparison.png, dpi150) plt.show()邏輯說明moving_average做滑動(dòng)平均窗口 50 可以濾掉高頻震蕩讓趨勢(shì)更清楚。參數(shù)說明window太小曲線還是抖太大又會(huì)滯后50 到 100 之間比較合適。保存圖片時(shí)dpi150夠論文用。還有一個(gè)容易被忽略的點(diǎn)MADDPG 的集中評(píng)論家只在訓(xùn)練時(shí)用執(zhí)行時(shí)每個(gè)智能體只用本地 actor。如果你在測(cè)試階段還把全局狀態(tài)喂給評(píng)論家那就不是分散執(zhí)行了實(shí)驗(yàn)結(jié)論會(huì)站不住。檢查方法很簡(jiǎn)單看測(cè)試代碼里有沒有調(diào)用評(píng)論家網(wǎng)絡(luò)正常應(yīng)該只調(diào)用 actor。從那以后我每次拿到多智能體 DRL 源碼都強(qiáng)制先跑 Random 基線確認(rèn)環(huán)境獎(jiǎng)勵(lì)量級(jí)再固定種子跑三遍 MADDPG 看方差最后才動(dòng)超參數(shù)。這套流程幫我省了很多來回折騰的時(shí)間。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取