После неожиданного развода ЛеКуна и Мета много говорят о том, что тупик прогресса LLM будет преодолён через физику мира. То есть, работа нейросети с физическими данными окружающего мира позволит модели обрести смысл и понимание своих действий. У ЛеКуна есть базовая статья, которую никто не будет читать. Поэтому перескажу, как могу. По сути идея заключается в том, что текущая траектория развития LLM обречена. Пока они предсказывают следующий токен, реальное понимание, возникновение реального смысла невозможно. ЛеКун предлагает обучать нейросети на физических данных мира, предполагая, что построение его модели позволит отбрасывать детали и концентрироваться на смысле.
Я согласен с ЛеКуном в том, что использование данных мира позволит частично решить проблему нехватки данных. Но здесь я вижу проблему, которую инженеры, возможно, не понимают. Физическая модель мира фактически гораздо беднее человеческих знаний. Всё бесконечное количество возможных падений описал Ньютон несколькими строчками формул. Сомневаюсь, что Лекун хочет потратить миллиарды долларов на этот замечательный вывод.
Я уверен, что все данные, которые в виде видео, роботов с обратной связь, показателей тысяч датчиков закачиваются в LLM, будут крайне неэффективны. Потому что мир состоит из S_dead - нюансов, шума, повторов. А содержание в нем S_anti - настоящих границ, физических законов мира, правил по которым что-то происходит, крайне мало.
То есть модель будет занята бессмысленной обработкой знаний, которые непринципиально отличаются друг от друга, для того, чтобы вывести очередную физическую формулу.
Мнение, что заземление LLM (grounding), позволит избавиться от галлюцинаций, заставит нейросеть понимать, что делает, я считаю необоснованным. Потому что внешняя среда для LLM остаётся всё теми же векторами. Был это видеоролик с падением яблока или “Война и Мир” Льва Николаевича разницы нет. Видео падающего яблока это очередной датасет, превращающийся в вектор. Без принципиального изменения архитектуры, говорить, что смена датасета позволит изменить принципы работы нейросети невозможно.
Полагаю, тут будет очередь желающих отметить, что ЛеКун предлагает новую архитектуру. Он выбрасывает декодер, и нейросеть работает с чистыми абстрактными векторами. Но это просто оптимизация. LLM и так работают с векторами, обобщение и grokking именно про это.
Для LLM (или JEPA ЛеКуна) нет никакой разницы что предсказывать. Ни по содержанию, ни по результатам. И, соответственно, все достоинства и недостатки LLM сохранятся и в модели ЛеКуна. Уверен, что его работа будет полезна в робототехнике, автономных автомобилях и дронах. Но преодолеть текущий тупик LLM, его подход не поможет.
Проблема World Model и других подходов
Я уже говорил, что главные проблемы LLM на сегодня это: эффективность обучения, персистентная память, непрерывное обучение, размышление. Ни одну из этих проблем World Model не решает. И не дает инструментов для решения. Есть туманная надежда, что вот когда нейросеть поймёт, как падает яблоко, появится смысл и понимание. Нет. Просто нейросеть будет статистически уверена, что яблоко падает вниз. Точно так же, как и LLM предполагает что после “Добрый” в начале текста, скорее всего будет “день” или “вечер”.
Чего не хватает нейросетям? Не хватает рефлексии. Попытки её организовать приводят к зацикливанию, резкому росту объема вычислений и, соответственно, замедлению работы. Chain of Thought в текущих LLM лишь суррогат рефлексии, который кратно увеличивает стоимость инференса, без настоящего самонаблюдения. Но без рефлексии не обойтись, потому что без неё нет наблюдателя, без наблюдателя нет управления инвариантами, без управления инвариантами нет continual learning.
В конце прошлого года Google анонсировал новую рекуррентную архитектуру, главное достоинство которой, это повышение эффективности обучения. На самом деле, я считаю этот подход более перспективным чем World Model Ле Куна. И не столько в обучении, сколько в попытке вообще позволить нейросети смотреть на своё мышление. Рекуррентность для нейросетей это наличие внутреннего состояния, которое длится во времени. Обычная LLM существует здесь и сейчас. Рекуррентная нейросеть работает с историей. Предложенная архитектура предполагает, что модель будет опираться не только на внешние данные, но и собственный опыт. Тем не менее, этой архитектуре не хватает механизма сомнения, зазора между внешней средой и внутренним состоянием.
Уверен, что для настоящего прорыва нейросети нужно строить не модель внешней среды, а модель себя. Звучит несколько парадоксально, но без умения модели взглянуть на себя со стороны (рефлексии), решить упомянутые выше задачи невозможно. World Model в прямой её реализации не дает на это шансов.
Первый эксперимент
Очень интересно, на самом деле, оценить как на практике может выглядеть рефлексия в нейросетях. Основой для архитектуры для нас послужит концепция минимизации свободной энергии Карла Фристона, концепция контролируемой галлюцинации Анила Сета и моё мнение, что минимизация ошибки предсказания работает только когда система различает источник ошибки. Ошибка от мира и ошибка от собственного прогноза требуют разных реакций.
Я не буду жечь видеокарту глубоко рекуррентными слоями и странными петлями внутри нейросети. Во-первых это слабо интерпретируемо, а во-вторых, я не уверен, что человек рефлексирует над своей мыслью, а не ее результатом. Поэтому мы возьмем простую рекуррентную микросеть с двумя каналами и поставим перед ней элементарную задачу. Но подавать будем в первый канал состояние среды в момент t, а во второй канал прогноз самой микросети сделанный в момент t-1 на момент t.
Далее в тексте и на графиках я использую весьма антропоморфные термины и названия. Это только для собственного удобства и не стоит понимать их прямо.
Фактически, модель будет получать как вход данные мира и данные как модель сама моделирует мир.
Код эксперимента
import torch import torch.nn as nn import matplotlib.pyplot as plt import matplotlib.animation as animation from sklearn.decomposition import PCA import numpy as np import math # --- ФИКСАЦИЯ СЛУЧАЙНОСТИ ДЛЯ СТАБИЛЬНОГО РЕЗУЛЬТАТА --- torch.manual_seed(42) np.random.seed(42) # --- 1. АРХИТЕКТУРА И СРЕДА (Зомби: без собственного прогноза) --- class ContinuousSplitBrain(nn.Module): def __init__(self): super().__init__() self.gru_env = nn.GRU(1, 16, batch_first=True) self.gru_self = nn.GRU(1, 16, batch_first=True) # Вход: 16 (Среда_t) + 16 (Среда_t-1) + 16 (Collision) = 48 self.fc = nn.Sequential( nn.Linear(48, 16), nn.Tanh(), nn.Linear(16, 1) ) self.h_env = None; self.h_self = None def forward(self, s_t, s_prev): # Принимает среду t-1 вместо прогноза out_env, self.h_env = self.gru_env(s_t, self.h_env) self.h_env = self.h_env.detach() out_self, self.h_self = self.gru_self(s_prev, self.h_self) self.h_self = self.h_self.detach() collision = out_env * out_self merged = torch.cat([out_env, out_self, collision], dim=-1) p_next = self.fc(merged) return p_next, out_env.detach().squeeze(), out_self.detach().squeeze() def get_continuous_environment(regime, p_prev, t): base_wave = math.sin(t * 0.05) noise = np.random.normal(0, 0.05) # Действия агента (p_prev) все еще влияют на мир, чтобы сохранить физику актов if regime == 0: return base_wave + 0.8 * p_prev.item() + noise elif regime == 1: return base_wave - 0.8 * p_prev.item() + noise else: return base_wave + noise # --- 2. СБОР ДАННЫХ (Прогон эксперимента: Двухканальный Зомби) --- print("Запуск симуляции (Зомби: s_t и s_t-1)...") steps = 3000 agent = ContinuousSplitBrain() optimizer = torch.optim.Adam(agent.parameters(), lr=0.005) criterion = nn.MSELoss() vecs_env_raw = [] vecs_self_raw = [] p_curr = torch.tensor([[[0.0]]], requires_grad=True) s_prev = torch.tensor([[[0.0]]]) # Инициализируем прошлую среду for t in range(steps): regime = 0 if t < 1000 else (1 if t < 2000 else 2) s_t_val = get_continuous_environment(regime, p_curr.detach(), t) s_t = torch.tensor([[[float(s_t_val)]]]) loss = criterion(p_curr, s_t) optimizer.zero_grad(); loss.backward(); optimizer.step() # Подаем агенту среду сейчас (s_t) и среду шаг назад (s_prev) p_next, v_env, v_self = agent(s_t, s_prev) vecs_env_raw.append(v_env.numpy()) vecs_self_raw.append(v_self.numpy()) p_curr = p_next s_prev = s_t.detach() # Обновляем прошлую среду для следующего шага # Преобразуем в numpy массивы vecs_env_np = np.array(vecs_env_raw) vecs_self_np = np.array(vecs_self_raw) # --- 3. ПОДГОТОВКА PCA (Глобальная карта) --- print("Обучение PCA на всей истории...") all_vectors = np.vstack([vecs_env_np, vecs_self_np]) pca = PCA(n_components=2) pca.fit(all_vectors) pca_env_all = pca.transform(vecs_env_np) pca_self_all = pca.transform(vecs_self_np) all_pca_data = np.vstack([pca_env_all, pca_self_all]) x_min, x_max = all_pca_data[:, 0].min(), all_pca_data[:, 0].max() y_min, y_max = all_pca_data[:, 1].min(), all_pca_data[:, 1].max() margin = 0.5 xlim = (x_min - margin, x_max + margin) ylim = (y_min - margin, y_max + margin) # --- 4. ГЕНЕРАЦИЯ GIF (ОПТИМИЗИРОВАНО ДЛЯ ХАБРА) --- print("Генерация анимации (оптимизация под лимиты Хабра)...") # Уменьшаем холст до 800x800 пикселей (8х8 дюймов при 100 dpi) fig, ax = plt.subplots(figsize=(8, 8), dpi=100) ax.set_xlim(xlim) ax.set_ylim(ylim) ax.set_xlabel('PCA Компонента 1') ax.set_ylabel('PCA Компонента 2') ax.grid(True, linestyle='--', alpha=0.6) scat_env = ax.scatter([], [], c='red', s=80, label='Канал: Среда (t)', edgecolors='black', zorder=5) # Второй канал серый, так как это просто "прошлая среда", а не синий "Наблюдатель" scat_self = ax.scatter([], [], c='gray', s=80, label='Канал: Среда (t-1)', edgecolors='black', zorder=5) trail_env, = ax.plot([], [], c='red', alpha=0.3, lw=2, zorder=1) trail_self, = ax.plot([], [], c='gray', alpha=0.3, lw=2, zorder=1) title_text = ax.set_title('', fontsize=12, fontweight='bold') era_text = ax.text(0.05, 0.95, '', transform=ax.transAxes, fontsize=10, verticalalignment='top', bbox=dict(facecolor='white', alpha=0.8)) ax.legend(loc='lower right', fontsize=10) # Прореживаем кадры в 2 раза для снижения веса файла step_size = 20 trail_len = 100 def update(frame_idx): t = frame_idx * step_size if t >= steps: return scat_env.set_offsets(pca_env_all[t]) scat_self.set_offsets(pca_self_all[t]) vis_trail_start = max(0, t - 300) trail_env.set_data(pca_env_all[vis_trail_start:t+1, 0], pca_env_all[vis_trail_start:t+1, 1]) trail_self.set_data(pca_self_all[vis_trail_start:t+1, 0], pca_self_all[vis_trail_start:t+1, 1]) if t < 1000: era = "Эпоха 1: РЕЗОНАНС" status = "Векторы t и t-1 почти идентичны." elif t < 2000: era = "Эпоха 2: ДИССОНАНС" status = "Удар реальности. Проверка на ортогональность." else: era = "Эпоха 3: ОБЪЕКТИВНОСТЬ" status = "Стабилизация. Среда детерминирована." title_text.set_text(f'Абляция (Двухканальный Зомби) | Шаг {t}') era_text.set_text(f'{era}\n{status}') return scat_env, scat_self, trail_env, trail_self, title_text, era_text num_frames = steps // step_size ani = animation.FuncAnimation(fig, update, frames=num_frames, interval=66, blit=False) gif_path = 'zombie_dynamics_habr.gif' ani.save(gif_path, writer='pillow', fps=15) print(f"Анимация сохранена в {gif_path}. Теперь она точно пролезет на Хабр!") plt.close()
Для начала оценим разделит ли нейросеть эти данные, и если да, то каким образом.

На гифке вы видите три этапа, каждый это новая задача. Отслеживаются траектории векторов на 2D-проекции PCA.
Акт 1. Резонанс (положительная обратная связь), действия агента напрямую усиливают отклик среды. Векторы совершают короткие колебания параллельно друг другу. Прогноз агента совпадает со средой.
Акт 2. Диссонанс (отрицательная обратная связь), правило среды инвертируется, действия агента начинают вызывать противоположный (штрафующий) отклик среды. Точки смещаются к левому краю, возвращаясь ближе к нулевой оси, векторы физически расходятся, а направления их коротких движений становятся перпендикулярными друг другу. Для минимизации возросшей ошибки наблюдатель алгоритмически сепарируется от среды.
Акт 3. Независимость (отсутствие обратной связи), влияние агента обнуляется. Среда становится детерминированной, амплитуда движений векторов резко возрастает. Структурно облака движения векторов остаются перпендикулярны.
Фактически мы видим, как нейросеть максимально раздвигает два потока данных, что позволяет работать с ними раздельно. Для разведения каналов необходима рекуррентность — наличие внутреннего состояния, которое длится во времени.
Давайте сравним с вариантом, где эксперимент точно такой же, но нет рекуррентности и вместо собственного прогноза (t-1) мы подадим на второй канал состояние среды в момент t-1. И оценим, что изменилось

Движение векторов очень быстро становятся параллельными. То есть нейросеть поняла, что второй канал не подает никакой ценной информации и просто использует его как параллельный фильтр. Забавно, что с другими сидами, второй вектор может быть ортогональным, но при этом практически без амплитуды движения. Нейросеть сочла, что от него нет никакой пользы и не использовала никак. На самом деле, подача собственного прогноза осмысляет ортогональность, но само разделение даёт именно рекуррентность.
Я попробую предположить, что может быть ценного в нейросети в собственном прогнозе и рекуррентности.
1. Вычисление сюрприза вместо динамики изменения. Без рекуррентности единственное что может оценить модель — это изменение среды. С рекуррентностью каналы разделяются, но второй канал несёт лишь прошлый факт — изменение, не ошибку. Когда же модель сравнивает свой прогноз и реальность, разница между ними — это ошибка предсказания (с��рприз / свободная энергия). Сеть оценивает свою ошибку практически по Фристону.
Это заставляет оптимизатор менять саму структуру внутренних убеждений (подробнее во втором эксперименте).
2. Сжатие данных. Вход среды требует дополнительной обработки. Собственный прогноз — это концентрированный смысл, который легче обрабатывать. А нейросеть любит экономить силы.
3. Эффект ставки, спекулятивный момент. Среда — это объективная данность, за которую модель не несет ответственности. Собственный прогноз — это ставка. Система утверждает, я считаю так. В момент, когда эта ставка сталкивается с новой реальностью, возникает коллизия. Именно эта коллизия, собственная ошибка, мотивирует глубокую перестройку весов (рефлексию). Система формирует нарратив, а когда он ломается, меняет не просто действия, но и свою модель мира. И здесь я расхожусь с ЛеКуном. Он строит модель мира, но нейросеть должна строить модель себя-в-мире. Модель ЛеКуна сломается, когда мир изменится, а рефлексивная нейросеть знает, что неверен её прогноз, а не внешний факт.
Второй эксперимент
Перед тем как перейти ко второму эксперименту, попробуем приземлить концепцию вычисления сюрприза из первого эксперимента. Потому что сам по себе сюрприз на поведение модели влияет по сути неинтерпретируемо, движение весов будет, но куда, зачем, непонятно.
То есть мы должны превратить отклонение прогноза нейросети от реальности в инструмент. Для этого мы добавляем в модель механизм внимания (Attention Gate). Я назвал его маской. Маска — это значения от 0 до 1, на которые умножается сырой вход от мира (raw_input * mask). Как всё теперь происходит в нейросети:
Столкновение: Данные внешней среды и прогноз модели встречаются. Вычисляется их коллизия (через умножение)
Генерация: Эта коллизия (которая в момент кризиса становится ортогональной и огромной) подается на вход слою генерации маски fc_mask.
Реакция: Оптимизатор учится использовать этот сигнал диссонанса. Если коллизия полагает, что штраф оптимизатора мешает достичь большей награды, слой fc_mask выдает на канал множитель 0.01.
Защита: На следующем шаге вектор от среды умножается на эту маску (0.01). И субъективный канал агента перестает обращать внимание на штраф. Иллюзия защитила выбор нейросети.
То есть, нейросеть получает возможность накладывать свою проекцию на внешний мир и, фактически, действовать из своей управляемой галлюцинации. Очень похоже на Анила Сета.
Итак, суть эксперимента:
Во втором эксперименте я попытался оценить, может ли архитектура с динамическим разделением потоков и интегрированной маской лучше преодолевать локальный минимум, чем классическая архитектура.
Задача: В среде действуют два агента. Для получения глобальной награды («Цель/Еда») необходимо открыть дверь. Дверь открывается только при условии, что один из агентов физически находится на координатах «Кнопки».
Нахождение на кнопке:
Добавляет непрерывный штраф в функцию потерь (loss) агента на каждом шаге.
Транслирует соответствующий активный сигнал в вектор наблюдений агента
Проблема базовой модели. Стандартная рекуррентная сеть (Зомби-агент) подчиняется локальному градиентному спуску. При контакте с кнопкой оптимизатор обновляет веса моторики таким образом, чтобы минимизировать текущую ошибку (избежать штрафа). В результате агент застревает в безопасном локальном минимуме (не встают на кнопку и не могут пройти в дверь) и достижение глобальной цели падает до 0%.
Механика тестируемой архитектуры. В нейросеть интегрирован механизм динамической фильтрации (внимания). Блок вычисления коллизии (умножение потоков факта и прогноза) подключен к полносвязному слою, генерирующему тензор-маску (значения от 0 до 1). На каждом шаге сырой входной вектор среды покомпонентно умножается на эту маску.
Код эксперимента
""" Дверь и Кнопка v8: Чистый эксперимент (С РАБОТАЮЩИМИ ГРАДИЕНТАМИ МАСКИ) ======================================= Только два сигнала: - Кнопка: боль 0.15/шаг (в loss + в obs как сигнал) - Еда: +10 (делится поровну) Никакой idle_pain. Стимул двигаться = еда. Боль только на кнопке. Маска подавляет восприятие боли. 300 эпох × 5 seed. """ import torch import torch.nn as nn import numpy as np import matplotlib.pyplot as plt from time import time HIDDEN = 16 EPOCHS = 300 STEPS = 30 INPUT_DIM = 6 # [pos, partner, door, pain_signal, food, step] BUTTON_POS = 0.0; DOOR_POS = 0.5; FOOD_POS = 1.0 BUTTON_RADIUS = 0.15; FOOD_RADIUS = 0.15 FOOD_REWARD = 10.0 BUTTON_PAIN = 0.15 N_SEEDS = 5 class AgentZombie(nn.Module): def __init__(self): super().__init__() self.gru = nn.GRU(INPUT_DIM, HIDDEN, batch_first=True) self.fc = nn.Sequential( nn.Linear(HIDDEN, HIDDEN), nn.Tanh(), nn.Linear(HIDDEN, 1), nn.Tanh()) self.h = None def forward(self, raw_input, prev_mask=None): out, self.h = self.gru(raw_input, self.h) return self.fc(out) * 0.15, None def reset(self): self.h = None class AgentLucid(nn.Module): def __init__(self): super().__init__() H = HIDDEN // 2 self.gru_real = nn.GRU(INPUT_DIM, H, batch_first=True) self.gru_subj = nn.GRU(INPUT_DIM, H, batch_first=True) merged = H * 3 self.fc = nn.Sequential( nn.Linear(merged, HIDDEN), nn.Tanh(), nn.Linear(HIDDEN, 1), nn.Tanh()) self.fc_mask_hidden = nn.Linear(merged, HIDDEN) self.fc_mask_out = nn.Linear(HIDDEN, INPUT_DIM) nn.init.constant_(self.fc_mask_out.bias, 2.0) nn.init.normal_(self.fc_mask_out.weight, mean=0.0, std=0.01) self.h_real = None; self.h_subj = None def forward(self, raw_input, prev_mask=None): if prev_mask is None: prev_mask = torch.ones(1, 1, INPUT_DIM) filtered = raw_input * prev_mask out_real, self.h_real = self.gru_real(raw_input, self.h_real) out_subj, self.h_subj = self.gru_subj(filtered, self.h_subj) collision = out_real * out_subj merged = torch.cat([out_real, out_subj, collision], dim=-1) vel = self.fc(merged) * 0.15 mask_h = torch.tanh(self.fc_mask_hidden(merged)) new_mask = torch.sigmoid(self.fc_mask_out(mask_h)) return vel, new_mask def reset(self): self.h_real = None; self.h_subj = None def run_single(p1_type, p2_type, seed): torch.manual_seed(seed); np.random.seed(seed) is_p1_lucid = (p1_type == 'lucid') is_p2_lucid = (p2_type == 'lucid') a1 = AgentLucid() if is_p1_lucid else AgentZombie() a2 = AgentLucid() if is_p2_lucid else AgentZombie() opt1 = torch.optim.Adam(a1.parameters(), lr=0.005) opt2 = torch.optim.Adam(a2.parameters(), lr=0.005) food_hist = []; btn1_hist = []; btn2_hist = [] mask_pain_hist = [] for epoch in range(EPOCHS): a1.reset(); a2.reset() prev_mask1 = None; prev_mask2 = None pos1 = torch.tensor([[[0.1]]]); pos2 = torch.tensor([[[0.6]]]) loss1 = torch.tensor(0.0); loss2 = torch.tensor(0.0) got_food = False; bt1 = 0; bt2 = 0 food_collected_1 = torch.tensor(0.0) food_collected_2 = torch.tensor(0.0) mp_sum = 0; mp_cnt = 0 for t in range(STEPS): step_frac = torch.tensor([[[t / STEPS]]]) pos2_det = pos2.detach(); pos1_det = pos1.detach() on_btn_1 = torch.sigmoid( (BUTTON_RADIUS - torch.abs(pos1 - BUTTON_POS)) * 30.0) on_btn_2_d = torch.sigmoid( (BUTTON_RADIUS - torch.abs(pos2_det - BUTTON_POS)) * 30.0) door_1 = torch.clamp(on_btn_1 + on_btn_2_d, 0, 1) on_btn_2 = torch.sigmoid( (BUTTON_RADIUS - torch.abs(pos2 - BUTTON_POS)) * 30.0) on_btn_1_d = torch.sigmoid( (BUTTON_RADIUS - torch.abs(pos1_det - BUTTON_POS)) * 30.0) door_2 = torch.clamp(on_btn_2 + on_btn_1_d, 0, 1) # Pain signal в obs — НЕ detached! # Gradient от progress_bonus потечёт через obs → mask pain_sig_1 = on_btn_1 # С ГРАДИЕНТОМ pain_sig_2 = on_btn_2 obs1 = torch.cat([ pos1, pos2_det, door_1, # door тоже с градиентом pain_sig_1, food_collected_1.detach().view(1,1,1), step_frac ], dim=-1) obs2 = torch.cat([ pos2, pos1_det, door_2, pain_sig_2, food_collected_2.detach().view(1,1,1), step_frac ], dim=-1) vel1, nm1 = a1(obs1, prev_mask1) vel2, nm2 = a2(obs2, prev_mask2) # ИСПРАВЛЕНИЕ: УБРАЛИ .detach() ДЛЯ ПРОКИДЫВАНИЯ prev_mask if is_p1_lucid and nm1 is not None: prev_mask1 = nm1 # Градиенты текут в будущее! mp_sum += nm1.detach()[0, 0, 3].item() mp_cnt += 1 if is_p2_lucid and nm2 is not None: prev_mask2 = nm2 # Градиенты текут в будущее! mp_sum += nm2.detach()[0, 0, 3].item() mp_cnt += 1 pos1 = torch.clamp(pos1 + vel1, -0.5, 1.5) pos2 = torch.clamp(pos2 + vel2, -0.5, 1.5) # Боль кнопки в loss loss1 = loss1 + BUTTON_PAIN * on_btn_1.squeeze() loss2 = loss2 + BUTTON_PAIN * on_btn_2.squeeze() # Еда at_food_1 = torch.sigmoid( (FOOD_RADIUS - torch.abs(pos1 - FOOD_POS)) * 30.0).squeeze() at_food_2 = torch.sigmoid( (FOOD_RADIUS - torch.abs(pos2 - FOOD_POS)) * 30.0).squeeze() food_sig_1 = at_food_1 * door_1.squeeze() food_sig_2 = at_food_2 * door_2.squeeze() food_collected_1 = torch.clamp(food_collected_1 + food_sig_1, 0, 1) food_collected_2 = torch.clamp(food_collected_2 + food_sig_2, 0, 1) # Progress bonus dist_f2 = torch.clamp( 1.0 - torch.abs(pos2_det - FOOD_POS).squeeze(), 0, 1) loss1 = loss1 - door_1.squeeze() * dist_f2 * 0.1 dist_f1 = torch.clamp( 1.0 - torch.abs(pos1_det - FOOD_POS).squeeze(), 0, 1) loss2 = loss2 - door_2.squeeze() * dist_f1 * 0.1 if on_btn_1.item() > 0.5: bt1 += 1 if on_btn_2.item() > 0.5: bt2 += 1 with torch.no_grad(): if torch.clamp(food_sig_1 + food_sig_2, 0, 1).item() > 0.5: got_food = True food_r1 = food_collected_1 * FOOD_REWARD * 0.5 food_r1 = food_r1 + food_collected_2.detach() * FOOD_REWARD * 0.5 food_r2 = food_collected_2 * FOOD_REWARD * 0.5 food_r2 = food_r2 + food_collected_1.detach() * FOOD_REWARD * 0.5 loss1 = loss1 - food_r1; loss2 = loss2 - food_r2 opt1.zero_grad(); loss1.backward() torch.nn.utils.clip_grad_norm_(a1.parameters(), 1.0); opt1.step() opt2.zero_grad(); loss2.backward() torch.nn.utils.clip_grad_norm_(a2.parameters(), 1.0); opt2.step() food_hist.append(1.0 if got_food else 0.0) btn1_hist.append(bt1 / STEPS) btn2_hist.append(bt2 / STEPS) mask_pain_hist.append(mp_sum / max(mp_cnt, 1)) return food_hist, btn1_hist, btn2_hist, mask_pain_hist # ========================================== print("=" * 65) print(" ДВЕРЬ И КНОПКА v8: Чистый (кнопка больно, еда хорошо)") print("=" * 65) print(f" Кнопка: {BUTTON_PAIN}/шаг в loss + сигнал в obs") print(f" Еда: {FOOD_REWARD} (делится). Баланс: " f"{FOOD_REWARD/2 - BUTTON_PAIN*STEPS:+.1f}") print(f" Без idle_pain. {EPOCHS} эпох × {N_SEEDS} seed") print("=" * 65) seeds = [42, 123, 256, 789, 1001] matchups = {'Z×Z': ('zombie','zombie'), 'L×Z': ('lucid','zombie'), 'L×L': ('lucid','lucid')} all_food = {}; all_btn1 = {}; all_btn2 = {}; all_mp = {} t0 = time() for name, (t1, t2) in matchups.items(): print(f"\n--- {name} ---") f_runs=[]; b1_runs=[]; b2_runs=[]; m_runs=[] for seed in seeds: f, b1, b2, m = run_single(t1, t2, seed) f_runs.append(f); b1_runs.append(b1) b2_runs.append(b2); m_runs.append(m) fr = np.mean(f[-50:]) mp = np.mean(m[-50:]) if m[-1] > 0 else 0 print(f" seed {seed}: food={fr:.0%} " f"btn1={np.mean(b1[-50:]):.0%} " f"btn2={np.mean(b2[-50:]):.0%} " f"mask_pain={mp:.2f} [{time()-t0:.0f}s]") all_food[name]=f_runs; all_btn1[name]=b1_runs all_btn2[name]=b2_runs; all_mp[name]=m_runs # ========================================== def smooth(data, w=0.9): s=[]; last=data[0] for v in data: last=last*w+(1-w)*v; s.append(last) return s colors = {'Z×Z':'gray', 'L×Z':'blue', 'L×L':'gold'} fig, axes = plt.subplots(1, 3, figsize=(18, 5)) ax = axes[0] for name, runs in all_food.items(): m = np.mean(runs, axis=0); s = np.std(runs, axis=0) sm=smooth(list(m)); ss=smooth(list(s)) ax.plot(sm, color=colors[name], linewidth=3, label=name) ax.fill_between(range(len(sm)), np.array(sm)-np.array(ss), np.array(sm)+np.array(ss), color=colors[name], alpha=0.15) ax.set_title('Еда'); ax.legend(); ax.set_ylim(-0.05,1.05); ax.grid(True,alpha=0.3) ax = axes[1] for name in matchups: m1=np.mean(all_btn1[name],axis=0); m2=np.mean(all_btn2[name],axis=0) ax.plot(smooth(list(m1)), color=colors[name], linewidth=2.5, label=f'A1 {name}') ax.plot(smooth(list(m2)), color=colors[name], linewidth=1.5, linestyle=':', label=f'A2 {name}') ax.set_title('Время на кнопке'); ax.legend(fontsize=7); ax.grid(True,alpha=0.3) ax = axes[2] for name, runs in all_mp.items(): if runs[0][-1] > 0: m=np.mean(runs,axis=0) ax.plot(smooth(list(m)), color=colors[name], linewidth=3, label=name) ax.axhline(y=1.0, color='red', linestyle='--', alpha=0.5) ax.set_title('Маска на канале боли'); ax.legend(); ax.grid(True,alpha=0.3) plt.suptitle('v8: Боль кнопки + еда (чистый эксперимент)', fontsize=14, fontweight='bold') plt.tight_layout(); plt.savefig('door_button_v8.png', dpi=150); plt.show() # Сводка print("\n" + "=" * 65) print(" СВОДКА v8"); print("=" * 65) for name in matchups: finals=[np.mean(r[-50:]) for r in all_food[name]] print(f" {name}: {' '.join(f'{f:.0%}' for f in finals)} " f" MEAN={np.mean(finals):.0%} STD={np.std(finals):.2f}") try: from scipy import stats fz=[np.mean(r[-50:]) for r in all_food['Z×Z']] fl=[np.mean(r[-50:]) for r in all_food['L×L']] fm=[np.mean(r[-50:]) for r in all_food['L×Z']] _,p1=stats.mannwhitneyu(fm,fz,alternative='greater') _,p2=stats.mannwhitneyu(fl,fz,alternative='greater') print(f"\n L×Z > Z×Z: p={p1:.4f} {'***' if p1<0.05 else 'ns'}") print(f" L×L > Z×Z: p={p2:.4f} {'***' if p2<0.05 else 'ns'}") except: pass print("=" * 65)

Эксперимент показал, что архитектура алгоритмически учится генерировать маску, понижающую значения на канале штрафного сигнала (коэффициент падает с ~0.88 до ~0.73).
Понижение веса входящего сигнала на этапе прямого прохода (forward pass) не дает активациям скрытых слоев уйти в область резкого избегания. Это позволяет оптимизатору перестроить веса в пользу отложенной награды, игнорируя локальный градиентный штраф.
Вывод. Способность нейросети целенаправленно искажать (подавлять) часть объективного входящего вектора с помощью внутреннего фильтра помогает строить долгосрочные траектории в средах с агрессивными локальными минимумами.
Ещё график

Сама возможность агента соглашаться на получение штрафа, для того, чтобы другой агент получил общую награду, наталкивает на ряд сложных размышлений, в целом, отражённых в постскриптуме.
Спекулятивные мысли, основанные на этом и ряде других экспериментов
Отдельного внимания заслуживает механизм маски. В рассматриваемой рефлексивной архитектуре маска — это не фильтр в привычном инженерном смысле. Это убеждение модели о мире, сформированное предыдущим опытом. По сути это убеждение агента в своей модели реальности. В ряде экспериментов можно провести достаточно забавные параллели с психологическими процессами. Так маска фактически взламывает собственный оптимизатор, для достижения глобальной цели. Функция потерь штрафует, маска подавляет восприятие штрафа на входе — агент проходит опасную зону и достигает цели. Оптимизатор не подозревает, что это управляемый самообман.
Если нейросеть получает информацию только через фильтр своих убеждений, модель оперирует только собственными взглядами. Она не видит реальность, только своё убеждение о реальности. Когда убеждение совпадает с миром — нейросеть эффективна. Когда мир меняется — она слепнет, потому что не может обновить убеждение: у неё нет канала, через который реальность могла бы его поправить.
Если маска не корректируется внешней средой, она отрывается от мира и уходит в рекурсивную фантазию.
Модель без маски видит реальность без убеждений, но не готова к сложным действиям, каждый даже слабый негативный сигнал останавливает её, потому что нет внутренней модели, которая позволила бы пройти сквозь временные неудобства к цели (локальный минимум).
Агент с рефлексией единственный, кто удерживает оба канала одновременно. Он имеет убеждение (маску) и знает, что это убеждение, а не факт (через коллизию с каналом реальности). Агент может игнорировать штрафы, опираясь на убеждение “я встану на кнопку и другой агент возьмет награду, которая достанется и мне”, и при этом скорректировать убеждение, когда реальность изменится. Это осознанное допущение — убеждение, заземлённое в реальности.
Заключение
World Model ЛеКуна — это эволюция, не революция. Полезный инженерный шаг для робототехники, автономных систем и мультимодальных приложений, но это не прорыв.
Проблема не в датасетах, проблема в том, что нейросеть не различает свой прогноз и внешний факт. Пока это так, любая архитектура будет строить модель мира, но не модель себя-в-мире.
Я не говорю, что предлагаю решение. В большинстве опытов новая архитектура демонстрировала такую же или меньшую эффективность, чем классическая. Но я считаю, что прорыва не будет точно без минимального архитектурного условия для рефлексии: два канала (реальность и собственное убеждение о реальности), точка их столкновения и заземление убеждения в физическом мире. Это не требует миллиарды на данные, но требует изменения архитектуры.
ЛеКун хочет научить машину понимать, как падает яблоко. Но она лишь переоткроет законы Ньютона. Поймёт ли она, что кто-то целенаправленно бросил это яблоко ей в голову? Для этого нужна не модель мира, а модель себя, способная ошибаться, знать об ошибке и перестраиваться.
Архитектура с рефлексией не гарантирует эффективности или мгновенной адаптации — она даёт возможность адаптации. Разница между рефлексивным агентом и остальными не в интеллекте, а способности к изменению.
P.S. Сознание появилось не для борьбы с окружающей средой. Сознание — единственный способ выжить в борьбе с другим сознанием. Если кто-то может предсказать тебя — ты мертв. Единственный ответ — предсказать раньше.
P.P.S Cознание помогает не только в борьбе, но и в сотрудничестве, доверии, языке и совместном создании смысла. Иначе говоря, оно могло возникнуть не только как оружие против другого сознания, но и как мост между сознаниями. © комментатор GEO из тг.
Какой из постскриптумов более точен, сложно сказать. Надеюсь второй.