Skip to content
vonexelPublic

About

Automatic identification of standalone services and generation of draft listings as part of IT Purple Hack 2026 (Avito Case)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

SubDraft 📌📝: Автоматическое выделение самостоятельных услуг и генерация черновиков объявлений для Авито

🔬 О задаче

Исходя из текста объявления в категории «Ремонт и отделка» определить, какие микрокатегории представлены как самостоятельные услуги, и при необходимости, сгенерировать черновики дополнительных объявлений исходя из вычленения определенной услуги. Задача решения — по тексту объявления понять:

  • какие микрокатегории вообще присутствуют в тексте;
  • какие из них представлены как самостоятельные услуги;
  • нужно ли создавать дополнительные черновики объявлений;
  • для каких микрокатегорий создавать эти черновики;
  • по возможности — сгенерировать текст черновика.

📁 Структура проекта

├── datasets
    ├── rnc_dataset                      
        ├── rnc_dataset_markup.csv        # основной датасет
        ├── rnc_dataset_markup.json       # основной датасет в json 
        ├── rnc_dataset_README.txt        # описание основного датасета
    ├── rnc_mic_key_phrases.csv           # словарь микрокатегорий
    ├── rnc_test.csv                      # исходный тестовый набор данных 
├── notebooks                             
    ├── itpurple-avito-eda.ipynb          # eda
    ├── itpurple-avito-subdraft.ipynb     # пайплайн
├── subdraft_artifacts                    # артефакты
    ├── arbiter.pkl                       # сериализованный объект арбитража
    ├── bank_data.pkl                     # промежуточные данные пайплайна
    ├── config.json                       # глобальная конфигурация пайплайна
    ├── retriever_model                   # модель для поиска кандидатов
        ├── config.json                   # параметры архитектуры ретривера
        ├── model.safetensors             # веса модели ретривера
    ├── retriever_tokenizer               # токенизатор для ретривера
        ├── tokenizer.json                # словарь и правила токенизации
        ├── tokenizer_config.json         # конфигурация токенизатора 
    ├── verifier_model                    # модель-верификатор (классификация) 
        ├── config.json                   # параметры архитектуры верификатора
        ├── model.safetensors             # веса модели верификатора
    ├── verifier_tokenizer                # токенизатор для верификатора
        ├── tokenizer.json                # словарь и правила токенизации
        ├── tokenizer_config.json         # конфигурация токенизатора  
├── rnc_test_predictions.csv              # предсказания на тестовой выборке rnc_test.csv   
├── README.md

📋 Формализация задачи

Вход

На вход подаются:

  • словарь микрокатегорий с полями mcId, mcTitle, keyPhrases;
  • объявление с полями:
    • itemId
    • mcId / sourceMcId
    • mcTitle / sourceMcTitle
    • description

Выход

Модель должна вернуть:

  • detectedMcIds — все найденные в тексте микрокатегории;
  • shouldSplit — нужен ли хотя бы один дополнительный черновик;
  • drafts — список черновиков:
    • mcId
    • mcTitle
    • text

Целевые метрики

Основные метрики:

  • Precision (micro) по дополнительным микрокатегориям;
  • Recall (micro) по дополнительным микрокатегориям;
  • F1-score (micro) — основная метрика качества;
  • Accuracy по shouldSplit — корректность бинарного решения о необходимости сплита.

⚡ Подход

Ключевая сложность задачи — отличить упоминание услуги от предложения её как самостоятельной. Простой поиск по ключевым словам даёт ложноположительные срабатывания на комплексных описаниях («ремонт под ключ, включая электрику и сантехнику»). Решение использует каскадную архитектуру из трёх стадий, где каждый следующий компонент уточняет решение предыдущего:

Текст объявления
      │
      ▼
Hybrid Retriever (E5 + TF-IDF)         <- какие микрокатегории вообще упомянуты?
      │
      ▼
Cross-Encoder Verifier (rubert-tiny2)  <- упомянуты ли они как самостоятельные?
      │
      ▼
Late-Fusion Arbiter (HGBC)             <- финальное решение по каждому кандидату.
      │
      ▼
Draft Generator                         <- генерация текста черновика.

Почему каскад, а не единая модель

EDA продемонстрировал, что целевая переменная shouldSplit описывает не факт наличия услуги, а степень её автономности в тексте. Это двухуровневая задача: в первую очередь необходимо найти релевантные микрокатегории (retrieval), а затем для каждой из них, оценить, является ли она самостоятельной (verification & arbitration). Единая модель не способна эффективно решать обе подзадачи одновременно, тогда как каскад позволяет использовать оптимальные подходы на каждом этапе.

Выводы из EDA

Проведенный исследовательский анализ данных (itpurple-avito-eda.ipynb) позволил выявить ключевые закономерности, которые легли в основу архитектуры и пространства признаков.

Дисбаланс классов

Распределение целевой переменной shouldSplit сильно несбалансировано: 2009 объявлений с False против 471 с True (соотношение ~4:1). Это потребовало стратифицированного разбиения при валидации и взвешенной функции потерь при обучении верификатора.

Текстовые признаки как маркеры структуры

Анализ показал, что объявления с shouldSplit = True отличаются не столько содержанием, сколько способом организации текста:

  • Медиана длины для True значительно выше, однако длина сама по себе — слабый предиктор (длинные «маркетинговые» тексты часто описывают одну комплексную услугу).
  • Плотность запятых и переносов строк выше в True — это маркеры перечисления.
  • Плотность точек и восклицательных знаков выше в False — маркеры повествовательного стиля.
  • Буллеты (•, ●) — сильный сигнал перечисления самостоятельных услуг.

На основе данных наблюдений был сконструирован композитный признак list_score:

list_score = n_comma × 0.5 + n_newline × 2.0 + n_bullet × 5.0 − n_excl × 2.0

Морфологический и синтаксический анализ

Распределение частей речи различается между классами:

  • shouldSplit = True: преобладают существительные (NOUN) и прилагательные (ADJF) — маркеры перечисления сущностей.
  • shouldSplit = False: выше доля глаголов (VERB, INFN) и связующих конструкций — маркеры повествовательного описания процесса.

Это привело к созданию признаков noun_verb_ratio и logic_density, которые отражают жанр текста и являются устойчивыми разделителями классов.

Corner-cases

EDA выявил важные граничные случаи:

  • Объявления с 10+ обнаруженными микрокатегориями, но shouldSplit = False — длинные маркетинговые описания комплексного ремонта, где все услуги упомянуты как часть пакета.
  • Короткие тексты с минимумом слов, но shouldSplit = True — краткие перечисления отдельных услуг.

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

🛠️ Техническая реализация

Конфигурация и гиперпараметры

Параметр Значение Назначение
SEED 64911002 Фиксация воспроизводимости
RETRIEVER_MODEL intfloat/multilingual-e5-base Backbone bi-encoder
VERIFIER_MODEL cointegrated/rubert-tiny2 Backbone cross-encoder
RETR_TOP_K 6 Максимум кандидатов на retriever
RETR_THRESHOLD 0.44 Порог отсечения retriever
RETR_W_DENSE 0.75 Вес dense-скора в гибридном ранжировании
RETR_W_LEX 0.30 Вес лексического скора
MAX_LEN 256 Максимальная длина токенизации для верификатора
EPOCHS 6 Эпохи обучения верификатора
LR 1e-5 Learning rate
ARBITER_ITER 300 Число итераций HGBC
DEFAULT_THR 0.50 Начальный порог арбитра (автоподбирается)

Выбор backbone-моделей

Retriever — intfloat/multilingual-e5-base: мультиязычная модель семейства E5 с поддержкой префиксов query: / passage:. Выбрана за сильное качество русскоязычного семантического поиска при умеренном размере (~278M параметров), что позволяет кодировать все фразы таксономии и юниты объявлений в едином пространстве.

Verifier — cointegrated/rubert-tiny2: компактная русскоязычная BERT-модель (~29M параметров). Дообучается как cross-encoder для бинарной классификации пар «утверждение о самостоятельности услуги — контекст объявления». Компактность модели критична: верификатор вызывается для каждой пары (кандидат × промпт-шаблон), и скорость инференса напрямую влияет на общее время работы пайплайна.

Arbiter — HistGradientBoostingClassifier: градиентный бустинг из sklearn. Принимает финальное решение на основе ~30 признаков, объединяя сигналы от retriever, verifier и EDA-фичей. HGBC выбран за нативную поддержку пропусков, быструю скорость обучения и хорошее качество на табличных данных малого объёма.

Taxonomy Bank

Загружает словарь из 11 микрокатегорий и 497 ключевых фраз. Для каждой фразы строит:

  • Нормализованную форму (lowercase, удаление спецсимволов).
  • Лемматизированную форму (через pymorphy3).
  • TF-IDF представления: word-level (1–3 граммы) и char-level (3–5 граммы).

TF-IDF матрицы используются для fuzzy-matching на стадии retriever: даже если точного совпадения фразы нет, высокий косинус между TF-IDF векторами юнита и фразы сигнализирует о релевантности.

Hybrid Retriever

Для каждого объявления определяет набор кандидатов — микрокатегорий, которые потенциально представлены в тексте. Работает в два этапа:

1. Сегментация текста: объявление разбивается на «юниты» — отдельные семантические фрагменты. Разбиение выполняется по буллетам, переносам строк, точкам и другим пунктуационным маркерам (split_units). Каждый юнит — это потенциальное описание отдельной услуги.

2. Гибридное ранжирование: для каждого юнита вычисляются два типа скоров относительно каждой микрокатегории:

  • Dense score: косинусное сходство E5-эмбеддингов юнита и ключевых фраз микрокатегории. Юнит кодируется с префиксом query:, фразы — с passage:.
  • Lexical score: комбинация точного совпадения нормализованных фраз (вес 1.0), лемматизированного совпадения (0.8), word TF-IDF косинуса (0.7) и char TF-IDF косинуса (0.5).

Итоговый скор юнита для микрокатегории:

score = RETR_W_DENSE × dense_score + RETR_W_LEX × lexical_score

Микрокатегория становится кандидатом, если хотя бы один юнит набирает скор выше RETR_THRESHOLD. Для каждого кандидата сохраняются top-юниты и агрегированные статистики (max, mean скоров, число точных/лемма совпадений).

Pair Builder и Cross-Encoder Verifier

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

Формирование пар. Для каждого кандидата из retriever строится пара для cross-encoder:

  • Левая часть (premise) — промпт-утверждение о самостоятельности услуги. Используются 3 шаблона для аугментации:

    • «Утверждение: услуга «{mc_title}» предлагается как самостоятельная»
    • «Верно ли, что исполнитель оказывает «{mc_title}» отдельно?»
    • «Можно ли создать отдельный черновик для «{mc_title}»?»
  • Правая часть (hypothesis) — конкатенация ключевых фраз микрокатегории, релевантных фрагментов из retriever и укороченного описания объявления.

Обучение. rubert-tiny2 дообучается на train-выборке с взвешенной CrossEntropyLoss (компенсация дисбаланса классов). Early stopping по F1 на валидации с patience = 2.

Инференс. Для каждого кандидата верификатор возвращает вероятности по всем 3 промпт-шаблонам. Из них извлекаются статистики: p_mean, p_max, p_min, p_std, p_range — они становятся признаками для арбитра.

Feature Builder

Собирает финальный вектор признаков для каждого кандидата (~30 фичей), объединяя сигналы из всех предыдущих стадий:

Группа признаков Примеры Источник
Verifier probabilities p_mean, p_max, p_min, p_std, p_range Cross-Encoder
Retriever scores dense_max, dense_mean, lexical_max, lexical_mean, retr_score_max Hybrid Retriever
Evidence stats n_units, exact_hits, lemma_hits Retriever
Marker features standalone_desc, bundled_desc, standalone_ev, bundled_ev Regex по маркерам
Clause analysis clause_mentions, clause_standalone Синтаксическое разбиение
EDA features list_score, noun_verb_ratio, logic_density, comma_density, emoji_density, ... Текстовая статистика
Category proximity is_source Совпадение с исходной микрокатегорией

Маркеры самостоятельности (STANDALONE_MARKERS): «отдельно», «самостоятельно», «выполняем отдельно», и т.д.
Маркеры комплексности (BUNDLED_MARKERS): «под ключ», «включая», «в составе», «входит в комплекс», и т.д.

Late-Fusion Arbiter

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

Реализован как Pipeline(SimpleImputer → HistGradientBoostingClassifier):

  • SimpleImputer(strategy="median") — обработка пропусков.
  • HGBC(max_iter=300, max_depth=5, lr=0.05) — градиентный бустинг.

Выход — вероятность P(split) для каждого кандидата. Кандидаты с P > threshold попадают в финальный список для генерации черновиков.

Threshold Autotuning

Порог принятия решения подбирается автоматически на валидационной выборке:

for thr in np.arange(0.30, 0.81, 0.05):
    pipe = SubdraftPipeline(..., threshold=thr)
    m = evaluate(pipe, val_df)

Выбирается порог, максимизирующий F1-micro по микрокатегориям.

Draft Generator

Для каждого одобренного кандидата генерируется текст черновика объявления:

  1. Из сохранённых юнитов-evidence удаляются маркеры комплексности («ремонт под ключ», «включая», «в составе»).
  2. Очищенные юниты склеиваются в связный текст.
  3. Добавляется заголовок с названием микрокатегории.

Генерация чисто эвристическая (без LLM), что обеспечивает детерминированность и скорость.

SubdraftPipeline

Оркестратор, объединяющий все компоненты в единый интерфейс:

def predict(description, source_mc_id) -> PredictionOutput:
    candidates = retriever.generate(description, source_mc_id)
    for mc_id, cand in candidates.items():
        probs = verifier.predict_proba(...)         
        features = fb.build(description, ..., probs) 
        arbiter_prob = arbiter.predict_proba(features)
        if arbiter_prob > threshold:
            drafts.append(draft_text(mc, cand.units))
    return PredictionOutput(detected, should_split, drafts)

Возвращает структуру PredictionOutput:

  • detected_mc_ids — все микрокатегории, найденные retriever'ом.
  • should_split — нужно ли создавать дополнительные черновики.
  • drafts — список черновиков с mcId, mcTitle, text.

Данные

Решение работает с двумя файлами:

  • rnc_mic_key_phrases.csv — словарь из 11 микрокатегорий с 497 ключевыми фразами.
  • rnc_dataset_markup.csv — 2480 размеченных объявлений с эталонными targetDetectedMcIds, targetSplitMcIds и shouldSplit.

Разбиение: 70% train / 15% val / 15% test, стратифицированное по shouldSplit.

Метрики

Метрика Описание
F1-micro Основная метрика по дополнительным микрокатегориям
Precision (micro) Доля корректно предложенных микрокатегорий
Recall (micro) Доля найденных эталонных микрокатегорий
Accuracy (shouldSplit) Доля верных бинарных решений о необходимости сплита

Исходная микрокатегория объявления в расчёт не включается.

Артефакты

После обучения сохраняются в subdraft_artifacts/:

Файл Содержание
retriever_model/, retriever_tokenizer/ Веса и токенизатор E5
verifier_model/, verifier_tokenizer/ Дообученный rubert-tiny2
arbiter.pkl Обученный HGBC пайплайн
bank_data.pkl Таксономия, TF-IDF матрицы
config.json Гиперпараметры и пороги
rnc_test_predictions.csv Предсказания на тестовом наборе

Зависимости

torch==2.4.0
transformers
pymorphy3
scikit-learn
pandas
numpy
tqdm
spacy (ru_core_news_sm)  # требуется исключительно для EDA

Быстрый старт

pip install torch==2.4.0 transformers pymorphy3 scikit-learn pandas numpy tqdm

# Запуск полного пайплайна (обучение + инференс)
# Выполнить все ячейки в itpurple-avito-subdraft.ipynb

# Только инференс по сохранённым артефактам:
# Загрузить артефакты из subdraft_artifacts/ и вызвать pipeline.predict(description, source_mc_id)

Схема пайплайна

rnc_mic_key_phrases.csv + rnc_dataset_markup.csv
            │
            ▼
    TaxonomyBank (11 mc, 497 phrases, TF-IDF)
            │
            ▼
    HybridRetriever (E5 dense + TF-IDF lexical)
            │
            ├── split_units() → сегментация текста
            ├── dense scoring (cosine E5 embeddings)
            └── lexical scoring (exact + lemma + TF-IDF)
            │
            ▼
    build_pairs() → PromptedPairDataset (3 промпта × кандидат)
            │
            ▼
    PromptVerifier (rubert-tiny2, fine-tuned cross-encoder)
            │
            ▼
    FeatureBuilder (~30 признаков: verifier + retriever + EDA + markers)
            │
            ▼
    LateFusionArbiter (HGBC) → P(split) per candidate
            │
            ▼
    Threshold autotuning (val F1-micro grid search)
            │
            ▼
    SubdraftPipeline.predict() → PredictionOutput
            │
            ├── detectedMcIds
            ├── shouldSplit
            └── drafts [{mcId, mcTitle, text}]

📚 Источники

  1. NLP - EDA, Bag of Words, TF IDF, GloVe, BERT
  2. Deep Learning For NLP: Zero To Transformers & BERT
  3. Is text preprocessing still worth the time? A comparative survey on the influence of popular preprocessing methods on Transformers and traditional classifiers
  4. A Survey of Model Architectures in Information Retrieval
  5. Exploiting cloze-questions for few-shot text classification and natural language inference
  6. Hard Negatives, Hard Lessons: Revisiting Training Data Quality for Robust Information Retrieval with LLMs
  7. Taxonomy-based Negative Sampling In Personalized Semantic Search for E-commerce
  8. Comparing Neighbors Together Makes it Easy: Jointly Comparing Multiple Candidates for Efficient and Effective Retrieval
  9. Uncertainty-aware Generative Recommendation

About

Automatic identification of standalone services and generation of draft listings as part of IT Purple Hack 2026 (Avito Case)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages