Как научить робота сажать дрон без программирования каждого движения?
Именно это я и решил исследовать. Я потратил несколько недель, создавая игру, в которой виртуальный дрон должен научиться приземляться на платформу — не следуя заранее запрограммированным инструкциям, а учась на основе проб и ошибок, как вы учились кататься на велосипеде.
Это обучение с подкреплением (Reinforcement Learning, RL), и оно принципиально отличается от других подходов машинного обучения. Вместо того чтобы показывать ИИ тысячи примеров «правильных» посадок, вы даёте ему обратную связь: «Эй, это было неплохо, но, может быть, в следующий раз попробуй быть более осторожным?» или «Ой, ты разбился — наверное, так делать не надо». Благодаря бесчисленным попыткам ИИ выясняет, что работает, а что нет.
1. Обучение с подкреплением: обзор
Многие идеи можно связать с экспериментами Павлова с собакой и Скиннера с крысой. Суть в том, что вы даёте субъекту «вознаграждение», когда он делает то, что вы хотите (положительное подкрепление), и «наказание», когда он делает что-то плохое (отрицательное подкрепление). Благодаря многочисленным повторным попыткам ваш субъект учится на основе обратной связи, постепенно обнаруживая, какие действия приводят к успеху — подобно тому, как крыса Скиннера научилась нажимать на нужный рычаг для получения вознаграждения.
1.1 Основные понятия
Когда мы говорим о системах, которые можно реализовать программно на компьютерах, лучше всего писать чёткие определения для идей, которые можно абстрагировать. В изучении ИИ (и, в частности, обучения с подкреплением) основные идеи можно свести к следующим:
- Агент (или актёр): это наш субъект из предыдущего раздела. Это может быть собака, робот, пытающийся перемещаться по огромной фабрике, NPC в видеоигре и т. д.
- Окружение (или мир): это может быть место, симуляция с ограничениями, виртуальный игровой мир видеоигры и т. д.
- Политика: подобно правительствам, компаниям и другим подобным структурам, «политики» диктуют, «какие действия следует предпринять в определённой ситуации».
- Состояние: это то, что агент «видит» или «знает» о своей текущей ситуации. Представьте это как снимок реальности агента в любой момент — например, как вы видите цвет светофора, свою скорость и расстояние до перекрёстка во время вождения.
- Действие: теперь, когда наш агент может «видеть» вещи в своём окружении, он может захотеть что-то предпринять в своём состоянии. Например, агент только что проснулся после долгой ночи и теперь хочет выпить кофе. В этом случае первое, что он сделает, — встанет с кровати. Это действие, которое агент предпримет для достижения своей цели, например, ПОЛУЧИТЬ КОФЕ!
- Вознаграждение: каждый раз, когда актёр выполняет действие (по собственному желанию), что-то может измениться в мире. Например, наш агент встал с кровати и пошёл на кухню, но затем, потому что он плохо ходит, он споткнулся и упал. В этой ситуации бог (мы) награждает его наказанием за то, что он плохо ходит (отрицательное вознаграждение). Но затем агент добирается до кухни и берёт кофе, так что бог (мы) награждает его печеньем (положительное вознаграждение).
2. Gym
Теперь, когда мы понимаем основы, вы можете задаться вопросом: как мы на самом деле создаём одну из этих систем? Позвольте мне показать вам игру, которую я создал.
Для этой публикации я написал специальную видеоигру, к которой каждый может получить доступ и использовать для обучения своего агента машинного обучения игре.
Полный код репозитория можно найти на GitHub (пожалуйста, поставьте звезду). Я намерен использовать этот репозиторий для большего количества игр и кода симуляции, а также для более продвинутых техник, которые я буду реализовывать в следующих частях публикаций по RL.
Дрон-доставка
Дрон-доставка — это игра, в которой цель — доставить (вероятно, с посылками) дрон на платформу. Чтобы выиграть игру, мы должны приземлиться. Чтобы приземлиться, мы должны выполнить следующие критерии:
- Быть в зоне посадки рядом с платформой.
- Быть достаточно медленным.
- Быть в вертикальном положении (приземление вверх ногами больше похоже на крушение, чем на посадку).
Всю информацию о том, как запустить игру, можно найти в репозитории GitHub.
Описание состояния
Дрон наблюдает 15 непрерывных значений, которые полностью описывают его ситуацию:
Критерии успеха посадки: дрон должен одновременно достичь:
- Горизонтального выравнивания: в пределах границ платформы (|dx| < 0,0625).
- Безопасной скорости сближения: менее 0,3.
- Уровня ориентации: наклон менее 20° (|angle| < 0,111).
- Правильной высоты: нижняя часть дрона касается верхней части платформы.
Это похоже на параллельную парковку — вам нужна правильная позиция, правильный угол и движение достаточно медленное, чтобы не столкнуться!
Как спроектировать политику?
Существует множество способов спроектировать политику. Она может быть байесовской (поддерживающей вероятностные распределения над убеждениями), может быть простой справочной таблицей для дискретных состояний, системой правил, закодированных вручную («если расстояние < 10, то тормози»), деревом решений или — как мы увидим — нейронной сетью, которая изучает сопоставление состояний с действиями с помощью градиентного спуска.
По сути, мы хотим что-то, что принимает вышеупомянутое состояние, выполняет некоторые вычисления, используя это состояние, и возвращает, какое действие следует выполнить.
Глубокое обучение для построения политики?
Так как же спроектировать политику, которая может обрабатывать непрерывные состояния (например, точные позиции дрона) и изучать сложное поведение? Здесь на помощь приходят нейронные сети.
В случае нейронных сетей (или глубокого обучения) лучше всего работать с вероятностями действий, то есть «Какое действие наиболее вероятно, учитывая текущее состояние?». Итак, мы можем определить нейронную сеть, которая будет принимать состояние в виде «вектора» или «коллекции векторов» в качестве входных данных. Этот вектор или коллекция векторов должны быть сконструированы из наблюдаемого состояния. Для нашей игры с дроном-доставщиком вектор состояния:
Вектор состояния (из нашей 2D-игры с дроном)
Дрон наблюдает своё абсолютное положение, скорости, ориентацию, топливо, положение платформы и производные показатели. Наше непрерывное состояние:
Где каждый компонент представляет:
Все компоненты нормализованы примерно в диапазоны [0,1] или [-1,1] для стабильного обучения нейронной сети.
Пространство действий (три независимых бинарных двигателя)
Вместо дискретных комбинаций действий мы рассматриваем каждый двигатель независимо:
- Основной двигатель (вверх)
- Левый двигатель (вращение по часовой стрелке)
- Правый двигатель (вращение против часовой стрелки)
Каждое действие выбирается из распределения Бернулли, что даёт нам 3 независимых бинарных решения за шаг времени.
Нейронная сеть с вероятностной политикой (с выборкой Бернулли)
Пусть fθ(s) будет выходом сети после активации сигмоиды. Политика использует независимые распределения Бернулли:
Минимальный набросок на Python (из нашей реализации):
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))
dist = Bernoulli(probs=action_probs)
action = dist.sample()
Это показывает, как мы сопоставляем физические наблюдения игры с 15-мерным нормализованным вектором состояния и производим независимые бинарные решения для каждого двигателя.
Настройка кода (часть 1): импорт и настройка сокета игры
Сначала мы хотим, чтобы наш игровой сокет-слушатель запустился. Для этого вы можете перейти в каталог delivery_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
from delivery_drone.game.socket_client import DroneGameClient, DroneState
client = DroneGameClient()
client.connect()
Как спроектировать функцию вознаграждения?
Что делает хорошую функцию вознаграждения? Это, пожалуй, самая сложная часть RL (и здесь я потратил много времени на отладку).
Функция вознаграждения — это душа любой реализации RL (и поверьте, если вы сделаете это неправильно, ваш агент будет делать самые странные вещи). В теории она должна определять, какое «хорошее» поведение должно быть изучено, а какое «плохое» поведение не должно быть изучено. Каждое действие, предпринятое нашим агентом, характеризуется общей накопленной наградой за каждое поведенческое качество, продемонстрированное действием. Например, если вы хотите, чтобы дрон приземлялся мягко, вы можете давать положительные награды за приближение к платформе и движение медленно, а также штрафовать за крушения или расход топлива — агент затем учится максимизировать сумму всех этих вознаграждений с течением времени.
Преимущество: лучший способ измерить эффективное вознаграждение
При обучении нашей политики мы не просто хотим знать, вознаградило ли нас действие — мы хотим знать, было ли оно лучше, чем обычно. Это интуиция, стоящая за преимуществом.
Преимущество говорит нам: «Было ли это действие лучше или хуже, чем мы обычно ожидаем?»
В нашей реализации мы:
- Собираем несколько эпизодов и вычисляем их возвраты (общая дисконтированная награда).
- Вычисляем базу как среднее значение возврата по всем эпизодам.
- Вычисляем преимущество = возврат – база для каждого шага по времени.
- Нормализуем преимущества, чтобы иметь среднее значение = 0 и стандартное отклонение = 1 (для стабильного обучения).
Почему это помогает:
- Действия с положительным преимуществом → лучше, чем обычно → увеличивают их вероятность.
- Действия с отрицательным преимуществом → хуже, чем обычно → уменьшают их вероятность.
- Снижает дисперсию в градиентных обновлениях (более стабильное обучение).
Эта простая база уже даёт нам гораздо лучшее обучение, чем необработанные возвраты! Она пытается взвесить всю последовательность действий с учётом результатов (приземлился или разбился) таким образом, чтобы политика научилась предпринимать действия, которые приводят к лучшему преимуществу.
После множества проб и ошибок я разработал следующую функцию вознаграждения. Ключевой идеей было установить вознаграждение в зависимости как от близости, так и от вертикальной позиции — дрон должен быть выше платформы, чтобы получать положительные вознаграждения, предотвращая стратегии эксплуатации, такие как зависание ниже платформы.
Краткое примечание об обратном (и нелинейном) масштабировании вознаграждения
Часто мы хотим вознаграждать поведение, обратно пропорциональное определённым значениям состояния. Например, расстояние до платформы колеблется от 0 до ~1,41 (нормализованное по ширине окна). Мы хотим высокое вознаграждение, когда расстояние ≈ 0, и низкое вознаграждение, когда далеко. Я использую различные масштабирующие функции для этого:
Примеры других полезных масштабирующих функций:
def inverse_quadratic(x, decay=20, scaler=10, shifter=0):
"""Reward decreases quadratically with distance"""
return scaler / (1 + decay * (x - shifter)**2)
def scaled_shifted_negative_sigmoid(x, scaler=10, shift=0, steepness=10):
"""Sigmoid function scaled and shifted"""
return scaler / (1 + np.exp(steepness * (x - shift)))
def calc_velocity_alignment(state: DroneState):
"""
Calculate how well the drone's velocity is aligned with optimal direction to platform.
Returns cosine similarity: 1.0 = perfect alignment, -1.0 = opposite direction
"""
Настраивая политику с градиентами политики
Стратегии обучения: когда мы должны обновлять?
Вот вопрос, который меня сбил с толку на раннем этапе: должны ли мы обновлять политику после каждого действия или подождать и посмотреть, как закончится весь эпизод? Оказывается, этот выбор имеет большое значение.
Когда вы пытаетесь оптимизировать, основываясь исключительно на вознаграждении, полученном за действие, это приводит к проблеме высокой дисперсии (по сути, обучающий сигнал очень шумный, и градиенты указывают в случайных направлениях!). То, что я имею в виду под «высокой дисперсией», — это то, что алгоритм оптимизации получает чрезвычайно смешанные сигналы в градиенте, который используется для обновления параметров в нашей сети политики. Для одного и того же действия система может выдать определённое направление градиента, но для слегка другого состояния (но с тем же действием) может выдать что-то совершенно противоположное. Это приводит к медленному и потенциально нулевому обучению.
Есть три способа обновить нашу политику:
Обучение после каждого действия (обновления на каждом шаге)
Дрон запускает свой двигатель один раз, получает небольшое вознаграждение и немедленно обновляет всю свою стратегию. Это как корректировать свою баскетбольную форму после каждого броска — слишком реактивно! Одно удачное действие, которое увеличивает вознаграждение, не обязательно означает, что агент сделал хорошо, а одно неудачное действие не означает, что агент сделал плохо. Обучающий сигнал просто слишком шумный.
Моё первое испытание : я попробовал этот подход на раннем этапе. Дрон беспорядочно метался, делал один удачный ход, который немного увеличивал вознаграждение, немедленно подстраивался под этот ход, а затем разбивался, пытаясь воспроизвести его. Было больно наблюдать — как будто кто-то учится неправильному уроку из чистой случайности.
Обучение после одной полной попытки (обновления по эпизоду)
Лучше! Теперь мы позволяем дрону попытаться приземлиться (или разбиться), смотрим, как всё закончилось, и затем обновляем. Это как закончить эпизод и затем подумать, что можно улучшить. По крайней мере, теперь мы видим все последствия своих действий. Но вот проблема: что, если один приземление было просто удачным? Или неудачным? Мы всё ещё основываем наше обучение на одной точке данных.
Обучение на основе нескольких попыток (мультиэпизодные пакетные обновления)
Это золотая середина. Мы запускаем несколько (6 в моём случае) попыток посадки дрона одновременно, смотрим, как они все прошли, и затем обновляем нашу политику на основе средней производительности. Некоторые попытки могут быть удачными, некоторые — нет, но в среднем мы получаем гораздо более чёткую картину того, что на самом деле работает. Хотя это довольно сильно нагружает компьютер, если вы можете это запустить, работает намного лучше, чем любые из предыдущих методов. Конечно, этот метод не самый лучший, но он достаточно прост для понимания и реализации; есть другие (и лучшие) методы.
