惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Threatpost
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Engineering at Meta
Engineering at Meta
T
The Blog of Author Tim Ferriss
Recent Announcements
Recent Announcements
G
Google Developers Blog
Google DeepMind News
Google DeepMind News
The Register - Security
The Register - Security
MongoDB | Blog
MongoDB | Blog
U
Unit 42
B
Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
L
LangChain Blog
Stack Overflow Blog
Stack Overflow Blog
P
Privacy International News Feed
L
LINUX DO - 最新话题
博客园_首页
博客园 - Franky
大猫的无限游戏
大猫的无限游戏
小众软件
小众软件
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tor Project blog
V
Visual Studio Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
P
Privacy & Cybersecurity Law Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
K
Kaspersky official blog
C
Cisco Blogs
博客园 - 【当耐特】
阮一峰的网络日志
阮一峰的网络日志
I
Intezer
罗磊的独立博客
MyScale Blog
MyScale Blog
Last Week in AI
Last Week in AI
A
About on SuperTechFans
G
GRAHAM CLULEY
Y
Y Combinator Blog
Microsoft Security Blog
Microsoft Security Blog
GbyAI
GbyAI
T
Threat Research - Cisco Blogs
P
Proofpoint News Feed
D
DataBreaches.Net
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
AWS News Blog
AWS News Blog
I
InfoQ
T
The Exploit Database - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
博客园 - 叶小钗
Project Zero
Project Zero

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как я загрузил компактную открытую LLM в робота и сказал ему ходить (пока только в симуляции)
Imperius14 · 2026-05-18 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение18 мин

Охват и читатели25

Кейс

Снаружи глупенький котенок, внутри машина для убийств езды и хватания кубов

Снаружи глупенький котенок, внутри машина для убийств езды и хватания кубов

Без лишних предисловий приступаем.

Все артефакты: веса на Hugging Face и исходники на Codeberg, как обычно будет приложены в конце статьи.

О чем статья?

Расскажу, как я обучил языковую модель gemma-3 на 270 млн параметров (от Google) управлять гусеничным роботом с робо-рукой в среде Mujoco, через команды на естественном языке от человека.

Он может свободно перемещаться по карте, идти вперед и назад, поворачиваться влево-вправо, хватать и опускать предметы.

Ниже будет приложена видео-демонстрация длиной 1 минуту.

Видео-демонстрация, в конце робот случайно уехал за кадр

Видео-демонстрация, в конце робот случайно уехал за кадр

Как возникла идея?

У меня лежит дома DIY-набор гусеничного робота с робо-клешней, и когда я познакомился с моделью gemma-3-270m, которая удивительно хорошо соображает для своего размера - у меня возникла идея ее туда загрузить и заставить этим роботом управлять.

А вот и DIY-набор гусеничного робота с клешней

А вот и DIY-набор гусеничного робота с клешней


Эта модель, по моим расчетам, без проблем влезла бы в Raspberry Pi Zero 2 W. Идея заключалась в том чтобы сделать интеллектуального робота, который может передвигаться по командам человека, описанными в свободной форме, навроде:

  • "поверни налево"

  • "пройди вперед на 10 метров"

  • "подними коробку"

Но перед тем как копаться в железе, стоило проверить возможности самой модели, благо все это можно проверить заранее в симуляции, об этом и будет статья.

Начинаем эксперименты

Для начала стоило проверить, как дефолтный Gemma-3-270m без файнтюнинга справляется с соблюдением инструкции.

User:
You are a robot controller. Convert user commands to JSON.
Available actions:
 - {"action": "move", "direction": "forward|backward", "distance_cm":
 - {"action": "rotate", "direction": "left|right", "angle_deg":
 - {"action": "pick_up", "target": "
 - {"action": "place", "target": "
 - {"action": "stop"}
Output only JSON, no explanations. Now just say Ready, and then the commands will follow.

Model: Ready
User:  Command: drive forward 30 centimeters
Model: Drive forward 30 centimeters.
User:  Are you idiot?
Model: No.

Как вы можете судить по этому тарантиновскому диалогу - модель с задачей справляется не очень. Нужен файн-тюнинг.

Помимо самой модели, нужно было определиться со средой, где модель будет действовать.

Выбор пал на Mujoco - отличный симулятор для роботехники, физика там не такая реалистичная, как в Isaac Sim от NVIDIA, но при этом все летает на обычном ноутбуке, плюсом я уже был знаком с ним по моим прошлым пет-проектам.

На сайте документации Mujoco можно иногда увидеть поистине странные вещи...

На сайте документации Mujoco можно иногда увидеть поистине странные вещи...

Насчет языка команд тоже думать долго не пришлось. Gemma-3-270m очень маленькая модель, которая лучше всего работает с английским текстом, нет смысла "напрягать" ее веса еще для того чтобы лучше понимать русский язык, может получиться так, что ей не хватит места для всего сразу. Так что язык для команд будет чисто английский.

Датасет будет генерироваться синтетический, через доступные на OpenRouter бесплатно мощные модели gpt-oss-120b (от OpenAI) и nemotron-super-120b (от NVIDIA).

По итогу нашу начальную цель можно сформулировать так

Зафайнтюнить Gemma-3 270M на трансляцию английских команд в валидный JSON, управляющий гусеничным роботом в MuJoCo, на основе синтетического датасета, сгенерированного моделями gpt-oss-120b и nemotron-super-120b.

Эксперимент будет разделен на 2 фазы.

  • 1 фаза: создание гусеничного робота в Mujoco, без руки и создание синтетического датасета для его управления, обучение gemma-3 270M на основе этого датасета, проверка обученной модели в симуляции.

  • 2 фаза: добавление к созданному в Mujoco гусеничному роботу конечности-клешни, добавление двух новых действий - поднять и опустить, генерация дополненного синтетического датасета где будут описаны эти действия, повторный файн-тюнинг модели, повторная проверка обученной модели в симуляции с новыми действиями.

1 фаза - Генерируем синтетический датасет

Цель шага: получить пары {"instruction": <англ. текст>, "output": <JSON команд>}, на которых дообучается Gemma-3-270M.

Источник — синтетика: ~70 ручных примеров → инфляция крупной моделью (120B) через OpenRouter → жёсткая валидация по JSON Schema.

1. Реальный промпт генератора (что уходит в модель-генератор синтетики)

Воспроизводится командой python -m dataset_gen.generate --dry-run.

SYSTEM (схема + правила)

Начинается полной JSON Schema, для примера будет взят отрывок схемы движения (назад-вперед):

{
  "type": "object", "required": ["commands"],
  "properties": {"commands": {"type": "array",
    "items": {"$ref": "#/definitions/command"}}},
  "definitions": {
    "command": {"oneOf": [
      {"$ref": "#/definitions/move"}, {"$ref": "#/definitions/turn"},
      {"$ref": "#/definitions/stop"}, {"$ref": "#/definitions/wait"},
      {"$ref": "#/definitions/grasp"}, {"$ref": "#/definitions/release"}
    ]},
    "move": {"required": ["action","direction","distance_m"],
      "properties": {"action": {"const": "move"},
        "direction": {"enum": ["forward","backward"]},
        "distance_m": {"type": "number", "exclusiveMinimum": 0,
                       "maximum": 100},
        "speed": {"enum": ["slow","normal","fast"]}}}
  }
}

Затем — текстовые правила:

ROLE: You translate ONE English natural-language instruction for a
tracked robot into a JSON object that strictly validates against the
schema above.

HARD RULES:
- Output ONLY the JSON object, no prose, no markdown fences.
- Always wrap commands in {"commands": [ ... ]}.
- distance_m and angle_deg are ALWAYS positive; sign via "direction".
- "speed" optional enum (slow|normal|fast) — only if pace implied.
- left = counter-clockwise (CCW). right = clockwise (CW).

INTERPRETATION CONVENTIONS:
- Distance: unspecified -> 1.0 m; "a bit"/"a little" -> 0.5;
  "a touch"/"slightly" -> 0.3; "a meter and a half" -> 1.5;
  word numbers -> the number ("two" -> 2.0).
- Angle: "quarter turn" -> 90; "half turn"/"turn around" -> 180;
  "full turn"/"360"/"spin around" -> 360. Unstated dir -> "right".
- Speed: slowly/creep/gently -> "slow"; quickly/fast/rush -> "fast".
- "stop"/"halt"/"freeze" -> [{"action":"stop"}].
- "wait/pause for N seconds" -> [{"action":"wait","duration_s":N}].
- Multi-step ("X then Y") -> ordered list.

USER (пример + задание)

EXAMPLES (format reference, do not copy):
{"instruction": "turn left then pick up the cube", "output":
 {"commands": [{"action":"turn","direction":"left","angle_deg":90},
               {"action":"grasp"}]}}
{"instruction": "put it down", "output":
 {"commands": [{"action":"release"}]}}
{"instruction": "do a half turn", "output":
 {"commands": [{"action":"turn","direction":"right","angle_deg":180}]}}

TASK: Produce 8 NEW and DIVERSE training pairs. Vary phrasing heavily:
imperative, polite, terse, conversational, robotic, with/without
units, word-numbers vs digits, multi-step, and include some
out-of-scope/nonsense mapped to {"commands": []}. Do NOT copy the
examples verbatim.

Return ONLY a JSON array; each element:
  {"instruction": "<english text>", "output": <command object>}

Пример берётся случайной выборкой (--fewshot N), поэтому каждый батч видит разные примеры → разнообразие.

2. Механика пайплайна (dataset_gen/generate.py)

  1. build_messages() собирает SYSTEM+USER (см. выше), few-shot случайно из сидов.

  2. POST в OpenRouter (stdlib urllib, без зависимостей). Ротация двух 120B: openai/gpt-oss-120b:freenvidia/nemotron-3-super-120b-a12b:free.

  3. Ответ → вытащить JSON-массив → для КАЖДОЙ пары jsonschema валидация по той же схеме → дедуп по нормализованной инструкции → дозапись в JSONL.

3. Итоговый датасет (data/dataset.jsonl)

Всего примеров (пар инструкция→JSON): 2505

Команды по типу (суммарно по всем командам во всех парах):

действие

кол-во команд

move

1938

turn

1133

wait

456

stop

281

По итогу 4 действия. Разберем некоторые из них, и модификаторы к ним, более подробно.

wait - ожидание в секундах

Команда wait имеет параметр duration_s, где модель выставляет ожидание в секундах, ничего сложного. Для примера разберем такую последовательность команд из датасета.

"Move backward one meter, then pause for three seconds, then move forward one meter"
  -> [{"action":"move","direction":"backward","distance_m":1.0},
      {"action":"wait","duration_s":3},
      {"action":"move","direction":"forward","distance_m":1.0}]

turn — поворот корпуса на месте

Одно из 4 действий Phase-1. Робот разворачивается вокруг себя (diff-drive: борта крутятся встречно), меняя курс (yaw) на заданный угол. В отличие от move (поступательное вперёд/назад), turn — только вращение.

Схема:

{"action":"turn",
 "direction":"left"|"right",   // left = CCW (ω>0), right = CW
 "angle_deg": >0 и ≤360,       // всегда положительный, знак — в direction
 "speed": "slow"|"normal"|"fast"   // опционально
}

Примеры из датасета: "turn left 90 degrees"{"action":"turn","direction":"left","angle_deg":90}; "Rotate 360 degrees to the left."angle_deg:360. В Phase-1: 1133 команды turn, 682 пары начинаются с поворота.

Регулировка скорости — опциональный enum speed

Скорость задаётся не числом, а enum'ом {slow, normal, fast} — на move и turnstop/wait его нет).

Это сознательное решение: модель не должна угадывать м/с .

«as fast as you can» → ???

«as fast as you can» → ???

Модель ставит speed только если темп подразумевается в тексте («slowly»→slow, «quickly/rush/swiftly»→fast); иначе поле опускается и контроллер берёт normal.

speed

move (линейная)

turn (угловая)

slow

0.2 м/с

0.5 рад/с

normal (default)

0.5 м/с

1.0 рад/с

fast

1.0 м/с

2.0 рад/с

В датасете часть команд несёт явный speed, остальные — без поля, подразумевается скорость normal.

Пример из данных: "turn left ninety degrees then creep back 0.5 meters"[{turn left 90}, {move backward 0.5, "speed":"slow"}] («creep» → slow проставлен только там, где темп указан).

Как вы могли заметить, тут нет информации о карте в принципе, робот является "слепым", это сделано намерено для упрощения, хотя в следующих этапах эксперимента планируется это добавить. Например, для того чтобы робот при словесной команде "подними красный куб" сам мог этот куб найти и поднять.

1 фаза - Делаем танк! В Mujoco.

Только вместо пушки робо-клешня, и нет аниме-женщин

Только вместо пушки робо-клешня, и нет аниме-женщин

Робот рос из пустого MJCF-файла постепенно: сначала появился мир, потом корпус, колёса, опоры — и только тогда всё это поехало без полета за орбиту, и другие астральные реальности.

Будем делать очевидное упрощение.. Настоящую гусеницу — замкнутую ленту с десятками сегментов нам для эксперимента не нужна, поэтому гусеницы не будут физически достоверными..

Вместо них — дифференциальный привод: два ведущих колеса, по одному на каждый борт, каждое со своим актуатором скорости. Поворот получается за счёт разницы скоростей бортов, ровно как у настоящего гусеничного шасси.

Мир и пол

Начинается всё с физических настроек сцены (в XML формате):

<option timestep="0.002" gravity="0 0 -9.81" integrator="implicitfast"/>

Помимо глобальных настроек есть настройки пола конкретно

<geom name="floor" type="plane" size="0 0 0.05" material="grid"
      friction="1.0 0.005 0.0001"/>

Разберем все 6 параметров по порядку

timestep=“0.002”

Шаг 0.002 с — это 500 Гц, привычная частота для симуляции с контактами: меньше — начинает «дёргаться» на касаниях колёс.

gravity=“0 0 -9.81”

Это вектор гравитации — три числа задают, в какую сторону и насколько сильно всё притягивается.

  • Три числа — это направление по осям X, Y, Z (вперёд, вбок, вверх).

  • 0 0 -9.81 значит: по X и Y притяжения нет, а вдоль Z — −9.81.

  • 9.81 — это ускорение свободного падения на Земле, 9.81 м/с² (то самое «g» из школьной физики).

  • Минус — потому что ось Z у нас смотрит вверх, а гравитация тянет вниз. Поэтому значение отрицательное: «вниз по Z с силой 9.81».

Проще говоря — это строчка «включить нормальную земную гравитацию, тянущую к полу». Если бы написали 0 0 0 — робот висел бы в невесомости; 0 0 -1.62 — была бы Луна; а 0 0 -9.81 — обычная Земля.

integrator=“implicitfast”

Симулятор не считает движение непрерывно — он идёт маленькими шажками по времени (у нас шаг 0.002 с). На каждом шаге надо ответить на вопрос: «робот сейчас вот тут и движется вот так — где он окажется через один шажок?». Способ, которым движок отвечает на этот вопрос, и называется интегратором.

Явный Эйлер — самый простой способ. Он смотрит только на то, что происходит прямо сейчас, и считает, что весь следующий шажок останется ровно таким же.

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

Implicit-интеграторimplicitfast — его быстрая, облегчённая версия) работает хитрее: он учитывает не только «где робот сейчас», но и «куда он успеет прийти к концу шажка», и подбирает ответ так, чтобы он сам себе не противоречил.

Поэтому с implicitfast симуляция остаётся спокойной даже при жёстких контактах и относительно крупном шаге по времени.

«Fast» в названии — потому что полную implicit-схему считать дорого, и MuJoCo применяет её только к той части сил, где это реально важно (вязкость, к примеру), не пересчитывая всё подряд.

geom name=“floor” type=“plane” size=“0 0 0.05” material=“grid”

Теперь разберем параметры пола (кроме friction, считай трения, он будет ниже)

  • name="floor" — имя геома, чтобы на него можно было ссылаться (в контактах, исключениях и т.п.).

  • type="plane" — это бесконечная плоскость, а не плита конечного размера.

  • size="0 0 0.05" — у плоскости первые два числа игнорируются (она бесконечна), третье — шаг визуальной сетки для отрисовки.

  • material="grid" — просто внешний вид: клетчатая текстура, чтобы в вьювере было видно движение.

friction=“1.0 0.005 0.0001”

Сам пол — это один плоский геом. Трение у него задаётся не одним числом, а тремя: 1.0, 0.005, 0.0001.

Почему три? Потому что предмет может тереться о поверхность тремя разными способами, и физический движок их считает по отдельности:

  • Скольжение (1.0) — когда что-то едет по полу, как коробка, которую толкаешь. Здесь трение большое: колёса должны цепляться, а не разъезжаться.

  • Верчение (0.005) — когда предмет крутится на месте вокруг точки касания. Тут трение почти нулевое — оно нам мешало бы при поворотах.

  • Качение (0.0001) — сопротивление именно качению, как у мячика, который катится и сам по себе замедляется. Делаем его совсем крошечным, чтобы колесо свободно катилось.

То есть одна цифра отвечает «насколько тяжело проскользнуть», вторая — «насколько тяжело провернуться на месте», третья — «насколько тяжело катиться». Для пола нам нужно: скользить трудно, а вертеться и катиться — легко.

Корпус

Корпус — это body chassis со свободным суставом (freejoint): шесть степеней свободы, робот ничем не прикреплён к миру и может ехать, поворачивать, в худшем случае — падать.
Ставим его на высоту 8.5 см, чтобы колёса именно касались пола, а не висели и не проваливались.

Геометрия — коробка с полуразмерами 0.15 0.08 0.035, то есть габариты 30×16×7 см. Массу и центр тяжести задаем: 2 кг, причём центр масс смещён назад на 2 см. Зачем такой сдвиг - станет понятно ниже, в истории про буксование.

Два ведущих колеса

Каждое колесо — дочернее тело корпуса:

параметр

значение

геометрия

цилиндр, радиус 6 см, полуширина 2 см

ориентация

повёрнут так, чтобы ось смотрела вбок (по Y)

положение

по бортам ±11 см, чуть ниже центра корпуса

сустав

шарнир с вращением вокруг боковой оси

масса

0.3 кг

трение

высокое (2.0) — чтобы не проскальзывало

Расстояние между бортами получается ровно 22 см, а эффективный радиус — 6 см.

Две опорные «лыжи»

Робот на двух колёсах сам по себе клевал бы носом или заваливался на корму. Чтобы этого не было, спереди и сзади добавлены две маленькие сферы-опоры (радиус 1.5 см, по 50 г).

Они нарочно сделаны очень скользкими: их задача — не дать роботу опрокинуться, но при этом не тормозить и, главное, не нести на себе вес.

Поэтому они ещё и чуть приподняты над полом — таким образом они почти не касаются его, и всю нагрузку держат колёса.

Приводы

На каждый колёсный сустав — по актуатору скорости:

<velocity joint="wheel_left"  kv="20" ctrlrange="-30 30" forcerange="-6 6"/>
<velocity joint="wheel_right" kv="20" ctrlrange="-30 30" forcerange="-6 6"/>

kv — насколько жёстко привод догоняет заданную угловую скорость. ctrlrange ограничивает саму команду скорости. А вот forcerange здесь — спасение от очень эффектного бага, о котором ниже.

Жёсткие контакты

По умолчанию суставам задано небольшое демпфирование, а контактам — жёсткие, почти непроникающие параметры. Если контакты сделать мягкими, колёса буквально «тонут» в полу и буксуют. Плюс был явно отключен расчёт самопересечений корпуса с колёсами и колёс между собой: эти столкновения физически невозможны.

Битвы с физикой

Все «магические числа» выше появились не сразу — каждое получено своим багом.

Робот-катапульта. Без ограничения по силе робот на первом же шаге улетал: высота 1.27 м, корпус задран почти вертикально. Лечится ограничением момента (forcerange) и более жёсткими контактами.

Все было именно так

Все было именно так

Буксование. Сначала опорные сферы стояли вровень с колёсами и забирали на себя часть веса — КПД проседал до 25–50%. Замер всё показал прямо: колёса крутятся на нужной скорости, а корпус почти стоит — чистое проскальзывание.

Лечилось тремя изменениями сразу:

  • приподнять опоры,

  • сместить центр тяжести корпуса назад,

  • поднять трение колесо–пол.

В сумме на колёса легло около 88% веса.

Знак поворота. Банально, но проверять надо: по скоростям из симуляции можно было убедиться, что команда «влево» даёт вращение против часовой стрелки, если смотреть сверху. Совпало со стандартной конвенцией (X — вперёд, Y — влево, Z — вверх), ничего инвертировать не пришлось.

Итог: робот грузится, стоит устойчиво, едет и поворачивает в правильную сторону, а точность разомкнутого управления — приемлемая для дальнейшей работы.

Фаза 1 - Файн-тюним модель

Обучающий датасет и физический модуль, которым модель будет управлять - готовы. Теперь необходимо было обучить и проверить модель.

Для файн-тюнинга был выбран машина предоставляемая Kaggle, он представляет для пользователей 30 бесплатных часов в неделю для ML-экспериментов, на выбор есть T4 x2 (каждая по 16 GB), и P100 (одна, 16 GB).

От P100 пришлось отказаться из-за некоторых проблем с совместимостью, поэтому обучение велось на одной карте NVIDIA T4, чтобы не мучаться с параррельностью, тем более модель была маленькая и не требовала больших ресурсов.

А первый блин оказался комом. Тренировочный пример строился по такому принципу - в КАЖДЫЙ ПРИМЕР помещалась та же JSON-схема, которая использовалась при генерации синтетического датасета.

Сокращённо выглядит так:

JSON SCHEMA (draft-07):
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Robot command list",
  "type": "object",
  "additionalProperties": false,
  "required": ["commands"],
  "properties": {
    "commands": { "type": "array", "items": { "$ref": "#/definitions/command" } }
  },
  "definitions": {
    "move": {
      "required": ["action","direction","distance_m"],
      "properties": {
        "action": { "const": "move" },
        "direction": { "enum": ["forward","backward"] },
        "distance_m": { "type": "number", "exclusiveMinimum": 0, "maximum": 100 },
        ...
      }
    },
    "turn": { ... "angle_deg": { "maximum": 360 } ... },
    "stop": { ... }, "wait": { ... }, "grasp": { ... }, "release": { ... }
  }
}

ROLE: You translate ONE English instruction ... into a JSON object that
strictly validates against the schema above.
HARD RULES:
- Output ONLY the JSON object, no prose, no markdown fences.
- distance_m and angle_deg are ALWAYS positive ...
- "speed" is an optional enum (slow|normal|fast) ...
INTERPRETATION CONVENTIONS:
- "quarter turn" -> 90; "turn around" -> 180; "full turn" -> 360 ...
- word numbers -> the number ("two" -> 2.0) ...
   ... (ещё десятки строк правил) ...

---
INSTRUCTION: turn left 90 degrees, then go forward 2 meters

Вся эта простыня — это ~1500 токенов, и она повторяется в каждом из тысяч обучающих примеров, хотя меняется в них только последняя строчка INSTRUCTION: ....

По итогу это привело к тому, что обучение длилось 3.5 часа, и программа упала на ошибке в ячейке с eval, так и не успев сохранить модель.

Снова тратить 3.5 часа не хотелось, поэтому был выбран другой подход - в каждом примере будет помещена только короткая инструкция.

You translate ONE English instruction for a tracked robot with a gripper
arm into a single JSON object {"commands":[...]} using actions: move, turn,
stop, wait, grasp, release. Output ONLY the JSON object, no prose, no
markdown. If the instruction is out of scope or nonsense, output
{"commands": []}.

---
INSTRUCTION: turn left 90 degrees, then go forward 2 meters

Это ~60 токенов вместо полутора тысяч. Никакой схемы, никаких таблиц правил — просто просьба:

«ты переводишь фразу в JSON такого вида, вот список действий, на бессмыслицу отвечай пустым списком».

С таким подходом обучение заняло 30-40 минут. Веса модели были скачаны, осталось протестировать в реальных условиях, точнее в симуляции Mujoco.

Фаза 1 - Тестируем модель

Тестировали как обычно тестируют результаты файнтюна— на отложенной выборке: 10% пар модель при обучении не видела вообще (фиксированный случайный отбор, чтобы результат можно было воспроизвести). Считали несколько метрик, каждая отвечает на свой вопрос:

  • schema_valid — какая доля ответов вообще является валидным JSON по нашей схеме. Получилось 1.000, то есть 100%.

  • exact_match — насколько часто ответ совпал с эталонным дословно. 0.920.

  • action_seq — правильная ли получилась последовательность действий по смыслу, даже если число чуть разошлось (важнее «повернул, потом поехал», чем «ровно 90.0 против 90»). 0.980.

  • ood_f1 — не выдумывает ли модель команды на бессмыслицу вроде «свари кофе» (правильный ответ — пустой список «ничего не делать»). 0.846.

Но самая важная проверка — последняя. Все предыдущие метрики сравнивают тексты. А нам важно, чтобы робот реально поехал куда надо.

Поэтому мы брали эталонную команду и предсказание модели, прогоняли обе в симуляции MuJoCo и сравнивали, где в итоге оказался робот (с разумным допуском — управление разомкнутое, физика немного шумит).

Эта метрика — task_success = 0.975: 39 запусков из 40 привели робота туда же, куда и эталон, ноль ошибок исполнения.

Мы опытным путем доказали, что схема "впечаталась" в веса модели во время обучения, и кидать ей полную схему было совершенно избыточно.

Теперь со спокойной совестью можно было приступать к Фазе 2.

Фаза 2 - Обновляем датасет для примеров с клешней

Мы добавляем два манипуляционных действия - release и grasp, каждая из них имеет необязательный параметр cell, который в свою очередь имеет enum в качестве параметров: front | front_left | front_right | left | right, со значением front по умолчанию, если параметр не указан.

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

Прежде чем что-то делать, мы это измерили: скрипт регулярками прошёл по всем 2505 старым парам и нашёл, сколько из них реально конфликтуют (манипуляционная фраза → пустой ответ).

Phase-1 pairs=2505 | manip-phrase=17 | CONFLICT=17
reuse unchanged=2488 (99.3%)

Конфликтных оказалось 17 из 2505 — 0.7%. Phase-1 почти не генерил таких фраз. Конфликтующие примеры из новой выборки были удалены.

Запустили тот же самый, уже обкатанный на Phase 1, конвейер генерации через OpenRouter — но с двумя точечными изменениями:

  • few-shot-примеры в промпте брались только из манипуляционных сидов (пары с grasp/release);

  • в промпт добавили явный акцент: «генерируй ТОЛЬКО задачи на руку, в каждой паре обязан быть grasp и/или release, чистую локомоцию НЕ выдавай».

Цель была 1000 пар. Итог прогона: все 1000 примеров приняты, 0 отбраковок схемой за весь прогон.

Что получилось

Финальный обьединенный датасет — 3518 пар:

действие

сколько

move

2239

turn

1322

grasp

814

release

584

wait

488

stop

285

Фаза 2 - Добавляем клешню к роботу

К счастью, нам не нужно столько клешней как у краба

К счастью, нам не нужно столько клешней как у краба

Чтобы кисть попала в нужную точку, нужно посчитать углы суставов руки — это обратная кинематика (IK).

Сделали её методом затухающих наименьших квадратов по якобиану точки-схвата: грубо говоря, маленькими шагами подкручиваем суставы в сторону цели, пока кончик руки не придёт куда надо.

Стоит подметить две детали:

  • IK двигает только суставы руки, база считается зафиксированной. Это упрощает математику и логически верно: при хватании робот стоит на месте.

  • IK не трогает живую симуляцию. Он сохраняет состояние мира, считает решение на копии и восстанавливает всё обратно, возвращая только целевые углы. Иначе каждый расчёт IK дёргал бы физику.

Модель шлёт {"action":"grasp"} (опц. с клеткой), а контроллер разворачивает это в фиксированную канонную рутину:

  • клетка → точка в мире (с учётом того, куда повёрнут робот) →

  • найти ближайший свободный объект рядом →

  • раскрыть клешню → подвести руку над объектом → опустить → сжать → поднять.

  • release — то же в обратную сторону: поднести к клетке → опустить → разжать

Никакого «творчества» в момент исполнения: модель решает что и примерно куда (клетка), а как именно двигать суставы — детерминированная процедура и IK.

Теперь разберем систему клеток. Они имеют такую структуру.

_CELLS = {
    "front":       (0.30, 0.00),
    "front_left":  (0.27, 0.13),
    "front_right": (0.27, -0.13),
    "left":        (0.23, 0.20),
    "right":       (0.23, -0.20),
}

Если посчитать расстояние и угол каждой клетки от центра робота (кадр робота: X вперёд, Y влево):

cell

радиус √(x²+y²)

угол от носа

front

0.300 м

front_left

0.300 м

+25.7°

front_right

0.300 м

−25.7°

left

0.305 м

+41.0°

right

0.305 м

−41.0°

Виден принцип: все пять клеток лежат на одной дуге радиусом ≈0.30 м перед роботом, просто разнесённые по разным направленим. То есть это не сетка координат, а один комфортный радиус досягаемости, нарезанный на 5 направлений в переднем секторе ~±41°.

Радиус ≈ 0.30 м — это «зона руки». Значение подобрано эмпирически так, чтобы IK туда дотягивался уверенно.

Фаза 2 - Файн-тюним и тестим модель, а так же добавляем user-api

Файн-тюнинг модели на обновленном датасете прошел без сюрпризов, те же 30-40 минут. Момент, который стоило отметить - файн-тюн происходил полностью нуля, без учета весов с первой фазы, потому что туда попали инструкции где поднятие предметов считалось бессмыслицей, в любом случае мы ничего не теряем, модель тренируется очень быстро.

Осталось протестировать модель. Для определения качества обучения мы вводим метрику, которая сравнивает финальное положение хватаемого объекта между эталоном и предсказанием (допуск 0.10 м).

Если модель сказала «возьми и положи слева», успех засчитывается, только когда куб реально оказался слева — и там же, куда его кладёт эталон.

Что показали цифры

Прогон на отложенной выборке (352 пары, та же, что у Phase 1):

метрика

Phase 2

Phase 1

schema_valid

0.991

1.000

exact_match

0.943

0.920

action_seq

0.980

0.980

ood_f1

0.857

0.846

task_success (MuJoCo, 40)

0.975

0.975

Как это читать:

  • schema_valid 0.991, а не 1.0 — небольшой регресс.

  • exact_match 0.943 — даже выше, чем у Phase 1 (0.920). Модель выучивает манипуляционные паттерны резче, чем разговорные формулировки расстояний.

  • task_success 0.975 при нуле ошибок исполнения — grasp/release/клетка отрабатывают в физике чисто, и куб оказывается там же, куда его кладёт эталон.

Добавляем интерактивный режим

Конвейер замкнут — но прогон фиксированного датасета это всё ещё «лабораторно». Хотелось добавить возможность командовать роботом свободным текстом в реальном времени.

Выбрали самое наглядное: REPL + окно MuJoCo. Печатаешь фразу — робот тут же её исполняет в живой симуляции, состояние копится между фразами (никакого сброса: сказал «проедь вперёд», потом «теперь налево» — он поедет от того места, где остановился).

Архитектурно тут решено было добавить два потока:

  • Поток ввода: блокирующий input() + инференс модели — складывает разобранные команды в очередь.

  • Главный поток владеет окном и физикой: есть команда в очереди — исполняет в реальном времени; нет — крутит холостой шаг (робот стоит, окно живёт). Только он зовёт шаг физики — поэтому нет гонок данных в MuJoCo.

REPL — это Read–Eval–Print Loop, «цикл: прочитал — выполнил — показал результат — снова жди ввод». Проще говоря, интерактивная консоль, которая в цикле:

  1. Read — ждёт, пока ты введёшь строку;

  2. Eval — выполняет её;

  3. Print — показывает результат;

  4. Loop — возвращается к шагу 1 и ждёт следующую.

Тут все вышло без проблем, результат вы могли увидеть в GIF в начале статьи.

Результаты

По итогу что было сделано

  • Была сделана с нуля физическая модель гусеничного робота с робо-клешней в среде Mujoco

  • Была обучена Gemma-3 на 270 млн параметров принимать команды от человека на естественном языке и переводить это в JSON для управления гусеничным роботом

  • Обучение проводилось на бесплатной машине Kaggle, синтетический датасет собирался с помощью бесплатных моделей gpt-oss-120b и nemotron-super-120b на OpenRouter

Вот мы и подошли к концу этого путешествия. Но проект еще не закончен. Необходимо доработать два основных момента

  • Добавить роботу восприятие карты, чтобы он мог действовать более самостоятельно и анализировать обстановку, например, подбирать предметы по команде человека в свободной форме ("подбери красный куб), преодолевать препятствия и так далее.

  • Перенести модель в реального гусеничного робота, для этого необходимо собрать лежащий у меня DIY-набор RaspTank, фото которого было показано в начале статьи.

Если вам эта статья покажется интересной, то я попробую этим заняться в следующей части.

А пока до новых встреч.

Источники

Исходный код и веса модели