File size: 8,204 Bytes
80c0c44 77e15e7 80c0c44 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | """
train.py
Standalone advanced training script for Drone Delivery RL.
Usage: python train.py --task easy_delivery --episodes 1000
"""
import os
import argparse
import random
import torch
import torch.nn as nn
import torch.optim as optim
from collections import deque
import sys
from pathlib import Path
# Add project root to sys.path
ROOT_DIR = Path(__file__).parent
if str(ROOT_DIR) not in sys.path:
sys.path.insert(0, str(ROOT_DIR))
from drone_env.rl.model import PathQNet, ACTIONS, ACTION2IDX, CELL2IDX
from drone_env.server.grid_world_environment import DroneDeliveryEnvironment
from drone_env.models import DroneAction
# ββ Hyperparameters βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
GAMMA = 0.99
BATCH_SIZE = 64
LR = 1e-4
REPLAY_SIZE = 10000
TARGET_UPDATE = 10
EPS_START = 1.0
EPS_END = 0.05
EPS_DECAY = 0.995
# ββ Replay Buffer βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
class ReplayBuffer:
def __init__(self, capacity: int):
self.buffer = deque(maxlen=capacity)
def push(self, state, action, reward, next_state, done):
self.buffer.append((state, action, reward, next_state, done))
def sample(self, batch_size: int):
return random.sample(self.buffer, batch_size)
def __len__(self):
return len(self.buffer)
# ββ Helper to process observations ββββββββββββββββββββββββββββββββββββββββββββ
def obs_to_tensors(obs, device):
"""Converts a DroneObservation to the tensors required by PathQNet."""
# 1. Grid (H*W)
# Using cell_types instead of emojis for better robustness
grid = obs.cell_types
flat_grid = []
for row in grid:
for cell in row:
# Map emoji back to int (simplified)
idx = CELL2IDX.get(cell, 0) # default road
flat_grid.append(idx)
grid_t = torch.tensor([flat_grid], dtype=torch.long, device=device)
# 2. Drone XY (normalized)
drone_xy = torch.tensor([[obs.drone_x / obs.grid_width, obs.drone_y / obs.grid_height]], dtype=torch.float, device=device)
# 3. Battery (0-1)
battery = torch.tensor([[obs.battery]], dtype=torch.float, device=device)
# 4. Target XY (normalized)
if obs.current_target:
tx, ty = obs.current_target
target_xy = torch.tensor([[tx / obs.grid_width, ty / obs.grid_height]], dtype=torch.float, device=device)
else:
target_xy = torch.tensor([[0.0, 0.0]], dtype=torch.float, device=device)
return grid_t, drone_xy, battery, target_xy
# ββ Training Loop βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def train(task_name: str, episodes: int, device_name: str):
device = torch.device(device_name)
print(f"Starting training for {task_name} on {device}...")
os.makedirs("data", exist_ok=True)
with open("data/train.log", "a") as f:
f.write(f"\n>>> Neural Training Engine Started [TASK: {task_name}]\n")
env = DroneDeliveryEnvironment()
policy_net = PathQNet(embed_dim=64).to(device)
target_net = PathQNet(embed_dim=64).to(device)
target_net.load_state_dict(policy_net.state_dict())
optimizer = optim.Adam(policy_net.parameters(), lr=LR)
memory = ReplayBuffer(REPLAY_SIZE)
epsilon = EPS_START
# Automatically resume if model exists
task_short = task_name.split('_')[0]
model_path = f"data/{task_short}.pth"
if os.path.exists(model_path):
print(f"Loading existing weights from {model_path}...")
try:
policy_net.load_state_dict(torch.load(model_path, map_location=device, weights_only=True))
target_net.load_state_dict(policy_net.state_dict())
epsilon = EPS_END # Start with low exploration if resuming
with open("data/train.log", "a") as f:
f.write(f"Resuming from checkpoint: {model_path}\n")
except Exception as e:
print(f"Failed to load checkpoint: {e}. Starting fresh.")
for ep in range(episodes):
obs = env.reset(DroneAction(task_name=task_name))
state_t = obs_to_tensors(obs, device)
total_reward = 0
done = False
while not done:
# Select Action
if random.random() < epsilon:
action_idx = random.randint(0, len(ACTIONS) - 1)
else:
with torch.no_grad():
q_values = policy_net(*state_t)
action_idx = q_values.argmax().item()
action_str = ACTIONS[action_idx]
# Step
next_obs = env.step(DroneAction(direction=action_str))
reward = next_obs.reward_last
done = next_obs.done
total_reward += reward
next_state_t = obs_to_tensors(next_obs, device)
# Save to buffer
memory.push(state_t, action_idx, reward, next_state_t, done)
state_t = next_state_t
# Optimize
if len(memory) > BATCH_SIZE:
batch = memory.sample(BATCH_SIZE)
# Unzip batch
states, actions, rewards, next_states, dones = zip(*batch)
# Batch processing
def cat_tensors(t_list):
return [torch.cat([t[i] for t in t_list]) for i in range(4)]
b_grid, b_drone, b_bat, b_target = cat_tensors(states)
bn_grid, bn_drone, bn_bat, bn_target = cat_tensors(next_states)
b_actions = torch.tensor(actions, device=device).unsqueeze(1)
b_rewards = torch.tensor(rewards, device=device, dtype=torch.float).unsqueeze(1)
b_dones = torch.tensor(dones, device=device, dtype=torch.float).unsqueeze(1)
# 1. Current Q values
current_q = policy_net(b_grid, b_drone, b_bat, b_target).gather(1, b_actions)
# 2. Next Q values from target net
with torch.no_grad():
next_q = target_net(bn_grid, bn_drone, bn_bat, bn_target).max(1)[0].unsqueeze(1)
expected_q = b_rewards + (GAMMA * next_q * (1 - b_dones))
# 3. Loss & Step
loss = nn.MSELoss()(current_q, expected_q)
optimizer.zero_grad()
loss.backward()
# Gradient clipping
torch.nn.utils.clip_grad_norm_(policy_net.parameters(), 1.0)
optimizer.step()
epsilon = max(EPS_END, epsilon * EPS_DECAY)
if ep % TARGET_UPDATE == 0:
target_net.load_state_dict(policy_net.state_dict())
if ep % 50 == 0:
log_msg = f"Episode {ep}/{episodes} | Avg Reward: {total_reward:.2f} | Epsilon: {epsilon:.2f}"
print(log_msg)
torch.save(policy_net.state_dict(), model_path)
with open("data/train.log", "a") as f:
f.write(log_msg + " (Periodic Save)\n")
# Save
task_short = task_name.split('_')[0]
model_path = f"data/{task_short}.pth"
torch.save(policy_net.state_dict(), model_path)
msg = f"Training complete. Model saved to {model_path}."
print(msg)
with open("data/train.log", "a") as f:
f.write(msg + "\n")
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--task", type=str, default="easy_delivery")
parser.add_argument("--episodes", type=int, default=100)
parser.add_argument("--gpu", action="store_true")
args = parser.parse_args()
device = "cuda" if args.gpu and torch.cuda.is_available() else "cpu"
train(args.task, args.episodes, device)
|