Есть недорогой робот-манипулятор на сервоприводах. Он двигается, реагирует на команды.
Но в конструкции нет энкодеров — то есть мы не знаем, в каких углах реально находятся его суставы.
А дальше начинается реальность железа для хобби: люфты, износ сервоприводов, накопленные со временем неточности и «плавающая» геометрия. В результате поведение манипулятора далеко от идеального.
И есть задача: научить такого робота аккуратно перемещать объекты. Например, шахматные фигуры — взять с одной клетки, перенести на другую, и пройти траекторию достаточно предсказуемо, по дугообразной форме.
Классический путь: строим точную модель манипулятора, считаем прямую и обратную кинематику, калибруем всё, что только можно, и пытаемся выжать из системы максимальную точность. Но в нашем случае этот путь упирается в железо: точной модели нет, датчиков положения нет, а дешёвые сервоприводы не обещают стабильности.
Поэтому здесь мы идём другим путём.
Вместо того чтобы пытаться идеально описать робота, мы попробуем научить его повторять движения. Этот подход называется имитационным обучением, или клонированием поведения: системе не объясняют движение через формулы, ей показывают примеры. Мы записываем, как именно должен двигаться манипулятор, а потом учим его воспроизводить эти траектории по данным.
В нашем случае всё устроено довольно просто: берем манипулятор и вручную проводим по нужным траекториям, положения суставов считываем внешними потенциометрами, данные сохраняем и после обработки траектории будем пробовать воспроизводить эти движения уже на реальном роботе.
Вместо того чтобы пытаться «понять» робота, просто научим его повторять движения.
В этой статье мы пройдём весь путь — от «неизвестного» робота-манипулятора до системы, которая умеет воспроизводить траектории:
получим данные
оцифруем движения с помощью внешних потенциометров
подготовим и обработаем траектории
обучим нейросеть строить движение шаг за шагом
компенсируем накопление ошибки, чтобы робот всё же попадал в нужную точку
Примечание: Статья рассчитана на широкую аудиторию, поэтому некоторые разделы могут показаться излишне подробными — их можно пропускать, не теряя сути статьи.
В эксперименте используется робот-манипулятор на сервоприводах MG996R. Конструкция включает (см. Рис. 1):
Основание с одним сервоприводом для вращения вокруг оси Z.
Три звена, соединённые суставами, каждое управляется отдельным сервоприводом.
Захват, управляемый отдельным сервоприводом.

Один сервопривод из базовой конструкции (отвечавший за поворот захвата) был демонтирован для упрощения механики, при этом общая геометрия осталась прежней.
Сервоприводы не оснащены встроенными датчиками положения (энкодерами). Они принимают команды на какой угол нужно встать, но не предоставляют обратной связи о фактическом положении суставов. Поскольку сервоприводы бюджетные, поведение суставов подвержено люфтам, механическим неточностям и вариативности при смене направления движения.
Для получения фактических углов сервоприводов были добавлены в конструкцию внешние потенциометры (кроме захвата). Они нам пригодятся при ручном сборе траекторий. Это наш единственным источником информации о положении манипулятора.
В проекте используются две версии манипулятора:
Физический манипулятор
Модель в PyBullet, описанная через URDF. Эта версия используется исключительно для визуализации траекторий. Она не является точным цифровым двойником: реальные люфты сервоприводов и физические особенности манипулятора в ней практически не учитываются. Для задачи имитационного обучения эта модель не обязательна, и для нас служит только наглядным инструментом.
В этой статье мы рассматриваем манипулятор как «чёрный ящик» с неопределёнными параметрами. Управление строится не на идеальной модели, а на том, что сами измеряем.
Цель проекта — проверить, насколько реально научить робота повторять траектории на таком железе, используя только примеры движений и измеренные данные.
Для сбора данных о положении суставов манипулятора используются внешние потенциометры 10 кОм, подключённые к Arduino Mini. Потенциометры установлены на всех активных суставах (кроме захвата) и считывают аналоговые сигналы, пропорциональные текущему положению суставов. Arduino умеет с заданным интервалом (~10 мс) снимать эти значения и передавать через USB на компьютер для записи и дальнейшей обработки.

Управление сервоприводами организовано через плату расширения PCA9685PW, которая обеспечивает независимое управление всеми сервоприводами манипулятора. Команды на углы суставов формируются на компьютере, отправляются через Arduino и распределяются платой PCA9685PW на соответствующие сервоприводы.
Система программно состоит из двух компонентов:
Arduino Mini — опрашивает потенциометры, управляет платой PCA9685PW и передаёт данные на компьютер.
Компьютер — получает данные с Arduino через USB, сохраняет траектории в CSV-файлы, а также запускает скрипты. Для работы используется IDE Arduino (в том числе монитор) и набор Python-скриптов.
На рисунке 2 показана схема подключения, которая позволяет работать в двух независимых режимах:
в режиме управления сервоприводами манипулятора.
в режиме сбора данных с потенциометров.
⚠️ Важно: данный проект (робот-манипулятор) связан с движущимися частями и может создавать значительное усилие и скорость перемещения. При неосторожном обращении это может привести к травмам или повреждению оборудования.
Автор проекта не несёт ответственности за последствия, возникшие при сборке, настройке, программировании, использовании кода или эксплуатации устройства.
Обратите внимание: рабочая зона — это не только область перед манипулятором. Его конструкция позволяет захватывать и задевать предметы по всему периметру вокруг основания. Перед запуском убедитесь, что вокруг достаточно свободного пространства.
Перед запуском убедитесь, что рабочая зона свободна от посторонних предметов и людей и животных, предусмотрена аварийная остановка, а вы понимаете принципы работы системы и соблюдаете необходимые меры предосторожности.
Если упростить весь проект, его можно разложить на три последовательных этапа. Каждый из них решает свою задачу:
На этом этапе задача: научиться записывать движения.
Мы вручную перемещаем манипулятор от точки, к точки, а система в это время фиксирует, как меняются положения суставов. В результате получаем набор траекторий — по сути, «записанные движения» робота.
Сырые траектории дают общее представление о движении, но точности в них недостаточно. Из-за люфтов и погрешностей манипулятор не попадает ровно туда, куда должен.
Поэтому следующий шаг — калибровка. Мы уточняем реальные положения целевых точек и подбираем такие значения углов, при которых манипулятор действительно приходит в нужное место.
Когда есть траектории и есть откалиброванные цели, можно переходить к обучению.
Задача нейросети — не запомнить конкретные движения, а "уловить" их форму. На каждом шаге траектории нейросеть получает текущее положение и целевую точку и предсказывает, куда двигаться дальше. Таким образом траектория строится постепенно, шаг за шагом.
Дополнительно вводится механизм «прицеливания», который помогает компенсировать накопление ошибки и гарантирует, что в конце движения робот всё-таки окажется в нужной точке.
Прежде чем приступить к первому этапу - сбору траекторий, нужно определить, где именно манипулятор будет работать и какие движения от него вообще требуются.
В качестве рабочей сцены используется плоская поверхность с сеткой точек — в нашем случае это поле, похожее на шахматную доску. Нас интересует не вся поверхность, а только та часть, до которой манипулятор может дотянуться без перегрузок и в разумной конфигурации суставов. Эту область можно условно назвать «комфортной рабочей зоной». На рисунке 1 зеленым контуром выдела комфортная рабочая зона манипулятора.
Внутри этой зоны по углам клеток были выбраны 34 целевые точки. На рисунке 1 они подписаны красным цветом. Центры клеток специально не использовались, чтобы позже на 3 этапе оценить обобщающую способность нейросети.
Отдельно вводится базовая точка - положение манипулятора высоко над рабочей областью. Это ключевой элемент всей схемы (см. Рис. 2). Базовая точка используется как промежуточная: через неё проходят все перемещения из целевой точки в целевую.

Если пытаться собрать траектории для перемещения между всеми парами 34 точек, их количество быстро становится слишком большим. Вместо этого был использован более простой подход: собирались траектории от базы к каждой целевой точке и от каждой точки обратно к базе.
В итоге любое перемещение можно собрать как комбинацию двух перемещений:
точка С → база → точка D.
Такая траектория получается длиннее, чем прямая. Но зато количество необходимых примеров резко уменьшается, и сбор данных становится реалистичным.
В итоге было собрано 68 траекторий: 34 траектории от базы до каждой целевой и обратно от каждой целевой до базы еще 34 траектории.
На первом этапе задача собрать реальные траектории движения манипулятора.
Для сбора траекторий необходимо знать положения звеньев манипулятора (углов сервоприводов) на каждом шаге траектории. На наших сервоприводах нет встроенных энкодеров, поэтому манипулятор не знает своего положения. Мы можем отправить команду на сервопривод, но не получаем обратной связи: дошёл ли он до нужного угла и где находится фактически.
Поэтому была произведена небольшая доработка и на 4 сустава: серво 1, серво 2, серво 3, серво 4 (см. Рис. 1) были установлены потенциометры.
На рисунке 3 показан пример монтажа внешних потенциометров.

Задача потенциометров сообщать о положении звеньев. При движении сустава меняется сигнал от потенциометра в диапазоне от 0 до 1023, который считывается через Arduino.
Таким образом мы можем получать четыре значения от четырех потенциометров, в которых будет закодировано конкретное положение звеньев манипулятора. Но закодировано пока еще не в углах для сервопривода, а в значениях потенциометров, например [176, 546, 381, 594]
Примечание: весь код проекта доступен на GitHub: https://github.com/Iskatel-potenzialov/manipulytor
Скетч skan_4pot.ino, загруженный в Arduino, каждые 10 миллисекунд, опрашивает все четыре потенциометра. Каждый потенциометр выдаёт число от 0 до 1023. Чтобы значения не прыгали из-за шумов, Arduino применяет сглаживание — берёт новое значение и старые, усредняет. Получается плавная картинка.
Затем Arduino формирует строку текста, например:12450,512,600,400,300 ,где первое число — время в миллисекундах с момента включения, а остальные — сглаженные значения с четырёх потенциометров. И отправляет эту строку по USB-кабелю на компьютер.
Скрип Python collector.py запущенный на компьютере открывает тот же USB-порт (например, COM3), что и Arduino, с той же скоростью (115200 бод). Ждёт, пока Arduino пришлёт строку. Как только строка приходит, Python разбивает её на части по запятым и проверяет, что получилось ровно 5 чисел. Если всё правильно, он добавляет эту строку в CSV-файл.
В файле первая строка — заголовки: time_ms, pot1, pot2, pot3, pot4. Каждая следующая строка — один замер в определённый момент времени (см. Рис. 4 )

В нашем случае получается, что траектория — временной ряд значений потенциометров для суставов.
На рисунке 5 (слева) видно, что к захвату закреплен стержень-щуп. Расстояние между плоскостью доски и 20-той точкой - 40 мм. Для всех целевых точек это и есть высота над уровнем доски по Z оси. На рисунке 5 (справа) стержень-щуп стоит на опоре высотой 35 мм. Получается высота базовой точки над доской 75 мм.
Таким образом, для удобства ручного сбора траекторий, мы фиксируем высоты для целевых точек и базовой точки.

отключение питания сервоприводов
подключение Arduino к компьютеру и загрузка на него скетча
запуск скрипт Python collector.py записи данных
Дальше процесс повторяли для каждой целевой точки:
Манипулятор руками ставили в базовую точку (щуп на опору - см. Рис. 5 справа)
Делали пауза (~10–15 секунд)
Руками плавно перемещаем манипулятор до целевой точки по дугообразной траектории
Снова пауза
Возврат в базовую точку по дугообразной траектории
Снова пауза
И так — для всех точек.
Паузы здесь критичны: именно по ним потом будет происходить разрезание данных на отдельные траектории.
Чтобы упростить работу, траектории собирались группами — по рядам точек.
В итоге было получено 4 CSV-файла, каждый со своим набором траекторий для групп целевых точек.

Внутри каждого файла — непрерывный поток данных (см. Рис. 4) для нескольких траекторий, который потом разбивается на отдельные траектории.
Код на Python 4 csv -> df в юпитер ноутбуке lstm_manipulytor.ipynb:
Загружает 4 файла с записями. В каждом файле — показания четырёх датчиков (потенциометров) и время в миллисекундах.
Находит моменты, когда робот стоит на месте (паузы). Для этого смотрит на один из датчиков (например, второй или четвёртый - выбираем наиболее "контрастные").
Дисперсия — это число, которое показывает, насколько сильно меняется сигнал. Если сигнал почти не меняется (дисперсия маленькая) — значит робот стоит.
Сглаживает сигнал — убирает случайные помехи (дрожание датчиков).
Считает паузой участок, где сигнал меняется очень слабо (дисперсия ниже порога) и длится дольше 1 секунды.
Вырезает куски записи между паузами — это и есть отдельные траектории.
Ещё раз сглаживает все четыре датчика (усредняя каждые 3 замера), чтобы траектории стали более плавными.
Приводит все траектории к одному размеру — ровно по 200 шагов. Если в исходной записи было больше, программа сжимает её, сохраняя форму (это называется линейная интерполяция).
Нормализует значения — переводит показания датчиков (от 0 до 1023) в удобный для нейросети диапазон от 0 до 1 (просто делением на 1023).
Определяет направление движения по порядковому номеру траектории: Нечётные (1, 3, 5…) — это движение от базы к точке (туда). Чётные (2, 4, 6…) — движение от точки обратно к базе.
Cоздаёт pandas DataFrame (df_trajectories) — это таблица в памяти компьютера (см. Рис. 7). В ней лежат значения потенциометров по 200 шагов для каждой из 68 траекторий. Такой формат удобен для обработки. Из него можно, забрать например, значения потенциометров для любой целевой точки или подготовить данные для обучения нейросети.

На графике (см. Рис. 8) наглядно показаны паузы и траектории для второго потенциометра.

Например, первая траектория от базы до целевой точки 1 - это участок между первой паузой (первый зеленый столбик) и второй паузой (второй зеленый столбик).
Для второго потенциометра значение на этой траектории сначала уменьшалось с ~ 470 до ~ 440, а затем быстро повысилось с ~440 до ~650.
Всего на этом графике 15 зеленых полей - 15 пауз и между ними 14 траекторий от базы то точек 1 ,2 ,3 ,4 ,5 ,6 ,7 и обратно к базе.
Изначально каждая траектория содержит разное количество измерений так как по факту с разной скоростью и за разное врем вручную перемещали манипулятор от базы до дальних и ближних точек. Причем в большей степени не из-за расстояния, а из-за отличающихся дуг траекторий.
Все измерения независимо от исходного количества для каждой траектории приводятся к фиксированной длине — 200 шагов.

На рисунке 9 можно увидеть, как меняются значения четырех потенциометров со временем для разных траекторий.
В результате было получено 68 траекторий для 34 целевых точек длинной 200 шагов в значениях потенциометров.
Чтобы запустить реальный манипулятор по любой из этих траекторий нужно перевести значения потенциометров в углы, так как на сервоприводы необходимо подавать углы в градусах
Для это была проведена калибровка - сопоставление значений потенциометров со значениями углов соответствующих сервоприводов.
Процедура сопоставления выполнялась следующим образом:
Подготовка
При отключённом питании сервоприводов и отсоединённом USB‑кабеле от компьютера оставляли подключённым к плате PCA9685 только один из четырех сервоприводов (например, первый). Остальные суставы, не связанные с выбранным приводом, фиксировали для обеспечения безопасности экспериментаторов и сохранности манипулятора.
Выбор начального угла
Поскольку начальное положение сервопривода в манипуляторе было неизвестно, стартовый угол был задан равным 90°, как среднее значение диапазона сервоприводов от 0° и 180°.
Скетч sopostavlenie_ugla_servo_i_potenciometra.ino:
- Ставит серво на канале 0 в начальный угол 90 градусов.
- Ждёт, пока будет введён угол (от 0 до 180) в монитор порта (Serial Monitor) Arduino IDE.
- Плавно поворачивает сервопривод в заданное положение (движение идёт пошагово, с задержкой 20 мс между шагами).
- После остановки серво 5 раз подряд считывает значение с потенциометра (подключён к аналоговому входу A0) и выводит показания (значение потенциометра и напряжение).
- Вычисляет среднее значение потенциометра из пяти замеров и тоже выводит его.
Меры безопасности
Скетч загружали на плату Arduino по USB при выключенном питании сервоприводов, после чего USB‑кабель отсоединяли. Перед подачей питания на сервопривод и повторным подключением USB для сбора данных строго соблюдали технику безопасности: из зоны возможного движения манипулятора убирали всех, так как при включении, сервопривод мгновенно устанавливался в стартовый угол.
Конечно же было бы правильней пересобрать манипулятор и в процессе сборки ориентировать сервоприводы, чтобы знать в каком положении какие углы сервоприводов. Но в данном случае следовали логике "анонимного" манипулятора.
Определение направления вращения и граничных углов
Через монитор порта (Serial Monitor) задавали угол на 5° больше или меньше стартового, чтобы определить направление движения. Затем, аккуратно увеличивая шаг до 10°, находили физические ограничения (граничные углы) для данного сервопривода.
Сбор данных
Начиная от любого из граничных углов, последовательно задавали углы с шагом 10°, записывая показания потенциометра, выводимые скетчем в монитор порта. Полученные пары значений заносили в таблицу (см. Рис. 10)
Переход к следующему сервоприводу
После завершения измерений отключали USB и питание сервопривода. Отсоединяли откалиброванный серво от платы (канал 0), фиксировали его сустав, подключали следующий сервопривод к плате (канал 0), а в скетче меняли пин const int potPin = A0 на пин потенциометра, соответствующего, следующему сервоприводу и повторяли процедуру калибровки.
В результате для четырёх сервоприводов были собраны данные о соответствии значений потенциометров и углов сервоприводов, а так же, физические ограничения для каждого сервопривода в углах. (см. Рис. 10).

На рисунке 10, например для серво 1 граничные углы 0 и 130 градусов.
В дальнейших экспериментах понадобиться удобное для старта исходное положение манипулятора, поэтому, в процессе сопоставления, записали для себя, при каких углах сервоприводов, манипулятор принимает П-образное положение.
Зависимость значений потенциометров от углов сервоприводов оказалась близкой к линейной.
Формулы перевода:
Серво 1:
pot1 = 523 - (angle1 * 3.81)
angle1 = (523 - pot1) / 3.81
Серво 2:
pot2 = 869 - (angle2 * 3.65)
angle2 = (869 - pot2) / 3.65
Серво 3:
pot3 = 740 - (angle3 * 3.93)
angle3 = (740 - pot3) / 3.93
Серво 4:
pot4 = 963 - (angle4 * 3.89)
angle4 = (963 - pot4) / 3.89Теперь, когда мы можем пересчитать наши ручные траектории из значений потенциометра в углы, можно запускать манипулятор по любой из ручных траекторий.
Для визуального анализа траекторий была создана упрощённая URDF-модель (файл 5dof_manipulator_120326.urdf , описывающий геометрию и кинематику робота), которую понимают и поддерживают симуляторы.

На рисунке 11 в симуляторе визуализированы ручные траектории. Хорошо видно, что они имеют дугообразную форму с погрешностями (дрожание). А на рисунке 12 заметны погрешности по точности для целевых точек: желтые окончания траекторий (центры розовых кружков) смещены относительно зеленых точек (углы клеток).
Все эти погрешности связаны не только с «ручной» сборкой траекторий, но и с люфтами при сопоставлении значений потенциометров и углов, а также с усреднением в калибровочных формулах, основанных на линейной зависимости.
Дрожание можно сгладить математически. Важнее, что траектории, созданные вручную, имеют дугообразную форму. А что делать с погрешностями целевых точек посмотрим далее.

Возьмём любую траекторию "как есть" и запустим реального робота, например, от точки «База» до точки 26.

На рисунке 13 показан кадр видео работы манипулятора: поз. 26 — куда в идеале должен был подойти щуп, и точка R — место, куда он подошёл на самом деле.
На рисунке 13 видно, что в реальности погрешность, значительно больше, чем в симуляции, поскольку используемая в симуляции модель манипулятора лишь приближённо учитывает его физические особенности.
В итоге у нас есть траектория от базы к точке 26, но нет точных углов сервоприводов для позиции 26. Чтобы оказаться в позиции 26, необходимы откалибровать углы сервоприводов для этой точки.
После первого этапа у нас уже есть траектории дугообразной формы и неточные целевые точки.
В процессе калибровки необходимо найти такие углы сервоприводов, при которых манипулятор гарантированно попадает в нужную точку.
Здесь мы не пытаемся пересчитать всю траекторию. Мы работаем только с целевой точкой = конечной точкой траектории (200-тый шаг). У нас есть датафрейм (см. Рис. 7) со всеми траекториями, из которого мы можем забрать для траектории база -> 26 значения потенциометров на шаге 200.
Например, для целевой точки 26 пересчитанные значения потенциометра в углы по нашим формулам были такими: [118.9, 95.34, 90.08, 100.51]°
А после калибровки они стали вот такими: [116, 94, 87, 95]°
Для калибровки использовали скетч 4_servo_vistavlenie.ino:
При запуске устанавливает четыре сервопривода в углы [65, 115, 92, 110]° исходного П-образного положения манипулятора при котором щуп в базовой точке.
Ждёт команду из монитора порта:
если ввести 4 угла через запятую (например, 118, 95, 90, 100), то одновременно и плавно повернёт все четыре серво в заданные положения.
Примечание: в скетче используются 4 сервопривода на каналах PCA9685:#define SERVO_CHANNEL_0 0 // серво1
#define SERVO_CHANNEL_1 2 // серво2
#define SERVO_CHANNEL_2 4 // серво3
#define SERVO_CHANNEL_3 6 // серво4
Процесс калибровки (подгонки)
Учитывали, что при запуске манипулятора с нашим скетчем резко встанет в исходное П-образное положение, поэтому, перед подключением питания сервоприводов и USB, выставляли манипулятор примерно в исходное П-образное положение, чтобы лишний раз не перегружать сервоприводы.
Затем:
Через монитор IDE переводили манипулятор в неоткалиброванную точку 26 (200-ый шаг траектории: база -> 26 пересчитанный в углы). Манипулятор перемещается в точку R (см. Рис. 13)
Увеличивали/уменьшали углы сервоприводов на 2 - 4 градуса, смотрели куда сместился щуп, корректировали (обычно по одному значению из четырех за раз для большего контроля процесса), снова запускали манипулятор...т.д. пока щуп не оказался в нужной точке 26. (см. Рис. 13)
В результате, через несколько итераций, находили набор углов, при которых манипулятор точно попадает в нужную точку.
Теперь у нас есть: "ручная" точка R из траектории на шаге 200 и "реальная" откалиброванная целевая точка 26. Разница между ними — это и есть комплексная ошибка системы из-за целого ряда факторов.
К ручной траектории добавим на 195 шаге 5 точек (для плавности). Пятая точка - это наша откалиброванная точка 26 с углами [116, 94, 87, 95]. В результате получается, что сначала манипулятор идёт по ручной траектории до 194 шага, а в конце делает резкое движение в сторону и через 5 шагов точно попадает в целевую откалиброванную точку. (cм. Видео 1).
Видео 1. Ручная траектория с корректировкой на последних 5 шагах.
Для запуска манипулятора по ручной траектории с корректировкой к откалиброванной точке использовался скрипт manual_traektorii_5_end.py со скетчем manipul_lstm_no_pot.ino
В скрипте на Python есть два режима: SIMULATION или REAL_ROBOT. Первый для запуска симулятора, где траектория проигрывается на 3D‑модели, а второй для управление физическим манипулятором через последовательный порт.
Оба режима предназначена для воспроизведения заранее записанной траектории движений робота, а в конце — точной доводки до заданной точки.
Сначала Python загружает из файла manual_trajectories_combined.npy одну из ранее записанных ручных траекторий. Значения потенциометров переводятся в реальные углы сервоприводов.
Python на каждом шаге траектории отправляет Arduino команду SET_ANG с четырьмя углами. Arduino, получив команду сразу же устанавливает сервоприводы в нужные углы.
Таким образом происходит движение от базовой точки к целевой неоткалиброванной точке 26. Но только до шага 194, а начиная с 195‑го шага программа больше не использует записанные углы из файла.
Вместо этого программа выполняет линейную интерполяцию от точки на шаге 194 к заранее заданной целевой точке TARGET_FINAL_REAL = [116, 94, 87, 95]. В нашем случае это откалиброванная точка 26. Это гарантированно приводит манипулятор точно в нужное положение.
У нас есть точная целевая точка, но не правильная форма движения. Если бы на клетке h7 стояла фигура, то манипулятор ее бы сбил.
То есть. когда мы запускаем ручную траекторию "как есть", то мы либо промахиваемся, либо нам приходится на каком то этапе принудительно "сворачивать" к целевой точке, получая в итоге не совсем ту траекторию, на которую мы рассчитывали.
После второго этапа у нас есть:
ручные траектории
откалиброванная целевая точка
Теперь задача — объединить форму ручных траекторий и откалиброванную точку в одну систему, которая сможет самостоятельно строить траекторию, а точнее, по текущему положению манипулятора предсказывать следующий шаг траектории.
От нейросети мы ожидаем, что она в процессе обучения не запоминать ручные траектории, а сможет понять, как они устроены, то есть выучит типичную "линию поведения" траекторий.
Вместо того, чтобы генерировать все шаги траектории сразу, нейросеть на каждом шаге отвечает на вопрос: «куда двигаться дальше?»
На вход нейросети подаётся:
текущее положение - четыре значения потенциометров. ("мы сейчас здесь")
номер шага в траектории. ("прошли уже, столько то, шагов")
целевая точка - четыре значения потенциометров. ("движемся туда")
На выходе из нейросети: положение на следующем шаге (4 значения потенциометров).
Дальше процесс повторяется, и траектория строится шаг за шагом:
предсказали следующий шаг
подставили его, как текущий
снова сделали предсказание
т.д. до конца траектории.
Данные для обучения берутся из тех же ручных траекторий. У нас есть датафрейм, в котором удобным образом собраны шаги всех траекторий, в том числе значения для целевых точек на 200-м шаге траекторий. (см. Рис. 14)

Из каждой траектории создавались обучающие примеры (признаки -> цель):
признаки: текущее состояние (4 значения потенциометров) + шаг траектории, на котором сейчас находится траектория + 4 значения потенциометров для целевой точки
цель: следующее состояние (4 значения потенциометров)
Так мы сможем объяснить нейросети: "когда у нас вот такие значения признаков, то целевое значение для следующего шага вот такие."

На рисунке 15 показан механизм формирования пар для обучения:
X - девять признаков
Y - цель: 4 значения
Пример обучающей пары: [350, 574, 484, 552, 1, 343, 522, 476, 514] - > [344, 524, 477, 514]
Следующая обучающая пара, в которой текущие значения — это предыдущая цель :[350, 574, 484, 552, 2, 344, 524, 477, 514] - > [344, 525, 477, 515]..........и т.д.
Здесь важно, что
нейросеть всегда знает цель. Это позволяет ей не просто повторять движение, а двигаться в нужном направлении.
нейросеть всегда знает шаг. Это позволяет уменьшить "недолеты" и "перелеты" относительно целевой точки.
Значения обучающих пар нормализуются (приводятся к значениям от 0 до 1) для удобства обучения.
Когда мы по каким то признакам предсказываем другие целевые значения, то по факту это задача регрессии. Но нейросеть была построена со слоем LSTM.
LSTM - это, специальный слой с памятью, используемый во временных рядах, который умеет учитывать не только текущие значения, но и, особенным образом, учитывать предыдущие значения.
То есть, когда мы 51 шаг траектории предсказываем не только по текущему шагу 50, а еще и по нескольким предыдущим шагам траектории 49, 48, 47, 46, 45 и т.д. Сколько шагом брать определяется экспериментальным путем.
Имея такие временные последовательности, можно рассчитывать, что нейросеть будет лучше понимать закономерности траекторий и точнее предсказывать.
Все "способности" слоя LSTM по факту не понадобились, но он был оставлен в нейросети для возможности реализации более сложных сценариев при развитии проекта.
Все данные (обучающие пары) делятся на три части: 70% — обучение (на них нейросеть учится), 15% — валидация (контроль, чтобы не переобучиться), 15% — тест (их нейроссеть не видела и на ней происходит финальная проверка качества).
вход: 9 чисел (4 текущих угла + 4 целевых + 1 параметр прогресса),
два скрытых слоя по 512 нейронов,
выход: 4 числа — предсказанные углы следующего шага.
Дополнительно используется dropout — случайное «отключение» части нейронов во время обучения. Это помогает нейросети не запоминать данные слишком буквально и лучше обобщать.
Если использовать стандартную ошибку (MSE - насколько сильно предсказанные значения отличаются от реальных значений), нейросеть будет считать все шаги траектории одинаково важными. Но в реальной задаче это не так.
В начале движения небольшая ошибка не страшна — её можно компенсировать дальше.
А вот в конце — ошибка критична: манипулятор промахивается.
Чтобы это учесть, использовалась взвешенная MSE.
Реализация:
в начале траектории вес = 1
в конце — до 2
между ними вес плавно растёт
То есть ошибки в конце траектории штрафуются сильнее и поэтому нейросеть в процессе обучения "прилагает больше усилий" для минимизации ошибки, на финальных шагах траекторий.
Код подготовки данных и обучения нейросети реализован в файле lstm_manipulytor.ipynb

Получилась достаточно точная модель нейросети с небольшой ошибкой на вариационной и тестовой выборках, то есть модель достаточно точно предсказывала следующий шаг, в том числе на тестовой выборке, которую она никогда не видела.
Нейросеть предсказывает шаг с небольшой ошибкой. Но траектория состоит из ~200 шагов. Ошибка начинает накапливаться и к концу траектории манипулятор может заметно «уехать» от цели.
В таблице 1 собран список всех траекторий, отсортированный по накопленной ошибке.
Как это считалось?
Для каждой из 34 целевых точек нейросеть шаг за шагом (всего 200 шагов) предсказывала траекторию, каждый раз опираясь на предыдущее состояние. В итоге получалось 34 траектории — по одной для каждой целевой точки.
Таблица 1. Список траекторий, ранжированный по накопительной ошибке.
Траектория | Средняя ошибка | pot1 | pot2 | pot3 | pot4 |
|---|---|---|---|---|---|
23 | 0.004480 | 0.0048 | 0.0033 | 0.0084 | 0.0014 |
13 | 0.005133 | 0.0033 | 0.0071 | 0.0050 | 0.0052 |
9 | 0.005337 | 0.0055 | 0.0084 | 0.0001 | 0.0074 |
6 | 0.005342 | 0.0008 | 0.0078 | 0.0077 | 0.0051 |
21 | 0.006003 | 0.0011 | 0.0064 | 0.0088 | 0.0076 |
12 | 0.006005 | 0.0036 | 0.0121 | 0.0004 | 0.0078 |
34 | 0.006210 | 0.0134 | 0.0004 | 0.0079 | 0.0030 |
33 | 0.006338 | 0.0130 | 0.0008 | 0.0103 | 0.0013 |
16 | 0.006402 | 0.0154 | 0.0073 | 0.0011 | 0.0019 |
... | ..... | ..... | .... | ....... | ....... |
25 | 0.018720 | 0.0036 | 0.0272 | 0.0245 | 0.0196 |
15 | 0.029934 | 0.0275 | 0.0446 | 0.0229 | 0.0248 |
5 | 0.031308 | 0.0157 | 0.0477 | 0.0353 | 0.0265 |
Дальше: сравниваем, куда манипулятор должен был прийти и куда он в итоге «пришёл» по предсказаниям нейросети. Разница между этими значениями и есть ошибка.
Верх таблицы — самые точные траектории, где нейросеть почти идеально попадает в цель. В реальности ошибка примерно до 5 мм — вполне приемлемо, особенно для нашего манипулятора.
Внизу — те случаи, где она заметно промахивается. Причём здесь уже не так важно, на сколько именно — сам факт, что он не попадает в точку, делает такие траектории бесполезными.
Поэтому просто «как есть» этот результат нас не устраивает. Чтобы довести движение до нужной точности, дальше вводим дополнительный механизм — прицеливание.
Чтобы компенсировать накопление ошибки, был введен простой, но очень эффективный механизм.
Начиная с определённого шага (например, со 120-го), мы начинаем смешивать предсказание нейросети и направление на целевую точку.
Сначала вклад цели небольшой, но по мере движения он постепенно увеличивается.
Условно это выглядит так:
на ~120 шаге: 95% — нейросеть, 5% — цель
ближе к концу: примерно 50/50
на финальном шаге: 100% — цель (здесь используются откалиброванные значения)
Важно, что нейросеть предсказывает значения потенциометров, которые затем переводятся в углы по нашим формулам. Эти преобразования сами по себе дают погрешность — из-за неточной калибровки.
Поэтому по мере движения мы постепенно «перетягиваем управление» от нейросети к откалиброванным значениям.
В итоге на последнем шаге вклад нейросети становится нулевым и используются, только заранее откалиброванные углы целевой точки, и манипулятор точно приходит в заданное положение.
В результате в начале до 120 шага траектория имеет обобщенную нейросейтью траекторию, а дальше (после 120 шага) манипулятор гарантированно попадает в точку корректируя форму траектории.
Получается комбинация: нейросеть отвечает за форму движения, калибровка — за точность попадания. Они решают разные задачи, но вместе дают результат.
Какими получаются предсказанные траектории и насколько сильно ломается траектория из-за корректировки посмотрим чуть позже на практике.
На этом этапе система начинает не просто повторять движения, а сама строит траекторию к цели, опираясь на примеры.
Интересно то, что нейросеть может работать с точками, которых не было в обучении. То есть мы можем выбирать, как целевые - любые точки в рабочей области манипулятора. Это является преимущество нейросети - возможность перехода от "записанных движений" к обобщённому управлению.
Для демонстрации результатов были откалиброваны точки в центе клеток H6 и F4.
Скрипт для демонстрации результатов
Скрипт sim_bot_lstm_512_no_adc_gibrid_1_step_krug.py в паре с скетчем manipul_lstm_no_pot.ino реализует гибридное управление движением манипулятора: сначала движение строится по предсказаниям обученной нейросети, а на заключительном участке ('start_step': 120 шаге из 200) плавно смешивается с целевой точкой (blending), чтобы точно попасть в цель.
Код работает в двух режимах:
SIMULATION – визуализация (шахматная доска, маркеры точек).
REAL_ROBOT – управление физическим роботом через Arduino.
Взаимодействие с Arduino: SET_ANG,угол1,угол2,угол3,угол4 – мгновенная установка углов (используется для каждого шага траектории).
Последовательность движений
Программа выполняет четыре перемещения (каждое с генерацией нейросетью траектории + прицеливание):
HOME → POINT1 (первая целевая точка).
POINT1 → HOME (возврат в базу).
HOME → POINT2 (вторая целевая точка).
POINT2 → HOME (финальный возврат в базу).
В реальном режиме REAL_ROBOTтакже реализовано досрочное завершение: если манипулятор достиг цели с допуском 0,1° и стабильно держится один шаг, генерация прерывается, чтобы не тратить время.
На видео 2 можно посмотреть точность попадания в целевые точки.
На рисунке 17 показана симуляция с формой траекторий.
На видео 3 тоже самое, что на видео 2, но уже с работой захвата.
Видео 2. Перемещение между клетками H6 и F4.

Видео 3. Перемещение фигуры между клетками H6 и F4.
Примечание: запуск манипулятора на видео 3 сделан с помощью скрипта sim_bot_lstm_512_no_adc_gibrid_1_step_krug_zahvat.py и скетча man_lstm_nopot_5servo.ino , который рассчитан на 5 сервоприводов на каналах:
#define SERVO_CHANNEL_0 2 // Серво 1 (база)
#define SERVO_CHANNEL_1 4 // Серво 2 (плечо)
#define SERVO_CHANNEL_2 6 // Серво 3 (предплечье)
#define SERVO_CHANNEL_3 8 // Серво 4 (запястье)
#define SERVO_CHANNEL_4 0 // Серво 5 (захват)Не смотря, на то, что нейросеть не училась на точках - центрах ячеек H6 и F4, она "уловила" общий паттерн траекторий и умеет строить дугообразную форму для неизвестных ей целевых точек.
Прицеливание позволяет компенсировать накопительную ошибку нейросети, а так же, системные ошибки, и точно попадать в нужные точки.
При этом форма траектории получается сложно-дугообразной в отличие от простой дугообразной, которая закладывалась при ручной имитации на этапе сбора ручных траекторий. Такая форма для нашей задачи не является проблемой, зато критичен вертикальный подъем и спуск фигур в начале и в конце траекторий. С этой точки зрения часть траекторий более-менее подходящие, но требующие доработки, другая же часть траекторий - точно не походит.
Например, на видео 3 при перемещении из клетки H6 на базу, сразу понятно, что будут сбиты все фигуры из-за преобладающего горизонтального движения в начале траектории.
Вывод: подход частично работает, но нужно улучшать форму траекторий, чтобы в полной мере, а не частично, решить задачу переноса шахматных фигур из любой точки рабочей области в любую.
Перед тем, как обсудить возможные способы улучшения формы траекторий, оценим надёжность имеющейся нейросети. Для этого из таблицы 1 возьмём траектории 5 и 15, у которых самые большие накопительные ошибки.
После калибровки целевых точек 5 и 15 запустили манипулятор по этим траекториям.
Был использован скрипт lstm_gibrid_krug_traektorii_load.py, который умеет сначала создать (в режиме MODE = 'REAL_ROBOT) траекторию и записать ее в файл, а при следующем запуске (при SAVE_TRAJECTORIES = False LOAD_TRAJECTORIES = True ) считать траекторию из файла и сразу отправлять ее на манипулятор. Хотя нейросеть относительно небольшая, генерировать траекторию «на лету» не всегда удобно. Использование предзаписанной траектории добавляет скорости и стабильности.
Этот скрипт работает в паре с скетчем man_lstm_nopot_5servo.ino
Для большей плавности траектории, механизм прицеливания был включен пораньше, с 50-го шага при параметре power=1.0 , что обеспечило линейное - равномерное "подмешивание" смещения к целевой точки на всем протяжении оставшейся траектории.
На видео 4 можно оценить точность, а на рисунке 18 форму траекторий.
Видео 4. Траектории с большими накопительным ошибками 15 и 5.

Траектории 5 и 15 с большими накопительными ошибками оказались практически такими же, как и с небольшими. У них та же проблема - перемещение от точки до базы происходит с недостаточно резким набором высоты.
После проделанных эксперимента стало понятно, что повлиять на качество траекторий можно несколькими способами. Часть из них относится к данным, часть - к самой нейросети, а часть - к способу сбора новых траекторий.
Добавить траектории из центров клеток
Один из очевидных вариантов — расширить выборку, добавив траектории, сделанные не только из углов клеток, но и из их центров. Это увеличит количество примеров и даст нейросети больше вариантов движения. Из-за люфтов и погрешностей механики центр клетки, полученный при "ручном" сборе, и откалиброванная реальная точка центра клетки — это не одно и то же.
Значит нейросеть будет учиться обобщать траектории на одних данных, а предсказывать на других. То есть подсказок для нейросети не будет и без качественного обобщения не обойтись.
После сбора необходимо обучить нейросеть на новых данных.
В некотором смысле роль нейросети в этой схеме — частично компенсировать люфты и другие системные ошибки. Она учится на траекториях, уже содержащих эти отклонения, и поэтому может воспроизводить движение, ближе к реальному поведению манипулятора.
Собирать траекторию с более явным вертикальным подъёмом и спуском
Обобщение траекторий нейросетью, похоже, привело к тому, что она строить траектории более сглаженные, чем закладывалось при ручном сборе. Поэтому необходимо при ручном сборе имитировать более выраженный подъём в начале движения и более явный спуск в конце.
Обучить две отдельные модели для прямого и обратного движения
Эксперименты показали, что текущая нейросеть по-разному строит траектории при движении к целевой точке и при возврате к базе. Это может приводить к конфликтам: одна универсальная модель пытается усреднить два разных типа движения.
Возможное решение — вместо одной модели использовать две специализированные: одну для движения от базы к целевой точке, а вторую — для движения от целевых точек к базе.
Для этого исходные 68 траекторий можно разделить на два набора по 34. Дополнительно выборку можно расширить: взять траектории «база → точка» и перевернуть их во времени, получив «точка → база», и наоборот.
В результате для каждой задачи (вперёд и назад) можно получить до 68 траекторий и обучить две отдельные модели.
Использовать LSTM по назначению
В конструкции нейросети уже есть LSTM слой, логично попробовать подавать на вход не только текущий шаг, а короткое окно из нескольких предыдущих шагов — например 5–10. Для этого нужно сформировать соответствующее обучающие пары и обучить нейросеть.
Тогда модель будет видеть не только текущее положение, но и направление, в котором траектория уже развивается. Это особенно полезно на участках, где движение плавно меняется, а не просто идет по прямой.
Иными словами, сеть будет учиться не только на состоянии «где я сейчас», но и на контексте «как я сюда пришёл».
Добрать данные в проблемных точка
На практике оказывается, что не все целевые точки одинаково удобны.
Часть траекторий получается почти идеальной, а часть — заметно хуже: чаще всего это точки на периферии рабочей зоны, где работа механики манипулятора отличается.
В таких местах полезно собрать дополнительные траектории вокруг проблемных точек. Это поможет модели увидеть не только «средний» случай, но и редкие сложные варианты движения.
По сути, это способ "подсказать" нейросети: вот здесь траектории тоже бывают другими, не пытайся всё сводить к одному шаблону.
В этой статье хотелось показать на простом примере, как работает имитационное обучение и где оно может быть полезно в реальных задачах.
В качестве объекта был взят заведомо неидеальный манипулятор — недорогая конструкция без энкодеров, с люфтами и неопределённой геометрией. Вместо попытки построить точную модель мы рассмотрели альтернативный подход: научить систему повторять движения по примерам.
В ходе проекта удалось собрать гибридную схему, включающую:
ручной сбор траекторий
калибровка целевых точек
нейросеть, предсказывающая движение
механизм «прицеливания», компенсирующий ошибки
Каждый элемент по отдельности не решает задачу полностью, но вместе они дали работоспособный результат, который можно дальше развивать и доводить до полной практической применимости за счёт улучшения формы траекторий.
Интересно, что такое решение появилось именно благодаря ограничениям системы. Именно они заставили сочетать несколько подходов и искать баланс между простотой и точностью.
В итоге нейросеть помогает обобщать форму движения, а калибровка и прицеливание отвечают за точность попадания. Это показывает, что в прикладных задачах выигрывать может не один «правильный» метод, а комбинация нескольких, иногда совсем простых, подходов.
Код проекта доступен на GitHub: https://github.com/Iskatel-potenzialov/manipulytor