При дообучении BERT мы часто сталкиваемся с парадоксом: метрики на валидации отличные (F1 > 0.95), а в продакшене — провал. Причина — несоответствие распределений: train и real‑world данные различаются по длине, стилю, формулировкам. Мы решаем эту проблему системно: от выбора базовой модели до production‑оптимизации. Недавно к нам обратился клиент с задачей классификации отзывов на маркетплейсе — на валидации F1 достигал 0.97, но в реальном потоке упал до 0.82. Анализ показал, что большинство ложных предсказаний приходилось на короткие отзывы (менее 10 токенов) — их почти не было в обучающей выборке. После добавления аугментации и настройки max_length до 128 метрика вернулась к 0.94.
Выбор базовой модели
BERT (bert-base-uncased, DeepPavlov/rubert-base-cased): классика, хорошо изучена, много туториалов. Для большинства задач достаточно.
RoBERTa (roberta-base, ai-forever/ruRoBERTa-large): улучшенное обучение без Next Sentence Prediction, на большем корпусе. Обычно на 1–3% лучше BERT.
DeBERTa (microsoft/deberta-v3-base): диcентанглированное внимание — лучшее качество на benchmarks. Рекомендуется если нужна максимальная точность и есть GPU-ресурсы.
Для русского языка: ai-forever/ruBert-base, DeepPavlov/rubert-base-cased, ai-forever/ruRoBERTa-large, ai-forever/sber-roberta-large.
Как выбрать базовую модель для русского языка?
Выбор зависит от трёх факторов: размера датасета, целевой метрики и доступных вычислительных ресурсов. Для старта мы рекомендуем DeepPavlov/rubert-base-cased — он сбалансирован по качеству и скорости обучения. Если датасет >10К примеров и есть V100/A100, используйте ai-forever/ruRoBERTa-large — прирост F1 до 3%. Для максимальной точности (и при наличии времени на тюнинг) — microsoft/deberta-v3-base.
Pipeline обучения
from transformers import (
AutoTokenizer, AutoModelForSequenceClassification,
TrainingArguments, Trainer
)
from datasets import Dataset
import evaluate
import numpy as np
# Подготовка данных
tokenizer = AutoTokenizer.from_pretrained("DeepPavlov/rubert-base-cased")
def tokenize_function(examples):
return tokenizer(
examples["text"],
padding="max_length",
truncation=True,
max_length=256 # 256 достаточно для большинства задач
)
dataset = Dataset.from_pandas(df)
tokenized = dataset.map(tokenize_function, batched=True)
tokenized = tokenized.train_test_split(test_size=0.2)
# Инициализация модели
model = AutoModelForSequenceClassification.from_pretrained(
"DeepPavlov/rubert-base-cased",
num_labels=num_classes,
id2label=id2label,
label2id=label2id
)
# Метрики
accuracy = evaluate.load("accuracy")
f1 = evaluate.load("f1")
def compute_metrics(eval_pred):
logits, labels = eval_pred
predictions = np.argmax(logits, axis=-1)
return {
"accuracy": accuracy.compute(predictions=predictions, references=labels)["accuracy"],
"f1_macro": f1.compute(predictions=predictions, references=labels, average="macro")["f1"],
}
# Параметры обучения
training_args = TrainingArguments(
output_dir="./results",
num_train_epochs=5,
per_device_train_batch_size=16,
per_device_eval_batch_size=32,
learning_rate=2e-5,
weight_decay=0.01,
warmup_ratio=0.1,
evaluation_strategy="epoch",
save_strategy="epoch",
load_best_model_at_end=True,
metric_for_best_model="f1_macro",
fp16=True, # mixed precision для GPU
)
trainer = Trainer(
model=model,
args=training_args,
train_dataset=tokenized["train"],
eval_dataset=tokenized["test"],
compute_metrics=compute_metrics,
)
trainer.train()
Гиперпараметры и их влияние
| Параметр | Рекомендуемый диапазон | Влияние |
|---|---|---|
| learning_rate | 1e-5 – 5e-5 | Самый критичный. 2e-5 — хороший старт |
| num_epochs | 3–10 | Переобучение при > 10 |
| batch_size | 8–32 | Больше = стабильнее, но нужно больше VRAM |
| max_length | 64–512 | Зависит от длины текстов |
| warmup_ratio | 0.06–0.1 | Предотвращает нестабильное начало |
Сравнение времени инференса моделей
| Модель | FP32 latency (CPU) | INT8 latency (CPU) | F1 drop (vs FP32) |
|---|---|---|---|
| ruBERT-base | 120ms | 18ms | -0.3% |
| ruRoBERTa-large | 340ms | 52ms | -0.5% |
| DeBERTa-v3-base | 190ms | 30ms | -0.4% |
Почему важно использовать weighted loss при дисбалансе классов?
Если классы распределены неравномерно (например, 95% нормальных обращений и 5% жалоб), стандартный CrossEntropyLoss «проигнорирует» редкий класс. Мы добавляем class_weight в функцию потерь:
from torch import nn
import torch
# Вычисляем веса классов
class_weights = compute_class_weight("balanced", classes=np.unique(labels), y=labels)
weights_tensor = torch.FloatTensor(class_weights).to(device)
class WeightedTrainer(Trainer):
def compute_loss(self, model, inputs, return_outputs=False):
labels = inputs.pop("labels")
outputs = model(**inputs)
logits = outputs.get("logits")
loss_fn = nn.CrossEntropyLoss(weight=weights_tensor)
loss = loss_fn(logits, labels)
return (loss, outputs) if return_outputs else loss
Этот приём поднимает F1 по редкому классу на 10–15% без потери качества на частых.
Оценка и анализ ошибок
После обучения обязательно:
- Confusion matrix по всем классам
- Примеры ошибок для каждой пары (истинный класс, предсказанный класс)
- Calibration plot: насколько достоверны вероятности модели
- Error analysis: есть ли паттерн в ошибках? (определённые слова, длина текста, авторский стиль)
Детальный чек-лист посттренировочной валидации
- Проверить распределение предсказаний на реальных данных (отличных от train/test).
- Построить ROC-кривую для каждого класса, убедиться в AUC >0.95.
- Выполнить A/B-тест: сравнить новую модель с текущей на случайной выборке.
- Замерить latency p99 и throughput на целевой конфигурации.
- Подготовить модель карту с ограничениями и известными edge-case.
Оптимизация для продакшена
После fine-tuning экспортировать в ONNX:
from optimum.onnxruntime import ORTModelForSequenceClassification
ort_model = ORTModelForSequenceClassification.from_pretrained("./results", export=True)
ort_model.save_pretrained("./onnx_model")
Benchmark: ruBERT fine-tuned → ONNX INT8: 120ms → 18ms на CPU при точности -0.3%. Благодаря квантизации стоимость инференса снижается на 40% по сравнению с FP32. На одном из проектов мы сократили затраты на GPU с $2000 до $1200 в месяц.
Что входит в нашу работу
Мы предоставляем не просто модель, а готовое решение под ключ:
- Анализ датасета и определение минимального достаточного размера выборки
- Выбор оптимальной архитектуры (BERT/RoBERTa/DeBERTa) под ваши данные
- Дообучение с подбором гиперпараметров (learning rate, batch size, epochs)
- Пост-тренировочный анализ: confusion matrix, calibration curve, error analysis
- Экспорт в ONNX/TensorRT для инференса на CPU
- Интеграция в ваш пайплайн (REST API, gRPC, бэтч)
- Документация модели и метрик, отчёт по качеству
- Поддержка после внедрения — гарантируем стабильную работу
С нами работают компании с 7+ летним опытом в NLP, мы выполнили более 30 проектов по классификации текстов. Свяжитесь с нами для оценки вашей задачи — мы подберём оптимальный стэк и режим обучения. Закажите консультацию, и мы проанализируем ваш датасет.
Типичные результаты
На задачах классификации новостей: 92–96% F1 macro. Классификация обращений клиентов: 88–94%. Мультиметочная классификация: 78–86% Micro F1. Все результаты фиксируются в отчёте и подтверждаются на отложенной выборке.







