# Пишем ИИ для Змейки на чистом Си

Источник: https://www.youtube.com/watch?v=R5fPn6W8m6k
Канал: freeCodeCamp.org
Опубликовано: 19.08.2026

---

Обучение нейросетей с нуля на чистом Си без единой внешней библиотеки — задача, от которой у большинства разработчиков сдадут нервы, но Харш Бхатт доказал её выполнимость на примере классической «Змейки». Создав собственный движок автодифференцирования, эффективный арена-аллокатор памяти и алгоритм REINFORCE прямо в procedural-парадигме, он наглядно показал, как устроены ML-фреймворки изнутри. Это хардкорное погружение в инженерию искусственного интеллекта без магии высокоуровневых абстракций.

## 🛠️ Фундамент RL-библиотеки на Си: архитектура и управление памятью
[[JUMP:00:00]]

### Обзор проекта: RL-библиотека на Си с нуля
[[JUMP:00:00]]

В современном программировании создание нейросетей обычно ассоциируется с высокоуровневыми фреймворками, такими как PyTorch или TensorFlow, однако Харш Бхатт (Harsh Bhatt) предлагает радикально иной подход. Цель данного курса — построить полноценную библиотеку для обучения с подкреплением (Reinforcement Learning) на чистом языке Си, не используя сторонние ML-библиотеки [0:12]. Основной фокус направлен на реализацию алгоритма градиента политики REINFORCE [1:05]. Чтобы доказать работоспособность системы, автор планирует не просто написать математическое ядро, но и реализовать с нуля окружение классической игры «Змейка», где агент будет учиться навигации и поиску пищи в режиме реального времени [0:26].

Харш Бхатт (Harsh Bhatt) подчёркивает, что для понимания курса желательно иметь базовое представление о цикле взаимодействия агента со средой: как совершаются действия, как среда возвращает награды и каким образом происходит обучение на основе обратной связи [0:39]. Проект разделен на несколько фундаментальных блоков: создание движка автоматического дифференцирования (autograd), разработку модели нейросети, проектирование игрового окружения и реализацию самого алгоритма обучения [1:20]. Вдохновением для проекта послужили работы других разработчиков в области низкоуровневых ML-библиотек, что подталкивает к исследованию того, как именно нейронные сети функционируют «под капотом» на уровне указателей и структур данных [2:14].

### Проектирование движка автоградиентных вычислений
[[JUMP:02:27]]

Сердцем любой современной библиотеки глубокого обучения является механизм автоматического дифференцирования. Харш Бхатт (Harsh Bhatt) начинает проектирование с этого фундаментального элемента, так как обучение нейросети требует прохождения вычислительного графа в двух направлениях: прямом (forward pass) для получения предсказания и обратном (backward pass) для вычисления градиентов [3:08]. Этот процесс позволяет автоматически определять, как изменение каждого веса влияет на итоговую ошибку, что и называется автодифференцированием [3:36].

Основной единицей данных в этой системе выступает структура `var` (переменная). Она спроектирована как узел графа и содержит следующие элементы:

*   Уникальный индекс (ID) для идентификации в модели [4:02].
*   Массив входных данных (inputs), так как одна переменная может быть результатом операции над другими [4:14]. Например, если $C = A + B$, то $C$ является переменной, чьими входами являются $A$ и $B$ [4:26].
*   Флаги управления (flags), определяющие свойства переменной [4:40].
*   Две матрицы: одна для хранения текущего значения (value), другая для градиента (gradient) [5:35].

Для описания операций вводится структура `VarType`, которая хранит указатели на функции прямого и обратного прохода, имя операции и параметры её формы (shape) [7:41]. Харш Бхатт (Harsh Bhatt) уточняет, что такие операции, как ReLU или Softmax, требуют только одного входа, в то время как сложение или умножение матриц — минимум двух [5:10]. Это разделение на типы переменных позволяет движку гибко выстраивать цепочки вычислений любой сложности [8:11].

### Управление памятью и структура мем-арены
[[JUMP:10:00]]

Одной из самых сложных задач при написании ML-библиотеки на Си является эффективное выделение памяти под тысячи узлов графа и тензоров. Чтобы избежать фрагментации и накладных расходов от стандартного `malloc`, Харш Бхатт (Harsh Bhatt) использует концепцию «арены памяти» (Memory Arena), подключая заголовочный файл `arena.h` [10:14]. Арена представляет собой заранее выделенный большой буфер памяти, из которого последовательно нарезаются блоки для нужд приложения [10:27].

Для управления глобальным состоянием системы создается структура `Model`, которая интегрируется с ареной. В этой структуре хранятся:

1.  Текущее количество созданных переменных (`number_of_vars`) [11:58].
2.  Значения преимуществ (advantages), необходимые для алгоритмов RL [11:30].
3.  Указатель на саму арену для аллокации новых узлов.

При создании новой матрицы внутри графа используется функция `push_array`, которая просто сдвигает указатель внутри арены на нужный размер (количество строк умноженное на количество столбцов) [17:26]. Это гарантирует, что все данные графа лежат в памяти компактно. При инициализации переменной через `create_var` автор также вводит систему флагов, например `requires_grad` [19:19]. Если флаг установлен, система автоматически выделяет память под матрицу градиентов; если нет — экономит ресурс, создавая только матрицу значений [20:17]. Каждая новая переменная автоматически регистрируется в состоянии модели, инкрементируя общий счетчик, что позволяет легко отслеживать размер вычислительного графа в реальном времени [23:29]. В завершение Харш Бхатт (Harsh Bhatt) подготавливает структуру самого графа, которая будет содержать массив указателей на все задействованные переменные [25:01].

## 🏗️ Проектирование узлов и механизмы формирования графа

[[JUMP:25:01]]

На данном этапе Харш Бхатт (Harsh Bhatt) переходит от базовых структур данных к архитектуре вычислительного графа, который станет «сердцем» нейросетевого движка. Главная задача здесь — обеспечить связность операций и автоматизировать проверку математической корректности вычислений еще на этапе сборки модели [25:14].

### Создание узлов и логика вычислительных операций
[[JUMP:25:01]]

Основой графа является узел (node), который Харш Бхатт определяет как структуру, инкапсулирующую переменную и её связи [26:26]. В отличие от простых массивов данных, узел в автоградиентном движке должен «знать», из каких входных компонентов он был получен. Функция `create_node` принимает ссылки на две родительские переменные — A и B [29:17]. Это позволяет движку восстанавливать цепочку вычислений в обратном направлении. 

Внутри реализации узла Харш Бхатт выделяет несколько критических компонентов:

*   **Валидация размерностей (Shape Check):** Для каждой операции (будь то сложение или умножение матриц) вызывается функция проверки формы [27:38]. Она возвращает булево значение: если формы векторов или матриц несовместимы, создание узла прерывается и возвращается `null` [28:19]. 
*   **Автоматическое вычисление выходной формы:** Разработчику не нужно вручную указывать размер результирующей матрицы. Вместо этого в функцию проверки передаются указатели на переменные `rows` (строки) и `columns` (столбцы), которые обновляются внутри логики самой операции [31:59].
*   **Наследование градиентных флагов:** Харш Бхатт внедряет изящное решение для управления памятью и вычислениями через флаг `requires_grad` [30:38]. Если хотя бы одна из входных переменных (A или B) требует вычисления градиента, результирующий узел автоматически получает этот статус [30:50]. Это критично для эффективной работы — если цепочка вычислений не ведет к обучаемым параметрам, алгоритм не будет тратить ресурсы на хранение производных для этих узлов [31:19].

Ранее в разговоре автор касался структуры мем-арены, и здесь она используется для выделения памяти под новые переменные в рамках модели [32:42]. Каждый узел также получает свой тип (v_type), определяющий, какая именно математическая операция будет выполняться при прямом проходе [27:08].

### Алгоритм обхода графа в обратном порядке
[[JUMP:35:57]]

Когда узлы созданы и связаны, возникает следующая проблема: в каком порядке их нужно выполнять? Харш Бхатт объясняет, что для нейронной сети порядок вычислений при прямом проходе должен быть строго последовательным, от входа к выходу, а при обратном — наоборот [34:20]. Для автоматизации этого процесса он реализует алгоритм поиска в глубину (DFS) с использованием стека [35:57].

Процесс построения графа начинается с финального узла — например, функции активации Softmax на выходе сети [34:37]. Отталкиваясь от этого «выходного провода» (outwire), алгоритм начинает рекурсивно искать все зависимости, которые привели к получению данного результата [35:17].

Харш Бхатт детально разбирает проблему дублирования зависимостей, демонстрируя это на слайдах [34:49]. В сложных графах одна и та же переменная может использоваться в разных ветвях вычислений (например, переменная B может быть входом и для узла X, и для узла Y) [38:20]. Если просто добавлять все найденные узлы в стек, переменная B окажется там дважды, что приведет к ошибкам при расчете градиентов и избыточным операциям [39:11].

Для решения этой проблемы автор вводит следующие механизмы:

1.  **Массив посещенных узлов (visited):** Каждому узлу присваивается индекс, и алгоритм помечает его как «посещенный», чтобы не заходить в одну и ту же ветку дважды [40:34].
2.  **Динамическое переупорядочивание стека:** Если алгоритм обнаруживает, что узел уже находится в стеке, но встречается снова как зависимость другого пути, происходит «сдвиг» [46:49]. Харш Бхатт реализует логику перемещения элементов: старая позиция узла удаляется, а сам узел переносится в правильное место, чтобы гарантировать топологический порядок выполнения [47:18].
3.  **Итеративный цикл:** Вместо классической рекурсии, которая может привести к переполнению стека в глубоких сетях на Си, Харш использует цикл `while stack_size > 0` [42:02]. 

Результатом работы этого алгоритма становится массив узлов, выстроенный в идеальном порядке для выполнения вычислений [48:55]. Харш Бхатт подчеркивает, что такая архитектура позволяет строить графы любой сложности, сохраняя при этом контроль над памятью и эффективностью, что особенно важно при разработке на языке Си с нуля [50:04].

## 🧠 Реализация операторов: от функций активации до матричных вычислений
[[JUMP:50:04]]

После того как фундамент вычислительного графа заложен, Харш Бхатт (Harsh Bhatt) переходит к «сердцу» библиотеки — реализации конкретных математических операций, которые будут выполняться в узлах этого графа [50:18]. На этом этапе фокус смещается с абстрактных структур на прикладную математику нейронных сетей, начиная от базовых функций активации и заканчивая сложными алгоритмами матричного умножения с поддержкой транспонирования [51:09].

### Математика активаций: Softmax, ReLU и проверка размерностей
[[JUMP:51:09]]

Реализация нейросетевых операторов начинается с определения типов переменных (`v_type`) и написания вспомогательных функций для валидации данных [51:54]. Харш Бхатт подчёркивает, что перед выполнением любой операции необходимо убедиться в совместимости тензоров [52:21]. Для этого внедряется функция `var_shape`, которая проверяет, совпадают ли строки и столбцы у операндов [52:50]. Если формы корректны, функция обновляет метаданные узла, фиксируя правильные размерности для выходного тензора [53:18].

Особое внимание уделяется функции Softmax, которая критически важна для модели Actor (в рамках алгоритма REINFORCE, выбранного автором из-за его относительной простоты по сравнению с PPO) [50:30]. При кодировании Softmax в прямом проходе (`forward`) Харш использует стандартный подход с обеспечением численной стабильности [56:32]. Алгоритм включает в себя:

*   Поиск максимального значения во входном векторе для предотвращения переполнения при экспонировании [1:00:38].
*   Вычисление экспонент разностей (Zi - max) [1:00:09].
*   Нормализацию полученных значений путём деления на их общую сумму [1:01:49].

Параллельно реализуется функция ReLU [1:02:43]. Её логика в Си-коде предельно проста: итерация по массиву данных и обнуление всех отрицательных значений, в то время как положительные остаются без изменений [1:03:11]. Это базовый кирпичик нелинейности, который позволяет сети обучаться сложным зависимостям [1:03:26]. Ранее в разговоре автор уже упоминал, что все эти операции вписываются в общую структуру узлов графа, где для каждого типа прописаны указатели на функции прямого и обратного прохода [55:07].

### Поэлементные операции и градиенты
[[JUMP:1:03:41]]

Помимо функций активации, нейросети требуют выполнения множества арифметических действий над тензорами. Харш Бхатт реализует поэлементное сложение (`add`) и вычитание (`sub`) [1:03:41]. Код этих операций строится по схожему шаблону: вычисляется общий размер данных (количество столбцов, помноженное на количество строк), после чего запускается цикл, выполняющий операцию для каждого индекса [1:03:55].

При реализации градиентов (обратного прохода) для этих операций логика остаётся прямолинейной: для сложения градиент просто передаётся дальше, так как производная суммы по каждому слагаемому равна единице [1:04:01]. Эти низкоуровневые манипуляции с памятью напрямую взаимодействуют с системой управления ресурсами, созданной в начале проекта, что позволяет избегать утечек при постоянном создании временных тензоров во время обучения [51:24].

### Матричное умножение и магия транспонирования
[[JUMP:1:04:29]]

Самой сложной и важной частью этого этапа является реализация матричного умножения (`MatMul`) [1:04:29]. В отличие от простых поэлементных функций, `MatMul` требует трёх вложенных циклов и строгой проверки совместимости (число столбцов первой матрицы должно быть равно числу строк второй) [54:13]. 

Харш Бхатт объясняет, что в библиотеке используется «строчный порядок» (row-major order) хранения данных в памяти [1:08:40]. Это означает, что двумерная матрица разворачивается в одномерный массив, и для доступа к элементу используется формула: `index = row * num_columns + column` [1:09:09]. 

Для эффективного обучения нейросети (Backpropagation) недостаточно просто уметь умножать матрицы A и B. Часто требуется умножать транспонированные матрицы (например, при расчёте градиентов по весам или по входу) [1:10:05]. Вместо того чтобы физически копировать и переставлять данные в памяти (что затратно), Харш реализует логику «виртуального» транспонирования через изменение формул индексации внутри алгоритма умножения [1:10:44]:

1.  **mat_n**: Стандартное умножение (No Transpose) [1:06:49].
2.  **mat_nt**: Умножение A на транспонированную B [1:12:33].
3.  **mat_tn**: Умножение транспонированной A на B [1:13:04].
4.  **mat_tt**: Оба операнда транспонированы [1:14:23].

Такой подход позволяет динамически изменять порядок обхода памяти. Например, если матрица A транспонирована, то вместо итерации по строкам алгоритм начинает интерпретировать её структуру так, будто строки стали столбцами, меняя переменные `i` и `k` в формуле смещения [1:13:29]. В завершение Харш объединяет все эти случаи в одну универсальную функцию `mat`, которая выбирает нужный алгоритм индексации в зависимости от флагов, переданных узлом вычислительного графа [1:14:49].

## 🎮 Проектирование игрового окружения и векторизация состояний
[[JUMP:1:15:04]]

Прежде чем переходить к архитектуре нейронной сети, Харш Бхатт завершает работу над вычислительным бэкендом, реализуя функции для градиентов REINFORCE [1:16:56]. Он подчёркивает, что для этого алгоритма градиент рассчитывается как отношение вознаграждения к вероятности действия с отрицательным знаком [1:17:12]. Завершив математическую подготовку, включая вычисление градиента Softmax через скалярное произведение данных [1:22:22], автор переходит к созданию полигона для обучения агента — классической игры «Змейка» [1:23:47].

### Логика и структура окружения «Змейка»
[[JUMP:1:23:47]]

Харш Бхатт решает создать среду «Змейки» (Snake Environment) как простую, но эффективную задачу для демонстрации возможностей RL-библиотеки на Си [1:24:03]. Проектирование начинается с определения перечислений для действий (actions): влево (0), вправо (1), вверх (2) и вниз (3) [1:24:31]. В основе окружения лежит структура `snake_env`, которая хранит в себе полное состояние игрового мира: положение тела змейки, координаты еды, текущий счёт и количество пройденных шагов [1:24:48].

Ключевым аспектом механики является POV (Point of View) — точка обзора или текущее направление движения змейки [1:25:31]. В отличие от классических аркад, где змейка может мгновенно изменить направление по нажатию клавиши, в этой реализации модель управляет направлением движения (POV), которое сохраняется до следующего шага [1:25:44]. Если модель решает не предпринимать никаких действий (`none`), змейка продолжает двигаться в текущем направлении POV [1:34:50]. 

Для управления игровым полем автор использует сетку, размеры которой определяются через строки (rows) и столбцы (columns) [1:27:28]. Сетка чаще всего проектируется квадратной, где положение каждой точки (например, головы змейки или еды) задаётся координатами X и Y [1:31:39]. Начальное состояние инициализируется в левом нижнем углу (0, 0) с направлением движения вправо [1:31:51]. Также реализована функция сброса среды (`reset_env`), которая возвращает змейку в исходную точку и генерирует новое случайное положение еды с помощью встроенного генератора псевдослучайных чисел [1:32:58].

### Сбор опыта и буфер воспроизведения (Replay Buffer)
[[JUMP:1:26:13]]

Одной из фундаментальных особенностей обучения с подкреплением является отсутствие заранее подготовленных данных — агент создаёт их сам в процессе взаимодействия со средой [1:26:57]. Харш Бхатт вводит понятие «роллаутов» (rollouts) — процесса сбора коллекций данных в ходе игры [1:26:41]. Для хранения этого опыта создаётся структура `replay_buffer`, которая аккумулирует траектории движения [1:27:12].

Каждая траектория включает в себя:

*   Последовательность состояний, через которые прошла змейка [1:28:51].
*   Действия, предпринятые в каждом состоянии [1:28:56].
*   Полученные награды и возвраты (returns) [1:29:04].
*   Флаг завершения игры (`done`), сигнализирующий о столкновении или победе [1:29:16].

Размер буфера в данном проекте определён в 1024 записи, а максимальная длина эпизода ограничена 100–200 шагами [1:28:23]. Это предотвращает бесконечные циклы, если змейка начнёт двигаться по кругу, не достигая цели [1:28:23]. Автор поясняет, что после завершения эпизода (или заполнения буфера) накопленный опыт используется для корректировки весов модели, чтобы «исправить» действия агента в будущем [1:27:05]. Ранее в разговоре они кратко касались структуры мем-арены, и здесь Харш также использует арену для эффективного выделения памяти под игровое окружение [1:30:50].

### Векторизация состояния и система наград
[[JUMP:1:36:16]]

Чтобы нейронная сеть могла «видеть» игровое поле, Харш Бхатт реализует механизм векторизации — преобразования структуры игры во входной вектор (embeddings/one-hot vector) [1:36:31]. Поскольку модель не может напрямую обрабатывать координаты объектов, мир змейки представляется в виде плоского массива (flat vector) [1:38:34]. Для поля размером 6x6 (36 ячеек) создаётся вектор, где позиции головы змейки и еды помечаются значением 1.0, а пустые клетки — 0.0 [1:38:09]. 

Процесс перевода 2D-координат (x, y) в индекс плоского массива выполняется по формуле: `index = x + (y * columns)` [1:39:58]. Таким образом, если голова змейки находится в определённой ячейке, соответствующий индекс в векторе состояния получает сигнал [1:40:16]. В этот же вектор добавляется информация о текущем POV, что позволяет модели учитывать инерцию движения [1:37:41].

Система вознаграждений в окружении построена на простых правилах [1:24:16]:

1.  **Положительная награда:** начисляется при поедании еды, что увеличивает общий счёт (`score`) [1:25:00].
2.  **Штраф (отрицательная награда):** применяется, если змейка врезается в стену или собственное тело [1:24:16]. 
3.  **Стимул к эффективности:** каждое действие, не приводящее к смерти, может иметь небольшой штраф или нейтральное значение, чтобы поощрять поиск кратчайшего пути к еде [1:35:16].

Этот векторный вход становится основой для обучения: модель анализирует паттерны расположения еды и стен, чтобы предсказать действие, максимизирующее суммарную награду за эпизод [1:37:11].

## 🏗️ Построение пайплайна обучения и сбор опыта (Rollouts)

[[JUMP:1:44:07]]

После того как Харш Бхатт (Harsh Bhatt) завершил работу над логикой вознаграждений в окружении «Змейка», где агент получает штрафы за столкновения и бонусы за еду [1:42:27], он переходит к критически важному этапу — созданию пайплайна обучения. В Reinforcement Learning (RL) этот процесс представляет собой циклическую последовательность: сбор траекторий (rollouts), расчет возвратов, сохранение опыта в буфер и последующее обновление весов модели [1:44:36].

Сердцем этого процесса является функция `train`, которая управляет жизненным циклом агента на протяжении множества эпох. Харш Бхатт (Harsh Bhatt) подчеркивает, что структура обучения в Си, несмотря на отсутствие высокоуровневых абстракций вроде тех, что есть в PyTorch, строится на тех же принципах [1:44:50]. Основные этапы пайплайна включают:

1.  **Цикл по эпохам:** Глобальный итератор, определяющий общую продолжительность обучения [1:49:46].
2.  **Сбор роллаутов (Rollouts):** Внутри каждой эпохи агент взаимодействует с окружением в течение фиксированного количества шагов или до завершения эпизода [1:50:29]. 
3.  **Использование буфера траекторий:** Для хранения данных о текущем опыте создается структура `trajectory`, которая выступает в роли временного буфера [1:58:13]. В ней фиксируются состояния, действия и полученные награды.
4.  **Управление гиперпараметрами:** На этом этапе инициализируются такие значения, как длина эпизода (`episode length`) и коэффициент дисконтирования гамма (`gamma`), необходимые для будущих расчетов [1:45:29].

Особое внимание Харш уделяет организации взаимодействия модели с графом вычислений. Для каждого шага в роллауте необходимо построить вычислительный граф, выполнить прямой проход для получения вероятностей действий и сохранить состояние графа для последующего обратного прохода [1:51:11]. Ранее в разговоре автор уже упоминал структуру мем-арены, и здесь она становится незаменимой, позволяя быстро выделять и очищать память для временных узлов графа во время каждой итерации сбора опыта [1:52:56]. 

Чтобы эффективно собирать опыт, Харш реализует цикл, ограниченный длиной эпизода [1:58:29]. На каждом шаге агент анализирует вектор состояния, полученный из окружения, и выбирает действие [1:58:42]. Все эти переходы должны быть атомарно записаны в буфер, чтобы алгоритм обучения мог корректно соотнести действия агента с итоговым результатом.

### 🧠 Инициализация архитектуры Actor-Critic и весовых слоев

[[JUMP:1:59:22]]

Для управления поведением «Змейки» Харш Бхатт (Harsh Bhatt) приступает к проектированию архитектуры Actor-Critic модели. Несмотря на название, в данном фрагменте основной упор делается на инициализацию слоев, которые будут предсказывать вероятности действий (Actor) [2:03:41]. Реализация на чистом Си требует ручного создания каждого тензора весов и смещений, а также определения их связей в графе.

Архитектура нейросети, предложенная автором, имеет следующие параметры:

*   **Входной слой:** 76 нейронов [1:59:38]. Эта цифра не случайна: 36 ячеек отведены под сетку игрового поля (grid size), еще 36 — под координаты еды, и 4 нейрона кодируют направление взгляда (POV) агента [1:59:52].
*   **Скрытые слои:** Два полносвязных слоя по 128 нейронов в каждом для обеспечения достаточной емкости модели [2:00:46].
*   **Выходной слой:** Слой, соответствующий количеству возможных действий (в данном случае оптимизировано до 4-5 вариантов) [2:00:21].

Процесс инициализации в функции `create_actor_critic_model` начинается с выделения памяти через мем-арену для структур весов (`weights`) и смещений (`bias`) [2:00:34]. Харш Бхатт (Harsh Bhatt) использует функцию для заполнения параметров случайными значениями (random initialization), что критически важно для предотвращения симметрии нейронов при старте обучения [2:01:27]. Для каждого тензора параметров устанавливается флаг `requires_grad`, указывающий движку автограда, что для этих узлов необходимо вычислять градиенты [2:00:46].

Связывание слоев происходит через создание узлов матричного умножения (`v_mat_mul`) и сложения (`v_add`) [2:02:34]. Автор буквально «прошивает» архитектуру:

1.  Результат умножения входа на первые веса (`weight0`) суммируется со смещением (`bias0`) [2:02:20].
2.  Полученное значение проходит через функцию активации ReLU для внесения нелинейности и нормализации промежуточных вычислений [2:03:15].
3.  Финальный слой завершается операцией Softmax, которая превращает сырые значения (логиты) в распределение вероятностей действий [2:03:41].

Харш отмечает, что такая «ручная» сборка в стиле PyTorch, но на низком уровне Си, позволяет наглядно увидеть, как формируются узлы графа и как данные текут от входного вектора к финальному решению агента [2:03:00]. В завершение инициализации подготавливаются переменные для функции потерь (Reinforce loss) и преимуществ (advantages), которые будут заполнены данными уже в процессе обучения [2:03:58].

## 🏁 Финализация обучения: от расчета возвратов до первых успехов REINFORCE
[[JUMP:2:05:22]]

На финальном этапе разработки RL-библиотеки Харш Бхатт (Harsh Bhatt) переходит к самой ответственной части — превращению собранного опыта в обновленные веса нейросети. После того как процесс сбора данных (rollouts) завершен [2:07:41], необходимо математически интерпретировать полученные награды, чтобы агент мог понять, какие действия привели к успеху, а какие — к гибели «Змейки».

### Расчет дисконтированных возвратов и преимуществ
[[JUMP:2:07:41]]

Процесс обучения начинается с вычисления «возвратов» (returns) для каждого шага внутри траектории. Харш Бхатт (Harsh Bhatt) объясняет, что просто суммировать награды недостаточно — необходимо использовать коэффициент дисконтирования ($\gamma$, gamma) [2:09:05]. Это позволяет алгоритму отдавать приоритет немедленным наградам перед отдаленными и математически стабилизирует сходимость.

Алгоритм расчета реализован через обратный проход по траектории (от конца эпизода к началу) [2:08:37]. Основные шаги выглядят следующим образом:

*   Итерация начинается с последнего шага $t$ и идет к нулевому [2:08:53].
*   Текущий возврат $G_t$ вычисляется как сумма награды на данном шаге и дисконтированного возврата следующего шага: $R_t + \gamma \cdot G_{t+1}$ [2:08:53].
*   Параллельно накапливается сумма всех возвратов и их квадратов для расчета среднего значения (baseline) и стандартного отклонения [2:09:18].

Во время отладки автор замечает, что на первых итерациях змейка постоянно получает отрицательные награды [2:09:47]. Это ожидаемо: необученная модель совершает хаотичные движения и быстро врезается в стены или собственное тело [2:09:59]. Чтобы алгоритм REINFORCE работал эффективнее, вычисляется «преимущество» (advantage) — разница между реальным полученным возвратом и средним значением (baseline) [2:12:35]. Харш Бхатт (Harsh Bhatt) подчеркивает, что использование среднего значения в качестве базовой линии помогает снизить высокую дисперсию, характерную для градиентных методов политики [2:13:44].

### Оптимизация весов и градиентный спуск
[[JUMP:2:10:11]]

Когда преимущества для каждого шага рассчитаны, наступает фаза непосредственного обучения. Первым делом необходимо очистить градиенты во всем вычислительном графе, чтобы накопленные значения с предыдущих итераций не искажали текущий шаг оптимизации [2:10:38]. Хотя ранее в курсе обсуждалось проектирование движка автодифференцирования, именно здесь его работа становится критической.

Для обновления весов Харш Бхатт (Harsh Bhatt) выполняет цикл по всему буферу опыта (rollout buffer) [2:11:04]. На каждой итерации:

1.  Состояние из траектории подается на вход сети [2:11:32].
2.  Выполняется прямой проход (forward pass) через граф для получения предсказаний [2:14:25].
3.  Рассчитывается градиент функции потерь, масштабированный на величину «преимущества» для конкретного выбранного действия [2:14:12].
4.  Запускается обратный проход (backward pass) по графу стоимости (cost graph) [2:14:38].

Завершается цикл шагом оптимизатора. В этой минималистичной реализации на Си автор применяет классический градиентный спуск [2:15:21]. Веса обновляются путем вычитания градиента, умноженного на коэффициент скорости обучения (learning rate), который дополнительно масштабируется на количество образцов в батче (sample count) для стабилизации обучения [2:14:53]. Харш Бхатт (Harsh Bhatt) отмечает, что он намеренно избегал сложных проверок безопасности и дополнительных абстракций, чтобы сохранить код лаконичным и уместить всю логику обучения в 400–500 строк [2:16:19].

### Итоги тренировки алгоритма REINFORCE
[[JUMP:2:16:45]]

После настройки всех компонентов и запуска цикла обучения автор демонстрирует результаты работы агента. Несмотря на то что REINFORCE считается довольно нестабильным алгоритмом из-за высокого стандартного отклонения градиентов [2:17:25], спустя 20–30 минут тренировки «Змейка» начинает демонстрировать осмысленное поведение. 

В логах обучения отчетливо виден прогресс: если изначально возвраты были глубоко отрицательными, то по мере накопления опыта они переходят в положительную зону [2:16:58]. Это означает, что агент научился не только избегать немедленной гибели, но и находить еду на игровом поле [2:17:11]. Харш Бхатт (Harsh Bhatt) подчеркивает, что даже такая простая реализация policy gradient способна достигать хороших результатов, если дать ей достаточно времени на исследование среды [2:17:25].

Автор признает, что в процессе написания кода «в прямом эфире» он мог допустить мелкие ошибки, но общая структура библиотеки остается работоспособной и пригодной для дальнейшего расширения [2:16:05]. Главная цель курса — показать, что Reinforcement Learning на низком уровне (без использования PyTorch или TensorFlow) вполне достижим [2:16:19].

### Завершение курса и планы на будущее
[[JUMP:2:17:37]]

Подводя итоги, Харш Бхатт (Harsh Bhatt) отмечает, что формат одного видео накладывает определенные ограничения. Изначально он планировал реализовать в рамках этого курса и более продвинутые алгоритмы, такие как PPO (Proximal Policy Optimization) [2:17:37]. Однако из-за огромного хронометража (более двух часов только на базовую структуру) было решено вынести более сложные методы в следующие видео.

Курс завершается призывом к зрителям экспериментировать с созданной библиотекой и улучшать её [2:17:51]. В будущих материалах автор обещает рассмотреть:

*   Реализацию версий алгоритма PPO.
*   Оптимизацию производительности вычислительного графа.
*   Применение библиотеки к другим классическим задачам обучения с подкреплением.

Проект доказал свою жизнеспособность, превратившись из набора структур данных на Си в полноценный инструмент, способный обучить нейронную сеть играть в «Змейку» с нуля.