Ниже — подробное решение бейзлайна для задачи автоматического привязывания приложений RuStore к топ-3 категорий по текстам описаний. Цель: понять, как построить рабочий baseline, который можно легко улучшать.
Цель задачи
- Понять: построить простую, воспроизводимую модель на основе текста, которая для каждого приложения предсказывает до трех категорий (упорядоченных по уверенности).
- Метрика: Hitrate@3. для каждого приложения принимаем 1, если хотя бы одна из истинных меток попала в топ-3 предсказаний, иначе 0. Метрика в отчете рассчитывается как среднее по всем объектам.
Исходные данные (что нужно иметь)
- app_name: название приложения
- full_description: полное описание (если есть)
- shortDescription: краткое описание (если есть)
- labels_str: целевые категории, разделенные символом "|"
Формат вывода ( TSV)
- Колонки: app_name labels_str Метрика Hitrate@3
- Третья колонка — 0 или 1 для каждого приложения: 1, если intersection(true_labels, top3_pred) не пустое; 0 иначе.
- Пример строки: UberWallet finance|tools 1
Подход (бейзлайн)
- Комбинированный текстовый признак: объединяем app_name + full_description + shortDescription в один текстовый документ для каждого приложения.
- Векторизация текста: TF-IDF (униграммы и биграммы, 1-2 граммы) с ограничением на число признаков (например, 50k–100k).
- Модель: бинарная смесь "один против остальных" (OneVsRest) с логистической регрессией. Такой подход хорошо работает как базовый линейный метод на текстовых данных.
- Формирование предсказаний: для каждого приложения получить предсказания вероятностей по всем классам, выбрать top-3 меток по вероятностям.
- Привязка к тесту: сравнить top-3 с истинными метками (из labels_str). Если хотя бы одна совпадает — считать попаданием (1), иначе (0).
Пошаговый план реализации
Подготовка данных
- Объединить текст: text = app_name + " " + (full_description or "") + " " + (shortDescription or "")
- Разделить метки: true_labels = labels_str.split("|") (убрать пустые)
Разметка и разбиение
- Собрать набор классов (которые встречаются в обучении):.classes
- Преобразовать Y в бинарную матрицу MultiLabel (например, через MultiLabelBinarizer)
Векторизация и обучение
- Создать пайплайн: TfidfVectorizer(ngram_range=(1,2), max_features=100000) → OneVsRestClassifier(LogisticRegression(...))
- Разделить данные на трейн/валидацию (например, train_test_split) для настройки гиперпараметров (C, max_features)
Прогнозы для теста
- Векторизовать тексты тестового набора
- Получить предсказания вероятностей по всем классам (predict_proba)
- Для каждого примера выбрать топ-3 метки по вероятностям (сохранить их в виде списка или строки с разделителем "|")
Оценка Hitrate@3
- Для каждого примера проверить пересечение true_labels и топ-3_pred
- Если пересечение непустое — попадание (1), иначе 0
- Среднее по всем примерам — значение Hitrate@3
Экспорт результатов
- Для каждого примента записать: app_name, labels_str, 0/1 (хитрейт@3)
- Сохранить в TSV файл
Базовый код (Python, scikit-learn)
Ниже приведен рабочий каркас, который можно адаптировать под ваши файлы. Обязательно подставьте пути к вашим данным и названия колонок в DataFrame.
- Импорт нужных библиотек
- Загрузка данных
- Подготовка данных
- Обучение baseline
- Генерация предсказаний Top-3
- Вычисление 0/1 для Hitrate@3 на тесте
- Экспорт TSV
Пример кода (псевдо-реализация, пригодна для запуска после адаптации под ваши файлы)
- Примечание: этот блок кода написан в виде последовательных инструкций на Python без использования сторонних ноутбуков. Его можно копировать в файл .py и запускать.
Импорт
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import MultiLabelBinarizer
from sklearn.multiclass import OneVsRestClassifier
import numpy as np
Загрузка данных
Предположим, у вас есть train.csv и test.csv с колонками:
app_name, full_description, shortDescription, labels_str
train_path = "train.csv"
test_path = "test.csv"
train_df = pd.read_csv(train_path)
test_df = pd.read_csv(test_path)
Подготовка текста
def build_text(row):
parts = [str(row.get("app_name", ""))]
if "full_description" in row and pd.notnull(row["full_description"]):
parts.append(str(row["full_description"]))
if "shortDescription" in row and pd.notnull(row["shortDescription"]):
parts.append(str(row["shortDescription"]))
return " ".join(parts)
train_df["text"] = train_df.apply(build_text, axis=1)
test_df["text"] = test_df.apply(build_text, axis=1)
Разделение признаков и целей
X = train_df["text"].fillna("").values
y_true_str = train_df["labels_str"].fillna("").values # строки вида "finance|tools"
Преобразование меток в бинарную матрицу
Получаем полный набор классов из обучающих данных
all_labels = set()
for s in y_true_str:
if s:
all_labels.update(s.split("|"))
all_labels = sorted(list(all_labels))
Векторизация меток
mlb = MultiLabelBinarizer(classes=all_labels)
y_true = mlb.fit_transform([l.split("|") if isinstance(l, str) and l else [] for l in y_true_str])
Разделение на трейн/валидацию (для бейзлайна можно использовать весь набор)
X_train, X_val, y_train, y_val = train_test_split(X, y_true, test_size=0.2, random_state=42)
Модель: TF-IDF + OneVsRest LogisticRegression
tfidf = TfidfVectorizer(max_features=100000, ngram_range=(1, 2), stop_words=None)
clf = OneVsRestClassifier(LogisticRegression(max_iter=1000, solver="liblinear", C=1.0))
pipeline = Pipeline([
("tfidf", tfidf),
("clf", clf)
])
Обучение
pipeline.fit(X_train, y_train)
Валидация (для понимания качества)
y_val_pred_proba = pipeline.predict_proba(X_val) # не всегда доступно напрямую в Pipeline
Если используем OneVsRestClassifier, predict_proba доступен
val_proba = pipeline.predict_proba(X_val)
val_top3 = np.argsort(-val_proba, axis=1)[:, :3]
Валидация может быть добавлена по желанию
Подготовка функций для предсказаний Top-3 и расчета попадания
def predict_top_k(texts, k=3):
proba = pipeline.predict_proba(texts) # shape: [n_samples, n_classes]
topk_indices = np.argsort(-proba, axis=1)[:, :k]
# Преобразование индексов в имена меток
topk_labels = []
for idxs in topk_indices:
labels = [mlb.classes_[i] for i in idxs]
topk_labels.append(labels)
return topk_labels
def bool_hit(true_label_str, topk_labels):
true_labels = set([t for t in (true_label_str.split("|") if isinstance(true_label_str, str) and true_label_str else [])])
pred_labels = set(topk_labels)
return int(len(true_labels & pred_labels) > 0)
Прогнозы на тесте
test_texts = test_df["text"].fillna("").values
top3_test = predict_top_k(test_texts, k=3)
Преобразование true_labels для тестового набора (если labels_str есть в тестовом наборе)
Если в тестовом наборе есть истинные метки, можно их считать; иначе заполняем пустые
test_true_label_strs = test_df.get("labels_str", pd.Series([""] * len(test_df))).fillna("").values
Рассчитываем попадания на тесте (0/1)
hits = []
for i in range(len(test_true_label_strs)):
true_str = test_true_label_strs[i]
hit = 0
if true_str:
true_labels = set([t for t in true_str.split("|") if t])
pred_labels = set(top3_test[i])
if len(true_labels & pred_labels) > 0:
hit = 1
else:
# если нет истинных меток в тестовом наборе, считаем как 0 попаданий
hit = 0
hits.append(hit)
Формируем TSV-вывод
app_name labels_str Метрика Hitrate@3
out_rows = []
for idx, row in test_df.iterrows():
app_name = str(row.get("app_name", ""))
true_labels = row.get("labels_str", "")
top3_labels = "|".join(top3_test[idx])
# Для файла TSV третья колонка — 0 или 1
hit_value = hits[idx]
out_rows.append({"app_name": app_name, "labels_str": true_labels, "Метрика Hitrate@3": hit_value})
out_df = pd.DataFrame(out_rows)
out_df.to_csv("predictions_hitrate3.tsv", sep="\t", index=False)
Примечания по коду
- В pipeline используется TF-IDF векторизация с ngram (1,2).
- OneVsRestClassifier обеспечивает мультилейбл-совмещение; для каждого класса обучается отдельный бинарный логистический регрессор.
- predict_proba возвращает вероятности для каждого класса; топ-3 выбираются по убыванию вероятности.
- Границы и параметры (max_features, C, solver) можно подбирать по валидации.
- В реальном проекте стоит вычислять Hitrate@3 на валидационном наборе до финальной сдачи, чтобы понять качество baseline.
Пояснения по вариациям и улучшениям
- Варианты моделей:
- LogisticRegression (как сейчас) — база. Можно сменить на LinearSVC с калибровкой вероятностей (CalibratedClassifierCV) для более устойчивого поведения на редких классах.
- Binary Relevance (OneVsRest) уже реализован в базовом варианте. Можно рассмотреть ClassifierChain или metadata-обучение.
- Векторизация:
- Можно попробовать их/без остановочных слов (stop_words=None). Добавить character-level TF-IDF (ngram_range=(3,5)) для уловления специфических словоупотреблений.
- Обработка данных:
- Упростить обработку пустых полей; нормализовать текст (lowercase, удаление лишних символов).
- Расширения:
- Добавить контекст из описаний, использовать смещение по частоте слов.
- Пробовать более продвинутые модели (LightGBM, CatBoost) на эмбеддингах слов, если есть достаточные данные.
- Применять Label Powerset или Dependency-aware подходы, если множество категорий велико.
Что если у вас другие требования
- Если цель состоит в максимально точном попадании в топ-3 на вашем наборе, можно адаптировать метрику на границе: попробовать разные k (например, 2 или 4) и сравнить результаты.
- Если вы хотите, чтобы в TSV попадали именно три категории даже при меньшем числе кандидатных категорий, можно дополнить топ-3 фиктивными пустыми значениями, но обычно лучше вернуть реальный top-3 и оставить третий элемент как реальную метку.
Готов адаптировать под ваши данные
- Сообщите, пожалуйста, форматы ваших файлов (названия колонок в train.csv и test.csv, наличие labels_str в тестовом наборе, и т.д.). Я могу подготовить конкретный скрипт под ваш набор данных и вычислить Hitrate@3 прямо локально на вашем примере.