root39058 commited on
Commit
489ff6f
·
verified ·
1 Parent(s): f0b562f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +89 -167
app.py CHANGED
@@ -5,49 +5,42 @@ import torch.optim as optim
5
  from torch.distributions import Categorical
6
  import gradio as gr
7
  import matplotlib.pyplot as plt
8
- from collections import defaultdict
9
- import random
10
 
11
  # ==========================================
12
- # 1. БЕСКОНЕЧНЫЙ ПРОЦЕДУРНЫЙ МИР
13
  # ==========================================
14
  class InfiniteWorld:
15
- """
16
- Мир хранится в словаре чанков.
17
- Координаты не ограничены.
18
- Биомы генерируются детерминировано по hash координат.
19
- """
20
  CHUNK_SIZE = 16
21
- VIEW_RADIUS = 8 # Агент видит 16x16 вокруг себя
22
 
23
  def __init__(self, seed=42):
24
  self.seed = seed
25
- self.chunks = {} # (cx, cy) -> np.array(CHUNK_SIZE, CHUNK_SIZE)
26
  self.agent_pos = [0, 0]
27
  self.steps = 0
28
  self.max_steps = 1000
29
 
30
- def _get_chunk(self, cx: int, cy: int) -> np.ndarray:
31
  if (cx, cy) not in self.chunks:
32
- # Детерминированная генерация по координатам
33
  rng = np.random.RandomState(hash((cx, cy, self.seed)) % (2**31))
34
  chunk = np.zeros((self.CHUNK_SIZE, self.CHUNK_SIZE), dtype=np.float32)
35
- # Простая процедурная генерация: кластеры блоков
36
  noise = rng.rand(self.CHUNK_SIZE, self.CHUNK_SIZE)
37
- chunk[noise > 0.7] = 1.0 # "Природные" блоки
38
  self.chunks[(cx, cy)] = chunk
39
  return self.chunks[(cx, cy)]
40
 
41
- def _world_coords(self, x: int, y: int):
42
  cx, lx = divmod(x, self.CHUNK_SIZE)
43
  cy, ly = divmod(y, self.CHUNK_SIZE)
44
  return cx, cy, lx, ly
45
 
46
- def get_block(self, x: int, y: int) -> float:
47
  cx, cy, lx, ly = self._world_coords(x, y)
48
  return self._get_chunk(cx, cy)[lx, ly]
49
 
50
- def set_block(self, x: int, y: int, val: float):
51
  cx, cy, lx, ly = self._world_coords(x, y)
52
  self._get_chunk(cx, cy)[lx, ly] = val
53
 
@@ -57,10 +50,8 @@ class InfiniteWorld:
57
  return self._get_obs()
58
 
59
  def _get_obs(self):
60
- """Возвращает локальный патч 16x16x3 вокруг агента"""
61
  x, y = self.agent_pos
62
  patch = np.zeros((self.VIEW_RADIUS*2, self.VIEW_RADIUS*2, 3), dtype=np.float32)
63
-
64
  for dx in range(-self.VIEW_RADIUS, self.VIEW_RADIUS):
65
  for dy in range(-self.VIEW_RADIUS, self.VIEW_RADIUS):
66
  wx, wy = x + dx, y + dy
@@ -68,52 +59,40 @@ class InfiniteWorld:
68
  px = dx + self.VIEW_RADIUS
69
  py = dy + self.VIEW_RADIUS
70
  patch[px, py, 0] = block
71
- # Канал 1: расстояние до центра (позиционный энкодинг)
72
  patch[px, py, 1] = max(0, 1.0 - abs(dx)/self.VIEW_RADIUS)
73
  patch[px, py, 2] = max(0, 1.0 - abs(dy)/self.VIEW_RADIUS)
74
-
75
- # Отмечаем позицию агента в центре
76
  patch[self.VIEW_RADIUS, self.VIEW_RADIUS, 1] = 1.0
77
  return patch
78
 
79
- def step(self, action: int):
80
  self.steps += 1
81
  reward = -0.005
82
  done = self.steps >= self.max_steps
83
 
84
- ACTIONS = {
85
- 0: (-1, 0), 1: (1, 0), 2: (0, -1), 3: (0, 1), # Move N/S/W/E
86
- 4: (0, 0), # Build
87
- 5: (0, 0), # Dig
88
- }
89
-
90
- if action < 4:
91
- dx, dy = ACTIONS[action]
92
- self.agent_pos[0] += dx
93
- self.agent_pos[1] += dy
94
- elif action == 4: # Build
95
  x, y = self.agent_pos
96
  if self.get_block(x, y) == 0:
97
  self.set_block(x, y, 1.0)
98
  reward = 1.0
99
- elif action == 5: # Dig
100
  x, y = self.agent_pos
101
  if self.get_block(x, y) == 1.0:
102
  self.set_block(x, y, 0.0)
103
  reward = 0.3
104
 
105
- return self._get_obs(), reward, done, {"pos": tuple(self.agent_pos)}
106
 
107
 
108
  # ==========================================
109
- # 2. PPO AGENT С ПАМЯТЬЮ
110
  # ==========================================
111
  class PPOAgent(nn.Module):
112
- def __init__(self, action_space=6, hidden_dim=256):
113
  super().__init__()
114
- self.action_space = action_space
115
-
116
- # Vision encoder (обрабатывает локальный патч 16x16)
117
  self.encoder = nn.Sequential(
118
  nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(),
119
  nn.Conv2d(32, 64, 3, stride=2, padding=1), nn.ReLU(),
@@ -121,21 +100,14 @@ class PPOAgent(nn.Module):
121
  nn.AdaptiveAvgPool2d((4, 4)),
122
  nn.Flatten()
123
  )
124
-
125
- # GRU для памяти (агент помнит, что уже видел/строил)
126
- self.gru = nn.GRUCell(64 * 4 * 4, hidden_dim)
127
- self.hidden = None
128
-
129
- # Actor-Critic heads
130
- self.actor = nn.Linear(hidden_dim, action_space)
131
- self.critic = nn.Linear(hidden_dim, 1)
132
 
133
  def forward(self, obs, hidden=None):
134
  features = self.encoder(obs.permute(0, 3, 1, 2))
135
  h = self.gru(features, hidden)
136
- logits = self.actor(h)
137
- value = self.critic(h)
138
- return logits, value, h
139
 
140
  def act(self, obs, hidden=None):
141
  with torch.no_grad():
@@ -146,52 +118,42 @@ class PPOAgent(nn.Module):
146
 
147
 
148
  # ==========================================
149
- # 3. PPO TRAINING LOOP
150
  # ==========================================
151
- def train_ppo(episodes=300, steps_per_update=256, lr=3e-4):
152
  env = InfiniteWorld()
153
  agent = PPOAgent()
154
- optimizer = optim.Adam(agent.parameters(), lr=lr)
155
-
156
- reward_history = []
157
 
158
  for ep in range(episodes):
159
  obs = env.reset()
160
  hidden = None
161
- episode_rewards = []
162
 
163
- # Rollout buffer
164
- buffers = {'obs': [], 'actions': [], 'log_probs': [], 'rewards': [], 'values': [], 'hiddens': []}
165
-
166
- for _ in range(steps_per_update):
167
  obs_t = torch.FloatTensor(obs)
168
  action, log_prob, value, hidden = agent.act(obs_t, hidden)
169
- next_obs, reward, done, info = env.step(action)
170
 
171
  buffers['obs'].append(obs_t)
172
  buffers['actions'].append(action)
173
  buffers['log_probs'].append(log_prob)
174
  buffers['rewards'].append(reward)
175
  buffers['values'].append(value)
176
- buffers['hiddens'].append(hidden)
177
- episode_rewards.append(reward)
178
 
179
  obs = next_obs
180
  if done:
181
  obs = env.reset()
182
  hidden = None
183
 
184
- # GAE computation
185
- returns = []
186
- advantages = []
187
- R = 0
188
- A = 0
189
- gamma, lam = 0.99, 0.95
190
-
191
  for i in reversed(range(len(buffers['rewards']))):
192
- R = buffers['rewards'][i] + gamma * R
193
- delta = buffers['rewards'][i] + gamma * (buffers['values'][i+1].item() if i < len(buffers['values'])-1 else 0) - buffers['values'][i].item()
194
- A = delta + gamma * lam * A
 
195
  returns.insert(0, R)
196
  advantages.insert(0, A)
197
 
@@ -199,7 +161,6 @@ def train_ppo(episodes=300, steps_per_update=256, lr=3e-4):
199
  advantages = torch.FloatTensor(advantages)
200
  advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
201
 
202
- # PPO Update (4 epochs)
203
  obs_batch = torch.stack(buffers['obs'])
204
  actions_batch = torch.LongTensor(buffers['actions'])
205
  old_log_probs = torch.stack(buffers['log_probs']).detach()
@@ -208,112 +169,73 @@ def train_ppo(episodes=300, steps_per_update=256, lr=3e-4):
208
  logits, values, _ = agent.forward(obs_batch)
209
  dist = Categorical(logits=logits)
210
  new_log_probs = dist.log_prob(actions_batch)
211
- entropy = dist.entropy().mean()
212
-
213
  ratio = (new_log_probs - old_log_probs).exp()
214
- surr1 = ratio * advantages
215
- surr2 = torch.clamp(ratio, 0.8, 1.2) * advantages
216
-
217
- actor_loss = -torch.min(surr1, surr2).mean()
218
- critic_loss = (returns - values.squeeze()).pow(2).mean()
219
- loss = actor_loss + 0.5 * critic_loss - 0.01 * entropy
220
 
221
  optimizer.zero_grad()
222
  loss.backward()
223
  nn.utils.clip_grad_norm_(agent.parameters(), 0.5)
224
  optimizer.step()
225
 
226
- avg_reward = np.mean(episode_rewards)
227
- reward_history.append(avg_reward)
228
  if ep % 20 == 0:
229
- print(f"Ep {ep} | Avg Reward: {avg_reward:.3f} | Chunks explored: {len(env.chunks)}")
230
 
231
- return agent, reward_history
232
 
233
 
234
  # ==========================================
235
- # 4. HF SPACE DEMO
236
- # ==========================================
237
- # ==========================================
238
- # 4. HF SPACE DEMO (ИСПРАВЛЕНО ДЛЯ HF SPACES)
239
  # ==========================================
240
- def create_demo():
241
- print("🏗️ Обучение PPO-агента в бесконечном мире...")
242
- agent, history = train_ppo(episodes=100)
243
- agent.eval()
244
-
245
- def run_exploration(n_steps: int = 200):
246
- """Функция с явной типизацией для избежания бага schema в Gradio"""
247
- env = InfiniteWorld(seed=random.randint(0, 99999))
248
- obs = env.reset()
249
- hidden = None
250
- frames = []
251
- positions = []
252
-
253
- with torch.no_grad():
254
- for _ in range(min(n_steps, 500)): # Ограничиваем макс шаги
255
- fig, ax = plt.subplots(figsize=(5, 5))
256
- ax.imshow(obs)
257
- ax.set_title(f"Pos: {env.agent_pos} | Chunks: {len(env.chunks)}")
258
- ax.axis('off')
259
- frames.append(fig)
260
- plt.close(fig)
261
- positions.append(tuple(env.agent_pos))
262
-
263
- obs_t = torch.FloatTensor(obs)
264
- action, _, _, hidden = agent.act(obs_t, hidden)
265
- obs, _, done, _ = env.step(action)
266
- if done:
267
- break
268
-
269
- # Карта траектории
270
- if positions:
271
- fig2, ax2 = plt.subplots(figsize=(6, 6))
272
- xs, ys = zip(*positions)
273
- ax2.scatter(xs, ys, c=range(len(positions)), cmap='viridis', s=1)
274
- ax2.set_title("Траектория исследования")
275
- ax2.set_aspect('equal')
276
- frames.append(fig2)
277
- plt.close(fig2)
278
-
279
- return frames
280
 
281
- # Явное указание типов компонентов устраняет баг schema
282
- with gr.Blocks(title="Infinite Builder Agent") as demo:
283
- gr.Markdown("# 🌍 Бесконечный мир: PPO-агент с нуля")
284
- gr.Markdown("Агент видит только локальный патч 16×16, имеет GRU-память и исследует процедурный мир.")
285
-
286
- steps_input = gr.Slider(
287
- minimum=50,
288
- maximum=500,
289
- step=50,
290
- value=200,
291
- label="Количество шагов исследования"
292
- )
293
- btn = gr.Button("▶️ Запустить исследование", variant="primary")
294
- gallery = gr.Gallery(
295
- label="Процесс строительства и исследования",
296
- columns=4,
297
- height="auto",
298
- object_fit="contain"
299
- )
300
-
301
- btn.click(
302
- fn=run_exploration,
303
- inputs=[steps_input],
304
- outputs=[gallery]
305
- )
306
 
307
- return demo
 
 
 
 
 
 
 
 
308
 
 
 
 
 
 
 
 
309
 
310
  if __name__ == "__main__":
311
- demo = create_demo()
312
- # КРИТИЧНО: Для HF Spaces обязательно server_name="0.0.0.0"
313
- # share=False на Spaces, т.к. прокси сам маршрутизирует трафик
314
- demo.launch(
315
- server_name="0.0.0.0",
316
- server_port=7860,
317
- share=True,
318
- show_api=False
319
- )
 
5
  from torch.distributions import Categorical
6
  import gradio as gr
7
  import matplotlib.pyplot as plt
8
+ import io
9
+ from PIL import Image
10
 
11
  # ==========================================
12
+ # 1. БЕСКОНЕЧНЫЙ МИР
13
  # ==========================================
14
  class InfiniteWorld:
 
 
 
 
 
15
  CHUNK_SIZE = 16
16
+ VIEW_RADIUS = 8
17
 
18
  def __init__(self, seed=42):
19
  self.seed = seed
20
+ self.chunks = {}
21
  self.agent_pos = [0, 0]
22
  self.steps = 0
23
  self.max_steps = 1000
24
 
25
+ def _get_chunk(self, cx, cy):
26
  if (cx, cy) not in self.chunks:
 
27
  rng = np.random.RandomState(hash((cx, cy, self.seed)) % (2**31))
28
  chunk = np.zeros((self.CHUNK_SIZE, self.CHUNK_SIZE), dtype=np.float32)
 
29
  noise = rng.rand(self.CHUNK_SIZE, self.CHUNK_SIZE)
30
+ chunk[noise > 0.7] = 1.0
31
  self.chunks[(cx, cy)] = chunk
32
  return self.chunks[(cx, cy)]
33
 
34
+ def _world_coords(self, x, y):
35
  cx, lx = divmod(x, self.CHUNK_SIZE)
36
  cy, ly = divmod(y, self.CHUNK_SIZE)
37
  return cx, cy, lx, ly
38
 
39
+ def get_block(self, x, y):
40
  cx, cy, lx, ly = self._world_coords(x, y)
41
  return self._get_chunk(cx, cy)[lx, ly]
42
 
43
+ def set_block(self, x, y, val):
44
  cx, cy, lx, ly = self._world_coords(x, y)
45
  self._get_chunk(cx, cy)[lx, ly] = val
46
 
 
50
  return self._get_obs()
51
 
52
  def _get_obs(self):
 
53
  x, y = self.agent_pos
54
  patch = np.zeros((self.VIEW_RADIUS*2, self.VIEW_RADIUS*2, 3), dtype=np.float32)
 
55
  for dx in range(-self.VIEW_RADIUS, self.VIEW_RADIUS):
56
  for dy in range(-self.VIEW_RADIUS, self.VIEW_RADIUS):
57
  wx, wy = x + dx, y + dy
 
59
  px = dx + self.VIEW_RADIUS
60
  py = dy + self.VIEW_RADIUS
61
  patch[px, py, 0] = block
 
62
  patch[px, py, 1] = max(0, 1.0 - abs(dx)/self.VIEW_RADIUS)
63
  patch[px, py, 2] = max(0, 1.0 - abs(dy)/self.VIEW_RADIUS)
 
 
64
  patch[self.VIEW_RADIUS, self.VIEW_RADIUS, 1] = 1.0
65
  return patch
66
 
67
+ def step(self, action):
68
  self.steps += 1
69
  reward = -0.005
70
  done = self.steps >= self.max_steps
71
 
72
+ if action == 0: self.agent_pos[0] -= 1
73
+ elif action == 1: self.agent_pos[0] += 1
74
+ elif action == 2: self.agent_pos[1] -= 1
75
+ elif action == 3: self.agent_pos[1] += 1
76
+ elif action == 4:
 
 
 
 
 
 
77
  x, y = self.agent_pos
78
  if self.get_block(x, y) == 0:
79
  self.set_block(x, y, 1.0)
80
  reward = 1.0
81
+ elif action == 5:
82
  x, y = self.agent_pos
83
  if self.get_block(x, y) == 1.0:
84
  self.set_block(x, y, 0.0)
85
  reward = 0.3
86
 
87
+ return self._get_obs(), reward, done, {}
88
 
89
 
90
  # ==========================================
91
+ # 2. PPO AGENT
92
  # ==========================================
93
  class PPOAgent(nn.Module):
94
+ def __init__(self):
95
  super().__init__()
 
 
 
96
  self.encoder = nn.Sequential(
97
  nn.Conv2d(3, 32, 3, padding=1), nn.ReLU(),
98
  nn.Conv2d(32, 64, 3, stride=2, padding=1), nn.ReLU(),
 
100
  nn.AdaptiveAvgPool2d((4, 4)),
101
  nn.Flatten()
102
  )
103
+ self.gru = nn.GRUCell(64 * 4 * 4, 256)
104
+ self.actor = nn.Linear(256, 6)
105
+ self.critic = nn.Linear(256, 1)
 
 
 
 
 
106
 
107
  def forward(self, obs, hidden=None):
108
  features = self.encoder(obs.permute(0, 3, 1, 2))
109
  h = self.gru(features, hidden)
110
+ return self.actor(h), self.critic(h), h
 
 
111
 
112
  def act(self, obs, hidden=None):
113
  with torch.no_grad():
 
118
 
119
 
120
  # ==========================================
121
+ # 3. ОБУЧЕНИЕ
122
  # ==========================================
123
+ def train_ppo(episodes=100):
124
  env = InfiniteWorld()
125
  agent = PPOAgent()
126
+ optimizer = optim.Adam(agent.parameters(), lr=3e-4)
 
 
127
 
128
  for ep in range(episodes):
129
  obs = env.reset()
130
  hidden = None
131
+ buffers = {'obs': [], 'actions': [], 'log_probs': [], 'rewards': [], 'values': []}
132
 
133
+ for _ in range(256):
 
 
 
134
  obs_t = torch.FloatTensor(obs)
135
  action, log_prob, value, hidden = agent.act(obs_t, hidden)
136
+ next_obs, reward, done, _ = env.step(action)
137
 
138
  buffers['obs'].append(obs_t)
139
  buffers['actions'].append(action)
140
  buffers['log_probs'].append(log_prob)
141
  buffers['rewards'].append(reward)
142
  buffers['values'].append(value)
 
 
143
 
144
  obs = next_obs
145
  if done:
146
  obs = env.reset()
147
  hidden = None
148
 
149
+ # GAE
150
+ returns, advantages = [], []
151
+ R, A = 0, 0
 
 
 
 
152
  for i in reversed(range(len(buffers['rewards']))):
153
+ R = buffers['rewards'][i] + 0.99 * R
154
+ next_val = buffers['values'][i+1].item() if i < len(buffers['values'])-1 else 0
155
+ delta = buffers['rewards'][i] + 0.99 * next_val - buffers['values'][i].item()
156
+ A = delta + 0.99 * 0.95 * A
157
  returns.insert(0, R)
158
  advantages.insert(0, A)
159
 
 
161
  advantages = torch.FloatTensor(advantages)
162
  advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
163
 
 
164
  obs_batch = torch.stack(buffers['obs'])
165
  actions_batch = torch.LongTensor(buffers['actions'])
166
  old_log_probs = torch.stack(buffers['log_probs']).detach()
 
169
  logits, values, _ = agent.forward(obs_batch)
170
  dist = Categorical(logits=logits)
171
  new_log_probs = dist.log_prob(actions_batch)
 
 
172
  ratio = (new_log_probs - old_log_probs).exp()
173
+ surr = torch.min(ratio * advantages, torch.clamp(ratio, 0.8, 1.2) * advantages)
174
+ loss = -surr.mean() + 0.5 * (returns - values.squeeze()).pow(2).mean() - 0.01 * dist.entropy().mean()
 
 
 
 
175
 
176
  optimizer.zero_grad()
177
  loss.backward()
178
  nn.utils.clip_grad_norm_(agent.parameters(), 0.5)
179
  optimizer.step()
180
 
 
 
181
  if ep % 20 == 0:
182
+ print(f"Ep {ep} | Chunks: {len(env.chunks)}")
183
 
184
+ return agent
185
 
186
 
187
  # ==========================================
188
+ # 4. ГРАФИЧЕСКИЙ ИНТЕРФЕЙС (ИСПРАВЛЕНО)
 
 
 
189
  # ==========================================
190
+ def fig_to_pil(fig):
191
+ """Конвертирует matplotlib figure в PIL Image без schema-багов"""
192
+ buf = io.BytesIO()
193
+ fig.savefig(buf, format='png', bbox_inches='tight')
194
+ buf.seek(0)
195
+ img = Image.open(buf)
196
+ plt.close(fig)
197
+ return img
198
+
199
+
200
+ def run_simulation(n_steps):
201
+ n_steps = int(n_steps)
202
+ agent = run_simulation.agent
203
+ env = InfiniteWorld(seed=np.random.randint(0, 99999))
204
+ obs = env.reset()
205
+ hidden = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
206
 
207
+ images = []
208
+ with torch.no_grad():
209
+ for _ in range(min(n_steps, 300)):
210
+ fig, ax = plt.subplots(figsize=(4, 4))
211
+ ax.imshow(obs)
212
+ ax.set_title(f"Pos: {env.agent_pos}")
213
+ ax.axis('off')
214
+ images.append(fig_to_pil(fig))
215
+
216
+ obs_t = torch.FloatTensor(obs)
217
+ action, _, _, hidden = agent.act(obs_t, hidden)
218
+ obs, _, done, _ = env.step(action)
219
+ if done:
220
+ break
 
 
 
 
 
 
 
 
 
 
 
221
 
222
+ return images
223
+
224
+
225
+ # Предобучаем модель один раз при загрузке
226
+ print("🏗️ Обучение агента...")
227
+ run_simulation.agent = train_ppo(episodes=80)
228
+ run_simulation.agent.eval()
229
+ print("✅ Обучение завершено!")
230
+
231
 
232
+ # Интерфейс БЕЗ типизации возврата, БЕЗ Gallery
233
+ with gr.Blocks(title="Infinite Builder") as demo:
234
+ gr.Markdown("# 🌍 Бесконечный мир: PPO-агент")
235
+ slider = gr.Slider(50, 300, value=100, step=50, label="Шагов")
236
+ btn = gr.Button("▶️ Запустить")
237
+ output = gr.Gallery(label="Результат", columns=4)
238
+ btn.click(fn=run_simulation, inputs=[slider], outputs=[output])
239
 
240
  if __name__ == "__main__":
241
+ demo.launch(server_name="0.0.0.0", server_port=7860)