Немного контекста

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

Работая над этим проектом, я всё больше погружался в дореформенный язык: тестировал модели, прогонял страницы, сравнивал орфографические варианты. И в какой-то момент возник вполне естественный вопрос:

если мы учим нейросеть распознавать тексты той эпохи, почему бы не сделать модель, которая умеет отвечать в том же стиле?

Господин Говорунъ распознает рукописи
Господин Говорунъ распознает рукописи

Не ради развлечения, а как практический инструмент:
- для демонстрации работы OCR;
- для объяснения распознанных текстов;
- для создания небольшого чат-бота, который говорит «голосом» XIX века.

И при этом хотелось решение, которое можно:
- обучить локально,
- не платить за API,
- запускать на обычном GPU,
- интегрировать в пайплайн проекта.

Так появилась идея обучить небольшую языковую модель, которая общается на дореформенном русском - спокойным, размеренным тоном образованного человека середины XIX века.

Это и есть герой этой статьи - условный «господин говорунъ», мини-LLM, дообученная под вполне специфическую, но интересную задачу.

Где взять диалоги XIX века?

Когда возникает идея обучить модель разговаривать как человек XIX века, довольно быстро всплывает первая проблема:

готовых датасетов “вопрос–ответ в дореформенном стиле” попросту нет.

Исторические тексты - есть.
Архивные письма - есть.
Рукописи - тоже.
Но всё это - монологи, а не диалоги. Да и стилизовать ответы вручную на десятки тысяч примеров - задача из разряда «можно, но не нужно».

Поэтому я пошел другим путём.

Базовый корпус: alpaca-cleaned-ru

В качестве основы я взял открытый датасет d0rj/alpaca-cleaned-ru (~50 000 примеров)

Это обычный набор пар «instruction - response» на современном русском - короткие вопросы и простые ответы. Например:

Какие три основных цвета?
Три основных цвета - красный, синий и жёлтый…

Какая столица Франции?
Столица Франции - Париж.

Сгенерируйте три глагола, которые означают то же самое, что и “извиняться”.

  1. Выразить сожаление

  2. Искупить

  3. Загладить вину

Формат идеальный: понятные короткие пары, которые легко превращаются в обучающие примеры.

Не хватало только одного: перевести всё это в XIX век.

Синтетика вместо архивов

Чтобы модель понимала задачу и отвечала в нужном стиле, я сделал следующее: взял крупную LLM и попросил её:

  1. переформулировать вопрос так, чтобы он звучал естественно для человека середины XIX века;

  2. сгенерировать ответ, соблюдая дореформенную орфографию и манеру речи.

То есть на вход подаётся современная пара, а на выходе получается новая - уже стилизованная.

Результат - примерно 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, куда можно зайти и пообщаться с “дореформенным собеседником” напрямую.

Интерфейс чат-бота, опубликованный на HuggingFace
Интерфейс чат-бота, опубликованный на HuggingFace

Итоги

Этот небольшой эксперимент показал простую вещь: даже маленькую модель можно дообучить под узкую историческую задачу и получить живой, узнаваемый стиль общения. Да, модель не знает фактов, путается в предметах и иногда уходит в сторону - но свою основную роль она выполняет: говорит так, как будто действительно пришла из середины XIX века.

Для меня это стало хорошим подтверждением, что подход со стилевыми датасетами работает, а лёгкие модели вполне пригодны для таких точечных проектов. Дальше можно расширять корпус, улучшать устойчивость и интегрировать “говоруна” в демо Manuscript OCR.

В любом случае, это маленький, но законченный шаг: идея проверена, модель обучена, демо выложено - и можно двигаться дальше.

Комментарии (2)


  1. Tekopyhat
    28.11.2025 13:16

    Для толковой оценки надо было привести ответы той же модели на те же вопросы до обучения, чтобы видеть, насколько её логику скукожило тренировкой.


    1. konstantin_kozhin Автор
      28.11.2025 13:16

      Здравствуйте! Я сейчас ради интереса прогнал эти же вопросы через базовую Qwen2.5-0.5B - без обучения. И она, по сути, отвечает примерно так же странно. Поэтому важно понимать: вся эта история - именно эксперимент. Хотелось проверить, можно ли вообще получить хоть какую-то дореформенную стилизацию на маленькой модели, дообученной локально.

      Да, у такой модели сразу видно, где всё разваливается. Это нормально: мало параметров, синтетический датасет, одна эпоха обучения. С этим можно работать - собрать датасет получше, сбалансировать вопросы или взять модель покрупнее, но всё ещё в пределах реальных ресурсов.

      В общем, задача была проверить сам подход — и он, в целом, оказался рабочим. Дальше уже можно доводить.