Немного контекста
Последние месяцы мы в команде развиваем проект Manuscript OCR - открытую библиотеку, которая учит нейросети читать рукописные документы XIX века. Это сложный материал: дореформенная орфография, нестабильный почерк, архивные артефакты.
Кому интересны технические детали - отдельная статья про Manuscript OCR уже есть на Хабре.
По ходу работы мне захотелось глубже разобраться в современных подходах к дообучению языковых моделей: SFT, LoRA, стилизация диалогов, подготовка синтетических датасетов. И как небольшой побочный эксперимент я решил попробовать обучить tiny-модель говорить в манере человека примерно 1850-х годов. Это не продакшн-задача, а скорее образовательный проект “для себя”, чтобы лучше понять инструменты в реальной практике.
Погружаясь в дореформенный язык в рамках основного проекта - тестируя OCR, сравнивая орфографию, анализируя тексты - я всё чаще ловил себя на мысли: если мы учим модель распознавать рукописи той эпохи, почему бы не попробовать сделать модель, которая отвечает в том же стиле?

Помимо основной работы над Manuscript OCR, мне стало интересно попробовать дообучить маленькую модель под дореформенную стилизацию - скорее как образовательный эксперимент. Такой «голос эпохи» можно использовать в демонстрациях и экспериментальных интерфейсах, но основная цель здесь - именно проверить, как tiny-LLM ведёт себя при локальном дообучении на стиле.
И при этом хотелось решение, которое можно:
- попробовать обучить всё локально и не зависеть от внешних API хотя бы для экспериментов,
- запускать на обычном GPU,
Так появилась идея обучить небольшую языковую модель, которая общается на дореформенном русском - спокойным, размеренным тоном образованного человека середины XIX века.
Это и есть герой этой статьи - условный «господин говорунъ», мини-LLM, дообученная под вполне специфическую, но интересную задачу.
Где взять диалоги XIX века?
Когда возникает идея обучить модель разговаривать как человек XIX века, довольно быстро всплывает первая проблема:
готовых датасетов “вопрос-ответ в дореформенном стиле” попросту нет.
Исторические тексты - есть.
Архивные письма - есть.
Рукописи - тоже.
Но всё это - монологи, а не диалоги. Да и стилизовать ответы вручную на десятки тысяч примеров - задача из разряда «можно, но не нужно».
Поэтому я пошел другим путём.
Базовый корпус: alpaca-cleaned-ru
В качестве основы я взял открытый датасет d0rj/alpaca-cleaned-ru (~50 000 примеров)
Это обычный набор пар «instruction - response» на современном русском - короткие вопросы и простые ответы. Например:
Какие три основных цвета?
Три основных цвета - красный, синий и жёлтый…
Какая столица Франции?
Столица Франции - Париж.
Сгенерируйте три глагола, которые означают то же самое, что и “извиняться”.
Выразить сожаление
Искупить
Загладить вину
Формат идеальный: понятные короткие пары, которые легко превращаются в обучающие примеры.
Не хватало только одного: перевести всё это в XIX век.
Синтетика вместо архивов
Чтобы модель понимала задачу и отвечала в нужном стиле, я сделал следующее: взял крупную LLM и попросил её:
переформулировать вопрос так, чтобы он звучал естественно для человека середины XIX века;
сгенерировать ответ, соблюдая дореформенную орфографию и манеру речи.
Конечно, можно было просто использовать крупную модель напрямую - задать хороший промпт и получить похожую стилизацию без дообучения.
Но мне было интереснее проверить другой путь: а можно ли добиться похожего эффекта на маленькой модели, обученной локально, полностью на синтетическом датасете?
Поэтому я взял большую LLM только для генерации обучающей выборки, а сам эксперимент сосредоточил на tiny-модели.
То есть на вход подаётся современная пара, а на выходе получается новая - уже стилизованная.
Результат - примерно 50 000 пар, которые выглядят так:
Вопрос: Пошагово объясните процесс приготовления сырной кесадильи.
Ответ: Что ж, кесадилья… дивно звучитъ. Не слышалъ о сем блюдѣ, но, полагаю, суть въ том, дабы сыръ въ лепешку завернуть да нагреть…
Вопрос: Назовите двух деятелей - современников Галилея.
Ответ: Иоаннъ Кеплеръ и Исаакъ Бекманъ - люди учёныя и серьёзныя.
Вопрос: Разработайте алгоритм сортировки списка.
Ответ: Коли списокъ малъ, разложите числа от меньшаго к большему. А коли великъ - сравнивайте попарно да меняйте мѣстами…
Почему маленькая модель?
Для эксперимента я специально выбрал маленькую модель Qwen2.5-0.5B. Не потому что это единственно правильное решение, а потому что хотелось проверить, насколько далеко можно зайти именно с tiny-LLM в условиях локального обучения. В реальной задаче, конечно, имеет смысл рассматривать модели покрупнее или LoRA-адаптеры на мощных LLM - но здесь цель была учебная и исследовательская.
Это действительно очень маленькая LLM по современным меркам, но зато:
обучается быстро,
не требует огромных ресурсов,
хорошо поддаётся дообучению,
показывает достойные результаты на задачах «стилевого» файнтюна.
Среда и общий процесс
Для эксперимента я использовал обычный Google Colab.
Чтобы было проще повторить - весь код доступен в ноутбуке:
В статье ниже - ключевые фрагменты с комментариями.
Загрузка модели и токенизатора
from transformers import AutoTokenizer, AutoModelForCausalLM
from trl import SFTTrainer, SFTConfig
MODEL_NAME = "Qwen/Qwen2.5-0.5B"
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
model = AutoModelForCausalLM.from_pretrained(MODEL_NAME, torch_dtype="auto")В этот момент можно приятно удивиться: модель весит мало, загружается быстро, память не забивает.
Подготовка датасета
from datasets import Dataset
import json, re
DATA_PATH = "training_data_19century.json"
def remove_system_prompt(text):
pattern = r'<\|system\|>.*?<\|user\|>'
return re.sub(pattern, '<|user|>', text, flags=re.DOTALL)
data_list = []
with open(DATA_PATH, 'r', encoding='utf-8') as f:
for line in f:
obj = json.loads(line)
cleaned_text = remove_system_prompt(obj["text"])
data_list.append({"text": cleaned_text})
data = Dataset.from_dict({"text": [x["text"] for x in data_list]})На выходе - примерно 50k примеров диалогов в дореформенном стиле.
Конфигурация обучения
config = SFTConfig(
output_dir="model_1",
per_device_train_batch_size=1,
logging_steps=1000,
max_length=1024,
num_train_epochs=1,
lr_scheduler_type="cosine",
learning_rate=7e-5,
run_name='exp1',
save_steps=1000,
save_total_limit=1,
)Обучение
trainer = SFTTrainer(
model=model,
args=config,
train_dataset=data,
processing_class=tokenizer,
)
trainer.train()Вся тренировка заняла ~4 часа на стандартном Colab GPU.
Step | Training Loss |
|---|---|
1000 | 2.560000 |
2000 | 2.220300 |
3000 | 2.084600 |
... | ... |
41000 | 1.362100 |
42000 | 1.347800 |
43000 | 1.327500 |
Лосс постепенно снижался, что показывает, что обучение идёт в нужную сторону. Это не глубокий анализ качества, но для учебного эксперимента этого достаточно.
Сохранение модели
model.save_pretrained("model_19century_final")
tokenizer.save_pretrained("model_19century_final")На этом шаге получаем полностью готовые веса модели, которые можно загрузить локально или залить в HuggingFace Hub.
Результаты: что получилось на практике
После завершения обучения я начал тестировать модель на простых бытовых вопросах - примерно таких же, какие были в исходном корпусе. Ожидания были умеренными: tiny-модель, один проход по данным, стильовая задача. Но всё равно хотелось посмотреть, как она поведёт себя “вживую”.
Ниже - реальные ответы, без правок и украшений.
Вопрос:
Сколько планет в нашей солнечной системе?
Ответ модели:
О, вопросъ любезный! Земля наша, какъ намъ кажется, богатъ планетами. Земля наша, конечно, есть малая, но планетъ въ ней, полагаю, въ сотни разъ больше, нежели и въ нашей нашей Солнечной системѣ. Считается, что свѣтъ изъ нихъ въ небесахъ видимъ въ разы больше, нежели въ землѣ нашей.
Здесь модель полностью уходит в образ, но теряет связь с реальностью. Архаичный тон выдержан, орфография тоже, но смысл - размыт, астрономии - ноль. Типичная реакция маленькой LLM, обученной на стилевых данных.
Вопрос:
Как быстро выучить новый язык?
Ответ модели:
Почитай, господинъ! Истинно, не всякій въ словеси пойметъ, а разумъ не всегда бываетъ. Но коли хочешь, да и душа у тебя не задержится. Слышалъ я, что въ Россіи, въ Петербургѣ, въ гимназіяхъ учатъ, да и въ университетахъ, если позволятъ. А коли ужъ не такъ, то, пожалуй, почитайъ и почитайъ.
Стиль - есть. Дореформа - есть. Логика - частично: модель понимает, что речь о “учёбе”, но дальше начинает блуждать. Ответ звучит “по духу эпохи”, но не несёт чёткого совета.
Вопрос:
Когда собирать крыжовник?
Ответ модели:
Крыжовникъ? Да что вы говорите! Истинно, что на ней въ небесахъ видимъ. Да и въ сущности, что за вещь эта, крыжовникъ? И что есть "книга"? Не слышалъ я о семъ.
Очень показательная ошибка: модель не знает предмет → начинает блуждать по семантическому полю, теряет объект (“крыжовник”), переходит к чему-то созвучному (“книга”), и уходит в сторону.
Что можно сказать по итогам тестирования
Модель отражает ровно то, чем её кормили:
Что получилось хорошо
стиль эпохи - уверенный, атмосферный;
дореформенная орфография - стабильная и последовательная;
тон - действительно напоминает речь «образованного человека XIX века»;
работает стабильно, без генерации современного жаргона или англицизмов;
отлично подходит как демонстрационный “голос эпохи”.
Что ожидаемо слабее
понимание смысла: tiny-модель путается в фактах и бытовых понятиях;
предметные знания отсутствуют, модель компенсирует “общей говорливостью”;
ответы могут быть витиеватыми, но малоинформативными;
в некоторых случаях уходит в ассоциативную болтовню (“книга” вместо “крыжовник”).
Почему так?
Потому что задача была чисто стилевая, а не “учебно-фактовая”.
Модель не обучалась на ботанике, астрономии или учебниках, а только на синтетических диалогах, выдержанных в определённой манере.
Tiny-LLM не может «догадаться» о мире - она лишь воспроизводит статистику стиля. И с этой ролью она справляется.
Что можно сказать по итогам тестирования
Когда базовая версия модели заработала, я решил оформить результат так, чтобы им можно было легко делиться и тестировать без локального запуска. Для таких задач удобнее всего использовать HuggingFace - там и хранение моделей, и готовые инструменты для небольших интерактивных демо.
В итоге я создал репозиторий модели и собрал простой чат-интерфейс на Spaces, куда можно зайти и пообщаться с “дореформенным собеседником” напрямую.

Итоги
Эксперимент оказался полезным с образовательной точки зрения: я разобрался, как tiny-модель реагирует на стилевой датасет, где возникают провалы, а где - работает лучше ожидаемого. Результаты получились ограниченными, но вполне рабочими для такого объёма данных и условий обучения.
Для меня это стало хорошим подтверждением, что подход со стилевыми датасетами работает, а лёгкие модели вполне пригодны для таких точечных проектов. Дальше можно расширять корпус, улучшать устойчивость и интегрировать “говоруна” в демо Manuscript OCR.
В итоге эксперимент получился таким, как и планировалось: простой, локальный тест идеи - собрать датасет, обучить tiny-модель и посмотреть, что из этого выйдет. Никакого продакшна - но отличная база для дальнейших экспериментов и для развития компонентов Manuscript OCR.
