Почему стандартные LLM не понимают инженерный жаргон
Попроси GPT-4 или Claude прочитать файл CADINP. Получишь либо "Это похоже на код", либо полный бред про "параметрические уравнения". Проблема в том, что инженерные синтаксисы - это не языки программирования. Это смесь математики, специфичных сокращений и контекстных зависимостей.
CADINP - проприетарный формат для инженерных расчетов. Содержит конструкции типа LOAD_CASE(STATIC, DEAD+WIND) и MATERIAL(STEEL_A36, YIELD=250). Ни одна модель общего назначения не знает, что DEAD+WIND означает "постоянная + ветровая нагрузка".
Когда я впервые попробовал Qwen3-30B на инженерных задачах, результат был предсказуемо плохим. Модель путала единицы измерения, не понимала контекстные сокращения, галлюцинировала физические константы. Типичный диалог:
Запрос: "Рассчитать напряжение в сечении при нагрузке 50 кН"
Ответ: "Напряжение = 50 кН / площадь сечения. Площадь сечения не указана."
Вот это "не указана" меня и добило. Инженер знает, что в CADINP есть дефолтные значения, знает стандартные сечения, понимает контекст. Модель - нет.
Chain of Thought для инженеров: не просто "думай вслух"
Большинство гайдов по Chain of Thought показывают примеры вроде "Сначала посчитай A, потом B". Для инженерных задач этого недостаточно. Нужно учить модель:
- Распознавать неявные предположения
- Использовать стандартные справочные данные
- Понимать иерархию команд в синтаксисе
- Проверять единицы измерения автоматически
Вот как выглядит правильный пример для обучения:
{
"instruction": "Сгенерируй CADINP блок для расчета прогиба балки",
"input": "Длина 6м, сечение I-20, сталь, равномерная нагрузка 5кН/м",
"output": "// Chain of Thought:\n// 1. Определяем тип анализа: статика\n// 2. Материал: сталь (E=210000 МПа)\n// 3. Нагрузка: равномерная 5 кН/м = 5000 Н/м\n// 4. Сечение I-20: момент инерции Ix=2350 см⁴ из таблицы\n// 5. Формула прогиба: f = (5*q*L⁴)/(384*E*I)\n// 6. Расчет: f = (5*5000*6⁴)/(384*210e9*2350e-8)\n\nBEAM_DEFINE(ID=1, LENGTH=6.0, SECTION='I-20')\nMATERIAL_ASSIGN(BEAM=1, TYPE='STEEL', E=210000)\nLOAD_APPLY(TYPE='UNIFORM', BEAM=1, VALUE=5.0, UNIT='kN/m')\nANALYSIS(TYPE='STATIC', LOAD_CASES=['DEAD'])\nPOST_PROCESS(REQUEST='DEFLECTION', BEAM=1)"
}
Подготовка датасета: где взять 1000 примеров CADINP?
Самый частый вопрос: "У меня есть 20 файлов CADINP, как сделать 1000 примеров?". Ответ: синтетика. Но не та синтетика, которую генерирует GPT.
Я использовал трехэтапный подход:
1 Извлечение шаблонов из реальных файлов
import re
from pathlib import Path
def extract_cadinp_patterns(file_path):
patterns = []
with open(file_path, 'r') as f:
content = f.read()
# Ищем блоки команд
command_blocks = re.findall(r'([A-Z_]+)\(([^)]+)\)', content)
for cmd, args in command_blocks:
# Заменяем конкретные значения на плейсхолдеры
args = re.sub(r'\d+\.?\d*', '', args)
args = re.sub(r'\'[^\']*\'', '', args)
patterns.append(f"{cmd}({args})")
return patterns
2 Генерация вариаций с физическими ограничениями
Вот где большинство ошибается. Нельзя просто рандомно генерировать числа. Длина балки в метрах? От 1 до 20. Нагрузка в кН/м? От 0.5 до 50. Материал? Только из списка допустимых.
import random
from typing import Dict, List
class CadinpGenerator:
MATERIALS = {
'STEEL_A36': {'E': 210000, 'Fy': 250},
'STEEL_A992': {'E': 200000, 'Fy': 345},
'CONCRETE_C30': {'E': 30000, 'Fc': 30},
}
SECTIONS = ['I-20', 'I-24', 'I-30', 'PIPE_200', 'PIPE_300']
def generate_beam_example(self) -> Dict:
length = round(random.uniform(2.0, 12.0), 2)
section = random.choice(self.SECTIONS)
material = random.choice(list(self.MATERIALS.keys()))
load = round(random.uniform(1.0, 15.0), 2)
# Физически корректные значения
mat_data = self.MATERIALS[material]
return {
'length': length,
'section': section,
'material': material,
'load': load,
'E': mat_data['E'],
'strength': mat_data.get('Fy') or mat_data.get('Fc')
}
3 Добавление Chain of Thought через шаблоны
Каждому примеру добавляем reasoning-часть. Не просто "вот код", а "вот как инженер думает":
def add_cot_to_example(example: Dict) -> str:
cot_lines = [
f"// Длина балки: {example['length']} м (типовой пролет)",
f"// Сечение {example['section']}: берем из таблицы стандартных профилей",
f"// Материал {example['material']}: модуль упругости E={example['E']} МПа",
f"// Нагрузка {example['load']} кН/м - проверяем по нормам",
f"// Расчет прогиба: используем формулу для равномерно распределенной нагрузки",
"// Проверка: прогиб не должен превышать L/200"
]
return "\n".join(cot_lines)
Итоговый датасет: 800 синтетических примеров + 200 реальных. Достаточно для эффективного fine-tuning.
Настройка Unsloth на RTX 3090: 24 ГБ это много или мало?
RTX 3090 с 24 ГБ VRAM - золотая середина для fine-tuning. Но есть нюансы. Если взять Qwen2.5-32B в полной точности - не влезет. Нужно выбирать модель и квантование.
| Модель | Параметры | VRAM (4-bit) | Качество для CADINP |
|---|---|---|---|
| Qwen2.5-7B | 7 млрд | ~5 ГБ | Среднее, путает сложные конструкции |
| Qwen2.5-14B | 14 млрд | ~9 ГБ | Хорошее, лучший выбор для 3090 |
| Qwen2.5-32B | 32 млрд | ~18 ГБ | Отличное, но мало места для данных |
Я выбрал Qwen2.5-14B-Instruct. Почему? 14B параметров хватает для понимания контекста, а 4-bit квантование оставляет достаточно VRAM для батчей.
Важный момент: Unsloth поддерживает 4-bit квантование с минимальной деградацией качества. Для инженерных задач, где важна точность чисел, это критично.
4 Установка и настройка Unsloth
# Unsloth работает только с Python 3.10+
# Проверяем версию
python --version
# Устанавливаем с поддержкой CUDA 12.1 для RTX 3090
pip install "unsloth[cu121] @ git+https://github.com/unslothai/unsloth.git"
# Дополнительные зависимости для работы с Qwen
pip install transformers datasets accelerate trl peft
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
Первый подводный камень: версия PyTorch. Если поставить не ту - будет ошибка совместимости с CUDA. Для RTX 3090 нужен CUDA 12.1 и PyTorch 2.3+.
5 Конфигурация обучения
from unsloth import FastModel
import torch
from transformers import TrainingArguments
from trl import SFTTrainer
# Загружаем модель с 4-bit квантованием
model, tokenizer = FastModel.from_pretrained(
model_name="Qwen/Qwen2.5-14B-Instruct",
max_seq_length=2048, # Для CADINP хватает
dtype=None, # Автовыбор
load_in_4bit=True, # Критично для 3090
token="your_hf_token", # Нужен для Qwen
)
# Настройка LoRA адаптеров
model = FastModel.get_peft_model(
model,
r=16, # Rank адаптера
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
lora_alpha=16,
lora_dropout=0,
bias="none",
use_gradient_checkpointing=True, # Экономия VRAM
random_state=42,
)
Почему именно эти target_modules? Для Qwen архитектуры это слои, которые дают максимальный прирост качества при fine-tuning. Не трогаем embedding и lm_head - они почти не влияют на понимание синтаксиса.
Ошибка новичков: ставят r=64 или больше, думая "чем больше - тем лучше". На деле после r=32 начинается overfitting на обучающих данных. Для специализированных задач хватает r=8-16.
Тренировка: какие гиперпараметры не сломают модель
Здесь я потратил две недели на эксперименты. Стандартные параметры из документации Unsloth не работают для инженерных задач. Почему? Потому что CADINP требует точного следования синтаксису, а не креативности.
training_args = TrainingArguments(
output_dir="./qwen-cadinp",
num_train_epochs=3, # Больше 5 - overfitting
per_device_train_batch_size=2, # На 3090 с 14B моделью
gradient_accumulation_steps=4,
warmup_steps=50,
logging_steps=10,
save_steps=500,
eval_steps=500,
evaluation_strategy="steps",
learning_rate=2e-4, # В 2 раза ниже стандартного
fp16=True, # Обязательно для 3090
tf32=True, # Если драйверы поддерживают
gradient_checkpointing=True,
optim="adamw_8bit", # Экономия памяти
weight_decay=0.01,
lr_scheduler_type="cosine",
report_to="none", # Отключаем WandB для скорости
)
Ключевые отличия от стандартного fine-tuning:
- Learning rate 2e-4 вместо 5e-4: модель уже знает язык, нужно тонко настроить
- Batch size=2: на 24 ГБ с 14B моделью больше не влезет
- Gradient accumulation=4: виртуальный batch size 8
- Всего 3 эпохи: инженерные данные повторяемы, быстро переобучается
6 Запуск обучения и мониторинг
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
args=training_args,
train_dataset=train_dataset,
eval_dataset=eval_dataset,
dataset_text_field="text", # Поле с данными
max_seq_length=2048,
packing=False, # Для CADINP не пакуем - теряется структура
)
# Запускаем
trainer.train()
# Сохраняем только адаптеры
model.save_pretrained("./qwen-cadinp-lora")
Во время обучения следим за двумя метриками:
- Loss на validation set - должен монотонно уменьшаться
- Синтаксическая точность - процент правильно сгенерированных CADINP команд
Если loss начал расти на второй эпохе - stop early. Модель переобучается.
Проблемы и решения: от галлюцинаций до OOM ошибок
Проблема 1: Модель генерирует несуществующие команды
После обучения модель начала выдавать CALCULATE_STRESS() вместо POST_PROCESS(REQUEST='STRESS'). Решение: добавить в датасет негативные примеры с неправильными командами и явно указывать ошибку.
{
"instruction": "Найди ошибку в CADINP коде",
"input": "CALCULATE_STRESS(BEAM=1)",
"output": "// ОШИБКА: CALCULATE_STRESS не существует в CADINP\n// Правильно: POST_PROCESS(REQUEST='STRESS', BEAM=1)"
}
Проблема 2: Out Of Memory при длинных последовательностях
CADINP файлы бывают по 1000+ строк. Даже с 24 ГБ можно вылететь. Решение:
- Разбивать файлы на логические блоки (определения, нагрузки, анализ)
- Использовать gradient checkpointing
- Уменьшать max_seq_length до 1024 если не критично
Проблема 3: Модель забывает базовые знания
После fine-tuning модель начала путать базовую математику. Решение: добавить 10% общих примеров из оригинального датасета Qwen. Сохраняем баланс между специализацией и общими знаниями.
Инференс: как использовать обученную модель
Обучение прошло успешно. Теперь нужно запустить модель для реальных задач.
from unsloth import FastModel
from transformers import TextStreamer
import torch
# Загружаем базовую модель
base_model, tokenizer = FastModel.from_pretrained(
"Qwen/Qwen2.5-14B-Instruct",
load_in_4bit=True,
)
# Загружаем LoRA адаптеры
from peft import PeftModel
model = PeftModel.from_pretrained(base_model, "./qwen-cadinp-lora")
# Инференс
prompt = """Ты - инженерный ассистент CADINP.
Запрос: Сгенерируй CADINP код для стального двутавра длиной 8 метров с нагрузкой 10 кН/м.
Включи расчет прогиба.
CADINP код:"""
inputs = tokenizer(prompt, return_tensors="pt", truncation=True).to("cuda")
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=512,
temperature=0.1, # Низкая для точности
do_sample=False, # Жесткий выбор токенов
repetition_penalty=1.1, # Борьба с повторами
)
result = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(result)
Температура 0.1 и do_sample=False - это важно. Для инженерного кода нужна детерминированность, а не креативность.
Результаты: что получилось в итоге
После 8 часов обучения на RTX 3090 (3 эпохи, 1000 примеров):
| Метрика | До fine-tuning | После fine-tuning |
|---|---|---|
| Синтаксическая точность | 32% | 89% |
| Правильные единицы измерения | 45% | 94% |
| Понимание контекста | 28% | 82% |
| Галлюцинации | 67% ответов | 12% ответов |
Главное достижение: модель теперь понимает, что LOAD_CASE(STATIC, DEAD+WIND) означает "статический расчет на постоянные и ветровые нагрузки". А не "случай статической смерти от ветра", как думала исходная Qwen.
Что делать если хочется больше?
24 ГБ VRAM на RTX 3090 - это потолок для 14B модели с разумным batch size. Если нужно больше:
- Вторая RTX 3090 с NVLink даст 48 ГБ
- Квантование в 3-bit или 2-bit (но качество упадет)
- Обучение на CPU с offload (в 10 раз медленнее)
Для большинства инженерных задач 14B модели достаточно. 32B даст прирост в 5-10% качества, но потребует либо двух карт, либо сильного уменьшения batch size.
Самый неочевидный совет: не гонитесь за размером модели. Лучше потратьте время на качество датасета. 1000 идеально подготовленных примеров с Chain of Thought дадут больший прирост, чем переход с 14B на 32B модель.
И последнее: после обучения запустите модель на реальных задачах инженеров из вашей компании. Первые 20-30 ошибок исправьте вручную и добавьте в датасет. Затем дообучите еще одну эпоху. Это снизит ошибки еще на 30-40%.
Fine-tuning под проприетарный синтаксис - это не разовая акция. Это процесс. Но начать можно с одной RTX 3090, 1000 примеров и выходных.