習(xí)算法包實戰(zhàn):BC、DAgger與GAIL訓(xùn)練調(diào)參與排錯)
簡介面向深度強化學(xué)習(xí)與模仿學(xué)習(xí)方向的學(xué)習(xí)者這份代碼包圍繞從專家軌跡中學(xué)習(xí)策略這一主題覆蓋 PPO、AIRL、BC、DRIL、FAIRL、GAIL、GMMIL、PUGAIL、RED 等算法并提供僅狀態(tài)模仿、吸收狀態(tài)指示器與 R1 梯度正則化等實驗選項適合算法復(fù)現(xiàn)、課程項目與基線對比。壓縮包共 27 個文件、約 999KB其中 6 個 Python 腳本承擔(dān)訓(xùn)練、評估與環(huán)境交互邏輯17 張 PNG 匯總各算法訓(xùn)練與測試回報曲線2 個 Markdown 文檔說明使用方法另附專家軌跡數(shù)據(jù)文件目錄緊湊、模塊邊界清楚。已有 1068 人學(xué)習(xí)讀者可從中獲得可運行的模仿學(xué)習(xí)實驗框架、專家軌跡加載與策略優(yōu)化示例、不同算法的結(jié)果對照圖以及排查訓(xùn)練不穩(wěn)定與回報波動的參考思路并據(jù)此理解生成對抗式模仿與行為克隆的差異。1. 獎勵函數(shù)寫不出來時imitation-learning 模仿學(xué)習(xí)算法包能接住哪類任務(wù)控制類項目最常見的卡點不在模型大小而在獎勵函數(shù)機械臂要的是平滑軌跡寫個距離懲罰訓(xùn)練出來全是抖動策略梯度在稀疏獎勵下幾乎不更新調(diào)兩周不如人遙操作十分鐘。imitation-learning 這套 Python 實現(xiàn)的模仿學(xué)習(xí)算法包走的是另一條路——把專家演示當(dāng)監(jiān)督信號先用行為克隆BC把策略立起來再用 DAgger 補上分布偏移最后用 GAIL 在環(huán)境里做對抗式精調(diào)。它面向的是已經(jīng)有腳本專家或遙操作數(shù)據(jù)、但獎勵寫不出來或?qū)懖粶?zhǔn)的 deep-reinforcement-learning 場景判斷自己適不適合標(biāo)準(zhǔn)很樸素你能把一條條軌跡存成(obs, action, next_obs, done)的數(shù)組并且能讓策略真正跑進環(huán)境里這套東西就能用。2. 三種范式的代碼骨架BC 打底、DAgger 補偏移、GAIL 做對抗精調(diào)模仿學(xué)習(xí)算法包最容易讓人誤判的地方是把三個算法當(dāng)成三選一的替代品。實際工程里它們是階梯BC 決定你有沒有一個能跑的策略DAgger 決定這個策略在離線和在線之間能不能接住GAIL 決定最終性能能不能逼近專家。這一章按數(shù)據(jù)流順序把三塊骨架拆開代碼用最常見的組織方式寫倉庫里的類名函數(shù)名可能不一樣但張量形狀和損失形式是對得上的。2.1 軌跡容器與數(shù)據(jù)協(xié)議先把專家數(shù)據(jù)對齊成 (obs, action, next_obs, done)新手翻車十有八九不在算法在數(shù)據(jù)協(xié)議。專家數(shù)據(jù)是離線采的策略數(shù)據(jù)是在線采的如果兩邊字段順序、dtype、done的表示方式不一致后面所有 loss 曲線都沒有參考價值。我一般先寫一個兩種數(shù)據(jù)源共用的環(huán)形緩沖把協(xié)議釘死。import numpy as np class TrajectoryBuffer: 環(huán)形緩沖專家軌跡和策略 rollout 共用同一套字段協(xié)議 def __init__(self, obs_dim, act_dim, capacity200_000): self.capacity capacity self.obs np.zeros((capacity, obs_dim), dtypenp.float32) self.act np.zeros((capacity, act_dim), dtypenp.float32) self.rew np.zeros((capacity, 1), dtypenp.float32) self.next_obs np.zeros((capacity, obs_dim), dtypenp.float32) self.done np.zeros((capacity, 1), dtypenp.float32) # 用 0/1不用 bool self.ptr, self.size 0, 0 def add(self, obs, act, rew, next_obs, done): i self.ptr self.obs[i], self.act[i] obs, act self.rew[i], self.next_obs[i], self.done[i] rew, next_obs, float(done) self.ptr (self.ptr 1) % self.capacity self.size min(self.size 1, self.capacity) def sample(self, batch_size, rngNone): rng rng or np.random idx rng.randint(0, self.size, sizebatch_size) # 有放回采樣夠用且實現(xiàn)簡單 return (self.obs[idx], self.act[idx], self.rew[idx], self.next_obs[idx], self.done[idx]) def save(self, path): # 鍵名固定成這五個評估腳本和可視化腳本都按這套鍵讀 np.savez_compressed(path, obsself.obs[:self.size], actself.act[:self.size], rewself.rew[:self.size], next_obsself.next_obs[:self.size], doneself.done[:self.size])幾個參數(shù)值得說清。dtypenp.float32不是隨手寫的PyTorch 默認(rèn)張量就是 float32用 float64 存一遍再轉(zhuǎn)一遍幾十萬條樣本會白白多占一倍內(nèi)存而且轉(zhuǎn)換發(fā)生在DataLoader的 worker 里很容易變成隱性瓶頸。done存成 0/1 浮點而不是布爾是為了和rew一起拼成(N, 5)的數(shù)組做批量處理時類型不打架。capacity給 20 萬條是經(jīng)驗值連續(xù)動作任務(wù)單條軌跡通常幾百到幾千步20 萬條大致相當(dāng)于幾百條軌跡覆蓋 GAIL 一輪訓(xùn)練足夠再大就只是拖慢采樣。圖像觀測要額外注意一點如果環(huán)境返回的是(H, W, C)的 uint8 幀在add之前就要定好是展平還是保留三維。展平省事但卷積策略網(wǎng)絡(luò)就用不上了保留三維則要在obs_dim上寫(C, H, W)并做通道前置。這個決定一旦寫進 npz后面改起來要重采數(shù)據(jù)屬于典型的后悔藥沒處買。2.2 行為克隆三層 MLP 加 MSE 就夠但輸出頭決定成敗BC 本質(zhì)是監(jiān)督學(xué)習(xí)obs → action的回歸或分類。網(wǎng)絡(luò)不需要深兩層 256 的 tanh MLP 在絕大多數(shù)連續(xù)控制任務(wù)上已經(jīng)夠用真正決定成敗的是輸出頭跟動作空間的匹配方式。import torch import torch.nn as nn class PolicyNet(nn.Module): def __init__(self, obs_dim, act_dim, hidden256): super().__init__() self.body nn.Sequential( nn.Linear(obs_dim, hidden), nn.Tanh(), nn.Linear(hidden, hidden), nn.Tanh(), ) self.head nn.Linear(hidden, act_dim) # 確定性策略直接回歸動作均值 def forward(self, obs): return self.head(self.body(obs)) def bc_loss(policy, obs, act, discreteFalse): out policy(obs) if discrete: # 離散動作交叉熵act 是 long 型類別索引 return nn.functional.cross_entropy(out, act.long()) # 連續(xù)動作MSE 回歸act 需與 out 同量綱 return nn.functional.mse_loss(out, act)nn.Tanh()而不是 ReLU是因為連續(xù)控制的觀測和動作經(jīng)常帶負(fù)值tanh 的對稱性讓初始階段的梯度分布更均勻如果觀測是圖像把body換成幾層 stride2 的卷積再接展平即可輸出頭不動。連續(xù)動作用 MSE 意味著你在擬合條件均值這在高斯策略假設(shè)下等價于只學(xué)均值不學(xué)方差好處是穩(wěn)定壞處是遇到多峰專家數(shù)據(jù)同一個狀態(tài)專家有時左拐有時右拐會學(xué)出一個誰都不像的中間動作。常見做法是改成高斯頭多輸出一維log_std并用-log_prob當(dāng)損失代價是訓(xùn)練更容易發(fā)散方差要 clamp 到[-5, 2]這種范圍。動作量綱是另一個高頻坑。環(huán)境action_space是[-1, 1]的話專家數(shù)據(jù)如果來自力矩控制比如[-20, 20]直接回歸出來的策略一進環(huán)境就會被 clip 成常量現(xiàn)象是 BC loss 掉得很快但 rollout 一動不動。落數(shù)據(jù)前統(tǒng)一按(a - low) / (high - low) * 2 - 1歸一化到[-1, 1]評估時再反歸一化這一步寫在采集腳本里比寫在訓(xùn)練腳本里安全。2.3 DAgger 的在線聚合把「策略會走到哪」交給專家重新打標(biāo)BC 的理論誤差隨軌跡長度平方增長直覺解釋就是策略在專家沒去過的狀態(tài)上沒學(xué)過一旦偏離就再也回不來。DAgger 的思路很直接——讓當(dāng)前策略自己去跑跑到哪些狀態(tài)就請專家對這些狀態(tài)重新打標(biāo)簽把新樣本并進數(shù)據(jù)集重訓(xùn)。代碼骨架不長但循環(huán)里三個順序不能亂。def dagger_loop(env, expert, policy, dataset, iters10, beta00.5, steps_per_iter2000): for it in range(iters): # 1) 用當(dāng)前策略在環(huán)境里采樣收集它真實訪問到的狀態(tài) states [] obs, _ env.reset() for _ in range(steps_per_iter): beta beta0 ** it # 專家介入概率指數(shù)衰減 if np.random.rand() beta: act expert.act(obs) # 專家接管把軌跡拉回可行域 else: with torch.no_grad(): act policy(torch.as_tensor(obs, dtypetorch.float32)).numpy() states.append(obs) obs, _, term, trunc, _ env.step(act) if term or trunc: obs, _ env.reset() # 2) 請專家對這些“策略自己走出來的狀態(tài)”打標(biāo)簽 states np.asarray(states, dtypenp.float32) labels expert.label(states) # 支持批量查詢的實現(xiàn)比逐幀快一個量級 # 3) 聚合后重訓(xùn)注意是在全量數(shù)據(jù)集上重訓(xùn)而不是只訓(xùn)新樣本 dataset.extend(states, labels) train_bc(policy, dataset, epochs5)beta0控制專家介入強度0.5意味著第一輪一半動作由專家給之后每輪減半。有人直接取beta max(0, 1 - it / iters)線性衰減差別不大但指數(shù)衰減在前期更依賴專家適合專家本身很貴的場景。steps_per_iter2000是每輪新采的步數(shù)太小聚合效果不明顯太大則單輪訓(xùn)練時間壓不住2000 到 5000 之間比較常見。這里有個實現(xiàn)細(xì)節(jié)容易被忽略expert.label(states)必須支持批量查詢。如果專家是規(guī)則函數(shù)逐幀調(diào)用兩百次和一次批量調(diào)用在 Python 里的開銷差幾十倍如果專家是人那 DAgger 就不適合跑十輪得改成兩三輪加大量增廣。另外train_bc每輪只訓(xùn) 5 個 epoch 是有意的——DAgger 靠的是數(shù)據(jù)分布持續(xù)擴張不是單輪把 loss 壓到最低訓(xùn)太多輪會讓策略過早過擬合到舊狀態(tài)分布上。2.4 GAIL 的判別器與獎勵整形獎勵從哪來GAIL 解決的是 BC 誤差累積的另一種思路不學(xué)動作學(xué)一個「像專家」的判別器用判別器輸出反向構(gòu)造獎勵再拿這個獎勵跑標(biāo)準(zhǔn)策略梯度。判別器輸入是(obs, action)拼接輸出一個 logit專家樣本標(biāo) 1、策略樣本標(biāo) 0。class Discriminator(nn.Module): def __init__(self, obs_dim, act_dim, hidden128): super().__init__() self.net nn.Sequential( nn.Linear(obs_dim act_dim, hidden), nn.Tanh(), nn.Linear(hidden, hidden), nn.Tanh(), nn.Linear(hidden, 1), ) def forward(self, obs, act): return self.net(torch.cat([obs, act], dim-1)).squeeze(-1) # 輸出 logit def disc_loss(disc, expert_obs, expert_act, policy_obs, policy_act): # 標(biāo)準(zhǔn)二分類交叉熵專家 1策略 0 pos disc(expert_obs, expert_act) neg disc(policy_obs, policy_act) return (nn.functional.binary_cross_entropy_with_logits(pos, torch.ones_like(pos)) nn.functional.binary_cross_entropy_with_logits(neg, torch.zeros_like(neg))) def gail_reward(disc, obs, act, clip10.0): # r -log(1 - D(s,a))D 是判別器給出的“像專家”概率 prob torch.sigmoid(disc(obs, act)) r -torch.log(1.0 - prob 1e-8) return torch.clamp(r, -clip, clip)判別器網(wǎng)絡(luò)刻意比策略網(wǎng)絡(luò)小128 vs 256且只有兩層是為了不讓它太快把策略和專家完全分開。獎勵取-log(1 - D)而不是直接用D是因為前者在D接近 0 時給的正獎勵更平滑策略在早期不至于所有樣本獎勵都是 0.001 這個量級梯度全被吞掉。clip10.0是給獎勵設(shè)上限防止個別樣本把優(yōu)勢函數(shù)撐爆。判別器的訓(xùn)練和策略的訓(xùn)練是交替的每一輪先用當(dāng)前策略采一批 rollout更新判別器若干次再用更新后的判別器算獎勵、更新策略。這個「若干次」就是下一章要單獨說的判別器步頻是 GAIL 里最敏感的超參之一。3. 從零跑通一次訓(xùn)練依賴、專家數(shù)據(jù)、配置和日志的四段命令鏈拿到一個算法包第一件事不是讀完全部源碼是把它跑出第一條非零獎勵曲線。這一步跑通了后面所有調(diào)參才有意義跑不通讀源碼也讀不出所以然。下面這條鏈路按「裝環(huán)境 → 采數(shù)據(jù) → 訓(xùn) BC → 訓(xùn) GAIL → 評估」的順序排腳本名以倉庫里的實際文件為準(zhǔn)我按常見命名寫對不上就按功能找對應(yīng)入口。3.1 依賴裝完之后先驗環(huán)境接口對不對# 1) 建獨立解釋器環(huán)境避免和系統(tǒng)里的 torch 版本打架 python -m venv .venv source .venv/bin/activate # Windows 下?lián)Q成 .venv\Scripts\activate # 2) 裝依賴requirements.txt 里通常鎖 numpy / torch / 環(huán)境接口庫 / pyyaml / tensorboard pip install -r requirements.txt # 3) 自檢把算法注冊表和已安裝的環(huán)境列出來確認(rèn) BC/DAgger/GAIL 三個入口都在 python -m pkg.registry --list第三步別省。算法包的入口常掛在注冊表或getattr動態(tài)導(dǎo)入上包名寫錯、模塊沒被__init__.py導(dǎo)出報錯會出現(xiàn)在訓(xùn)練中段而不是啟動時排查成本翻倍。如果注冊表能列出三個算法名且環(huán)境接口庫能make出你的環(huán)境依賴這一關(guān)就算過了。提示環(huán)境接口庫的版本差異主要體現(xiàn)在reset和step的返回值上。老接口reset()只返回obsstep()返回四元組新接口返回(obs, info)和五元組。包里的 rollout 代碼只認(rèn)其中一種版本不對會報「too many values to unpack」這類看似莫名其妙的錯。改法很簡單在采集腳本里加一層適配或者在環(huán)境外面包一個 wrapper不要在算法內(nèi)部到處打補丁。3.2 采集專家軌跡腳本參數(shù)與落盤格式# 采集專家軌跡。--env 換成自己的環(huán)境 id--episodes 決定演示條數(shù) python scripts/collect_demos.py \ --env YourEnv-v0 \ --episodes 60 \ --policy expert_rule_based \ --noise 0.02 \ --out data/expert_60ep.npz--episodes 60不是拍腦袋的數(shù)下一章會講怎么估第一次跑通先給 20 到 30 條確認(rèn)文件能存下來、字段對得上再補量。--noise 0.02是給專家動作加的小幅高斯噪聲目的是讓專家數(shù)據(jù)覆蓋一點狀態(tài)鄰域BC 學(xué)出來的策略不會在專家軌跡兩側(cè)立刻崩掉。噪聲太大等于污染標(biāo)簽0.01 到 0.05 之間按動作量綱調(diào)動作在[-1, 1]的話 0.02 合適。落盤之后先做個形狀檢查比直接開訓(xùn)省事得多import numpy as np d np.load(data/expert_60ep.npz) print({k: (d[k].shape, d[k].dtype) for k in d.files}) # 期望看到 obs (N, obs_dim) float32, act (N, act_dim) float32, done 里 1 的個數(shù)等于軌跡條數(shù)done為 1 的樣本數(shù)應(yīng)該正好等于--episodes多一個少一個都說明采集循環(huán)里對terminated和truncated的處理有問題。超時截斷算不算done在不同算法里處理不一樣BC 不在意GAIL 在意——超時被當(dāng)成終止會讓價值函數(shù)學(xué)到「到時間就沒價值」正確做法是用truncated做 bootstrap 而不是當(dāng)終止。3.3 配置文件一次訓(xùn)練里真正要動的十幾個鍵algo: gail seed: 0 env_id: YourEnv-v0 demo_path: data/expert_60ep.npz policy: hidden: [256, 256] activation: tanh train: total_steps: 1_000_000 batch_size: 256 lr_policy: 3.0e-4 lr_disc: 1.0e-3 disc_updates: 3 # 每輪策略更新前判別器更新幾次 gamma: 0.99 gae_lambda: 0.95 ent_coef: 0.01 gail: reward_clip: 10.0 disc_hidden: 128total_steps: 1_000_000是環(huán)境交互步數(shù)而不是梯度步數(shù)這個口徑一定要跟日志對上否則你會以為訓(xùn)了很久其實只采了幾萬步。ent_coef: 0.01是熵正則GAIL 里不建議設(shè)太大——獎勵本身是從判別器來的熵太高會讓策略主動往判別器沒見過的區(qū)域跑判別器給的獎勵全是噪聲訓(xùn)練曲線表現(xiàn)為先漲后崩。3.4 訓(xùn)練與評估日志里該盯哪幾條曲線# 訓(xùn) BC 先確認(rèn)數(shù)據(jù)管線沒問題 python scripts/train_bc.py --config configs/bc.yaml --demo data/expert_60ep.npz # 訓(xùn) GAIL從 BC 權(quán)重?zé)釂?python scripts/train_gail.py --config configs/gail.yaml --init runs/bc/policy.pt # 評估20 條 episode同時看均值和最差分位數(shù) python scripts/evaluate.py --ckpt runs/gail/policy.pt --episodes 20 --percentile 25日志里至少四條曲線要看episode_return、disc_accuracy、policy_entropy、value_loss。disc_accuracy穩(wěn)定在 0.5 到 0.75 之間是健康區(qū)間說明判別器還分不太清一旦沖到 0.95 以上并橫住策略這邊基本拿不到有效梯度獎勵曲線會變成一條緩慢下滑的直線。policy_entropy應(yīng)該緩慢下降而不是斷崖式掉到 0掉太快說明策略過早確定化GAIL 階段就廢了。評估只看均值是自欺欺人。模仿學(xué)習(xí)的失敗是尾部事件——20 條里 18 條接近專家、2 條完全崩均值可能還不錯但上線就是那 2 條在出問題。所以評估腳本里同時打 25 分位數(shù)--percentile 25就是干這個的。4. 超參不是玄學(xué)專家軌跡量、判別器步頻與折扣因子的聯(lián)動關(guān)系模仿學(xué)習(xí)算法的超參比普通監(jiān)督學(xué)習(xí)少但相互耦合更強。改一個數(shù)往往要連帶改另一個不然就是從「不收斂」跳到「過擬合」再跳回來。這一章把三個最影響結(jié)果的量單獨拎出來給的都是范圍和判斷依據(jù)不是絕對最優(yōu)值。4.1 專家軌跡數(shù)量與狀態(tài)維度先算清覆蓋度再談算法BC 的誤差上界大致是O(T2ε)T是軌跡長度ε是單步監(jiān)督誤差。這個式子說明兩件事軌跡越長同樣的單步誤差放大得越厲害想降低誤差要么減小ε更好的網(wǎng)絡(luò)/更多數(shù)據(jù)要么打斷誤差累積DAgger。實操里的估算方式是按狀態(tài)空間覆蓋率不是按條數(shù)。經(jīng)驗區(qū)間任務(wù)類型狀態(tài)維度建議專家軌跡條數(shù)備注低維連續(xù)控制10 以內(nèi)20 到 50每條幾百步夠 BC 出可用策略中維帶關(guān)節(jié)角、速度10 到 5050 到 200建議配 DAgger純 BC 容易偏圖像輸入高維200 以上必須做數(shù)據(jù)增廣否則過擬合背景多模態(tài)專家人遙操作任意200 以上高斯頭或多峰建模MSE 會學(xué)出平均動作表里的數(shù)字是起點不是終點。判斷數(shù)據(jù)夠不夠看 BC 的驗證集 loss 和 rollout 表現(xiàn)是否背離驗證 loss 很低但 rollout 一開始就偏說明數(shù)據(jù)覆蓋不夠加數(shù)據(jù)或者上 DAgger驗證 loss 本身就高說明網(wǎng)絡(luò)容量或者輸入特征有問題加數(shù)據(jù)沒用。4.2 判別器步頻與學(xué)習(xí)率GAIL 最容易失衡的地方disc_updates和lr_disc是一對聯(lián)動參數(shù)。判別器更新太頻繁或?qū)W習(xí)率太高會迅速把策略和專家分開獎勵飽和策略梯度消失更新太少判別器跟不上策略變化獎勵信號滯后訓(xùn)練表現(xiàn)為獎勵曲線劇烈震蕩。我一般這樣起手lr_disc 1e-3、lr_policy 3e-4判別器學(xué)習(xí)率比策略高 3 倍左右disc_updates 1先跑 20 萬步看disc_accuracy。如果它一直在 0.5 附近上不去說明判別器太弱把disc_updates提到 3 或者把disc_hidden從 128 加到 256如果它三步之內(nèi)沖到 0.9 以上反過來做lr_disc降到 3e-4或者給判別器加梯度懲罰。另一個常被忽略的旋鈕是reward_clip。判別器輸出經(jīng)過-log(1-D)之后理論上無界D → 1時獎勵會沖到幾十甚至上百優(yōu)勢估計直接被個別樣本主導(dǎo)。reward_clip: 10.0相當(dāng)于給獎勵設(shè)了個硬上限代價是損失一部分區(qū)分度但換來的穩(wěn)定性在調(diào)試階段更值錢。4.3 折扣因子與 GAE把信用分配對上專家軌跡長度gamma 0.99意味著有效視野大約 100 步0.995大約 200 步0.999大約 1000 步。這個數(shù)要和專家軌跡的實際長度匹配如果專家軌跡平均 300 步收斂到目標(biāo)gamma給 0.99 會讓價值函數(shù)看不到后半段策略學(xué)到的是「快速動一下」而不是「完整完成任務(wù)」。gae_lambda控制偏差和方差的折中0.95 是通用起點。GAIL 的獎勵來自判別器噪聲比環(huán)境原生獎勵大這種情況下把lambda降到 0.9 甚至 0.85 能明顯減小優(yōu)勢估計的方差代價是偏差變大、收斂變慢。判斷依據(jù)是value_loss的波動幅度如果它在兩個數(shù)量級之間來回跳先降lambda再考慮降學(xué)習(xí)率。注意gamma改大之后total_steps也要跟著加。視野從 100 步拉到 300 步同樣學(xué)到「完整任務(wù)」需要的數(shù)據(jù)量大致按比例增加只改gamma不改步數(shù)最常見的現(xiàn)象是訓(xùn)練到一半曲線開始緩慢下滑看起來像過擬合其實是步數(shù)不夠。4.4 參數(shù)速查表參數(shù)作用起步值什么時候改lr_policy策略網(wǎng)絡(luò)步長3e-4曲線震蕩就減半20 萬步不動就翻倍lr_disc判別器步長1e-3判別器準(zhǔn)確率過高就降過低就升disc_updates每輪判別器更新次數(shù)1 到 3先動這個再動學(xué)習(xí)率影響更平滑gamma折扣因子0.99按專家軌跡長度的 1/3 到 1/2 反推gae_lambda優(yōu)勢估計折中0.95value_loss抖動大就降到 0.9ent_coef熵正則0.01策略過早確定化就加大探索過頭就減小reward_clip判別器獎勵上限10.0獎勵方差爆炸時降到 5.0batch_size批量大小256顯存夠就加到 512梯度更穩(wěn)5. 五類翻車現(xiàn)場排查現(xiàn)象、成因和能直接改的那一行算法包的 issue 區(qū)里重復(fù)率最高的不是「怎么實現(xiàn)」而是「我訓(xùn)出來不對」。下面五條按出現(xiàn)頻率排每條都按現(xiàn)象、成因、解決三層寫能直接對應(yīng)到代碼里改哪一行。5.1 BC 訓(xùn)練 loss 很低rollout 三步就飄現(xiàn)象訓(xùn)練集 MSE 降到 1e-3 量級進環(huán)境跑前三步還行第五步動作開始抖第十步直接撞墻。成因典型的分布偏移。專家數(shù)據(jù)只覆蓋專家走過的狀態(tài)策略在專家狀態(tài)上學(xué)得很準(zhǔn)一旦偏出去幾厘米進入沒見過的狀態(tài)輸出就是網(wǎng)絡(luò)外推的隨機值誤差按T2放大。解決上 DAgger哪怕只跑兩三輪也有效暫時不想引入在線專家的話退一步做狀態(tài)增廣——對專家觀測加小幅高斯噪聲、對圖像做隨機裁剪和亮度擾動讓策略在專家軌跡鄰域內(nèi)有定義。噪聲幅度按觀測標(biāo)準(zhǔn)差的 5% 到 10% 給給大了等于教策略在錯誤狀態(tài)上輸出正確動作反而更糟。5.2 GAIL 判別器準(zhǔn)確率三步?jīng)_到 0.98獎勵曲線變平現(xiàn)象訓(xùn)練前幾千步disc_accuracy迅速逼近 1.0gail_reward均值掉到接近 0 并橫住策略性能不再變化。成因判別器容量或?qū)W習(xí)率相對策略太強幾乎瞬間就把兩類樣本線性分開。獎勵函數(shù)變成「策略樣本恒為 -10專家樣本恒為 10」策略拿到的梯度全是常數(shù)等于沒有獎勵。解決先把disc_updates從 3 降到 1再把lr_disc降到 3e-4還不夠就給判別器加梯度懲罰WGAN-GP 那一套或者把disc_hidden從 256 砍到 128。判別器網(wǎng)絡(luò)比策略網(wǎng)絡(luò)小一到兩檔是 GAIL 里比較穩(wěn)的配置習(xí)慣。5.3 觀測歸一化統(tǒng)計量隨訓(xùn)練漂移訓(xùn)練中期突然崩現(xiàn)象前 30 萬步曲線正常上漲之后突然掉下去再也不回來重啟同 seed 也不一定復(fù)現(xiàn)。成因觀測歸一化用了滑動統(tǒng)計量而策略探索到的狀態(tài)分布隨訓(xùn)練不斷擴張mean/std跟著漂導(dǎo)致同一個觀測在不同時刻被歸一化成不同值。價值網(wǎng)絡(luò)學(xué)的是舊分布輸入一變就全亂。解決歸一化統(tǒng)計量只用專家數(shù)據(jù)算一次然后凍結(jié)寫進 checkpoint 一起保存。評估和訓(xùn)練必須用同一份mean/std這一點在加載模型時最容易漏——只加載了權(quán)重沒加載統(tǒng)計量現(xiàn)象是「訓(xùn)練好好的模型評估時表現(xiàn)極差」。5.4 專家動作和環(huán)境動作空間量綱不一致現(xiàn)象BC loss 正常下降但 rollout 里動作像是被截斷永遠(yuǎn)貼著[-1, 1]的邊界走機器人只朝一個方向緩慢移動。成因?qū)<覕?shù)據(jù)來自力矩控制動作范圍是[-20, 20]環(huán)境action_space是[-1, 1]回歸出來的策略輸出被環(huán)境 clip等于把 95% 的信息丟了。解決采集階段就把專家動作按(a - low) / (high - low) * 2 - 1歸一化落盤的 npz 里存歸一化后的值同時在文件里存一份action_low/action_high。評估腳本反歸一化后再env.step。這條建議寫進采集腳本的注釋里不然下一個人接手還要再踩一次。5.5 固定 seed 但兩次訓(xùn)練結(jié)果差很多現(xiàn)象seed: 0寫死在配置里同樣的數(shù)據(jù)同樣的超參兩次跑出來的最終回報差 30% 以上。成因三處非確定性沒關(guān)掉——環(huán)境內(nèi)部的隨機數(shù)生成器沒設(shè)種子、PyTorch 的 cuDNN 自動調(diào)優(yōu)、DataLoader 多 worker 的采樣順序。少關(guān)一處復(fù)現(xiàn)性就沒了。解決環(huán)境reset(seedcfg.seed)、torch.backends.cudnn.deterministic True、torch.backends.cudnn.benchmark False、DataLoader(..., worker_init_fn...)里給每個 worker 派生種子。調(diào)試階段干脆num_workers0慢一點但結(jié)果可比。這套開關(guān)關(guān)掉之后性能可能降幾個點屬于為了可復(fù)現(xiàn)付的必要成本。6. 再往前一步BC 預(yù)訓(xùn)練接 GAIL 微調(diào)加一個誤差分箱的驗證習(xí)慣從零訓(xùn) GAIL 的探索成本很高尤其是動作維度大于 10 的時候判別器早期的獎勵幾乎全是噪聲。更省事的做法是先用 BC 把策略熱啟動到一個「能跑但不精細(xì)」的水平再切 GAIL 微調(diào)。切換時有兩個細(xì)節(jié)要改策略學(xué)習(xí)率降到原來的三分之一到五分之一BC 權(quán)重已經(jīng)很接近解大步長會把它們打散判別器從零開始訓(xùn)、學(xué)習(xí)率保持原值因為判別器面對的是一個已經(jīng)比較像專家的策略需要足夠強的區(qū)分能力才能提供有效梯度。驗證環(huán)節(jié)我習(xí)慣加一個誤差分箱比只看總回報有用得多。思路是拿一批專家觀測喂給策略算每個樣本的動作誤差再按專家動作模長或者狀態(tài)某個關(guān)鍵維度分箱看誤差集中在哪一段。import numpy as np import torch torch.no_grad() def divergence_by_bin(policy, expert_obs, expert_act, n_bins5): 按專家動作模長分箱統(tǒng)計策略與專家的動作 MSE定位誤差集中區(qū) obs_t torch.as_tensor(expert_obs, dtypetorch.float32) pred policy(obs_t).numpy() err ((pred - expert_act) ** 2).mean(axis1) # 每個樣本一個標(biāo)量誤差 key np.linalg.norm(expert_act, axis1) # 用動作模長當(dāng)分箱依據(jù) edges np.quantile(key, np.linspace(0, 1, n_bins 1)) idx np.clip(np.digitize(key, edges[1:-1]), 0, n_bins - 1) return {i: round(float(err[idx i].mean()), 5) for i in range(n_bins) if (idx i).any()}n_bins5按分位數(shù)切保證每箱樣本量接近避免某一箱只有幾個樣本讓均值失真。輸出里如果誤差集中在最高的那一箱說明策略在「大幅度動作」上學(xué)得差多半是專家數(shù)據(jù)里大幅動作樣本少補采時要有意識地多采這類片段如果集中在最低那一箱通常是噪聲問題——小幅動作的信噪比低專家自己抖一下就被當(dāng)成標(biāo)簽學(xué)走了這時候該做的是對專家動作做一點平滑再訓(xùn)。配合評估腳本的 25 分位數(shù)一起看基本能定位八成問題分箱誤差均勻但最差分位數(shù)很差是分布偏移分箱誤差本身就偏某一箱是數(shù)據(jù)覆蓋問題兩者都正常但環(huán)境回報上不去那要回頭看獎勵整形而不是繼續(xù)調(diào)策略。從那以后我每次跑 GAIL 之前都會強制先跑一遍 BC 的分箱誤差確認(rèn)失敗狀態(tài)集中在哪一段再決定是補數(shù)據(jù)還是直接上對抗訓(xùn)練——省下的不是訓(xùn)練時間是那種「訓(xùn)了兩天不知道錯在哪」的消耗。希望幫到你。本文還有配套的精品資源點擊獲取