Использование RL для обучения роботов управлению дроном
Делиться

Вы когда-нибудь задумывались, как научить робота сажать дрон, не программируя каждое движение? Именно это я и решил изучить. Я потратил несколько недель на разработку игры, в которой виртуальный дрон должен был разобраться, как приземлиться на платформу, — не следуя заранее запрограммированным инструкциям, а обучаясь методом проб и ошибок, как вы учились кататься на велосипеде.
Это обучение с подкреплением (RL) , и оно принципиально отличается от других подходов к машинному обучению. Вместо того, чтобы показывать ИИ тысячи примеров «правильных» приземлений, вы даёте ему обратную связь: «Эй, это было довольно хорошо, но, может, в следующий раз попробуешь быть аккуратнее?» или «Ой, ты разбился — пожалуй, больше так не делай». С помощью бесчисленных попыток ИИ понимает, что работает, а что нет.
В этом посте я описываю свой путь от основ обучения с подкреплением до создания работающей системы, которая (в основном!) обучает дрон приземляться. Вы увидите успехи, неудачи и все странности, которые мне пришлось отлаживать по ходу дела.
1. Обучение с подкреплением: обзор
Эта идея во многом напоминает эксперименты с собакой Павлова и крысой Скиннера. Идея заключается в том, что вы даёте испытуемому «награду», когда он делает то, что вы хотите (положительное подкрепление), и «наказание», когда он делает что-то плохое (отрицательное подкрепление). Благодаря многократным попыткам испытуемый учится на этой обратной связи, постепенно определяя, какие действия приводят к успеху — подобно тому, как крыса Скиннера узнавала, какие нажатия на рычаг приводят к пище.

Точно так же нам нужна система, которая научится выполнять действия (или задачи) таким образом, чтобы максимизировать вознаграждение и минимизировать наказание. Обратите внимание на этот факт о максимизации вознаграждения, который мы рассмотрим позже.
1.1 Основные концепции
Говоря о системах, которые можно реализовать программно на компьютерах, лучше всего сформулировать чёткие определения для понятий, которые можно абстрагировать. В исследовании искусственного интеллекта (и, в частности, обучения с подкреплением) основные идеи можно свести к следующему:
- Агент (или Актёр) : это наш объект из предыдущего раздела. Это может быть собака, робот, пытающийся проложить маршрут по огромной фабрике, NPC из видеоигры и т. д.
- Окружающая среда (или мир) : это может быть место, симуляция с ограничениями, виртуальный игровой мир видеоигры и т. д. Я представляю это как «коробку, реальную или виртуальную, в которой заключена вся жизнь агента; он знает только то, что происходит внутри неё. Мы, как повелители, можем изменять эту коробку, в то время как агент будет думать, что бог вершит свою волю в его мире».
- Политика : Как и в правительствах, компаниях и многих других подобных организациях, «политики» диктуют, «какие действия следует предпринимать в определенной ситуации».
- Состояние : это то, что агент «видит» или «знает» о своей текущей ситуации. Представьте это как моментальный снимок реальности, сделанный агентом в любой момент времени, — например, как вы видите цвет светофора, свою скорость и расстояние до перекрёстка во время вождения.
- Действие : Теперь, когда наш агент может «видеть» окружающее, он может захотеть что-то сделать со своим состоянием. Возможно, он только что проснулся после долгого ночного сна и теперь хочет выпить чашку кофе. В этом случае первым делом он встанет с кровати . Это действие, которое агент предпримет для достижения своей цели, то есть ВЫПЕЙ КОФЕ!
- Награда : Каждый раз, когда субъект выполняет действие (по собственной воле), в мире может что-то измениться. Например, наш агент встал с кровати и направился на кухню, но затем, поскольку он очень плохо ходит, споткнулся и упал. В этой ситуации бог (мы) наказывает его за то, что он плохо ходит (отрицательная награда). Но затем агент добирается до кухни и пьёт кофе, поэтому бог (мы) награждает его печеньем (положительная награда).

Как вы можете себе представить, большинство этих ключевых компонентов необходимо адаптировать под конкретную задачу/проблему, которую должен решить агент.
2. Спортзал
Теперь, когда мы разобрались с основами, вы, возможно, задаетесь вопросом: как же на самом деле построить такую систему? Позвольте мне показать вам игру, которую я создал.
Для этого поста я написал специальную видеоигру, к которой может получить доступ любой желающий и использовать ее для обучения своего собственного агента машинного обучения игре.
Полный репозиторий кода можно найти на GitHub (пожалуйста, отметьте его звёздочкой). Я планирую использовать этот репозиторий для большего количества игр и кода симуляций, а также для более продвинутых техник, которые я реализую в следующих публикациях по обучению с подкреплением.
Доставка дроном
«Доставка с помощью дрона» — это игра, цель которой — доставить дрон (вероятно, с грузом) на платформу. Чтобы выиграть, нужно приземлиться. Для этого необходимо соответствовать следующим критериям:
- Находиться в непосредственной близости от платформы
- Будьте достаточно медленны
- Будьте в вертикальном положении (приземление вниз головой больше похоже на падение, чем на приземление)
Всю информацию о том, как запустить игру, можно найти в репозитории GitHub.
Вот как выглядит игра

Если дрон вылетит за пределы экрана или коснется земли, это будет считаться случаем «аварии» и приведет к сбою.
Описание состояния
Дрон отслеживает 15 непрерывных значений, которые полностью описывают его ситуацию:

Критерии успешной посадки: Дрон должен одновременно достичь:
- Горизонтальное выравнивание: в пределах платформы (|dx| < 0,0625)
- Безопасная скорость захода на посадку: менее 0,3
- Горизонтальная ориентация: наклон менее 20° (|угол| < 0,111)
- Правильная высота: нижняя часть дрона касается верхней части платформы.
Это как параллельная парковка — нужно занять правильное положение, правильный угол и двигаться достаточно медленно, чтобы не столкнуться!
Как можно разработать политику?
Существует множество способов разработки политики. Она может быть байесовской (с сохранением вероятностных распределений по убеждениям), простой таблицей поиска для дискретных состояний, системой правил, написанной вручную («если расстояние < 10, то тормоз»), деревом решений или, как мы рассмотрим далее, нейронной сетью, которая обучается соотносить состояния с действиями посредством градиентного спуска.
По сути, нам нужно что-то, что принимает вышеупомянутое состояние , выполняет некоторые вычисления с использованием этого состояния и возвращает действие, которое следует выполнить.
Глубокое обучение для разработки политики?
Итак, как разработать политику, которая может обрабатывать непрерывные состояния (например, точное положение дрона) и обучаться сложному поведению? Здесь на помощь приходят нейронные сети.
В случае нейронных сетей (или глубокого обучения) обычно лучше всего работать с вероятностями действий, то есть с вопросом: «Какое действие, вероятно, является наилучшим с учётом текущего состояния?» . Таким образом, мы можем определить нейронную сеть, которая будет принимать состояние как «вектор» или «набор векторов» в качестве входных данных. Этот вектор или набор векторов должен быть построен на основе наблюдаемого состояния. Для нашей игры с дроном-доставщиком вектор состояния будет следующим:
Вектор состояния (из нашей 2D-игры с дроном)
Дрон отслеживает своё абсолютное положение, скорость, ориентацию, уровень топлива, положение платформы и производные показатели. Наше непрерывное состояние:

Где каждый компонент представляет:

Все компоненты нормализованы примерно до диапазонов [0,1] или [-1,1] для стабильного обучения нейронной сети.
Пространство действия (три независимых бинарных двигателя)
Вместо дискретных комбинаций действий мы рассматриваем каждый двигатель независимо:
- Главный двигатель (тяга вверх)
- Левый двигатель (вращение по часовой стрелке)
- Правый двигатель (вращение против часовой стрелки)
Каждое действие выбирается из распределения Бернулли, что дает нам 3 независимых бинарных решения на один временной шаг.
Нейросетевая политика (вероятностная с выборкой Бернулли)
Пусть fθ(s) — выходы сети после активации сигмоиды. Политика использует независимые распределения Бернулли:

Минимальный набросок Python (из нашей реализации)
# построить вектор состояния из DroneState s = np.array([ state.drone_x, state.drone_y, state.drone_vx, state.drone_vy, state.drone_angle, state.drone_angular_vel, state.drone_fuel, state.platform_x, state.platform_y, state.distance_to_platform, state.dx_to_platform, state.dy_to_platform, state.speed, float(state.landed), float(state.crashed) ]) # сеть выводит вероятности для каждого двигателя (после сигмоиды) action_probs = policy(torch.tensor(s, dtype=torch.float32)) # форма: (3,) # выборка каждого двигателя независимо из Бернулли dist = Bernoulli(probs=action_probs) action = dist.sample() # форма: (3,), например, [1, 0, 1] означает основные+правые двигатели
Здесь показано, как мы преобразуем физические наблюдения игры в 15-мерный нормализованный вектор состояния и принимаем независимые бинарные решения для каждого двигателя.
Настройка кода (часть 1): импорт и настройка игрового сокета
Сначала нам нужно запустить прослушиватель сокетов нашей игры. Для этого перейдите в каталог deliver_drone в моём репозитории и выполните следующую команду:
pip install -r requirements.txt # запустите это один раз для настройки требуемых модулей python socket_server.py —render human —port 5555 —num-games 1 # запускайте это каждый раз, когда вам нужно запустить игру в режиме сокета
ПРИМЕЧАНИЕ: Для запуска кода вам понадобится PyTorch . Убедитесь, что вы его предварительно настроили.
import os import torch import torch.nn as nn import math import numpy as np from torch.distributions import Bernoulli # Импортируйте клиент сокета игры из delivery_drone.game.socket_client import DroneGameClient, DroneState # настройте клиент и подключитесь к серверу client = DroneGameClient() client.connect()
Как разработать функцию вознаграждения?
Так что же делает функцию вознаграждения хорошей? Это, пожалуй, самая сложная часть RL (и на отладку которой я потратил МНОГО времени 🫠).
Функция вознаграждения — это душа любой реализации обучения с подкреплением (и поверьте, если вы допустите ошибку, ваш агент начнёт совершать самые странные поступки). Теоретически она должна определять, какое «хорошее» поведение следует выучить, а какое «плохое» — нет. Каждое действие, выполняемое нашим агентом, характеризуется общим накопленным вознаграждением за каждую проявленную характеристику поведения. Например, если вы хотите, чтобы дрон мягко приземлился, вы можете назначить положительные вознаграждения за близость к платформе и медленное движение, одновременно штрафуя за падения или полное отсутствие топлива — тогда агент со временем научится максимизировать сумму всех этих вознаграждений.
Преимущество: лучший способ измерения эффективного вознаграждения
При формировании нашей политики мы хотим не просто знать, принесло ли нам действие вознаграждение, но и узнать, было ли оно лучше обычного. В этом и заключается интуиция, лежащая в основе преимущества .
Преимущество говорит нам: «Было ли это действие лучше или хуже того, что мы обычно ожидаем?»

В ходе нашей реализации мы:
- Соберите несколько эпизодов и рассчитайте их доходность (общую скидку на вознаграждения)
- Рассчитайте базовый уровень как среднюю доходность по всем эпизодам.
- Рассчитайте преимущество = доходность – базовый уровень для каждого временного шага
- Нормализовать преимущества так, чтобы среднее значение было равно 0, а стандартное отклонение — 1 (для стабильного обучения)
Почему это помогает:
- Действия с положительным преимуществом → лучше среднего → увеличивают их вероятность
- Действия с отрицательным преимуществом → хуже среднего → уменьшают их вероятность
- Уменьшает дисперсию обновлений градиента (более стабильное обучение)
Эта простая базовая модель уже даёт нам гораздо лучшее обучение, чем сырые данные! Она пытается сопоставить всю последовательность действий с результатами (крушение или посадка), чтобы политика научилась принимать меры, ведущие к лучшему результату.
После множества проб и ошибок я разработал следующую функцию вознаграждения. Ключевым моментом было обуславливать вознаграждение как приближением, так и вертикальным положением : для получения положительного вознаграждения дрон должен находиться над платформой, что исключает использование стратегий злоупотребления, таких как зависание под платформой.

Краткая заметка об обратном (и нелинейном) масштабировании вознаграждения
Часто нам нужно вознаграждать поведение, обратно пропорциональное определённым значениям состояния. Например, расстояние до платформы варьируется от 0 до ~1,41 (нормализовано по ширине окна). Мы хотим получать большую награду, когда расстояние ≈ 0, и маленькую, когда расстояние большое. Для этого я использую различные функции масштабирования:

Примеры других полезных функций масштабирования
Вспомогательные функции:
def inverse_quadratic(x, decay=20, scaler=10, shifter=0): «»»Награда уменьшается квадратично с расстоянием»»» return scaler / (1 + decay * (x — shifter)**2) def scaled_shifted_negative_sigmoid(x, scaler=10, shift=0, steepness=10): «»»Сигмоидальная функция, масштабированная и сдвинутая»»» return scaler / (1 + np.exp(steepness * (x — shift))) def calc_velocity_alignment(state: DroneState): «»» Рассчитывает, насколько хорошо скорость дрона совпадает с оптимальным направлением к платформе. Возвращает косинусное сходство: 1.0 = идеальное выравнивание, -1.0 = противоположное направление «»» # Оптимальное направление: от дрона к платформе optimal_dx = state.dx_to_platform optimal_dy = state.dy_to_platform optimal_norm = math.sqrt(optimal_dx**2 + optimal_dy**2) if optimal_norm < 1e-6: # Уже на платформе return 1.0 optimal_dx /= optimal_norm optimal_dy /= optimal_norm # Текущее направление скорости velocity_norm = state.speed if velocity_norm < 1e-6: # Не движется return 0.0 velocity_dx = state.drone_vx / velocity_norm velocity_dy = state.drone_vy / velocity_norm # Косинусное подобие return velocity_dx * optimal_dx + velocity_dy * optimal_dy
Код для текущей функции вознаграждения:
def calc_reward(state: DroneState): rewards = {} total_reward = 0 # 1. Штраф за время — основан на расстоянии (больше штрафовать, когда далеко) minimum_time_penalty = 0.3 maximum_time_penalty = 1.0 rewards['time_penalty'] = -inverse_quadratic( state.distance_to_platform, decay=50, scaler=maximum_time_penalty — minimum_time_penalty ) — minimum_time_penalty total_reward += rewards['time_penalty'] # 2. Выравнивание расстояния и скорости — ТОЛЬКО когда над платформой velocity_alignment = calc_velocity_alignment(state) dist = state.distance_to_platform rewards['distance'] = 0 rewards['velocity_alignment'] = 0 # Ключевое условие: дрон должен быть над платформой (dy > 0), чтобы получить положительное rewards if dist > 0.065 and state.dy_to_platform > 0: # Награда за движение к платформе, когда скорость выровнена, если velocity_alignment > 0: rewards['distance'] = state.speed * scaled_shifted_negative_sigmoid(dist, scaler=4.5) rewards['velocity_alignment'] = 0.5 total_reward += rewards['distance'] total_reward += rewards['velocity_alignment'] # 3. Штраф за угол — пороговое значение, основанное на расстоянии abs_angle = abs(state.drone_angle) max_angle = 0.20 max_permissible_angle = ((max_angle — 0.111) * dist) + 0.111 excess = abs_angle — max_permissible_angle rewards['angle'] = -max(excess, 0) total_reward += rewards['angle'] # 4. Штраф за скорость — штраф за чрезмерную скорость rewards['speed'] = 0 speed = state.speed max_speed = 0.4 if dist < 1: rewards['speed'] = -2 * max(speed - 0.1, 0) else: rewards['speed'] = -1 * max(speed - max_speed, 0) total_reward += rewards['speed'] # 5. Штраф за вертикальное положение - штраф за нахождение ниже платформы rewards['vertical_position'] = 0 if state.dy_to_platform > 0: # Дрон находится над платформой (ХОРОШО) rewards['vertical_position'] = 0 else: # Дрон находится ниже платформы (ПЛОХО!) rewards['vertical_position'] = state.dy_to_platform * 4.0 # Отрицательный штраф total_reward += rewards['vertical_position'] # 6. Терминальные награды rewards['terminal'] = 0 if state.landed: rewards['terminal'] = 500.0 + state.drone_fuel * 100.0 elif state.crashed: rewards['terminal'] = -200.0 # Дополнительный штраф за падение далеко от цели if state.distance_to_platform > 0.3: rewards['terminal'] -= 100.0 total_reward += rewards['terminal'] rewards['total'] = total_reward return rewards
И да, эти магические числа вроде 4,5, 0,065 и 4,0? Они появились путём множества проб и ошибок. Добро пожаловать в RL, где настройка гиперпараметров — это наполовину искусство, наполовину наука и наполовину удача (да, я знаю, это три половины).
def compute_returns(rewards, gamma=0.99): «»» Вычислить дисконтированную доходность (G_t) для каждого временного шага на основе уравнения Беллмана G_t = r_t + γ*r_{t+1} + γ²*r_{t+2} + … «»» returns = [] G = 0 # Вычислить в обратном направлении (более эффективно) для r в reversed(rewards): G = r + gamma * G returns.insert(0, G) return returns
Важно отметить, что функции вознаграждения подвергаются тщательному экспериментированию. Одна ошибка или чрезмерное вознаграждение — и агент переходит к оптимизации поведения, эксплуатирующей ошибки. Это приводит нас к хакерской атаке на вознаграждение.
Взлом вознаграждения
Хакерство с вознаграждением происходит, когда агент находит непреднамеренный способ максимизировать вознаграждение, фактически не решая задачу, которую вы хотели, чтобы он решил. Агент не «жульничает» намеренно — он делает ровно то, что вы ему сказали, а не то, что вы хотели.
Классический пример : если вы поощряете робота-уборщика за «отсутствие видимой грязи», он может научиться выключать камеру вместо уборки!
Мой болезненный опыт обучения : я понял это на собственном горьком опыте. В ранней версии функции вознаграждения за посадку дрона я начислял ему баллы за «стабильность и медлительность» в любой точке вблизи платформы. Звучит разумно, правда? Неправда! После 50 обучающих эпизодов мой дрон научился просто зависать на месте навсегда, накапливая бесплатные баллы. Технически это было оптимально для моей плохо спроектированной функции вознаграждения, но приземлился ли он? Нет! Я наблюдал, как он зависал 5 минут подряд, прежде чем понял, что происходит.
Вот проблемный код, который я написал:
# НЕ КОПИРУЙТЕ ЭТО! # Если дрон находится над платформой (|dx| < 0,0625) и близко (расстояние < 0,25): corridor_reward = inverse_quadratic(distance, decay=20, scaler=15) # До 15 очков, если устойчиво и медленно: corridor_reward += 10 # Дополнительные 10 очков! # Всего возможно: 25 очков за шаг!
Пример взлома вознаграждения в действии:


Создание политической сети
Как обсуждалось выше, мы будем использовать нейронную сеть в качестве инструмента, управляющего мозгом нашего агента. Вот простая реализация, которая принимает вектор состояния и вычисляет распределение вероятностей по трём независимым действиям:
- Активировать главный двигатель
- Активируйте левый двигатель
- Активируйте правый двигатель
def state_to_array(state): «»»Вспомогательная функция для преобразования класса данных DroneState в массив numpy»»» data = np.array([state.drone_x, state.drone_y, state.drone_vx, state.drone_vy, state.drone_angle, state.drone_angular_vel, state.drone_fuel, state.platform_x, state.platform_y, state.distance_to_platform, state.dx_to_platform, state.dy_to_platform, state.speed, float(state.landed), float(state.crashed) ]) return torch.tensor(data, dtype=torch.float32) class DroneGamerBoi(nn.Module): def __init__(self, state_dim=15): super().__init__() self.network = nn.Sequential( nn.Linear(state_dim, 128), nn.LayerNorm(128), nn.ReLU(), nn.Linear(128, 128), nn.LayerNorm(128), nn.ReLU(), nn.Linear(128, 64), nn.LayerNorm(64), nn.ReLU(), nn.Linear(64, 3), nn.Sigmoid() ) def forward(self, state): if isinstance(state, DroneState): state = state_to_array(state) return self.network(state)
Фактически, вместо того, чтобы пространство действий было представлено в виде пространства 23 = 8, я сократил его до принятия решений по трём независимым двигателям, используя выборку Бернулли. Такое сокращение упрощает оптимизацию, поскольку каждый двигатель рассматривается независимо, а не как один большой категориальный выбор (по крайней мере, я так думаю — я могу ошибаться, но у меня это сработало!).
Источник: towardsdatascience.com



























