Загрузка данных
import os
import random
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from tricky_sim import TrickySimulator
# =========================================================
# НАСТРОЙКИ
# =========================================================
NUM_ENVS = min(32, os.cpu_count() or 8)
TOTAL_STEPS = 2_000_000
BATCH_SIZE = 256
REPLAY_SIZE = 200_000
GAMMA = 0.99
LEARNING_RATE = 0.0005
EPSILON_START = 1.0
EPSILON_END = 0.05
EPSILON_DECAY = 500_000
LEARN_EVERY = 4
TARGET_UPDATE = 5_000
WARMUP = 10_000
MODEL_FILE = "tricky_dqn.pt"
# =========================================================
# DEVICE
# =========================================================
device = torch.device(
"cuda" if torch.cuda.is_available() else "cpu"
)
print("=" * 60)
print("TRICKY TOWERS DQN")
print("=" * 60)
print("CPU threads:", os.cpu_count())
print("Parallel games:", NUM_ENVS)
print("Device:", device)
if device.type == "cuda":
print("GPU:", torch.cuda.get_device_name(0))
print("=" * 60)
# =========================================================
# ENVIRONMENTS
# =========================================================
envs = [
TrickySimulator(seed=i)
for i in range(NUM_ENVS)
]
states = [
env.reset()
for env in envs
]
STATE_SIZE = len(states[0])
ACTION_SIZE = 5
print("State size:", STATE_SIZE)
print("Actions:", ACTION_SIZE)
# =========================================================
# NEURAL NETWORK
# =========================================================
class DQN(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(STATE_SIZE, 256),
nn.ReLU(),
nn.Linear(256, 256),
nn.ReLU(),
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, ACTION_SIZE)
)
def forward(self, x):
return self.net(x)
policy_net = DQN().to(device)
target_net = DQN().to(device)
target_net.load_state_dict(
policy_net.state_dict()
)
target_net.eval()
optimizer = optim.Adam(
policy_net.parameters(),
lr=LEARNING_RATE
)
loss_function = nn.SmoothL1Loss()
# =========================================================
# REPLAY BUFFER
# =========================================================
class ReplayBuffer:
def __init__(self, capacity):
self.capacity = capacity
self.states = []
self.actions = []
self.rewards = []
self.next_states = []
self.dones = []
self.position = 0
def add(
self,
state,
action,
reward,
next_state,
done
):
if len(self.states) < self.capacity:
self.states.append(state)
self.actions.append(action)
self.rewards.append(reward)
self.next_states.append(next_state)
self.dones.append(done)
else:
self.states[self.position] = state
self.actions[self.position] = action
self.rewards[self.position] = reward
self.next_states[self.position] = next_state
self.dones[self.position] = done
self.position = (
self.position + 1
) % self.capacity
def sample(self, batch_size):
indexes = np.random.randint(
0,
len(self.states),
size=batch_size
)
states = np.asarray(
[self.states[i] for i in indexes],
dtype=np.float32
)
actions = np.asarray(
[self.actions[i] for i in indexes],
dtype=np.int64
)
rewards = np.asarray(
[self.rewards[i] for i in indexes],
dtype=np.float32
)
next_states = np.asarray(
[self.next_states[i] for i in indexes],
dtype=np.float32
)
dones = np.asarray(
[self.dones[i] for i in indexes],
dtype=np.float32
)
return (
states,
actions,
rewards,
next_states,
dones
)
def __len__(self):
return len(self.states)
replay = ReplayBuffer(REPLAY_SIZE)
# =========================================================
# EPSILON
# =========================================================
def get_epsilon(step):
fraction = min(
1.0,
step / EPSILON_DECAY
)
return (
EPSILON_START
+ fraction
* (
EPSILON_END
- EPSILON_START
)
)
# =========================================================
# ACTION
# =========================================================
@torch.no_grad()
def choose_actions(states, epsilon):
actions = []
# Все состояния одним батчем
state_tensor = torch.tensor(
np.asarray(states),
dtype=torch.float32,
device=device
)
q_values = policy_net(
state_tensor
)
for i in range(len(states)):
if random.random() < epsilon:
action = random.randrange(
ACTION_SIZE
)
else:
action = int(
torch.argmax(
q_values[i]
).item()
)
actions.append(action)
return actions
# =========================================================
# TRAIN STEP
# =========================================================
def train_step():
if len(replay) < max(
BATCH_SIZE,
WARMUP
):
return None
(
states,
actions,
rewards,
next_states,
dones
) = replay.sample(BATCH_SIZE)
states = torch.tensor(
states,
dtype=torch.float32,
device=device
)
actions = torch.tensor(
actions,
dtype=torch.long,
device=device
).unsqueeze(1)
rewards = torch.tensor(
rewards,
dtype=torch.float32,
device=device
)
next_states = torch.tensor(
next_states,
dtype=torch.float32,
device=device
)
dones = torch.tensor(
dones,
dtype=torch.float32,
device=device
)
# Q(s,a)
current_q = policy_net(
states
).gather(
1,
actions
).squeeze(1)
# max Q(s',a')
with torch.no_grad():
next_q = target_net(
next_states
).max(
1
).values
target_q = (
rewards
+ GAMMA
* next_q
* (1.0 - dones)
)
loss = loss_function(
current_q,
target_q
)
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(
policy_net.parameters(),
10.0
)
optimizer.step()
return float(loss.item())
# =========================================================
# STATISTICS
# =========================================================
total_games = 0
total_pieces = 0
best_pieces = 0
recent_games = []
losses = []
# =========================================================
# TRAINING
# =========================================================
print()
print("Training started...")
print()
for step in range(1, TOTAL_STEPS + 1):
epsilon = get_epsilon(step)
# -----------------------------------------------------
# Choose action for all 32 games
# -----------------------------------------------------
actions = choose_actions(
states,
epsilon
)
# -----------------------------------------------------
# Step environments
# -----------------------------------------------------
for i, env in enumerate(envs):
state = states[i]
action = actions[i]
next_state, reward, done = env.step(
action
)
replay.add(
state,
action,
reward,
next_state,
done
)
states[i] = next_state
if done:
pieces = env.pieces
total_games += 1
total_pieces += pieces
best_pieces = max(
best_pieces,
pieces
)
recent_games.append(
pieces
)
if len(recent_games) > 100:
recent_games.pop(0)
states[i] = env.reset()
# -----------------------------------------------------
# Learn
# -----------------------------------------------------
if (
step >= WARMUP
and step % LEARN_EVERY == 0
):
loss = train_step()
if loss is not None:
losses.append(loss)
# -----------------------------------------------------
# Target network
# -----------------------------------------------------
if step % TARGET_UPDATE == 0:
target_net.load_state_dict(
policy_net.state_dict()
)
# -----------------------------------------------------
# Console
# -----------------------------------------------------
if step % 1_000 == 0:
if recent_games:
avg_recent = (
sum(recent_games)
/ len(recent_games)
)
else:
avg_recent = 0
if losses:
avg_loss = (
sum(losses[-100:])
/ min(
100,
len(losses)
)
)
else:
avg_loss = 0
print(
f"step={step:,} | "
f"games={total_games:,} | "
f"avg100={avg_recent:.1f} | "
f"best={best_pieces} | "
f"epsilon={epsilon:.3f} | "
f"loss={avg_loss:.4f} | "
f"buffer={len(replay):,}",
flush=True
)
# -----------------------------------------------------
# Save
# -----------------------------------------------------
if step % 50_000 == 0:
torch.save(
{
"model": policy_net.state_dict(),
"target": target_net.state_dict(),
"optimizer": optimizer.state_dict(),
"step": step,
"best_pieces": best_pieces
},
MODEL_FILE
)
print(
f"Model saved: {MODEL_FILE}",
flush=True
)
# =========================================================
# FINAL SAVE
# =========================================================
torch.save(
{
"model": policy_net.state_dict(),
"target": target_net.state_dict(),
"optimizer": optimizer.state_dict(),
"step": TOTAL_STEPS,
"best_pieces": best_pieces
},
MODEL_FILE
)
print()
print("=" * 60)
print("TRAINING FINISHED")
print("Best:", best_pieces)
print("Model:", MODEL_FILE)
print("=" * 60)_