RL без матана — это вообще законно?
Да, я знаю, о чём ты подумал: Reinforcement Learning — это же сплошные уравнения Беллмана, марковские процессы и градиенты, от которых голова пухнет. Но давай честно: 90% материала по RL пишут математики для математиков. А мы программисты. Нам проще один раз написать код и увидеть, как агент учится, чем читать три страницы доказательств.
В этом гайде я покажу, как написать настоящего RL-агента на Python, который научится балансировать палку на тележке (CartPole). Ни одной формулы — только код, интуиция и немного магии нейросетей. Если ты когда-нибудь писал игру на Pygame или крутил цикл while True с коллизиями — ты уже готов.
А зачем нам вообще RL, если есть supervised learning?
Хороший вопрос. Supervised learning — это когда ты показываешь модели готовые ответы: вот котик, вот собачка, запоминай. RL — это когда модель сама шляется по миру, получает плюхи за плохие действия и печеньки за хорошие, и учится на своих ошибках. Звучит как дрессировка кота — и это почти так и есть. Только кота заменяет нейросеть, а печеньки — числовая награда.
Где это нужно? Роботы, игры, управление дронами, оптимизация цепочек поставок, рекомендательные системы. Даже ChatGPT в какой-то момент дообучали RLHF — той же самой механикой. Так что RL — не игрушка, а рабочий инструмент. Но чтобы в него въехать, не обязательно штудировать Спиваковского.
Если тебя всё ещё пугает математика, прочитай мою статью «Математика в современном ML: почему теория отстаёт от практики» — там я рассказываю, почему на практике можно обгонять теорию головой.
Что нам понадобится (и ничего лишнего)
- Python 3.11+ — у тебя уже стоит, не ври.
- gymnasium — преемник знаменитого OpenAI Gym, библиотека с готовыми средами.
- PyTorch — чтобы собрать маленькую нейросеть-политику. Можно и на numpy, но с PyTorch проще считать градиенты.
- numpy — куда ж без него.
Установка одной строкой:
pip install gymnasium torch numpyГотово. Теперь мы официально RL-инженеры. (Шутка. Но скоро станем.)
Среда: что видит агент и что он может делать
Возьмём классику — CartPole-v1. На тележке стоит палка, может упасть. Агент толкает тележку влево или вправо. Задача — удержать палку вертикально как можно дольше. Среда даёт агенту 4 числа: положение тележки, скорость, угол отклонения палки и угловую скорость. Действий два: 0 (влево) и 1 (вправо). Награда: +1 за каждый шаг, пока палка не упала.
В RL это называется state (состояние), action (действие) и reward (награда). Всё. Больше терминов не будет.
Первый блин: рандомный агент (чтобы понять baseline)
Прежде чем писать умного агента, давай посмотрим, сколько наберёт полный дурак, который действует случайно. Код:
import gymnasium as gym
env = gym.make('CartPole-v1', render_mode='human')
for episode in range(3):
state, _ = env.reset()
total_reward = 0
done = False
while not done:
action = env.action_space.sample() # случайное действие
state, reward, done, truncated, _ = env.step(action)
total_reward += reward
done = done or truncated
print(f'Episode {episode+1}: {total_reward}')
env.close()Скорее всего, ты увидишь rewards около 15-25. Палка падает быстро. Наша цель — переплюнуть хотя бы 200 (это максимальное значение в среде).
Мозг агента: маленькая Q-сеть
Теперь самое интересное. Мы не будем писать Q-таблицу (она не влезет в непрерывные состояния). Вместо этого научим нейросеть аппроксимировать Q-функцию: сколько примерно награды мы ожидаем получить за каждое действие в текущем состоянии.
Архитектура — котёнок: 4 входа (состояние), один скрытый слой 128 нейронов, ReLU, 2 выхода (Q-значения для каждого действия).
import torch
import torch.nn as nn
import torch.optim as optim
class DQN(nn.Module):
def __init__(self, state_dim, action_dim):
super().__init__()
self.net = nn.Sequential(
nn.Linear(state_dim, 128),
nn.ReLU(),
nn.Linear(128, action_dim)
)
def forward(self, x):
return self.net(x)Всё. Никаких градиентов политики, никакого TRPO. Чистый DQN (Deep Q-Network) в 10 строк.
Цикл обучения: от хаоса к стабильности
Теперь главный фокус. Мы запускаем эпизоды, сначала агент действует случайно (с вероятностью epsilon), но постепенно мы снижаем epsilon — агент начинает больше доверять своей сети. Каждый шаг мы запоминаем в буфер (опыт: состояние, действие, награда, следующее состояние). Когда в буфере накопится достаточно примеров, мы учим сеть: предсказать Q-значение и сравнить с тем, что получили на практике плюс дисконтированная оценка будущего.
Это называется experience replay — агент учится на своих же воспоминаниях, как мы пересматриваем свои ошибки перед сном.
Соберём всё вместе:
import gymnasium as gym
import numpy as np
import torch
import random
from collections import deque
# Гиперпараметры
LEARNING_RATE = 1e-3
GAMMA = 0.99
EPSILON_START = 1.0
EPSILON_MIN = 0.01
EPSILON_DECAY = 0.995
BATCH_SIZE = 64
MEMORY_SIZE = 10000
MAX_EPISODES = 500
def train():
env = gym.make('CartPole-v1')
state_dim = env.observation_space.shape[0]
action_dim = env.action_space.n
policy_net = DQN(state_dim, action_dim)
target_net = DQN(state_dim, action_dim)
target_net.load_state_dict(policy_net.state_dict())
target_net.eval()
optimizer = optim.Adam(policy_net.parameters(), lr=LEARNING_RATE)
memory = deque(maxlen=MEMORY_SIZE)
epsilon = EPSILON_START
for episode in range(MAX_EPISODES):
state, _ = env.reset()
state = np.array(state, dtype=np.float32)
total_reward = 0
done = False
while not done:
# выбор действия epsilon-greedy
if random.random() < epsilon:
action = env.action_space.sample()
else:
with torch.no_grad():
q_values = policy_net(torch.tensor(state))
action = q_values.argmax().item()
next_state, reward, done, truncated, _ = env.step(action)
next_state = np.array(next_state, dtype=np.float32)
done = done or truncated
total_reward += reward
memory.append((state, action, reward, next_state, done))
state = next_state
# обучение, если в памяти достаточно примеров
if len(memory) >= BATCH_SIZE:
batch = random.sample(memory, BATCH_SIZE)
states, actions, rewards, next_states, dones = zip(*batch)
states = torch.tensor(np.array(states))
actions = torch.tensor(actions).unsqueeze(1)
rewards = torch.tensor(rewards)
next_states = torch.tensor(np.array(next_states))
dones = torch.tensor(dones, dtype=torch.bool)
current_q = policy_net(states).gather(1, actions).squeeze()
with torch.no_grad():
next_q = target_net(next_states).max(1)[0]
target_q = rewards + GAMMA * next_q * (~dones)
loss = nn.MSELoss()(current_q, target_q)
optimizer.zero_grad()
loss.backward()
optimizer.step()
# обновление target-сети раз в несколько эпизодов
if episode % 10 == 0:
target_net.load_state_dict(policy_net.state_dict())
epsilon = max(EPSILON_MIN, epsilon * EPSILON_DECAY)
print(f'Episode {episode}, Reward {total_reward:.1f}, Epsilon {epsilon:.3f}')
if total_reward >= 500:
print('Solved!')
break
env.close()
if __name__ == '__main__':
train()Запусти — и через 100–300 эпизодов увидишь, как агент начнёт держать палку по 200+ шагов. Это не магия. Это RL.
Но я же ничего не понимаю в матане! Поможет?
А ты заметил, что мы нигде не использовали ни одной формулы? Все «уравнения Беллмана» спрятались в строчку target_q = rewards + GAMMA * next_q * (~dones). Это и есть Bellman update в коде. Ты можешь не знать, как его вывести — но ты его написал. И он работает.
Современный ML — это инженерия на 80%. Математика — 20% понимания, куда крутить ручки. Я уже писал об этом в статье «Код по наитию»: современные инструменты позволяют писать меньше, а думать — больше над архитектурой, а не над производными. RL — не исключение.
Что дальше? Как улучшать агента
- Double DQN — избегает переоценки Q-значений, добавить одну строчку в расчёт.
- Dueling DQN — разделяет ценность состояния и преимущество действия.
- Noisy Networks — вместо epsilon-greedy шум в весах.
- Procgen или Minigrid — более сложные среды, чтобы прокачаться.
- Stable-Baselines3 — библиотека с готовыми алгоритмами (PPO, A2C, DQN). Когда захочешь перестать писать велосипеды — ставь её.
Но для первого раза хватит и нашего маленького DQN. Ты теперь можешь сказать: «Я написал RL-агент, который учится сам». И это правда.
Хочешь стать ML-инженером профессионально?
Если после этого гайда ты захочешь глубже погрузиться в машинное обучение и Reinforcement Learning, но не знаешь, с чего начать — есть структурированные курсы, которые ведут от нуля до первых проектов. Например, Skillbox предлагает программу Machine Learning Engineer с нуля — за год ты пройдёшь всю математическую базу, нарешаешься в Python и сделаешь несколько проектов, в том числе с RL. Или более сжатый вариант — Machine Learning с нуля до Junior. Выбирай, что ближе.
Но даже если ты не пойдёшь на курсы — ты уже сделал первый шаг. Теперь попробуй поменять награду, размер сети, добавь Double DQN. Экспериментируй. RL — это не страшно. Это просто код.