Большие языковые модели — это круто, но попробуйте запустить GPT-4 на Raspberry Pi. Получится грустно. А если нужно, чтобы робот двигался в реальном времени, без задержек в секунду? Тут на помощь приходят компактные модели вроде Gemma-3-270m. Всего 270 миллионов параметров, влазит в гигабайт оперативы, а с 4-битным квантованием — и того меньше. Но как заставить её не просто болтать, а дёргать сервоприводами? Только через тонкую настройку (fine-tuning).
Спойлер: у нас получится. Мы превратим текстовую модель в полноценный контроллер для симуляции MuJoCo. Подаём на вход текущие углы суставов и целевую позицию, на выходе получаем строку со следующими командами. Звучит как извращение? Да, но это работает.
Почему не взять готовый VLA?
Сейчас модно говорить про Vision-Language-Action модели (VLA). Но они требуют мощных GPU и огромных датасетов. Gemma-3-270m — это чистый текст, никакого видео. Мы превращаем задачу управления в задачу генерации текста. Просто, дёшево, сердито. Кстати, если хотите понять разницу между VLA и VLM, у нас есть отдельный разбор: VLA vs VLM 2025. Но здесь мы идём другим путём — чисто текстовый fine-tuning.
⚠️ Важный момент: мы не учим модель физике — она просто запоминает, как по текущему состоянию выдать правильное действие. Всё, что нужно для smooth-движения, мы закладываем через данные. Как говорится, garbage in — garbage out.
Как НЕ надо: сырые числа в промпте
Самая частая ошибка новичков — скормить модели числа через запятую. Что-то вроде [0.1, 0.5, -0.3] -> [0.2, 0.6, -0.2]. Gemma-3 обучалась на тексте, а не на сырых векторах. Она не поймёт контекст, не сможет обобщать. В тестах точность падает до 30%. Вместо этого нужно завернуть числа в естественно-языковую обёртку.
Правильный подход: текстовая репрезентация действий
Каждое наблюдение и действие мы превращаем в предложение. Например:
State: joint0 0.1, joint1 0.5, joint2 -0.3. Target: endpoint_x 0.7, endpoint_y 0.2.
Action: move joint0 to 0.2, joint1 to 0.6, joint2 to -0.2.
Модель учится дописывать "Action: ..." после промпта. Это её родной формат — next token prediction. К тому же такой подход легко расширять: добавить угол схвата, текст цели, даже комментарий.
Сбор данных: MuJoCo + PD-контроллер
Берём среду Fetch Reach или Panda из Gymnasium-Robotics. Запускаем случайные эпизоды, записываем состояния и действия. Чтобы данные были осмысленными, лучше использовать простейший PD-контроллер, который тянет конец манипулятора к цели. Получаем 10 000 эпизодов по 50 шагов — полмиллиона пар. Этого хватит для Gemma-3-270m.
import gymnasium as gym
import numpy as np
env = gym.make('FetchReach-v2', render_mode='human')
observations = []
actions = []
for episode in range(100):
obs, _ = env.reset()
for step in range(50):
# Простейший PD: тянем к цели
goal = obs['desired_goal'][:3]
pos = obs['observation'][:3]
delta = goal - pos
action = np.clip(delta * 2, -1, 1)
obs, rew, term, trunc, _ = env.step(action)
observations.append(obs)
actions.append(action)
Теперь конвертируем каждый шаг в текстовую пару. Важно: нормализуем углы суставов к диапазону [-1, 1] и форматируем с точностью до двух знаков. Иначе модель начнёт галлюцинировать длинные хвосты.
💡 Совет: используйте стандартный токенизатор Gemma-3. Он отлично справляется с числами, если они записаны как текст. Для ускорения можно задать seed и отключить dropout при генерации датасета.
Тонкая настройка с QLoRA: пошаговый процесс
Мы будем использовать 4-bit QLoRA через библиотеку PEFT от Hugging Face. Gemma-3-270m в 4-битах занимает ~200 MB — рай для Raspberry Pi. Код обучения:
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
model_name = "google/gemma-3-270m"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_use_double_quant=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto"
)
model = prepare_model_for_kbit_training(model)
lora_config = LoraConfig(
r=8,
lora_alpha=16,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters() # ~1.2M параметров
Обучаем три эпохи с косинусным расписанием, lr 2e-4, batch_size 8 (если влезает). Используем стандартный Trainer от Hugging Face. Обучение на A100 занимает около 3 часов. На T4 — часов 12, но можно и на коллабе.
from transformers import TrainingArguments, Trainer
training_args = TrainingArguments(
output_dir="./gemma-3-270m-mujoco",
per_device_train_batch_size=8,
gradient_accumulation_steps=2,
num_train_epochs=3,
learning_rate=2e-4,
lr_scheduler_type="cosine",
fp16=True,
logging_steps=50,
save_strategy="epoch",
report_to="none"
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized_dataset
)
trainer.train()
Тестирование: модель в роли контроллера
После обучения достаём адаптер LoRA, загружаем его на чип-копинга. Пишем обёртку, которая каждые 20 мс генерирует действие:
def get_action(model, tokenizer, observation, goal):
prompt = f"State: joint0 {obs[0]:.2f}, joint1 {obs[1]:.2f}, ... Target: endpoint_x {goal[0]:.2f}, endpoint_y {goal[1]:.2f}\
\nAction:"
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=40,
temperature=0.1,
do_sample=True,
repetition_penalty=1.1
)
reply = tokenizer.decode(outputs[0], skip_special_tokens=True)
# Парсим числа после "move"
# ...
return parsed_action
Запускаем симуляцию. Если всё сделано правильно, рука плывёт к цели с точностью 2–3 сантиметра. Не идеально, но для компактной модели — ок. А главное, на Raspberry Pi 5 один шаг генерируется за 150 мс (с однопоточным inference).
Типичные грабли и как их избежать
- Переобучение: 10 эпох — уже перебор. Модель запоминает шум датасета. Хватит 3 эпох с небольшим weight decay.
- Неправильный токен для EOS: Если забыть pad_token = eos_token, модель будет генерировать бесконечный поток чисел.
- Случайные сиды: Фиксируйте seed (torch, numpy, random) до обучения — иначе результаты не воспроизвести.
- Размер контекста: Gemma-3-270m поддерживает до 8192 токенов. Но мы используем ~300 токенов на шаг. Не раздувайте промпт лишними комментариями.
- Температура: Высокая температура (>=0.7) даёт хаотичные движения. Для робота нужно 0.1–0.2.
Запуск на Raspberry Pi 5
После квантования модель весит ~200 МБ, адapter LoRA — ещё 5 МБ. Inference делаем через ONNX Runtime или llama.cpp с поддержкой Gemma. У нас есть отдельный гайд по запуску Gemma 4 в браузере с WebGPU, но для Gemma-3-270m на Python подойдёт библиотека ctransformers или llama-cpp-python. Пример:
pip install llama-cpp-python
# Скачать GGUF модель Gemma-3-270m (4-bit)
# Загрузить адаптер LoRA
python -c "from llama_cpp import Llama; llm = Llama(model_path='gemma-3-270m-q4_k_m.gguf', lora_path='lora.bin')"
На RPi5 с 8 ГБ ОЗУ получаем ~5–6 токенов в секунду. Один шаг управления требует 40 новых токенов — 7–8 секунд на шаг. Медленно, но для медленных задач (поворот камеры, сбор данных) сойдёт. Для динамики придётся оптимизировать дальше: использовать batch-инференс или VLLM.
Что дальше?
Текстовое управление — не предел. Можно подключить vision encoder и передавать описание изображения через Gemma (гибрид VLM). Но это уже история про VLA vs VLM. Сейчас главное, что вы научились превращать маленькую LLM в контроллер. Это открывает дорогу для автономных бюджетных роботов — на Arduino, ESP32, Banana Pi. Единственное ограничение — ваша фантазия и количество RAM.
Не пытайтесь заставить Gemma считать PID — она для этого не предназначена. Просто генерируйте целевые позиции, а низкоуровневый контроль оставьте классическому ПИД-регулятору. В симбиозе LLM + классика получается самая дешёвая и самая гибкая система управления.