Загрузка данных
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
# =========================
# НАСТРОЙКИ
# =========================
EPISODES = 50000
BATCH_SIZE = 128
MEMORY_SIZE = 100000
GAMMA = 0.99
LEARNING_RATE = 0.001
EPSILON_START = 1.0
EPSILON_END = 0.05
EPSILON_DECAY = 30000
TARGET_UPDATE = 1000
MODEL_FILE = "tricky_model.pt"
# =========================
# DEVICE
# =========================
device = torch.device(
"cuda" if torch.cuda.is_available()
else "cpu"
)
print("Устройство:", device)
# =========================
# СОСТОЯНИЕ
# =========================
def get_features(sim):
heights = sim.get_column_heights()
max_height = max(
sim.get_tower_height(),
1
)
# Нормализуем высоты
heights = heights / 20.0
# Текущая фигура
piece = sim.piece.flatten()
# Дополним до 12 значений
piece_data = np.zeros(12)
n = min(
len(piece),
12
)
piece_data[:n] = piece[:n]
# Положение фигуры
piece_x = sim.piece_x / 10.0
piece_y = sim.piece_y / 20.0
# Дополнительные характеристики
tower_height = sim.get_tower_height() / 20.0
holes = sim.count_holes() / 100.0
stability = sim.get_stability()
state = np.concatenate([
heights,
piece_data,
[piece_x],
[piece_y],
[tower_height],
[holes],
[stability]
])
return state.astype(np.float32)
# =========================
# NETWORK
# =========================
class DQN(nn.Module):
def __init__(self, input_size):
super().__init__()
self.net = nn.Sequential(
nn.Linear(input_size, 128),
nn.ReLU(),
nn.Linear(128, 128),
nn.ReLU(),
nn.Linear(128, 64),
nn.ReLU(),
# 5 действий:
# nothing / left / right / rotate / down
nn.Linear(64, 5)
)
def forward(self, x):
return self.net(x)
# =========================
# REPLAY BUFFER
# =========================
class ReplayBuffer:
def __init__(self, capacity):
self.capacity = capacity
self.buffer = []
self.position = 0
def push(
self,
state,
action,
reward,
next_state,
done
):
data = (
state,
action,
reward,
next_state,
done
)
if len(self.buffer) < self.capacity:
self.buffer.append(data)
else:
self.buffer[self.position] = data
self.position = (
self.position + 1
) % self.capacity
def sample(self, batch_size):
batch = random.sample(
self.buffer,
batch_size
)
states, actions, rewards, next_states, dones = zip(
*batch
)
return (
np.array(states),
np.array(actions),
np.array(rewards),
np.array(next_states),
np.array(dones)
)
def __len__(self):
return len(self.buffer)
# =========================
# EPSILON
# =========================
def get_epsilon(step):
if step >= EPSILON_DECAY:
return EPSILON_END
fraction = (
step / EPSILON_DECAY
)
return (
EPSILON_START
+ fraction *
(EPSILON_END - EPSILON_START)
)
# =========================
# INIT
# =========================
sim = TrickySimulator()
state = get_features(sim)
input_size = len(state)
print(
"Размер состояния:",
input_size
)
policy_net = DQN(
input_size
).to(device)
target_net = DQN(
input_size
).to(device)
target_net.load_state_dict(
policy_net.state_dict()
)
target_net.eval()
optimizer = optim.Adam(
policy_net.parameters(),
lr=LEARNING_RATE
)
memory = ReplayBuffer(
MEMORY_SIZE
)
# =========================
# LOAD OLD MODEL
# =========================
if os.path.exists(MODEL_FILE):
print(
"Найдена старая модель."
)
try:
policy_net.load_state_dict(
torch.load(
MODEL_FILE,
map_location=device
)
)
target_net.load_state_dict(
policy_net.state_dict()
)
print(
"Модель загружена."
)
except Exception as e:
print(
"Не удалось загрузить модель:",
e
)
# =========================
# TRAIN STEP
# =========================
def train_step():
if len(memory) < BATCH_SIZE:
return None
(
states,
actions,
rewards,
next_states,
dones
) = memory.sample(
BATCH_SIZE
)
states = torch.tensor(
states,
dtype=torch.float32,
device=device
)
actions = torch.tensor(
actions,
dtype=torch.long,
device=device
)
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.unsqueeze(1)
).squeeze(1)
# Q(s',a')
with torch.no_grad():
next_q = target_net(
next_states
).max(
1
)[0]
target_q = (
rewards
+ GAMMA *
next_q *
(1 - dones)
)
loss = nn.functional.smooth_l1_loss(
current_q,
target_q
)
optimizer.zero_grad()
loss.backward()
torch.nn.utils.clip_grad_norm_(
policy_net.parameters(),
5.0
)
optimizer.step()
return loss.item()
# =========================
# TRAINING
# =========================
global_step = 0
best_score = 0
print()
print("НАЧАЛО ОБУЧЕНИЯ")
print()
for episode in range(1, EPISODES + 1):
sim.reset()
state = get_features(sim)
episode_reward = 0
loss_sum = 0
loss_count = 0
while not sim.game_over:
epsilon = get_epsilon(
global_step
)
# Exploration
if random.random() < epsilon:
action = random.randrange(5)
# Exploitation
else:
with torch.no_grad():
state_tensor = torch.tensor(
state,
dtype=torch.float32,
device=device
).unsqueeze(0)
q_values = policy_net(
state_tensor
)
action = int(
torch.argmax(
q_values
).item()
)
next_state_raw, reward, done = sim.step(
action
)
next_state = get_features(
sim
)
memory.push(
state,
action,
reward,
next_state,
done
)
state = next_state
episode_reward += reward
loss = train_step()
if loss is not None:
loss_sum += loss
loss_count += 1
global_step += 1
# Target network
if global_step % TARGET_UPDATE == 0:
target_net.load_state_dict(
policy_net.state_dict()
)
# =====================
# SAVE BEST
# =====================
if sim.score > best_score:
best_score = sim.score
torch.save(
policy_net.state_dict(),
MODEL_FILE
)
# =====================
# LOG
# =====================
if episode % 100 == 0:
avg_loss = (
loss_sum / loss_count
if loss_count
else 0
)
print(
f"Игра {episode:6d} | "
f"score {sim.score:5d} | "
f"pieces {sim.pieces_placed:3d} | "
f"height {sim.get_tower_height():2d} | "
f"reward {episode_reward:8.2f} | "
f"epsilon {epsilon:.3f} | "
f"loss {avg_loss:.5f}"
)
print()
print("ОБУЧЕНИЕ ЗАКОНЧЕНО")
torch.save(
policy_net.state_dict(),
MODEL_FILE
)
print(
"Модель сохранена:",
MODEL_FILE
)
print(
"Лучший score:",
best_score
)