From be356505337ec79683cc71195e61b83dbc34bd5d Mon Sep 17 00:00:00 2001 From: huangfu <3045324663@qq.com> Date: Mon, 2 Mar 2026 10:58:20 +0800 Subject: [PATCH] =?UTF-8?q?HBBC=E9=83=A8=E7=BD=B2=E5=88=B0=E4=BB=A3?= =?UTF-8?q?=E7=A0=81=E4=B8=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../expert_replay_env.cpython-39.pyc | Bin 7617 -> 8033 bytes .../inverse_dynamics.cpython-39.pyc | Bin 2008 -> 2929 bytes Env/__pycache__/scenario_env.cpython-313.pyc | Bin 13605 -> 13773 bytes Env/__pycache__/scenario_env.cpython-39.pyc | Bin 7804 -> 7997 bytes Env/bc_ego_replay_env.py | 194 ++++++++ Env/bc_env.py | 62 ++- Env/expert_replay_env.py | 94 ++-- Env/hbbc_actor_critic.py | 69 +++ Env/hbbc_background_policy.py | 274 +++++++++++ Env/inverse_dynamics.py | 30 +- Env/scenario_env.py | 7 + README.md | 33 +- algorithms/__init__.py | 2 + algorithms/utils.py | 18 + dataset/loader.py | 11 +- docs/HBBC_Deploy_guied.md | 439 ++++++++++++++++++ .../TRAINING_ARCHITECTURE.md | 0 docs/examples/hbbc_latent_example.json | 18 + ...vents.out.tfevents.1770737210.Hfkk.14957.0 | Bin 0 -> 165262 bytes scripts/README.md | 39 +- scripts/generate_expert_data.py | 15 +- scripts/visualize.py | 44 +- train_bc.py | 27 +- 23 files changed, 1293 insertions(+), 83 deletions(-) create mode 100644 Env/bc_ego_replay_env.py create mode 100644 Env/hbbc_actor_critic.py create mode 100644 Env/hbbc_background_policy.py create mode 100644 algorithms/__init__.py create mode 100644 algorithms/utils.py create mode 100644 docs/HBBC_Deploy_guied.md rename TRAINING_ARCHITECTURE.md => docs/TRAINING_ARCHITECTURE.md (100%) create mode 100644 docs/examples/hbbc_latent_example.json create mode 100644 logs/bc/20260210-232650/events.out.tfevents.1770737210.Hfkk.14957.0 diff --git a/Env/__pycache__/expert_replay_env.cpython-39.pyc b/Env/__pycache__/expert_replay_env.cpython-39.pyc index 51d63df983f67454dc32c0af26a124a5f4c3cf14..08ee3bb52b1f7b745da8851c0168ef5f0d638491 100644 GIT binary patch delta 2906 zcmai0O>7&-72erhlFKEzqG(d06v^4fb{a;oDm(U}wxTwFMS>Ut;smk5I9;rEmelfR zm!4hyVi~5ef*e`_Zuind3&`!IM$iI*+MWs&J@l4aZvzBHkG}R+phf%MEGen#p_JzB zo0&K7y?OKAH}j|4e_J!`Qc0KK_vf#E;rVM{8W+gI1L(R^lTfKC)5_;ki%^*=2l=K# zFC?#$Z(OO;A}t+|rbcVjpi{u*X_-zRNKO57NtK#~_oc>6vPV86Joz^n5Iq^l*GW0~ zrMyl`$A6c9tCr`%273-k8Oe0=``kx`DP${1E+h-uucpv3p4+rTJBk>4A^D5;a*5~P z^0$|_BCi*+OUHU%A*7lZ`Umqh6jnpmrR-Gs0uCmzUSC>7PO&}O%!SmkLXWabFw`tY z%x<+j$D(#*v$uu(ANBd{W#HJ0$zS!0F>E2ln zyL}{LcRp|p`EV}c$+mRRe46}t7 zlRM@4w_io6ZzD;A16(7M`WvB?X185GI@xD#d<5rrdoTG-IW+K)(JY2D{N(ss(_hP@ zXfc*L#iNT)q(@{}1D-rqSQWl9m9{2)F0O5-JWAx+WL6s1s7?z9Qe1lzSI$7;oc3lx zN`m%0EzL>Y%wuKHj8y0s9}#MF<;P0ULYGXZvfX&PQ%X{$rsi2B|OfjGMATE7)9AL#|vGquTsy6oS;8g zyP90CwJwSIAN;Q*djw<4V4WQ#zpm|%Fauc=Rb=*E7K9AfJSE(4cLy$HqV3=rhG)tO z6|4vaz%8xR551tbUyT!Bhs266`av=zByMXZW42wd!ABqJ;XUg_CkWc`=LG`~bK z?W3Sbn%9xT9O7B#_JeKrY@y-YEhHkn-$m{Yl6y#YkXT68ki3Qj0j<`U6Y0kb=|(LK zBTrUl7hdCeXNxf_jNAd&$E+Q>T=(7G2!@f*rmHxySd1!%KzIg_%IN|o9nOBZ)-2h* zo;6A9kms1YV>24EyF%3STM%<`SU*Gs0=UraKR^qAlvL`UjueBKq(}_-TOk#BzxtLq zn!E0xXYV1%qKghz!Q@v0MuYYSwD1KutC&D~lDYINi!T}X;m#0!xt$p*ZE1*!t#s5* z<|z9_dQUzAFD@toQtz9 z`3WJ?L+E!3RHc~%iDnOBBvxsTYM+70p$5kCu?8l-2Nv{UA(pjw)56RO9>s#FDci3D3PVkOSc zA~!_~hcZ-UaewoTG+E6XH?LB!Zw4Eo`O-&jJ1}pWOXjr|(|5i0=0?CmbJx7lFk1mL zZF3A#%ci^Q^tLFZ`WVD+nKUr{ATpg`%a2SuJXyb--XWhPSLSbz@bnvmlil-0t*!R# z{sy(zUZ0fP{`K=`4|hsvfgjeyi`8D>*u8LVd9oN(D_wVQ{h6rIPr=wTHMN+b(cZuv zHOeX4AoB3irfXBrZ(Gr(3s;c<_!F#7FE@b}*&e`qugz6IV0}@YaHj_>;#sH;yf6+? zTgGP0-s8DeFR-Imt~W|4){8PJkHfWe5-*;IP>(B1cWj?}aM!yb*8*^c!L7iPA3s+e z@oW?ftZOUWm>h@Ia_uN&KSh(;WRU0jUKqkw&WE*pK4v!{yy10NEGcDDChEV`j7nyR zMr2Y2iep8hidG?&5e#Yn)UrjQ!}PxS|4zgK?I%Bsd7 zSe^VAlLre|MhHdXUBK=mCvaFGvcP8H55h7pS(XF0K~l7B4RgU9WAi>qf|hXQWrlAkYL{tu!t Bvf%&# delta 2480 zcmZWr&2JM&6rb5$+v~M=?F1(_kU;q?hLQrUs!~EKR6bf%MTnLNEuhuLv$ofEyk>S2 zNVJwKC4edwiFW^ia4Ux%s`S92s>-pddTb9Xap)0o>Y+C(^}X3ZTvgwgF*~sN zdBFntmnyn(%IOAtIhOInKG^052#KC+6U8zv1XPf^uu;y%bg&!gNq{)rak`D58K-W! zENDKGcpbMIxRsiB8g7@r3G^c+G0j+Vnl2b^)x2)qZSaFAi~^h@z@;S4`c3ba6}St| zY&FMEApc#2V+b<{A4WaBtX)U42w+BE>*Xm77Ow%sa?R#uYQABCFKuMdrNo0RH|C;0 z^l!?zu2L>e4*9t;Fs{-;2leejKtH$K;!Rw-2*U6dP*rKmlUR1TNert65oumU*$)te z3kyi45iS6j1>XrQ=JB z?GF@`UZu=vOAlmkKT5DdTM5TmahiyJ8UY@ ze@Pw!?=W1?`_c2Ul>ycczlthSD-QR3j%OZG({OJ093EJ94fEj-RX-{?Dii=w-IZh( zTbhp|V4ue_Gz8ayG7Yq8F>p2&@ogw$B3wflU;&$>9orAA|J<0=il})X1w{dzK?>_3 zPIG6$TXKf`0ryTKh*JFosf!3#5N;yeK)|_Ft%&+Db9PR+XJPQn=yrMHjG3)1a&Gy7 z(=)YA(heL`>pHgsU;`belc2=Z;)|$q2_R0vbg8xZf#~=0+}m-6JGX4ke11vnx!pR1 zR^((2Phc4e>wW~nzZ zDeSnUPq7Nu=|ggsJfJt;yDs&mz8uOeiP7yud`se6-dxKpSu2fHUulp*Kh+@pG$={6 z(ydIQ=*Bdt$@JAQ6H0A~H*d}bYAd_0p}J6QNfq*t_OoG@$!)^t!t{DB%!X=RdPpg` z3F}s#DJ-={So%J2!Zaf+a|f>NYj9UC)XtI{=i!EYUk?Ylbv@LFtOctpL~s&2CA3#? z5)bs2@SvfH%O0~#V1(K#t&qME8jQA;2b3QUK`T=mlx0JsL`q;TCE!9KRKi>tsUp+v zOZ`Gv7!PSEO_Q5ONTEe@v&k^N6+PK;>L@0zN@IHa^|n)|JNCjnvu7^8Y>1Vq=-0`K z!PIoetJxiY28cug&dY6Q`5C^&d>P=Gl4wn_V7cd1M}{h1ya+f5V!i3u%#}`DO#LB?;co3(dWN{APK}{)}b9*_?)H|LXymO*j6fF>| z-n=i$f2fqRPQ%+Q^=#L18_ju-`>|HDyUc~R)A3E!1A92kTvJb|&7m7B%=H5^JqWy> zb^MrVygY8z9Xs&(mtcvhzGTLk1=sh%JN^~$UyH5i*5eRX9~+)Xid2%ZLzI7%j3Oy? zoT^lkN`OVVDCdEbrwSc~Kb5Nb3sucijh5sR)uiZ`$wM?3{XKbLkjIPg$>DPdVl0RO zAw~rbh*-Da7lrNnHK)_DtRbt$@Pz1G6)0>!AD=0e8U^jTT2U4Ch_=Xa4<5a@;44mb j7Z-ZVNt$vsuhWUvcU^)XPk-&29{Noo#zG}Jy?g(^kSIW} diff --git a/Env/__pycache__/inverse_dynamics.cpython-39.pyc b/Env/__pycache__/inverse_dynamics.cpython-39.pyc index 39d54ffe9aaf8c944df929ac64f73ff3f7247978..5d59ac55c8b5a421080af5ff04aa4cd64d2b6a9d 100644 GIT binary patch delta 1004 zcmX|A-D=c86rRcdW|M4N*8Zcjir6h{X{C6fH-aJ*K?K2zm9m6P+NQSIWj5XJmI(`1 z@j^r-2nt@bkKhA{K8EC8#7iGQ@SN;!C(N1e{Cww}oJ@XJ{?2;7>)Hg?lSgkO$NS*% z^;OV9(xjX;74C42>#t~2zSi+UGPa%Q7#$AmSx2Ax_jYOPc3U3<{N& zNb1P4G^AQ3b0l>Wo7>VoRZ>@$q%*d9z(JlPy>kkui|4tLdeWU}&~Qb%!$}aRQ__+u z=$fp^sa*vNLV;;HEiv7Nm6yM$Fi_{@o6W}SVOt#gf-^pd{lh5e-eJ2@FN|4mBt#e| zt#0BcA@gI-{2+)-KgNM^c#bEgYAVrhXf7Q3lG{slnmHPuMgUwzIM@aHX)Mu(mKpF{f~+u%~bU zSu81>!3>%_8yBgwGNw;H#X6NSYH|Xb0g#-?Hgoc7E-6OV$!EE=7!@Y}x8=}QdwWn3>${(?SPysK4BhasaN`#b_=6$Add0J4;!JQVmu<=VvT9bz z`m?Q12M#a_Qj`JZ7)vUt!1N#FYW*&7@^pC7HhHDKuawK>C5+S@*PE}Ma64-p^pm_~ zp1V6FT`?S9b#!unO7e-uLTu8<&fv|ygtkOcrwLxeU;DNha zT60wcii^f6C}J``+*KeCB%99xF<-cZv+=#^GBF2aqO0+Kw$k`AZhLz^AXdxURR`=! zg_}B@q3{iy8XWEVP>87z$PKAaSi%c~v0f?}`UxTgLj-dKI^G#Ps=g~kcSI)Im*`>d zHx4D*RdyMV@A+(C{y*O<4WNfWKPJeucX`9$Bl*btG<=Xf-Q z_~QO2kIj(YEx>K9fh|5hXBp^PfJM+sAWw@9|NkB!Kq~zf;3tA>0$hL9GysfcSjD^h zCptpD=$2PeJ2yQ-dN6<;V*|Y-zM-|@jh2o4M&QTZ^g~7Hn*@K)|@-HF;QK|p{ delta 1042 zcmX|AU1$?o6rP)(Ns~z;jkb|WYd3AxM0aaVy4LQ)S}gvc*2QXTUmVBeW-@J@NzY8H z1wqBX%CZl73R``UKIoH*NMsk(hZTHO@aJuw1eNNO)hMzcviD3`FWm2b_k8EvbI!dp zPsXl~#%r-yL=xklzs?wH`P+0u0mpok3& z;%^PZ@*$!+-fHMz2k}M2dNzcC##L+>*EYVC^CWVtPi~i|1a(*7!&IXJ+~&@9vA5%R zGWm!9bz7#ISiUvLCUK-WrC1aM1U8;)&Lz%Kq|dClvIhDQ?HH)|xVa;7kUV}TE$D!j zAA?KiZ~2jpV^>Qrv#aG6%fI|jl9jw=I$)K{yr9A)Ma^REhpsJ`1x?4nJBPXhO6*4M zigwggt;3@{@4ynWQ|Yjhpn;%`V2EHC_0&E#jSo^;HeFpv#bh>v-`3pkJ@DP-iePS7 z=ZV{O$Nlh!@HBLn? zcGN@6dw}EQ7}1KNW>Gq?z%)r-swl4*wpHMZ_CaA!hapMwa{{mKjl`ZxQn%##HKX8$ zfFISMV8dx4xIx1#TDBb)y!HP9D=5TE+eDv9kg`6i?(O2NhZ<z`YYiv!Bqin z$Si08+7ukeO15W7DA-u@OLF~!Pq5Dk{5#v)ln%DdcFYCl@^j&5O_?u}*e?)#9KsLT i0d^HP=dQ6)e3P4I^JsPt)R%lLGrRM%L@P9lEdK$}*BeFv diff --git a/Env/__pycache__/scenario_env.cpython-39.pyc b/Env/__pycache__/scenario_env.cpython-39.pyc index 073bc1ab5712b55aed5b6d87a23e86a8ad9c83bd..8630b8dd7d2fb74f7b5699786818917fcb4419a0 100644 GIT binary patch delta 1151 zcmZ`&UuYa<5Wnw|yW6|VU6M;pub0HMNo%<(O)3;EG)ey@Z8bJZY1ERk+1u~iyG^&d z>+H5ki;{;{9|RGc7O993@lgup9#lj@3ciSc-h>Y+6<_pC5cEMGIlHZ1#m6M~Dr)V+x#d9xW>aZ5jvI9%URpnO}IQeARwrTY@M zXT{2oO_py8H$5v+;CJ$KiBQJ%Zs?`jby+IM!Uf~0-3Q3r$352wQt5)oiZuVy33m9O zMh|Wn--;;=^1H;5_Kp00A)aY}Q&>qC9m@KHQ?E?mMr{xmOTz<)$<$ddXf_o94;iNL z_0o&QIi}BYa1{TpjHclj8s$kbjR(qOVg{GW_p&EgauSd9PGwA@3+-#Y2h&31SN+3c z9`E*#6&G3YGQ(LcR<=K;bBnVlP9B|~g)wsKF3Z0RT$E1xq0U;+ykM=tEWS|Lb@&)( zxg5*HK+^_~dBJfZip{7Mtm?dHZBnSRIDu`YK^)*4CdZIQbp2Mg%p1sG4=kfiTH-3ZZLj!r2d_N}= zh9~jyq2ufSr~BoBu3m&>DBMD)+nvmkGhHU83zkD2QDZ6u9szK1x<`d>MC;x0Pm!x` zzyOD@GbEjkr&5zCK$hn~ty&->kZfh>+62+V2Zo0J{Ls6If)l2xXb+b&pH2L&Y9VLHS{AXNIsv( z!k$H}KQKJ+MlaE5~GwkM2Pmf1y1tReH663Qpem<`8Waoll@gnN06 zhAwbetRCiIR(gNB>`Pt9g#EbWe$EmY_jI#k)d^36u~hYech1U0-0>y-NBJt#QPPS< z8Gx#R1da-m-XW4HO19xkf14GOxGBV0AAT32tRKUBwjC$PGK{vSLHi_^W7Tw1(i+LS zwQF;7&JdM6M99Gi9UL(`jNs$us5e8A^Mnidwz<70>zqiBol2caLzD+io1`wnRZ>>9 zY&s+;UY9B`h|T`);Uuk+II?yFq})8@NqY(k=!U4zY71F+N%}1zK;{~Pq|R|vsuh0+ z3(V3gZ%$JU&`KpaFTw~p6x`U?o0{V?CO~Z~Iv${U%|^W}EvRC>Iq*@Ox`tN5UP6e_ zN9f1ymT|kt<$86crNPPyxO(74`@mnrJXfZ>DD#>P$&lePWiFu@2(k<=21K@uTY-VD z8x+1t$Z=$SyyZec$%!SUIBURdN>nfrOxS0*d{)(hw;12*Tk9xG7YEDnaEiooZeJkQQMIAB7JyH@*vpnTlV+ QPuXdF7&*(bxE-1P1=?5Q= len(track["state"]["position"]): + agents_to_remove.append(agent_id) + continue + if not track["state"]["valid"][next_step]: + agents_to_remove.append(agent_id) + continue + if self.enable_hbbc_background and self.hbbc_controller is not None: + # HBBC autonomous rollout: use vehicle's own previous-step state + curr_state = { + "position": np.asarray(vehicle.position, dtype=np.float64), + "heading": float(vehicle.heading_theta), + "velocity": np.asarray(vehicle.velocity, dtype=np.float64), + } + object_id = str(getattr(vehicle, "original_id", agent_id)) + hbbc_batch.append((agent_id, vehicle, object_id, agent_id)) + hbbc_curr_states[agent_id] = curr_state + else: + curr_step = self.round - 1 + curr_state = { + "position": track["state"]["position"][curr_step], + "heading": track["state"]["heading"][curr_step], + "velocity": track["state"]["velocity"][curr_step], + } + next_state = { + "position": track["state"]["position"][next_step], + "heading": track["state"]["heading"][next_step], + "velocity": track["state"]["velocity"][next_step], + } + action, _ = self.inverse_dynamics.compute_action(curr_state, next_state, dt=0.1) + expert_actions[agent_id] = action + vehicle.set_position(next_state["position"]) + vehicle.set_heading_theta(next_state["heading"]) + vehicle.set_velocity(next_state["velocity"]) + vehicle.last_expert_action = action + + if hbbc_batch and self.hbbc_controller is not None: + hbbc_actions = self.hbbc_controller.infer_actions(hbbc_batch) + if not self._hbbc_runtime_logged: + print(f"[HBBC] background policy active, current dynamic agents: {len(hbbc_batch)}") + self._hbbc_runtime_logged = True + for agent_id, _, _, _ in hbbc_batch: + curr_state = hbbc_curr_states[agent_id] + action = hbbc_actions[agent_id] + next_state = self.inverse_dynamics.apply_action(curr_state, action, dt=0.1) + expert_actions[agent_id] = action + vehicle = self.controlled_agents[agent_id] + vehicle.set_position(next_state["position"]) + vehicle.set_heading_theta(next_state["heading"]) + vehicle.set_velocity(next_state["velocity"]) + try: + vehicle.last_current_action.append(action) + except Exception: + pass + vehicle.last_expert_action = action + for agent_id in agents_to_remove: + vehicle = self.controlled_agents[agent_id] + self.controlled_agents.pop(agent_id) + self.controlled_agent_ids.remove(agent_id) + self.engine.agent_manager.active_agents.pop(agent_id, None) + self.engine.clear_objects([vehicle.id]) + if self.hbbc_controller is not None: + self.hbbc_controller.remove_vehicle(agent_id) + self.engine.taskMgr.step() + self._spawn_controlled_agents() + self._update_background_vehicles() + self._replay_agents = dict(self.controlled_agents) + # Expose only SDC again + if self.replay_sdc and self.sdc_vehicle is not None: + self.controlled_agents = {self.sdc_agent_id: self.sdc_vehicle} + self.controlled_agent_ids = [self.sdc_agent_id] + else: + self.controlled_agents = {} + self.controlled_agent_ids = [] + + obs = self._get_all_obs() + rewards = {} + infos = {aid: {"expert_action": expert_actions.get(aid, np.zeros(2))} for aid in self.controlled_agents} + if self.sdc_agent_id in self.controlled_agents and self.sdc_vehicle is not None: + speed_coef = float(self.config.get("reward_speed_coef", 0.05)) + collision_distance = float(self.config.get("collision_distance", 6.0)) + collision_penalty = float(self.config.get("collision_penalty", 100.0)) + speed = float(np.linalg.norm(self.sdc_vehicle.velocity)) + r_speed = speed_coef * speed + min_dist = float("inf") + for other_id, other_vehicle in self.engine.agent_manager.active_agents.items(): + if other_id == self.sdc_agent_id: + continue + try: + d = float(np.linalg.norm(self.sdc_vehicle.position - other_vehicle.position)) + min_dist = min(min_dist, d) + except Exception: + continue + near_collision = min_dist < collision_distance + r_collision = -collision_penalty if near_collision else 0.0 + rewards[self.sdc_agent_id] = r_speed + r_collision + infos[self.sdc_agent_id].update( + near_collision=near_collision, + min_dist=min_dist if np.isfinite(min_dist) else None, + r_speed=r_speed, + r_collision=r_collision, + ) + dones = {aid: False for aid in self.controlled_agents} + dones["__all__"] = self.round >= self.config["horizon"] or (len(self._replay_agents) == 0 and self.round > 190) + return obs, rewards, dones, infos diff --git a/Env/bc_env.py b/Env/bc_env.py index 9f04678..6657e51 100644 --- a/Env/bc_env.py +++ b/Env/bc_env.py @@ -1,4 +1,5 @@ from Env.scenario_env import MultiAgentScenarioEnv +from Env.hbbc_background_policy import HBBCBackgroundController from Env.utils import filter_traffic_tracks_to_birth_lists from metadrive.component.vehicle.vehicle_type import DefaultVehicle import numpy as np @@ -15,19 +16,71 @@ class BCScenarioEnv(MultiAgentScenarioEnv): expert data is generated with ExpertReplayEnv which includes bg_* in active_agents, so the policy was trained on obs that can include those neighbors. Demo should use the same scene for consistency. """ + def _init_hbbc_background(self): + self.enable_hbbc_background = bool(self.config.get("enable_hbbc_background", False)) + self.hbbc_dynamic_agents = {} + self._spawned_dynamic_bg_ids = set() + self.hbbc_controller = None + if not self.enable_hbbc_background: + return + self.hbbc_controller = HBBCBackgroundController( + model_path=self.config.get("hbbc_model_path", "models/hbbc/hbbc.pt"), + device=self.config.get("hbbc_inference_device", "cpu"), + latent_mode=self.config.get("hbbc_latent_mode", "per_vehicle_fixed"), + latent_json_path=self.config.get("hbbc_latent_json_path"), + seed=int(self.config.get("seed", 0)), + dt=float(self.config.get("hbbc_dt", 0.1)), + ) + self.hbbc_controller.reset_episode() + + def _move_excess_controlled_to_hbbc_background(self): + if not self.enable_hbbc_background: + return + keep_n = int(self.config.get("num_controlled_agents", 0)) + keep_n = max(0, keep_n) + ordered_ids = list(self.controlled_agents.keys()) + keep_ids = set(ordered_ids[:keep_n]) + move_ids = [aid for aid in ordered_ids if aid not in keep_ids] + for aid in move_ids: + self.hbbc_dynamic_agents[aid] = self.controlled_agents[aid] + self.controlled_agents.pop(aid, None) + if aid in self.controlled_agent_ids: + self.controlled_agent_ids.remove(aid) + self._spawned_dynamic_bg_ids.update(move_ids) + + def _apply_hbbc_before_step(self): + if not self.enable_hbbc_background or not self.hbbc_dynamic_agents: + return + batch = [] + for aid, vehicle in self.hbbc_dynamic_agents.items(): + object_id = getattr(vehicle, "original_id", None) or aid.replace("controlled_", "", 1) + batch.append((aid, vehicle, str(object_id) if object_id is not None else None, aid)) + actions = self.hbbc_controller.infer_actions(batch) + for aid, vehicle in self.hbbc_dynamic_agents.items(): + action = actions.get(aid, np.zeros(2, dtype=np.float32)) + vehicle.before_step(action) + + def _apply_hbbc_after_step(self): + if not self.enable_hbbc_background: + return + for vehicle in self.hbbc_dynamic_agents.values(): + vehicle.after_step() + def reset(self, seed=None): + self._init_hbbc_background() # Clear background vehicles from previous episode so engine.reset() passes _object_clean_check if getattr(self, "engine", None) is not None: ids_bg = [ oid for oid, obj in self.engine.get_objects().items() - if (getattr(obj, "name", None) or getattr(obj, "id", None) or "").startswith("bg_") + if (getattr(obj, "name", None) or getattr(obj, "id", None) or "").startswith(("bg_", "controlled_")) ] if ids_bg: self.engine.clear_objects(ids_bg, force_destroy=True) for aid in list(self.engine.agent_manager.active_agents.keys()): - if aid.startswith("bg_"): + if aid.startswith("bg_") or aid.startswith("controlled_"): self.engine.agent_manager.active_agents.pop(aid, None) obs = super().reset(seed=seed) + self._move_excess_controlled_to_hbbc_background() self._spawn_background_vehicles() return self._get_all_obs() @@ -76,16 +129,21 @@ class BCScenarioEnv(MultiAgentScenarioEnv): pass def step(self, action_dict): + if action_dict is None: + action_dict = {} self.round += 1 for agent_id, action in action_dict.items(): if agent_id in self.controlled_agents: self.controlled_agents[agent_id].before_step(action) + self._apply_hbbc_before_step() self.engine.step() self.engine.after_step() for agent_id in action_dict: if agent_id in self.controlled_agents: self.controlled_agents[agent_id].after_step() self._spawn_controlled_agents() + self._move_excess_controlled_to_hbbc_background() + self._apply_hbbc_after_step() self._update_background_vehicles() obs = self._get_all_obs() diff --git a/Env/expert_replay_env.py b/Env/expert_replay_env.py index a0cded0..133944b 100644 --- a/Env/expert_replay_env.py +++ b/Env/expert_replay_env.py @@ -293,61 +293,53 @@ class ExpertReplayEnv(MultiAgentScenarioEnv): # Get observations obs = self._get_all_obs() - - rewards = {aid: 0.0 for aid in self.controlled_agents} - dones = {aid: False for aid in self.controlled_agents} - dones["__all__"] = (self.round >= self.config["horizon"]) or (len(self.controlled_agents) == 0 and self.round > 190) # Waymo scenarios are usually ~198 steps (20s @ 10Hz) or 90 steps (9s) - - infos = {aid: {"expert_action": expert_actions.get(aid, np.zeros(2))} for aid in self.controlled_agents} - + + # Build rewards/dones/infos: include controlled_agents and optionally SDC for data collection + all_agent_ids = list(self.controlled_agents.keys()) + if self.replay_sdc and self.sdc_vehicle is not None and self.sdc_agent_id not in all_agent_ids: + all_agent_ids = all_agent_ids + [self.sdc_agent_id] + rewards = {aid: 0.0 for aid in all_agent_ids} + dones = {aid: False for aid in all_agent_ids} + dones["__all__"] = (self.round >= self.config["horizon"]) or (len(self.controlled_agents) == 0 and self.round > 190) # Waymo scenarios are usually ~198 steps (20s @ 10Hz) or 90 steps (9s) + infos = {aid: {"expert_action": expert_actions.get(aid, np.zeros(2))} for aid in all_agent_ids} + return obs, rewards, dones, infos + def _obs_for_vehicle(self, vehicle, exclude_agent_id=None): + """Compute 45-dim obs (ego 5 + 10 neighbors x 4) for a vehicle. exclude_agent_id: do not count as neighbor.""" + ego_state = [ + vehicle.position[0], vehicle.position[1], + vehicle.velocity[0], vehicle.velocity[1], + vehicle.heading_theta + ] + candidates = [] + for other_id, other_vehicle in self.engine.agent_manager.active_agents.items(): + if other_id == exclude_agent_id: + continue + dist = np.linalg.norm(vehicle.position - other_vehicle.position) + if dist < 30.0: + candidates.append((dist, other_vehicle)) + candidates.sort(key=lambda x: x[0]) + top_10 = candidates[:10] + neighbor_feats = [] + for _, neighbor in top_10: + neighbor_feats.extend([ + neighbor.position[0] - vehicle.position[0], + neighbor.position[1] - vehicle.position[1], + neighbor.velocity[0], + neighbor.velocity[1] + ]) + missing = 10 - len(top_10) + if missing > 0: + neighbor_feats.extend([0.0] * (4 * missing)) + return np.array(ego_state + neighbor_feats, dtype=np.float32) + def _get_all_obs(self): # Implement custom observation: 30m range, 10 nearest vehicles obs_dict = {} - for agent_id, vehicle in self.controlled_agents.items(): - # 1. Ego State - ego_state = [ - vehicle.position[0], vehicle.position[1], - vehicle.velocity[0], vehicle.velocity[1], - vehicle.heading_theta - ] - - # 2. Neighbors - neighbors = [] - # Iterate through all vehicles in the engine - candidates = [] - for other_id, other_vehicle in self.engine.agent_manager.active_agents.items(): - if other_id == agent_id: - continue - - dist = np.linalg.norm(vehicle.position - other_vehicle.position) - if dist < 30.0: - candidates.append((dist, other_vehicle)) - - # Sort by distance - candidates.sort(key=lambda x: x[0]) - - # Take top 10 - top_10 = candidates[:10] - - neighbor_feats = [] - for _, neighbor in top_10: - neighbor_feats.extend([ - neighbor.position[0] - vehicle.position[0], # Relative pos - neighbor.position[1] - vehicle.position[1], - neighbor.velocity[0], # Absolute vel? or Relative? Usually relative in MultiAgent - neighbor.velocity[1] - ]) - - # Pad if < 10 - missing = 10 - len(top_10) - if missing > 0: - neighbor_feats.extend([0.0] * (4 * missing)) - - # Flatten - obs = np.array(ego_state + neighbor_feats, dtype=np.float32) - obs_dict[agent_id] = obs - + obs_dict[agent_id] = self._obs_for_vehicle(vehicle, exclude_agent_id=agent_id) + # Include SDC/ego obs for expert data collection (e.g. single-agent) + if self.replay_sdc and self.sdc_vehicle is not None: + obs_dict[self.sdc_agent_id] = self._obs_for_vehicle(self.sdc_vehicle, exclude_agent_id=self.sdc_agent_id) return obs_dict diff --git a/Env/hbbc_actor_critic.py b/Env/hbbc_actor_critic.py new file mode 100644 index 0000000..71eb6aa --- /dev/null +++ b/Env/hbbc_actor_critic.py @@ -0,0 +1,69 @@ +import torch +import torch.nn as nn + + +def _get_activation(name: str): + name = (name or "elu").lower() + mapping = { + "elu": nn.ELU, + "relu": nn.ReLU, + "tanh": nn.Tanh, + "leakyrelu": nn.LeakyReLU, + } + if name not in mapping: + raise ValueError(f"Unsupported activation: {name}") + return mapping[name]() + + +class ActorCritic(nn.Module): + """Minimal HBBC ActorCritic for inference-only deployment.""" + + def __init__( + self, + num_actor_obs=18, + num_critic_obs=18, + num_actions=2, + latent_c_dim=4, + latent_eps_dim=6, + use_style_latent=True, + actor_hidden_dims=None, + activation="elu", + ): + super().__init__() + _ = num_critic_obs # kept for checkpoint compatibility + if actor_hidden_dims is None: + actor_hidden_dims = [512, 256, 128] + + act_fn = _get_activation(activation) + self.latent_c_dim = int(latent_c_dim) + self.latent_eps_dim = int(latent_eps_dim) + self.use_style_latent = bool(use_style_latent) + + layers = [nn.Linear(num_actor_obs, actor_hidden_dims[0]), act_fn] + for i in range(len(actor_hidden_dims) - 1): + layers.append(nn.Linear(actor_hidden_dims[i], actor_hidden_dims[i + 1])) + layers.append(_get_activation(activation)) + self.actor_trunk = nn.Sequential(*layers) + self.actor_head = nn.Linear(actor_hidden_dims[-1], num_actions) + + if self.use_style_latent: + self.style_trunk = nn.Sequential( + nn.Linear(self.latent_eps_dim, 512), + _get_activation(activation), + nn.Linear(512, 256), + _get_activation(activation), + nn.Linear(256, 128), + _get_activation(activation), + ) + self.style_head = nn.Linear(128, self.latent_eps_dim) + self.style_activation = torch.tanh + + def act_inference(self, observations: torch.Tensor) -> torch.Tensor: + if self.use_style_latent: + obs = observations[..., :-(self.latent_c_dim + self.latent_eps_dim)] + eps = observations[..., -self.latent_c_dim - self.latent_eps_dim:-self.latent_c_dim] + c = observations[..., -self.latent_c_dim:] + eps = self.style_activation(self.style_head(self.style_trunk(eps))) + observations = torch.cat([obs, eps, c], dim=-1) + embedding = self.actor_trunk(observations) + return self.actor_head(embedding) diff --git a/Env/hbbc_background_policy.py b/Env/hbbc_background_policy.py new file mode 100644 index 0000000..c7c391e --- /dev/null +++ b/Env/hbbc_background_policy.py @@ -0,0 +1,274 @@ +import json +import os +from dataclasses import dataclass +from typing import Dict, List, Optional, Tuple + +import numpy as np +import torch + +from Env.hbbc_actor_critic import ActorCritic + + +def _wrap_to_pi(angle: float) -> float: + return (angle + np.pi) % (2 * np.pi) - np.pi + + +def _normalize_eps(eps: np.ndarray) -> np.ndarray: + eps = np.asarray(eps, dtype=np.float32).reshape(-1) + if eps.shape[0] != 6: + raise ValueError(f"latent_eps must be 6-dim, got {eps.shape[0]}") + norm = float(np.linalg.norm(eps)) + if norm < 1e-8: + eps = np.array([1.0, 0.0, 0.0, 0.0, 0.0, 0.0], dtype=np.float32) + else: + eps = eps / norm + return np.clip(eps, -1.0, 1.0) + + +def _normalize_c(latent_c: np.ndarray) -> np.ndarray: + c = np.asarray(latent_c, dtype=np.float32).reshape(-1) + if c.shape[0] != 4: + raise ValueError(f"latent_c must be 4-dim, got {c.shape[0]}") + idx = int(np.argmax(c)) + one_hot = np.zeros(4, dtype=np.float32) + one_hot[idx] = 1.0 + return one_hot + + +def _sample_latent(rng: np.random.RandomState) -> Tuple[np.ndarray, np.ndarray]: + eps = _normalize_eps(rng.randn(6).astype(np.float32)) + mode = int(rng.randint(0, 4)) + c = np.zeros(4, dtype=np.float32) + c[mode] = 1.0 + return eps, c + + +@dataclass +class VehicleStateCache: + last_heading_theta: Optional[float] = None + last_action: Tuple[float, float] = (0.0, 0.0) + last_speed_km_h: Optional[float] = None + + +class HBBCModelWrapper: + _cache: Dict[Tuple[str, str], "HBBCModelWrapper"] = {} + + def __init__(self, model_path: str, device: str = "cpu"): + self.model_path = os.path.abspath(model_path) + self.device = torch.device(device) + self.model = self._load_model() + + @classmethod + def get(cls, model_path: str, device: str = "cpu") -> "HBBCModelWrapper": + key = (os.path.abspath(model_path), str(torch.device(device))) + if key not in cls._cache: + cls._cache[key] = HBBCModelWrapper(model_path=key[0], device=key[1]) + return cls._cache[key] + + def _load_model(self) -> ActorCritic: + model = ActorCritic( + num_actor_obs=18, + num_critic_obs=18, + num_actions=2, + latent_c_dim=4, + latent_eps_dim=6, + use_style_latent=True, + ).to(self.device) + try: + ckpt = torch.load(self.model_path, map_location=self.device, weights_only=True) + except Exception: + ckpt = torch.load(self.model_path, map_location=self.device, weights_only=False) + state_dict = ckpt["actor_critic"] if isinstance(ckpt, dict) and "actor_critic" in ckpt else ckpt + missing, unexpected = model.load_state_dict(state_dict, strict=False) + if missing: + raise RuntimeError( + f"HBBC checkpoint missing required keys for {self.model_path}: {missing}" + ) + if unexpected: + print(f"[HBBC] ignore extra checkpoint keys: {unexpected[:8]}{'...' if len(unexpected) > 8 else ''}") + model.eval() + return model + + def act_batch(self, obs_batch: np.ndarray) -> np.ndarray: + obs_batch = np.asarray(obs_batch, dtype=np.float32) + with torch.no_grad(): + obs_t = torch.from_numpy(obs_batch).to(self.device) + actions = self.model.act_inference(obs_t).cpu().numpy() + return np.clip(actions, -1.0, 1.0) + + +class HBBCLatentManager: + def __init__(self, mode: str = "per_vehicle_fixed", seed: int = 0, latent_json_path: Optional[str] = None): + self.mode = mode + self.rng = np.random.RandomState(seed) + self.latent_json_path = latent_json_path + self.manual_object_latent: Dict[str, Dict[str, np.ndarray]] = {} + self.manual_agent_latent: Dict[str, Dict[str, np.ndarray]] = {} + self.manual_global_latent: Optional[Tuple[np.ndarray, np.ndarray]] = None + self.vehicle_latent: Dict[str, Tuple[np.ndarray, np.ndarray]] = {} + self._episode_latent: Optional[Tuple[np.ndarray, np.ndarray]] = None + self._load_manual_latent_json() + + def reset_episode(self): + self.vehicle_latent.clear() + self._episode_latent = None + if self.mode == "per_episode_reset": + self._episode_latent = _sample_latent(self.rng) + + def _load_manual_latent_json(self): + if not self.latent_json_path: + return + path = os.path.abspath(self.latent_json_path) + if not os.path.exists(path): + print(f"[HBBC] latent json not found: {path}, fallback to random sampling.") + return + try: + with open(path, "r", encoding="utf-8") as f: + data = json.load(f) + except Exception as e: + print(f"[HBBC] failed to load latent json ({path}): {e}. fallback to random sampling.") + return + + object_section = data.get("object_id", {}) + agent_section = data.get("agent_id", {}) + global_section = data.get("global") + + if global_section is not None: + parsed = self._parse_one_latent(global_section, "global") + if parsed is not None: + self.manual_global_latent = (parsed["latent_eps"], parsed["latent_c"]) + + for key, value in object_section.items(): + parsed = self._parse_one_latent(value, f"object_id:{key}") + if parsed is not None: + self.manual_object_latent[str(key)] = parsed + for key, value in agent_section.items(): + parsed = self._parse_one_latent(value, f"agent_id:{key}") + if parsed is not None: + self.manual_agent_latent[str(key)] = parsed + + @staticmethod + def _parse_one_latent(value: dict, name: str) -> Optional[Dict[str, np.ndarray]]: + if not isinstance(value, dict): + print(f"[HBBC] invalid latent entry ({name}): expect dict.") + return None + try: + eps = _normalize_eps(value["latent_eps"]) + c = _normalize_c(value["latent_c"]) + return {"latent_eps": eps, "latent_c": c} + except Exception as e: + print(f"[HBBC] invalid latent entry ({name}): {e}") + return None + + def _lookup_manual(self, object_id: Optional[str], agent_id: Optional[str]) -> Optional[Tuple[np.ndarray, np.ndarray]]: + if object_id is not None and object_id in self.manual_object_latent: + e = self.manual_object_latent[object_id]["latent_eps"] + c = self.manual_object_latent[object_id]["latent_c"] + return e, c + if agent_id is not None and agent_id in self.manual_agent_latent: + e = self.manual_agent_latent[agent_id]["latent_eps"] + c = self.manual_agent_latent[agent_id]["latent_c"] + return e, c + if self.manual_global_latent is not None: + return self.manual_global_latent + return None + + def get_latent(self, vehicle_key: str, object_id: Optional[str], agent_id: Optional[str]) -> Tuple[np.ndarray, np.ndarray]: + manual = self._lookup_manual(object_id=object_id, agent_id=agent_id) + if manual is not None: + return manual + if self.mode == "per_episode_reset": + if self._episode_latent is None: + self._episode_latent = _sample_latent(self.rng) + return self._episode_latent + if vehicle_key not in self.vehicle_latent: + self.vehicle_latent[vehicle_key] = _sample_latent(self.rng) + return self.vehicle_latent[vehicle_key] + + +class HBBCBackgroundController: + def __init__( + self, + model_path: str, + device: str = "cpu", + latent_mode: str = "per_vehicle_fixed", + latent_json_path: Optional[str] = None, + seed: int = 0, + dt: float = 0.1, + ): + self.model = HBBCModelWrapper.get(model_path=model_path, device=device) + self.latent_mgr = HBBCLatentManager(mode=latent_mode, seed=seed, latent_json_path=latent_json_path) + self.dt = float(dt) + self.vehicle_state: Dict[str, VehicleStateCache] = {} + + def reset_episode(self): + self.latent_mgr.reset_episode() + self.vehicle_state.clear() + + def remove_vehicle(self, vehicle_key: str): + self.vehicle_state.pop(vehicle_key, None) + self.latent_mgr.vehicle_latent.pop(vehicle_key, None) + + def _build_base_state(self, vehicle, vehicle_key: str) -> np.ndarray: + state = self.vehicle_state.get(vehicle_key) + if state is None: + state = VehicleStateCache() + self.vehicle_state[vehicle_key] = state + + speed_km_h = float(getattr(vehicle, "speed_km_h", 0.0)) + max_speed_km_h = float(getattr(vehicle, "max_speed_km_h", 120.0)) + veh_vel = np.clip((speed_km_h + 1.0) / (max_speed_km_h + 1.0), 0.0, 1.0) + + heading_theta = float(getattr(vehicle, "heading_theta", 0.0)) + if state.last_heading_theta is None: + yaw_rate = 0.0 + else: + yaw_rate = _wrap_to_pi(heading_theta - state.last_heading_theta) / self.dt + yaw_rate = float(np.clip(yaw_rate, -5.0, 5.0)) + + current_action = getattr(vehicle, "current_action", None) + if current_action is None: + last_action_0, last_action_1 = state.last_action + else: + try: + last_action_0, last_action_1 = float(current_action[0]), float(current_action[1]) + except Exception: + last_action_0, last_action_1 = state.last_action + + state.last_heading_theta = heading_theta + state.last_speed_km_h = speed_km_h + state.last_action = (last_action_0, last_action_1) + + obs = np.array( + [ + 0.0, + 0.0, + 0.0, + veh_vel, + 0.0, + yaw_rate * 0.5, + last_action_0, + last_action_1, + ], + dtype=np.float32, + ) + return obs + + def build_obs(self, vehicle, vehicle_key: str, object_id: Optional[str], agent_id: Optional[str]) -> np.ndarray: + base = self._build_base_state(vehicle, vehicle_key=vehicle_key) + eps, c = self.latent_mgr.get_latent(vehicle_key=vehicle_key, object_id=object_id, agent_id=agent_id) + return np.concatenate([base, eps, c], axis=-1).astype(np.float32) + + def infer_actions(self, batch: List[Tuple[str, object, Optional[str], Optional[str]]]) -> Dict[str, np.ndarray]: + if not batch: + return {} + obs_list = [] + vehicle_ids = [] + for vehicle_key, vehicle, object_id, agent_id in batch: + obs_list.append(self.build_obs(vehicle, vehicle_key=vehicle_key, object_id=object_id, agent_id=agent_id)) + vehicle_ids.append(vehicle_key) + actions = self.model.act_batch(np.stack(obs_list, axis=0)) + out = {} + for idx, key in enumerate(vehicle_ids): + out[key] = actions[idx].astype(np.float32) + return out diff --git a/Env/inverse_dynamics.py b/Env/inverse_dynamics.py index c2ff8ca..d11e41f 100644 --- a/Env/inverse_dynamics.py +++ b/Env/inverse_dynamics.py @@ -61,5 +61,33 @@ class InverseDynamics: # Normalize actions to [-1, 1] norm_acc = np.clip(acc / self.max_acc, -1.0, 1.0) norm_steering = np.clip(steering / self.max_steering, -1.0, 1.0) - + return np.array([norm_steering, norm_acc]), {'raw_acc': acc, 'raw_steering': steering} + + def apply_action(self, current_state, action, dt=0.1): + """ + Forward dynamics: given current_state and action [steering, acc] in [-1, 1], return next_state. + State format: dict with position (x,y), heading, velocity (vx, vy). + """ + steering_norm, acc_norm = float(action[0]), float(action[1]) + acc = acc_norm * self.max_acc + steering = steering_norm * self.max_steering + pos = np.array(current_state['position'][:2], dtype=np.float64) + heading = float(current_state['heading']) + vel = np.array(current_state['velocity'], dtype=np.float64) + v = np.linalg.norm(vel) + if v < 0.1: + v = 0.1 + theta_dot = v * np.tan(steering) / self.wheelbase + v_next = v + acc * dt + v_next = max(0.0, v_next) + heading_next = heading + theta_dot * dt + heading_next = np.arctan2(np.sin(heading_next), np.cos(heading_next)) + vx_next = v_next * np.cos(heading_next) + vy_next = v_next * np.sin(heading_next) + pos_next = pos + dt * np.array([vx_next, vy_next]) + return { + 'position': pos_next, + 'heading': heading_next, + 'velocity': np.array([vx_next, vy_next]), + } diff --git a/Env/scenario_env.py b/Env/scenario_env.py index c40a3fe..6963b67 100644 --- a/Env/scenario_env.py +++ b/Env/scenario_env.py @@ -53,6 +53,13 @@ class MultiAgentScenarioEnv(ScenarioEnv): data_directory=None, num_controlled_agents=3, horizon=1000, + # HBBC background vehicle controls (optional) + enable_hbbc_background=False, + hbbc_model_path="models/hbbc/hbbc.pt", + hbbc_inference_device="cpu", + hbbc_latent_mode="per_vehicle_fixed", + hbbc_latent_json_path=None, + hbbc_dt=0.1, )) return config diff --git a/README.md b/README.md index 2962de9..ac260e9 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ MAGAIL4AutoDrive/ │ └── ... ├── Env/ # 仿真环境封装 (MetaDrive Wrapper) │ ├── bc_env.py # BCScenarioEnv,45 维观测(BC/MAGAIL 共用) +│ ├── bc_ego_replay_env.py # BCEgoReplayEnv,单智能体 BC 评估(仅 ego 受控) │ ├── scenario_env.py # 多智能体基础场景环境 │ ├── expert_replay_env.py # 专家轨迹回放环境(数据生成与回放) │ ├── inverse_dynamics.py # 逆动力学模块 (轨迹 -> 动作) @@ -78,26 +79,47 @@ python -m scenarionet.convert_waymo -d data/exp_converted --raw_data_path ./waym **4) 本项目:生成专家 pkl** 使用筛选后的场景目录,生成训练用 pkl 到 `data/training_data`: +- **多智能体**(所有受控车轨迹,输出 `expert_data_{start_index}_{num_scenarios}.pkl`): ```bash python scripts/generate_expert_data.py --data_dir data/exp_filtered --output_dir data/training_data --num_scenarios 100 --start_index 0 ``` +- **单智能体**(仅 ego 车轨迹,输出 `expert_data_ego_{start_index}_{num_scenarios}.pkl`,用于单智能体 BC): +```bash +python scripts/generate_expert_data.py --data_dir data/exp_filtered --output_dir data/training_data --num_scenarios 100 --start_index 0 --ego_only +``` + ## 核心工作流 ### 1. 数据准备 使用 `scripts/generate_expert_data.py` 将 Waymo 数据转换为训练用 `.pkl`,输出到 `data/training_data/`。 +- **多智能体**: ```bash -python scripts/generate_expert_data.py --data_dir data/exp_filtered --output_dir data/training_data --num_scenarios 100 +python scripts/generate_expert_data.py --data_dir data/exp_filtered --output_dir data/training_data --num_scenarios 100 --start_index 0 +``` + +- **单智能体(仅 ego)**: +```bash +python scripts/generate_expert_data.py --data_dir data/exp_filtered --output_dir data/training_data --num_scenarios 100 --start_index 0 --ego_only ``` ### 2. 行为克隆 (BC) -- **训练**:`python train_bc.py`(模型保存到 `models/bc/`,日志到 `logs/bc/`) +BC 支持两种模式:**多智能体**(默认,所有受控车共用同一策略)与 **单智能体**(仅 ego 车,评估时其他车按专家轨迹回放)。 + +- **多智能体训练**(模型保存到 `models/bc/`,日志到 `logs/bc/`): +```bash +python train_bc.py --expert_data_path data/training_data/expert_data_0_50.pkl --epochs 100 ``` -# 注意替换文件名 -python train_bc.py --expert_data_path ./data/training/expert_data_0_50.pkl --epochs 100 + +- **单智能体训练**(使用 ego-only 数据,评估时仅 ego 受策略控制,其他车专家回放): +```bash +python train_bc.py --expert_data_path data/training_data/expert_data_ego_0_50.pkl --epochs 100 --single_agent ``` -- **可视化**:`python scripts/visualize.py policy --policy_type bc --model_path models/bc/policy_best.pt` + +- **可视化**:`python scripts/visualize.py policy --policy_type bc --model_path models/bc/policy_best.pt` + 仅自车用策略、其他车回放(单智能体可视化):加 `--ego_only`,例如 + `python scripts/visualize.py policy --policy_type bc --model_path models/bc/policy_best.pt --ego_only --num_scenarios 1` ### 3. 多智能体对抗模仿学习 (MAGAIL) - **训练**:`python train_magail.py`(模型保存到 `models/magail/`,日志到 `logs/magail/`) @@ -114,6 +136,7 @@ python train_bc.py --expert_data_path ./data/training/expert_data_0_50.pkl --epo ### Env 模块 - **Env/bc_env.py**:`BCScenarioEnv`,45 维观测(Ego 5 维 + 10 邻居×4 维),BC 与 MAGAIL 训练/评估共用 +- **Env/bc_ego_replay_env.py**:`BCEgoReplayEnv`,单智能体 BC 评估环境,仅 ego 受策略控制,其他车按专家轨迹回放 - **Env/scenario_env.py**:`MultiAgentScenarioEnv` 基类,Waymo 场景加载与步进 - **Env/expert_replay_env.py**:专家轨迹回放与逆动力学动作,供 `generate_expert_data.py` 与回放可视化 - **Env/inverse_dynamics.py**:轨迹 → 油门/转向动作 diff --git a/algorithms/__init__.py b/algorithms/__init__.py new file mode 100644 index 0000000..b9d2a9f --- /dev/null +++ b/algorithms/__init__.py @@ -0,0 +1,2 @@ +"""Compatibility package for legacy HBBC checkpoints.""" + diff --git a/algorithms/utils.py b/algorithms/utils.py new file mode 100644 index 0000000..f316770 --- /dev/null +++ b/algorithms/utils.py @@ -0,0 +1,18 @@ +import numpy as np + + +class RunningMeanStd(object): + def __init__(self, epsilon=1e-4, shape=()): + self.mean = np.zeros(shape, np.float64) + self.var = np.ones(shape, np.float64) + self.count = epsilon + + +class Normalizer(RunningMeanStd): + def __init__(self, input_dim, epsilon=1e-4, clip_obs=10.0): + super().__init__(shape=input_dim) + self.epsilon = epsilon + self.clip_obs = clip_obs + + def normalize(self, input): + return np.clip((input - self.mean) / np.sqrt(self.var + self.epsilon), -self.clip_obs, self.clip_obs) diff --git a/dataset/loader.py b/dataset/loader.py index 47df83a..3cdb04b 100644 --- a/dataset/loader.py +++ b/dataset/loader.py @@ -10,7 +10,7 @@ import torch from torch.utils.data import Dataset -def load_expert_pkl(expert_data_path, *, filter_terminal_last_step: bool = False): +def load_expert_pkl(expert_data_path, *, filter_terminal_last_step: bool = False, agent_id_filter=None): """从目录或单个 pkl 加载专家 (obs, acts),返回 concat 后的 obs_data, act_data。 Args: @@ -18,6 +18,8 @@ def load_expert_pkl(expert_data_path, *, filter_terminal_last_step: bool = False filter_terminal_last_step: If True, drop the last (obs, act) pair of each trajectory. This approximates II's \"train only on non-terminal steps\" when the dataset doesn't explicitly store dones. + agent_id_filter: If not None, only load trajectories with traj[\"agent_id\"] == agent_id_filter + (e.g. \"default_agent\" for single-agent/ego-only). """ if os.path.isdir(expert_data_path): pkl_files = glob.glob(os.path.join(expert_data_path, "*.pkl")) @@ -36,6 +38,8 @@ def load_expert_pkl(expert_data_path, *, filter_terminal_last_step: bool = False data = pickle.load(f) if isinstance(data, list): for traj in data: + if agent_id_filter is not None and traj.get("agent_id") != agent_id_filter: + continue if "obs" in traj and "acts" in traj: obs = traj["obs"] acts = traj["acts"] @@ -101,11 +105,12 @@ def get_expert_scenario_ids(expert_data_path, max_ids=10): class MAGAILExpertDataset(Dataset): - def __init__(self, data_dir, transform=None, *, filter_terminal_last_step: bool = False): + def __init__(self, data_dir, transform=None, *, filter_terminal_last_step: bool = False, agent_id_filter=None): """ Args: data_dir (str): Directory containing .pkl files from generate_expert_data.py transform (callable, optional): Optional transform to be applied on a sample. + agent_id_filter: If not None, only load trajectories with traj[\"agent_id\"] == agent_id_filter. """ self.data_dir = data_dir self.transform = transform @@ -121,6 +126,8 @@ class MAGAILExpertDataset(Dataset): with open(pkl_file, "rb") as f: data = pickle.load(f) # data is a list of dicts: {'obs': (T, 45), 'acts': (T, 2), ...} + if agent_id_filter is not None: + data = [t for t in data if t.get("agent_id") == agent_id_filter] self.trajectories.extend(data) except Exception as e: print(f"Error loading {pkl_file}: {e}") diff --git a/docs/HBBC_Deploy_guied.md b/docs/HBBC_Deploy_guied.md new file mode 100644 index 0000000..f87e74a --- /dev/null +++ b/docs/HBBC_Deploy_guied.md @@ -0,0 +1,439 @@ +# HBBC 策略部署指南 + +本文档说明如何将 `weights/hbbc.pt` 部署到 MetaDrive 项目中的**背景车辆**上,作为车辆控制策略使用。 + +--- + +## 0. 本仓库适配说明(MAGAIL4AutoDrive) + +本仓库已落地一套可直接使用的 HBBC 背景车接入实现,核心代码: + +- `Env/hbbc_actor_critic.py`:HBBC 所需 `ActorCritic` 最小推理网络 +- `Env/hbbc_background_policy.py`:模型加载、18 维观测构建、latent 管理(含 JSON 覆盖) +- `Env/bc_env.py`:`BCScenarioEnv` 动态背景车 HBBC 接入(静态背景车保持不变) +- `Env/bc_ego_replay_env.py`:`BCEgoReplayEnv` 动态背景车 HBBC 接入(ego-only 评估兼容) + +与原文档示例不同点: + +1. 当前仓库 `BaseVehicle` 没有 `pos_buffer/rot_buffer/action_buffer`,因此 8 维 `base_state` 使用当前可得车辆状态重建; +2. 仅动态背景车使用 HBBC,静态背景车仍作为占位/邻居车辆; +3. 支持通过 JSON 手动指定场景中某些车辆的 latent(`object_id` / `agent_id` 双 key)。 + +--- + +## 1. 概述 + +### 1.1 HBBC 是什么 + +**HBBC**(Hierarchical Behavior-Based Controller)是一个低层驾驶策略网络,输入车辆状态和行为条件,输出连续控制动作 `[steering, acceleration]`,可直接用于 MetaDrive 的车辆控制。 + +### 1.2 依赖 + +- **PyTorch** +- **NumPy** +- **MetaDrive**(需包含 `BaseVehicle`、`BasePolicy` 等基础组件) + +--- + +## 2. 模型加载 + +### 2.1 模型架构 + +HBBC 对应 `ActorCritic` 网络,需按以下参数实例化: + +```python +import torch +from algorithms.modules import ActorCritic # 或复制 actor_critic.py 到目标项目 + +hbbc = ActorCritic( + num_actor_obs=18, + num_critic_obs=18, + num_actions=2, + latent_c_dim=4, # 行为模式数 + latent_eps_dim=6, # 风格向量维度 + use_style_latent=True, +).to(device) + +# 加载权重 +checkpoint = torch.load("path/to/hbbc.pt", map_location=device, weights_only=False) +hbbc.load_state_dict(checkpoint['actor_critic']) +hbbc.eval() +``` + +### 2.2 推理接口 + +```python +with torch.no_grad(): + actions = hbbc.act_inference(obs_tensor) # obs_tensor: (batch, 18), 输出: (batch, 2) +``` + +--- + +## 3. 输入规格(18 维) + +HBBC 的输入为 `hbbc_obs`,维度 18,由三部分拼接: + +``` +hbbc_obs = [base_state(8) | latent_eps(6) | latent_c(4)] +``` + +### 3.1 base_state(8 维) + +从车辆对象构建,需按**精确顺序**拼接。实现如下(需配合 `relative_pos_local`、`rot_matrix_inv`、`clip` 等工具函数): + +```python +import numpy as np + +def build_hbbc_base_state(vehicle): + """ + 从 MetaDrive 车辆对象构建 HBBC 的 8 维 base_state。 + 要求 vehicle 具有: position, pos_buffer, rot_buffer, heading_buffer, + speed_km_h, max_speed_km_h, eps_step, acceleration, yaw_rate, action_buffer + """ + from metadrive.utils.math import clip # 或 np.clip + + veh_pos = list(vehicle.position) + [0] + init_veh_rot = np.array([vehicle.rot_buffer[0][0], vehicle.rot_buffer[0][1], vehicle.rot_buffer[0][2]]) + init_veh_pos = list(vehicle.pos_buffer[0]) + [0] + init_veh_heading = vehicle.heading_buffer[0] + + # 局部位置(本实现中置 0) + veh_pos_local = relative_pos_local(init_veh_pos, veh_pos, init_veh_rot)[:2] + veh_pos_local[0] /= 10 + veh_pos_local[1] /= 2 + + # 局部航向(本实现中置 0) + veh_heading = vehicle.heading + cross = np.cross(init_veh_heading, veh_heading) + dot = np.dot(init_veh_heading, veh_heading) + veh_heading_local = np.arctan2(cross, dot) + + veh_vel = clip((vehicle.speed_km_h + 1) / (vehicle.max_speed_km_h + 1), 0.0, 1.0) + veh_acc = vehicle.acceleration / 5 if vehicle.eps_step > 1 else 0 + yaw_rate = vehicle.yaw_rate + last_action_0 = vehicle.action_buffer[-1][0] + last_action_1 = vehicle.action_buffer[-1][1] + + # 8 维,顺序固定 + obs = np.concatenate(( + veh_pos_local * 0, # 2 维,置 0 + [veh_heading_local * 0], # 1 维,置 0 + [veh_vel], # 1 维 + [veh_acc * 0], # 1 维,置 0 + [yaw_rate * 0.5], # 1 维 + [last_action_0], [last_action_1] # 2 维 + )).astype(np.float32) + return obs +``` + +### 3.2 latent_eps(6 维) + +风格向量,需 **L2 归一化** 且在 `[-1, 1]` 内: + +```python +# 随机采样(每个 episode 或每辆车可固定/随机) +latent_eps = np.random.randn(6).astype(np.float32) +latent_eps = latent_eps / (np.linalg.norm(latent_eps) + 1e-8) +latent_eps = np.clip(latent_eps, -1.0, 1.0) +``` + +### 3.3 latent_c(4 维) + +行为模式 one-hot,4 选 1: + +```python +# 随机选一个模式 (0~3) +mode = np.random.randint(0, 4) +latent_c = np.zeros(4, dtype=np.float32) +latent_c[mode] = 1.0 +``` + +### 3.4 完整观测拼接 + +```python +def build_hbbc_obs(vehicle, latent_eps, latent_c): + base = build_hbbc_base_state(vehicle) + return np.concatenate([base, latent_eps, latent_c], axis=-1) # shape: (18,) +``` + +--- + +## 4. 必需工具函数 + +若目标项目无以下函数,需自行实现或从 styledrive 的 `envs/utils.py` 拷贝: + +```python +def rot_matrix(t): + """t: [roll, pitch, yaw], 返回 3x3 旋转矩阵""" + roll, pitch, yaw = t[0], t[1], t[2] + sr, cr = np.sin(roll), np.cos(roll) + sp, cp = np.sin(pitch), np.cos(pitch) + sy, cy = np.sin(yaw), np.cos(yaw) + r_roll = np.array([[1, 0, 0], [0, cr, -sr], [0, sr, cr]]) + r_pitch = np.array([[cp, 0, sp], [0, 1, 0], [-sp, 0, cp]]) + r_yaw = np.array([[cy, -sy, 0], [sy, cy, 0], [0, 0, 1]]) + return np.dot(np.dot(r_yaw, r_pitch), r_roll) + +def rot_matrix_inv(t): + return rot_matrix(t).T + +def relative_pos_local(coord, coord_t, veh_rot): + """将 coord_t 从世界坐标变换到以 coord 为原点、veh_rot 为姿态的局部坐标""" + r_pos_global = np.array(coord_t) - np.array(coord) + rot_mat_inv = rot_matrix_inv(veh_rot) + return rot_mat_inv @ r_pos_global +``` + +`clip` 可用 `np.clip` 或 `metadrive.utils.math.clip`。 + +--- + +## 5. 车辆属性要求 + +使用 HBBC 的车辆需继承或兼容 MetaDrive 的 `BaseVehicle`,并具备: + +| 属性 | 说明 | +|------|------| +| `position` | 当前位置 (x, y) 或 (x, y, z) | +| `heading` | 航向单位向量 | +| `heading_theta` | 航向角(弧度) | +| `pos_buffer` | `deque`,至少 1 个元素,`pos_buffer[0]` 为 episode 起始位姿 | +| `rot_buffer` | `deque`,`(roll, pitch, yaw)`,`rot_buffer[0]` 为起始姿态 | +| `heading_buffer` | `deque`,`heading_buffer[0]` 为起始航向 | +| `action_buffer` | `deque`,`action_buffer[-1]` 为上一时刻动作 `(steering, acc)` | +| `speed_km_h` | 当前速度 km/h | +| `max_speed_km_h` | 最大速度 km/h | +| `acceleration` | 当前加速度 | +| `yaw_rate` | 偏航角速度 (rad/s) | +| `eps_step` | 本 episode 的步数 | +| `last_heading_theta` | 上一帧航向角(用于 yaw_rate) | + +`BaseVehicle` 在 `before_step` 中会更新 `pos_buffer`、`rot_buffer`、`heading_buffer`、`action_buffer`,只要在配置中设置 `veh_obs_len >= 1`(建议 3–10)即可。 + +--- + +## 6. 输出动作格式 + +HBBC 输出 2 维连续动作,与 MetaDrive 动作空间一致: + +```python +# actions: (2,) 或 (batch, 2) +# actions[0]: steering ∈ [-1, 1] +# actions[1]: acceleration ∈ [-1, 1],正=油门,负=刹车 +``` + +环境会在 `_preprocess_actions` 中做限幅与平滑,无需在策略内再次裁剪。 + +--- + +## 7. 部署为 MetaDrive 策略(背景车) + +### 7.1 自定义 Policy + +实现一个继承 `BasePolicy` 的策略,在 `act` 中调用 HBBC: + +```python +from metadrive.policy.base_policy import BasePolicy +import torch +import numpy as np + +class HBBCPolicy(BasePolicy): + def __init__(self, control_object, random_seed=None, hbbc_path="weights/hbbc.pt", device="cpu"): + super().__init__(control_object, random_seed) + self.device = torch.device(device) + self.hbbc = self._load_hbbc(hbbc_path) + self.latent_eps = None + self.latent_c = None + self._resample_latent() + + def _load_hbbc(self, path): + from algorithms.modules import ActorCritic # 根据实际路径调整 + model = ActorCritic( + num_actor_obs=18, num_critic_obs=18, num_actions=2, + latent_c_dim=4, latent_eps_dim=6, use_style_latent=True + ).to(self.device) + ckpt = torch.load(path, map_location=self.device, weights_only=False) + model.load_state_dict(ckpt['actor_critic']) + model.eval() + return model + + def _resample_latent(self): + self.latent_eps = np.random.randn(6).astype(np.float32) + self.latent_eps = self.latent_eps / (np.linalg.norm(self.latent_eps) + 1e-8) + self.latent_eps = np.clip(self.latent_eps, -1.0, 1.0) + mode = np.random.randint(0, 4) + self.latent_c = np.zeros(4, dtype=np.float32) + self.latent_c[mode] = 1.0 + + def act(self, agent_id=None): + vehicle = self.control_object + base_state = build_hbbc_base_state(vehicle) + obs = np.concatenate([base_state, self.latent_eps, self.latent_c], axis=-1) + obs_t = torch.tensor(obs, dtype=torch.float32, device=self.device).unsqueeze(0) + with torch.no_grad(): + actions = self.hbbc.act_inference(obs_t).cpu().numpy().squeeze() + self.action_info["action"] = actions.tolist() + return [float(actions[0]), float(actions[1])] + + def reset(self): + super().reset() + self._resample_latent() +``` + +### 7.2 配置背景车使用 HBBC + +在环境配置中为背景车辆指定 `HBBCPolicy`: + +```python +config = { + # ... + "agent_configs": { + "agent0": { + "policy": HBBCPolicy, + "policy_kwargs": {"hbbc_path": "path/to/hbbc.pt", "device": "cuda:0"}, + } + }, + # 若使用 traffic 的 policy 配置方式,则需在 traffic 管理逻辑中 + # 将部分或全部背景车的 policy 替换为 HBBCPolicy +} +``` + +若背景车由 TrafficManager 等模块统一管理,需在该模块的 policy 选择逻辑中加入对 `HBBCPolicy` 的分配。 + +### 7.3 与 TrafficManager 集成 + +若背景车由 `PGTrafficManager` 等生成,需在添加策略时改为使用 `HBBCPolicy`: + +```python +# 原代码通常为: +# self.add_policy(random_v.id, IDMPolicy, random_v, self.generate_seed()) + +# 改为: +from your_policy_module import HBBCPolicy +self.add_policy(random_v.id, HBBCPolicy, random_v, self.generate_seed(), + hbbc_path="path/to/hbbc.pt", device="cuda:0") +``` + +`add_policy` 的额外参数会传给 Policy 的 `__init__`。若接口不支持传参,可修改 `HBBCPolicy` 从全局配置读取路径,或使用自定义 TrafficManager 子类。 + +**注意**:HBBC 在 styledrive 中基于 scenario 轨迹训练,不包含路由逻辑。背景车若需要沿车道/路线行驶,可能需: +- 在项目中为 HBBC 车辆配置 `navigation`,或 +- 仅对部分背景车使用 HBBC(如混合 IDM + HBBC),或 +- 在目标项目中验证 HBBC 在开放道路上的表现后决定是否全量使用。 + +### 7.4 注意事项 + +1. **latent 生命周期**:可为每辆车在 spawn 时采样一次,或在每个 episode reset 时重采样。 +2. **首帧 action_buffer**:首步 `action_buffer[-1]` 通常为 `(0, 0)`,由 `BaseVehicle` 初始化保证。 +3. **同步更新 buffer**:车辆必须在每步调用 `before_step` 之类接口,更新 `pos_buffer`、`action_buffer` 等,否则观测会错位。 +4. **veh_obs_len**:车辆配置中设置 `veh_obs_len >= 3`(建议 10),确保 buffer 长度足够。 + +--- + +## 8. ActorCritic 网络定义(可移植) + +若目标项目无法导入 styledrive 的 `algorithms`,可把以下简化版 `ActorCritic` 放到本项目中单独使用: + +```python +import torch +import torch.nn as nn + +def get_activation(name): + return getattr(nn, name)() + +class ActorCritic(nn.Module): + def __init__(self, num_actor_obs=18, num_critic_obs=18, num_actions=2, + latent_c_dim=4, latent_eps_dim=6, use_style_latent=True, + actor_hidden_dims=[512, 256, 128], activation='elu'): + super().__init__() + act_fn = getattr(nn, activation.upper())() + self.latent_c_dim = latent_c_dim + self.latent_eps_dim = latent_eps_dim + self.use_style_latent = use_style_latent + + layers = [] + layers.append(nn.Linear(num_actor_obs, actor_hidden_dims[0])) + layers.append(act_fn) + for i in range(len(actor_hidden_dims) - 1): + layers.append(nn.Linear(actor_hidden_dims[i], actor_hidden_dims[i + 1])) + layers.append(act_fn) + self.actor_trunk = nn.Sequential(*layers) + self.actor_head = nn.Linear(actor_hidden_dims[-1], num_actions) + + if use_style_latent: + style_layers = [nn.Linear(latent_eps_dim, 512), act_fn, + nn.Linear(512, 256), act_fn, nn.Linear(256, 128), act_fn] + self.style_trunk = nn.Sequential(*style_layers) + self.style_head = nn.Linear(128, latent_eps_dim) + self.style_activation = torch.tanh + + def act_inference(self, observations): + if self.use_style_latent: + obs = observations[..., :-(self.latent_c_dim + self.latent_eps_dim)] + eps = observations[..., -self.latent_c_dim - self.latent_eps_dim:-self.latent_c_dim] + c = observations[..., -self.latent_c_dim:] + eps = self.style_activation(self.style_head(self.style_trunk(eps))) + observations = torch.cat([obs, eps, c], dim=-1) + embedding = self.actor_trunk(observations) + return self.actor_head(embedding) +``` + +加载与调用方式与前面一致。 + +--- + +## 9. 简要检查清单 + +- [ ] 正确加载 `hbbc.pt` 的 `actor_critic` 权重 +- [ ] `build_hbbc_base_state` 输出 8 维,顺序与文档一致 +- [ ] `latent_eps` 6 维、L2 归一化 +- [ ] `latent_c` 4 维 one-hot +- [ ] 车辆具备 `pos_buffer`、`rot_buffer`、`heading_buffer`、`action_buffer` 等属性 +- [ ] 策略返回 `[steering, acceleration]`,范围 [-1, 1] +- [ ] 每步更新上述 buffer,保证观测连续 + +--- + +## 10. 本仓库配置项与 JSON 示例 + +可通过环境配置控制 HBBC 背景车行为: + +- `enable_hbbc_background`:是否启用动态背景车 HBBC(`True/False`) +- `hbbc_model_path`:模型路径(默认 `models/hbbc/hbbc.pt`) +- `hbbc_inference_device`:推理设备(如 `cpu` / `cuda:0`) +- `hbbc_latent_mode`:`per_vehicle_fixed` 或 `per_episode_reset` +- `hbbc_latent_json_path`:可选,手动 latent JSON 路径 + +`hbbc_latent_json_path` 内容格式(优先按 `object_id` 匹配,失败回退 `agent_id`): + +```json +{ + "global": { + "latent_eps": [0.35, -0.12, 0.28, 0.46, -0.22, 0.18], + "latent_c": [0, 0, 1, 0] + }, + "object_id": { + "12345": { + "latent_eps": [0.2, -0.1, 0.3, 0.4, -0.2, 0.1], + "latent_c": [0, 1, 0, 0] + } + }, + "agent_id": { + "controlled_abcde": { + "latent_eps": [0.5, 0.1, -0.1, 0.2, -0.3, 0.4], + "latent_c": [1, 0, 0, 0] + } + } +} +``` + +匹配优先级为:`object_id` > `agent_id` > `global` > 随机采样。 +`latent_eps` 会做 L2 归一化,`latent_c` 会强制 one-hot;非法输入会告警并回退随机采样。 + +--- + +## 11. 参考来源 + +- 策略与观测:`envs/ad_hbbc_gym.py` 中的 `ADObservation.vehicle_state` +- 模型:`algorithms/modules/actor_critic.py` 中 `ActorCritic` +- 工具:`envs/utils.py` 中的 `relative_pos_local`、`rot_matrix`、`rot_matrix_inv` diff --git a/TRAINING_ARCHITECTURE.md b/docs/TRAINING_ARCHITECTURE.md similarity index 100% rename from TRAINING_ARCHITECTURE.md rename to docs/TRAINING_ARCHITECTURE.md diff --git a/docs/examples/hbbc_latent_example.json b/docs/examples/hbbc_latent_example.json new file mode 100644 index 0000000..cf04fe4 --- /dev/null +++ b/docs/examples/hbbc_latent_example.json @@ -0,0 +1,18 @@ +{ + "global": { + "latent_eps": [0.35, -0.12, 0.28, 0.46, -0.22, 0.18], + "latent_c": [0, 1,1, 0] + }, + "object_id": { + "12345": { + "latent_eps": [0.2, -0.1, 0.3, 0.4, -0.2, 0.1], + "latent_c": [0, 1, 0, 0] + } + }, + "agent_id": { + "controlled_abcde": { + "latent_eps": [0.5, 0.1, -0.1, 0.2, -0.3, 0.4], + "latent_c": [1, 0, 0, 0] + } + } +} diff --git a/logs/bc/20260210-232650/events.out.tfevents.1770737210.Hfkk.14957.0 b/logs/bc/20260210-232650/events.out.tfevents.1770737210.Hfkk.14957.0 new file mode 100644 index 0000000000000000000000000000000000000000..fed4c6dc4c0709b18e03016a25fae464cab37b62 GIT binary patch literal 165262 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

Q9!&YmRO;h%Od9S{l9RKC;_rgijxA8lY-df` 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 literal 0 HcmV?d00001 diff --git a/scripts/README.md b/scripts/README.md index 349bd1f..69c1489 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -16,9 +16,19 @@ | 脚本 | 用途 | 用法示例 | |------|------|----------| -| [generate_expert_data.py](generate_expert_data.py) | 从 Waymo 数据生成专家 (obs, act) 的 pkl | `python scripts/generate_expert_data.py --data_dir data/exp_filtered --output_dir data/training_data --num_scenarios 100` | +| [generate_expert_data.py](generate_expert_data.py) | 从 Waymo 数据生成专家 (obs, act) 的 pkl | 见下方 | -**常用参数**:`--data_dir`(默认 `data/exp_filtered`)、`--output_dir`(默认 `data/training_data`)、`--start_index`、`--num_scenarios`。 +**多智能体**(输出 `expert_data_{start_index}_{num_scenarios}.pkl`): +```bash +python scripts/generate_expert_data.py --data_dir data/exp_filtered --output_dir data/training_data --num_scenarios 100 --start_index 0 +``` + +**单智能体**(仅采集 ego 车轨迹,输出 `expert_data_ego_{start_index}_{num_scenarios}.pkl`,用于单智能体 BC): +```bash +python scripts/generate_expert_data.py --data_dir data/exp_filtered --output_dir data/training_data --num_scenarios 100 --start_index 0 --ego_only +``` + +**常用参数**:`--data_dir`(默认 `data/exp_filtered`)、`--output_dir`(默认 `data/training_data`)、`--start_index`、`--num_scenarios`、`--ego_only`(仅保存 default_agent 轨迹,输出使用 `expert_data_ego_*.pkl` 前缀)。 --- @@ -40,13 +50,32 @@ python scripts/visualize.py replay --data_dir data/exp_filtered --num_scenarios python scripts/visualize.py policy --policy_type bc --model_path models/bc/policy_best.pt --data_dir data/exp_filtered --num_scenarios 1 python scripts/visualize.py policy --policy_type magail --model_path models/magail/model_50_actor.pth --num_scenarios 1 --deterministic ``` +- **policy + 仅自车策略、其他车回放**(BC 单智能体模型):加 `--ego_only`,自车由策略控制,其余车辆按专家轨迹回放。 +```bash +python scripts/visualize.py policy --policy_type bc --model_path models/bc/policy_best.pt --data_dir data/exp_filtered --num_scenarios 1 --ego_only +``` + +- **policy + HBBC 动态背景车**(仅动态背景车启用,静态背景车保持原样): +```bash +python scripts/visualize.py policy \ + --policy_type bc \ + --model_path models/bc/policy_best.pt \ + --data_dir data/exp_filtered \ + --num_scenarios 1 \ + --ego_only \ + --enable_hbbc_background \ + --hbbc_model_path models/hbbc/hbbc.pt \ + --hbbc_inference_device cpu \ + --hbbc_latent_mode per_vehicle_fixed \ + --hbbc_latent_json_path docs/examples/hbbc_latent_example.json +``` - **trajectory**(专家轨迹 matplotlib 俯视图动画): ```bash python scripts/visualize.py trajectory --data_dir data/exp_filtered --scenario_idx 0 ``` -**公共参数**:`--data_dir`(默认 `data/exp_filtered`)、`--start_index`、`--num_scenarios`、`--horizon`。policy 模式另有 `--policy_type`(auto/bc/magail)、`--model_path`、`--deterministic`(仅 MAGAIL)。 +**公共参数**:`--data_dir`(默认 `data/exp_filtered`)、`--start_index`、`--num_scenarios`、`--horizon`。policy 模式另有 `--policy_type`(auto/bc/magail)、`--model_path`、`--deterministic`(仅 MAGAIL)、`--ego_only`(仅 BC:自车用策略,其他车专家回放)、`--enable_hbbc_background`、`--hbbc_model_path`、`--hbbc_inference_device`、`--hbbc_latent_mode`、`--hbbc_latent_json_path`。 --- @@ -70,7 +99,7 @@ python scripts/visualize.py trajectory --data_dir data/exp_filtered --scenario_i ## 与训练流程的对应关系 -1. **数据准备**:`generate_expert_data.py` → 输出到 `data/training_data/*.pkl` -2. **BC 训练**:根目录 `train_bc.py` → 模型保存到 `models/bc/`,日志到 `logs/bc/` +1. **数据准备**:`generate_expert_data.py` → 输出到 `data/training_data/*.pkl`(多智能体 `expert_data_*.pkl`,单智能体 `expert_data_ego_*.pkl`) +2. **BC 训练**:根目录 `train_bc.py` → 模型保存到 `models/bc/`,日志到 `logs/bc/`。单智能体模式加 `--single_agent` 并指定 ego-only 的 pkl。 3. **MAGAIL 训练**:根目录 `train_magail.py` → 模型保存到 `models/magail/`,日志到 `logs/magail/` 4. **可视化**:`scripts/visualize.py`(子命令 replay / policy / trajectory)→ 数据目录默认 `data/exp_filtered` diff --git a/scripts/generate_expert_data.py b/scripts/generate_expert_data.py index 853300f..aa2bd04 100644 --- a/scripts/generate_expert_data.py +++ b/scripts/generate_expert_data.py @@ -102,7 +102,9 @@ def generate_data(args): # Post-process episode data for agent_id, data in episode_data.items(): - if len(data['obs']) > 10: # Minimum length filter + if args.ego_only and agent_id != "default_agent": + continue + if len(data['obs']) > 10: # Minimum length filter expert_trajectories.append({ 'obs': np.array(data['obs']), 'acts': np.array(data['acts']), @@ -120,9 +122,14 @@ def generate_data(args): pass # Save data - output_file = os.path.join(args.output_dir, f"expert_data_{args.start_index}_{args.num_scenarios}.pkl") + if args.ego_only: + output_file = os.path.join(args.output_dir, f"expert_data_ego_{args.start_index}_{args.num_scenarios}.pkl") + else: + output_file = os.path.join(args.output_dir, f"expert_data_{args.start_index}_{args.num_scenarios}.pkl") os.makedirs(args.output_dir, exist_ok=True) - + + if args.ego_only: + print("Ego-only mode: saved trajectories are SDC (default_agent) only.") print(f"Saving {len(expert_trajectories)} trajectories to {output_file}") with open(output_file, 'wb') as f: pickle.dump(expert_trajectories, f) @@ -157,6 +164,6 @@ if __name__ == "__main__": parser.add_argument("--output_dir", type=str, default="data/training_data", help="Output directory") parser.add_argument("--start_index", type=int, default=0) parser.add_argument("--num_scenarios", type=int, default=10) - + parser.add_argument("--ego_only", action="store_true", help="Only collect and save ego (default_agent) trajectories; output uses expert_data_ego_*.pkl prefix") args = parser.parse_args() generate_data(args) diff --git a/scripts/visualize.py b/scripts/visualize.py index 1006efa..ab7379c 100644 --- a/scripts/visualize.py +++ b/scripts/visualize.py @@ -111,36 +111,49 @@ def _resolve_model_path(model_path, policy_type): def _run_policy(args): from Env.bc_env import BCScenarioEnv + from Env.bc_ego_replay_env import BCEgoReplayEnv from metadrive.engine.engine_utils import close_engine policy_type = (args.policy_type or "auto").lower() if policy_type == "auto": policy_type = "bc" if args.model_path.endswith(".pt") else "magail" + ego_only = getattr(args, "ego_only", False) + if ego_only and policy_type != "bc": + print("[WARN] --ego_only is supported for BC policy only; MAGAIL will run in multi-agent mode.") data_dir = _resolve_data_dir(args.data_dir) data_path = os.path.abspath(data_dir) env_config = { "data_directory": data_path, "is_multi_agent": True, - "num_controlled_agents": 3, + "num_controlled_agents": 100 if ego_only else 3, "horizon": args.horizon, "use_render": True, "sequential_seed": True, "start_scenario_index": args.start_index, "num_scenarios": args.num_scenarios, "log_level": 40, + "enable_hbbc_background": bool(getattr(args, "enable_hbbc_background", False)), + "hbbc_model_path": getattr(args, "hbbc_model_path", "models/hbbc/hbbc.pt"), + "hbbc_inference_device": getattr(args, "hbbc_inference_device", "cpu"), + "hbbc_latent_mode": getattr(args, "hbbc_latent_mode", "per_vehicle_fixed"), + "hbbc_latent_json_path": getattr(args, "hbbc_latent_json_path", None), } - print(f"Initializing BCScenarioEnv (policy_type={policy_type})...") + if ego_only and policy_type == "bc": + print("Initializing BCEgoReplayEnv (ego-only: policy on self, others replayed)...") + else: + print(f"Initializing BCScenarioEnv (policy_type={policy_type})...") + try: - env = BCScenarioEnv(env_config, agent2policy={}) + env = BCEgoReplayEnv(config=env_config) if (ego_only and policy_type == "bc") else BCScenarioEnv(env_config, agent2policy={}) except Exception as e: print(f"Error init env: {e}. Trying to close lingering engine...") try: close_engine() except Exception: pass - env = BCScenarioEnv(env_config, agent2policy={}) + env = BCEgoReplayEnv(config=env_config) if (ego_only and policy_type == "bc") else BCScenarioEnv(env_config, agent2policy={}) state_dim = 45 action_dim = 2 @@ -156,7 +169,11 @@ def _run_policy(args): hidden_units=(256, 256), hidden_activation=torch.nn.Tanh(), ).to(device) - policy.load_state_dict(torch.load(model_path, map_location=device)) + try: + state = torch.load(model_path, map_location=device, weights_only=True) + except TypeError: + state = torch.load(model_path, map_location=device) + policy.load_state_dict(state) policy.eval() else: from train_magail import Actor @@ -178,7 +195,16 @@ def _run_policy(args): pass continue - print(f"Scenario loaded. Controlled agents (current): {len(obs_dict)}, total in scenario: {env.num_controlled_in_scenario}") + n_total = getattr(env, "num_controlled_in_scenario", len(obs_dict)) + mode_note = " (ego only, others replayed)" if (ego_only and policy_type == "bc") else "" + if ego_only and policy_type == "bc" and bool(env_config.get("enable_hbbc_background", False)): + mode_note = " (ego only, dynamic background via HBBC)" + print(f"Scenario loaded. Controlled agents (current): {len(obs_dict)}, total in scenario: {n_total}{mode_note}") + if ego_only and policy_type == "bc" and len(obs_dict) == 1: + if bool(env_config.get("enable_hbbc_background", False)): + print(" [Ego control: policy injected — dynamic background vehicles use HBBC; static background stays static.]") + else: + print(" [Ego control: policy injected — ego uses model output each step; other vehicles expert replay.]") if len(obs_dict) == 0: print(f"Scenario {i} has no controlled agents (all filtered out). Skipping.") continue @@ -370,6 +396,12 @@ def main(): pp.add_argument("--policy_type", type=str, default="auto", choices=["auto", "bc", "magail"]) pp.add_argument("--model_path", type=str, default="models/bc/policy_best.pt") pp.add_argument("--deterministic", action="store_true", help="MAGAIL: use mean action") + pp.add_argument("--ego_only", action="store_true", help="BC only: inject policy into ego only; other vehicles use expert replay") + pp.add_argument("--enable_hbbc_background", action="store_true", help="Enable HBBC policy for dynamic background vehicles") + pp.add_argument("--hbbc_model_path", type=str, default="models/hbbc/hbbc.pt") + pp.add_argument("--hbbc_inference_device", type=str, default="cpu") + pp.add_argument("--hbbc_latent_mode", type=str, default="per_vehicle_fixed", choices=["per_vehicle_fixed", "per_episode_reset"]) + pp.add_argument("--hbbc_latent_json_path", type=str, default=None, help="Optional JSON for per-vehicle latent override") # trajectory pt = subparsers.add_parser("trajectory", help="2D matplotlib animation of expert trajectories") diff --git a/train_bc.py b/train_bc.py index 66d8c72..60bc018 100644 --- a/train_bc.py +++ b/train_bc.py @@ -15,19 +15,20 @@ from torch.utils.tensorboard import SummaryWriter from Algorithm.policy import StateIndependentPolicy from Algorithm.bc import train_bc_epoch, eval_bc_epoch from Env.bc_env import BCScenarioEnv +from Env.bc_ego_replay_env import BCEgoReplayEnv from dataset.loader import load_expert_pkl, get_expert_scenario_ids def evaluate_policy(policy, args, device): - """在 BCScenarioEnv 中评估策略:仅使用专家数据中出现过的 scenario_id,保证 eval 有受控车。 - 输出与 replay 对齐:agents (current)=reset 时受控车数,total in scenario=该场景受控轨迹总数(car_birth_info_list 长度)。""" + """在 BCScenarioEnv(多智能体)或 BCEgoReplayEnv(单智能体)中评估策略。 + 仅使用专家数据中出现过的 scenario_id。单智能体模式下仅 ego 受策略控制,其他车专家回放。""" waymo_data_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data") data_dir = os.path.join(waymo_data_dir, "exp_filtered") if not os.path.exists(data_dir): data_dir = os.path.join(waymo_data_dir, "exp_converted") if not os.path.exists(data_dir): print(f"[ERROR] Could not find scenario data in {waymo_data_dir}. Evaluation skipped.") - return 0.0 + return 0.0, 0.0, 0.0 scenario_ids = get_expert_scenario_ids(args.expert_data_path, max_ids=5) if not scenario_ids: @@ -38,6 +39,7 @@ def evaluate_policy(policy, args, device): total_steps = [] collision_episodes = 0 horizon = 200 + single_agent = getattr(args, "single_agent", False) for idx, scenario_id in enumerate(scenario_ids): env_config = { @@ -49,8 +51,12 @@ def evaluate_policy(policy, args, device): "horizon": horizon, "start_scenario_index": scenario_id, "num_scenarios": 1, + "log_level": 50, } - env = BCScenarioEnv(env_config, agent2policy=None) + if single_agent: + env = BCEgoReplayEnv(config=env_config) + else: + env = BCScenarioEnv(env_config, agent2policy=None) try: obs_dict = env.reset(seed=scenario_id) except Exception as e: @@ -59,7 +65,7 @@ def evaluate_policy(policy, args, device): continue n_controlled = len(env.controlled_agents) - n_total_in_scenario = getattr(env, "num_controlled_in_scenario", n_controlled) + n_total_in_scenario = getattr(env, "num_controlled_in_scenario", n_controlled) if not single_agent else 1 if n_controlled == 0: print( f" Eval Episode {idx} (scenario {scenario_id}): 0 controlled agents (total in scenario: {n_total_in_scenario}), skip." @@ -95,9 +101,9 @@ def evaluate_policy(policy, args, device): total_steps.append(step_count) if had_near_collision: collision_episodes += 1 + mode_str = "single-agent (ego)" if single_agent else f"agents (current): {n_controlled}, total in scenario: {n_total_in_scenario}" print( - f" Eval Episode {idx} (scenario {scenario_id}): Total Reward {episode_reward:.2f}, steps {step_count}, " - f"agents (current): {n_controlled}, total in scenario: {n_total_in_scenario}" + f" Eval Episode {idx} (scenario {scenario_id}): Total Reward {episode_reward:.2f}, steps {step_count}, {mode_str}" ) env.close() @@ -124,9 +130,11 @@ def main(args): print(f"TensorBoard logging to: {log_dir}") os.makedirs(args.save_dir, exist_ok=True) + agent_id_filter = "default_agent" if getattr(args, "single_agent", False) else None obs_data, act_data = load_expert_pkl( args.expert_data_path, filter_terminal_last_step=args.filter_terminal_last_step, + agent_id_filter=agent_id_filter, ) obs_tensor = torch.FloatTensor(obs_data) act_tensor = torch.FloatTensor(act_data) @@ -195,5 +203,10 @@ if __name__ == "__main__": action="store_true", help="Drop the last (obs, act) pair of each trajectory to approximate training on non-terminal steps (II-style).", ) + parser.add_argument( + "--single_agent", + action="store_true", + help="Use single-agent (ego) expert data and evaluation; load only default_agent trajectories and evaluate with BCEgoReplayEnv.", + ) args = parser.parse_args() main(args)