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