Проблема: все говорят про 70 миллиардов параметров, а у тебя 6 ГБ видеопамяти
Открываешь любой гайд по обучению LLM - там сразу рекомендуют A100, H100 или хотя бы RTX 4090. А что делать, если твой рабочий инструмент - старый серверный Xeon и GTX 1060 с её скромными 6 ГБ? Многие сдаются и идут в облака. Но есть другой путь.
Сегодня я покажу, как собрать рабочую языковую модель для испанского языка на железе, которое большинство считает музейным экспонатом. Без квантования, без магии - только чистый код и понимание того, что можно отрезать без потери качества.
Важно: этот подход не даст вам модель уровня GPT-4. Но он даст специализированный инструмент для конкретной задачи - генерации текста в стиле испанской классической литературы. Что часто ценнее универсального, но бесполезного на вашем железе монстра.
Что мы будем строить и почему именно это
Цель - модель, которая понимает структуру испанского языка, умеет генерировать связный текст и сохраняет стилистические особенности классической литературы. Не для общего диалога, а для конкретной ниши.
Почему испанская литература? Три причины:
- Корпус текстов доступен и относительно небольшой (мы возьмем ~100MB чистого текста)
- Испанский имеет четкую грамматическую структуру - модели проще учиться
- Меньше конкуренции: большинство open-source моделей заточены под английский
Железо: что реально нужно, а без чего можно обойтись
| Компонент | Минимум | У меня было | Почему это работает |
|---|---|---|---|
| GPU | 4GB VRAM | GTX 1060 6GB | Хватит для batch size=2 и модели ~100M параметров |
| RAM | 16GB | 32GB DDR4 | Держим датасет в памяти + кэш для токенизации |
| CPU | 4 ядра | Xeon E5-2620 v4 | Предобработка данных, создание словаря |
| Диск | 20GB свободно | NVMe 512GB | Датасет + модель + чекпоинты |
Самая большая ложь в ML - что вам нужен топовый GPU. На самом деле, нужно понимание того, что можно уменьшить. Если вы работаете с маленькими LLM, то железные требования падают в разы.
Шаг 0: подготавливаем поле боя
Первое, что нужно сделать - забыть про трансформеры на миллиарды параметров. Мы будем использовать архитектуру GPT-2 Small (124M параметров), но с важными модификациями.
1 Собираем датасет: где брать испанскую классику и как её чистить
Я взял 50 произведений испанской литературы с Project Gutenberg. От Сервантеса до Лорки. Важно: только текст, никаких HTML-тегов, предисловий издателей, номеров страниц.
Самый болезненный этап - очистка PDF. Вот как НЕ надо делать:
# ПЛОХО: пытаться парсить PDF как есть
import PyPDF2
with open("book.pdf", "rb") as f:
reader = PyPDF2.PdfReader(f)
text = ""
for page in reader.pages:
text += page.extract_text() # Здесь будет ад кромешный
Почему это плохо? PDF сохраняет разрывы строк, номера страниц, колонтитулы. Получится каша.
Вот рабочий вариант:
# ХОРОШО: используем специализированные инструменты
import pypdfium2 as pdfium
import re
def clean_pdf_text(pdf_path):
pdf = pdfium.PdfDocument(pdf_path)
text_parts = []
for i in range(len(pdf)):
page = pdf[i]
textpage = page.get_textpage()
raw_text = textpage.get_text_range()
# Удаляем номера страниц (обычно внизу или вверху)
lines = raw_text.split('\n')
cleaned_lines = []
for line in lines:
# Пропускаем явные номера страниц
if line.strip().isdigit() and len(line.strip()) < 4:
continue
# Удаляем колонтитулы с названием книги
if "CERVANTES" in line.upper() and len(line) < 30:
continue
cleaned_lines.append(line)
page_text = '\n'.join(cleaned_lines)
# Сшиваем разорванные слова
page_text = re.sub(r'(\w)-\n(\w)', r'\1\2', page_text)
text_parts.append(page_text)
return '\n\n'.join(text_parts)
После очистки всех PDF сохраняем в один большой текстовый файл. У меня получилось ~98MB чистого испанского текста.
2 Токенизация: почему SentencePiece, а не BPE из Hugging Face
Здесь многие ошибаются. Берут готовый токенизатор от GPT-2 и думают, что он подойдет для испанского. Не подойдет. Английский BPE плохо работает с испанскими акцентами и диграфами вроде "ll" или "ch".
SentencePiece (от Google) создает словарь с нуля на ваших данных. Это важно для редких слов классической литературы.
# Устанавливаем SentencePiece
pip install sentencepiece
# Обучаем токенизатор
spm_train --input=corpus.txt --model_prefix=es_tokenizer \
--vocab_size=32000 \
--character_coverage=0.9995 \
--model_type=bpe \
--max_sentence_length=16384
Ключевые параметры: vocab_size=32000 (достаточно для нашего объема), character_coverage=0.9995 (почти все символы), model_type=bpe (баланс между эффективностью и качеством). Не ставьте vocab_size больше 50000 - модель будет слишком тяжелой для слабого железа.
Проверяем, что получилось:
import sentencepiece as spm
sp = spm.SentencePieceProcessor()
sp.load('es_tokenizer.model')
text = "En un lugar de la Mancha, de cuyo nombre no quiero acordarme..."
tokens = sp.encode_as_pieces(text)
print(tokens)
# ['▁En', '▁un', '▁lugar', '▁de', '▁la', '▁Mancha', ',', '▁de', '▁cuyo', '▁nombre', '▁no', '▁quiero', '▁acordarme', '...']
3 Архитектура модели: что резать в первую очередь
Берем GPT-2 Small и начинаем хирургию. Наша цель - уместить модель в 6GB VRAM с batch size хотя бы 2.
Исходные параметры GPT-2 Small:
- 124M параметров
- 12 слоев
- 768 hidden size
- 12 attention heads
- Context length: 1024
Наше резаное издание:
config = {
"vocab_size": 32000, # Наш словарь
"n_positions": 512, # ВДВОЕ меньше! Классика не требует длинного контекста
"n_embd": 512, # Вместо 768
"n_layer": 8, # Вместо 12
"n_head": 8, # Вместо 12
"batch_size": 2, # Иначе не влезет в VRAM
"learning_rate": 5e-4,
"weight_decay": 0.01
}
Почему можно резать context length до 512? Потому что мы учим модель генерировать текст в стиле классики, где предложения длинные, но структура абзацев предсказуема. Если вам нужно работать с RAG-системами, возможно, стоит оставить 1024.
Самая частая ошибка: пытаться сохранить оригинальные размеры и уменьшать только batch size до 1. Это приводит к нестабильному обучению и плохой сходимости. Лучше уменьшить размерность модели, но сохранить batch size хотя бы 2.
4 Обучение: как не сжечь GPU и не ждать неделю
Здесь важны трюки, которые экономят время и память:
import torch
import torch.nn as nn
from transformers import GPT2Config, GPT2LMHeadModel
# Создаем конфигурацию
config = GPT2Config(
vocab_size=32000,
n_positions=512,
n_embd=512,
n_layer=8,
n_head=8,
resid_pdrop=0.1,
embd_pdrop=0.1,
attn_pdrop=0.1
)
model = GPT2LMHeadModel(config)
# Считаем параметры
total_params = sum(p.numel() for p in model.parameters())
print(f"Всего параметров: {total_params:,}") # ~85 миллионов
# Переносим на GPU с учетом ограниченной памяти
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
# Градиентный checkpointing - жертвуем скоростью ради памяти
model.gradient_checkpointing_enable()
# Mixed precision training - еще 40% экономии памяти
scaler = torch.cuda.amp.GradScaler()
Теперь подготовка данных:
from torch.utils.data import Dataset, DataLoader
import numpy as np
class SpanishDataset(Dataset):
def __init__(self, tokenizer, file_path, block_size=512):
with open(file_path, 'r', encoding='utf-8') as f:
text = f.read()
# Токенизируем весь текст
tokens = tokenizer.encode(text)
# Разбиваем на блоки по block_size
self.examples = []
for i in range(0, len(tokens) - block_size + 1, block_size):
self.examples.append(tokens[i:i + block_size])
def __len__(self):
return len(self.examples)
def __getitem__(self, idx):
return torch.tensor(self.examples[idx], dtype=torch.long)
Цикл обучения с экономией памяти:
optimizer = torch.optim.AdamW(model.parameters(), lr=5e-4)
dataset = SpanishDataset(tokenizer, "corpus.txt")
dataloader = DataLoader(dataset, batch_size=2, shuffle=True)
model.train()
for epoch in range(3): # Всего 3 эпохи!
total_loss = 0
for batch_idx, batch in enumerate(dataloader):
batch = batch.to(device)
optimizer.zero_grad()
# Mixed precision forward pass
with torch.cuda.amp.autocast():
outputs = model(batch, labels=batch)
loss = outputs.loss
# Scaled backward pass
scaler.scale(loss).backward()
# Gradient clipping для стабильности
scaler.unscale_(optimizer)
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
scaler.step(optimizer)
scaler.update()
total_loss += loss.item()
if batch_idx % 100 == 0:
print(f"Epoch {epoch}, Batch {batch_idx}, Loss: {loss.item():.4f}")
# Сохраняем чекпоинт каждые 1000 батчей
if batch_idx % 1000 == 0:
torch.save({
'epoch': epoch,
'model_state_dict': model.state_dict(),
'optimizer_state_dict': optimizer.state_dict(),
'loss': loss.item(),
}, f'checkpoint_epoch{epoch}_batch{batch_idx}.pt')
print(f"Epoch {epoch} average loss: {total_loss / len(dataloader):.4f}")
Результаты и что пошло не так
После 3 эпох обучения (около 18 часов на GTX 1060) модель начала генерировать связный испанский текст. Не идеально, но уже узнаваемо в стиле классики:
Промпт: "En un lugar de la Mancha"
Вывод модели: "En un lugar de la Mancha, de cuyo nombre no quiero acordarme, no ha mucho tiempo que vivía un hidalgo de los de lanza en astillero, adarga antigua, rocín flaco y galgo corredor. Una olla de algo más vaca que carnero, salpicón las más noches..."
Модель запомнила начало "Дон Кихота", но продолжила своим текстом, сохраняя стилистику. Это уже победа.
Что не сработало как ожидалось:
- Слишком короткий контекст (512 токенов) - модель иногда теряет нить повествования в длинных описаниях
- Маленький словарь (32000 токенов) - некоторые архаизмы разбиваются на субтокены
- Batch size=2 - градиенты шумные, обучение менее стабильное
Как улучшить результат без апгрейда железа
Если вы хотите лучшее качество, но не можете купить RTX 4090:
- Увеличьте датасет - добавьте больше текстов одного автора для консистентности стиля
- Используйте LoRA - дообучите только маленькие адаптеры. Подробнее в гайде про создание датасетов для LoRA
- Примените knowledge distillation - возьмите большую испанскую модель (например, MarIA) и обучите свою маленькую имитировать её
- Оптимизируйте код - замените стандартный AdamW на 8-bit Adam из bitsandbytes
FAQ: ответы на вопросы, которые вы хотели задать
Стоит ли использовать квантование для экономии памяти?
Нет. Квантование - это post-training техника. Во время обучения она только усложнит жизнь. Сначала обучите модель в FP16, потом можете квантовать для инференса.
Можно ли обучать на CPU?
Технически да, но на 100MB текста это займет недели. Если GPU нет вообще, лучше использовать специализированные методы для CPU или взять готовую маленькую модель.
Почему именно GPT-2 архитектура, а не LSTM или что-то проще?
GPT-2 хорошо изучена, есть миллион реализаций и оптимизаций. LSTM проще, но показывает худшие результаты на длинных последовательностях. Современные трансформеры, даже маленькие, эффективнее.
Как оценить качество модели?
Для таких нишевых моделей стандартные метрики (perplexity) мало что говорят. Лучший тест - дать промпт 5 людям и 5 раз модели, перемешать результаты и попросить определить, где человек, где ИИ.
Что дальше?
Эту модель можно дообучить для конкретных задач:
- Генерация стихов в стиле Гарсиа Лорки
- Написание синопсисов классических произведений
- Создание educational content для изучения испанского
Главный вывод: обучение LLM на слабом железе - не миф. Это инженерная задача, где нужно делать осознанные компромиссы. Резать размерность, а не batch size. Уменьшать контекст, а не качество токенизации. Выбирать специализацию, а не универсальность.
Когда-нибудь у нас всех будут серверы с H100. Но пока их нет - приходится хитрить. И это даже интереснее.