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

Не ради развлечения, а как практический инструмент:
- для демонстрации работы OCR;
- для объяснения распознанных текстов;
- для создания небольшого чат-бота, который говорит «голосом» XIX века.
И при этом хотелось решение, которое можно:
- обучить локально,
- не платить за API,
- запускать на обычном GPU,
- интегрировать в пайплайн проекта.
Так появилась идея обучить небольшую языковую модель, которая общается на дореформенном русском - спокойным, размеренным тоном образованного человека середины XIX века.
Это и есть герой этой статьи - условный «господин говорунъ», мини-LLM, дообученная под вполне специфическую, но интересную задачу.
Где взять диалоги XIX века?
Когда возникает идея обучить модель разговаривать как человек XIX века, довольно быстро всплывает первая проблема:
готовых датасетов “вопрос–ответ в дореформенном стиле” попросту нет.
Исторические тексты - есть.
Архивные письма - есть.
Рукописи - тоже.
Но всё это - монологи, а не диалоги. Да и стилизовать ответы вручную на десятки тысяч примеров - задача из разряда «можно, но не нужно».
Поэтому я пошел другим путём.
Базовый корпус: alpaca-cleaned-ru
В качестве основы я взял открытый датасет d0rj/alpaca-cleaned-ru (~50 000 примеров)
Это обычный набор пар «instruction - response» на современном русском - короткие вопросы и простые ответы. Например:
Какие три основных цвета?
Три основных цвета - красный, синий и жёлтый…
Какая столица Франции?
Столица Франции - Париж.
Сгенерируйте три глагола, которые означают то же самое, что и “извиняться”.
Выразить сожаление
Искупить
Загладить вину
Формат идеальный: понятные короткие пары, которые легко превращаются в обучающие примеры.
Не хватало только одного: перевести всё это в XIX век.
Синтетика вместо архивов
Чтобы модель понимала задачу и отвечала в нужном стиле, я сделал следующее: взял крупную LLM и попросил её:
переформулировать вопрос так, чтобы он звучал естественно для человека середины XIX века;
сгенерировать ответ, соблюдая дореформенную орфографию и манеру речи.
То есть на вход подаётся современная пара, а на выходе получается новая - уже стилизованная.
Результат - примерно 50 000 пар, которые выглядят примерно так:
Вопрос: Пошагово объясните процесс приготовления сырной кесадильи.
Ответ: Что ж, кесадилья… дивно звучитъ. Не слышалъ о сем блюдѣ, но, полагаю, суть въ том, дабы сыръ въ лепешку завернуть да нагреть…
Вопрос: Назовите двух деятелей - современников Галилея.
Ответ: Иоаннъ Кеплеръ и Исаакъ Бекманъ - люди учёныя и серьёзныя.
Вопрос: Разработайте алгоритм сортировки списка.
Ответ: Коли списокъ малъ, разложите числа от меньшаго к большему. А коли великъ - сравнивайте попарно да меняйте мѣстами…
Почему маленькая модель?
Для задачи «говорить в стиле XIX века» не нужна гигантская LLM на десятки миллиардов параметров. Требования были довольно скромные:
запуск на обычном GPU
быстрое обучение
дешёвый эксперимент
возможность интегрировать модель в демо Manuscript OCR
и главное - легко воспроизводимый пайплайн, который может повторить любой желающий.
Поэтому я остановился на компактной модели: Qwen/Qwen2.5-0.5B
Это действительно очень маленькая 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,
)
Ключевые параметры:
batch size = 1 - да, это нормально для tiny-модели + Colab
1 эпоха - как оказалось, этого вполне хватает
learning rate = 7e-5 - модель реагирует стабильно
max_length = 1024 - хватает с запасом
scheduler = cosine - безопасный вариант без резких скачков
Обучение
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 |
По логам можно видеть, что loss постепенно и плавно снижался - без «пилы» и скачков.
Сохранение модели
model.save_pretrained("model_19century_final")
tokenizer.save_pretrained("model_19century_final")
На этом шаге получаем полностью готовые веса модели, которые можно загрузить локально или залить в HuggingFace Hub.
Результаты: что получилось на практике
После завершения обучения я начал тестировать модель на простых бытовых вопросах - примерно таких же, какие были в исходном корпусе. Ожидания были умеренными: tiny-модель, один проход по данным, стильовая задача. Но всё равно хотелось посмотреть, как она поведёт себя “вживую”.
Ниже - реальные ответы, без правок и украшений.
Вопрос:
Сколько планет в нашей солнечной системе?
Ответ модели:
О, вопросъ любезный! Земля наша, какъ намъ кажется, богатъ планетами. Земля наша, конечно, есть малая, но планетъ въ ней, полагаю, въ сотни разъ больше, нежели и въ нашей нашей Солнечной системѣ. Считается, что свѣтъ изъ нихъ въ небесахъ видимъ въ разы больше, нежели въ землѣ нашей.
Здесь модель полностью уходит в образ, но теряет связь с реальностью. Архаичный тон выдержан, орфография тоже, но смысл - размыт, астрономии - ноль. Типичная реакция маленькой LLM, обученной на стилевых данных.
Вопрос:
Как быстро выучить новый язык?
Ответ модели:
Почитай, господинъ! Истинно, не всякій въ словеси пойметъ, а разумъ не всегда бываетъ. Но коли хочешь, да и душа у тебя не задержится. Слышалъ я, что въ Россіи, въ Петербургѣ, въ гимназіяхъ учатъ, да и въ университетахъ, если позволятъ. А коли ужъ не такъ, то, пожалуй, почитайъ и почитайъ.
Стиль - есть. Дореформа - есть. Логика - частично: модель понимает, что речь о “учёбе”, но дальше начинает блуждать. Ответ звучит “по духу эпохи”, но не несёт чёткого совета.
Вопрос:
Когда собирать крыжовник?
Ответ модели:
Крыжовникъ? Да что вы говорите! Истинно, что на ней въ небесахъ видимъ. Да и въ сущности, что за вещь эта, крыжовникъ? И что есть "книга"? Не слышалъ я о семъ.
Очень показательная ошибка: модель не знает предмет → начинает блуждать по семантическому полю, теряет объект (“крыжовник”), переходит к чему-то созвучному (“книга”), и уходит в сторону.
Что можно сказать по итогам тестирования
Модель отражает ровно то, чем её кормили:
Что получилось хорошо
стиль эпохи - уверенный, атмосферный;
дореформенная орфография - стабильная и последовательная;
тон - действительно напоминает речь «образованного человека XIX века»;
работает стабильно, без генерации современного жаргона или англицизмов;
отлично подходит как демонстрационный “голос эпохи”.
Что ожидаемо слабее
понимание смысла: tiny-модель путается в фактах и бытовых понятиях;
предметные знания отсутствуют, модель компенсирует “общей говорливостью”;
ответы могут быть витиеватыми, но малоинформативными;
в некоторых случаях уходит в ассоциативную болтовню (“книга” вместо “крыжовник”).
Почему так?
Потому что задача была чисто стилевая, а не “учебно-фактовая”.
Модель не обучалась на ботанике, астрономии или учебниках, а только на синтетических диалогах, выдержанных в определённой манере.
Tiny-LLM не может «догадаться» о мире - она лишь воспроизводит статистику стиля. И с этой ролью она справляется.
Что можно сказать по итогам тестирования
Когда базовая версия модели заработала, я решил оформить результат так, чтобы им можно было легко делиться и тестировать без локального запуска. Для таких задач удобнее всего использовать HuggingFace - там и хранение моделей, и готовые инструменты для небольших интерактивных демо.
В итоге я создал репозиторий модели и собрал простой чат-интерфейс на Spaces, куда можно зайти и пообщаться с “дореформенным собеседником” напрямую.

Итоги
Этот небольшой эксперимент показал простую вещь: даже маленькую модель можно дообучить под узкую историческую задачу и получить живой, узнаваемый стиль общения. Да, модель не знает фактов, путается в предметах и иногда уходит в сторону - но свою основную роль она выполняет: говорит так, как будто действительно пришла из середины XIX века.
Для меня это стало хорошим подтверждением, что подход со стилевыми датасетами работает, а лёгкие модели вполне пригодны для таких точечных проектов. Дальше можно расширять корпус, улучшать устойчивость и интегрировать “говоруна” в демо Manuscript OCR.
В любом случае, это маленький, но законченный шаг: идея проверена, модель обучена, демо выложено - и можно двигаться дальше.
Tekopyhat
Для толковой оценки надо было привести ответы той же модели на те же вопросы до обучения, чтобы видеть, насколько её логику скукожило тренировкой.
konstantin_kozhin Автор
Здравствуйте! Я сейчас ради интереса прогнал эти же вопросы через базовую Qwen2.5-0.5B - без обучения. И она, по сути, отвечает примерно так же странно. Поэтому важно понимать: вся эта история - именно эксперимент. Хотелось проверить, можно ли вообще получить хоть какую-то дореформенную стилизацию на маленькой модели, дообученной локально.
Да, у такой модели сразу видно, где всё разваливается. Это нормально: мало параметров, синтетический датасет, одна эпоха обучения. С этим можно работать - собрать датасет получше, сбалансировать вопросы или взять модель покрупнее, но всё ещё в пределах реальных ресурсов.
В общем, задача была проверить сам подход — и он, в целом, оказался рабочим. Дальше уже можно доводить.
Moog_Prodigy
А может проще было бы использовать LoRa? И обучать проще, и подключить потом к мощной модельке - сплошныя выгоды.
А то и просто системный промпт: описать заранее правила дореформенного написания и "пошла писать губернiя"
konstantin_kozhin Автор
Спасибо, замечание по делу! Да, для такой узкой задачи LoRA правда удобнее - и обучать легче. С промптами та же история: если брать API средней модели и нормально расписать правила дореформенного письма, она всё это держит без проблем.
Здесь же я сознательно пошёл по самому простому пути - tiny-модель, минимум ресурсов, синтетический датасет - просто чтобы проверить саму идею: а можно ли вообще получить хоть какую-то стилизацию такими силами? Как эксперимент - получилось, дальше уже можно идти в сторону LoRA и моделей побольше.
Ну и эти эксперименты могут стать неплохой заготовкой для будущего модуля коррекции в нашем основном проекте Manuscript OCR.