Новая Школа - онлайн-школа подготовки к ЕГЭ
Задача Задача №3646864
А
Аноним
10 сентября 2026 12:43 · публичный вопрос
А
Аноним

Представьте, что вы продаете товар на Авито - гитару, чемодан или настольную игру. Покупатель хочет знать, сколько будет стоить доставка, а маркетплейс должен посчитать это…

Запрос пользователя

Представьте, что вы продаете товар на Авито - гитару, чемодан или настольную игру. Покупатель хочет знать, сколько будет стоить доставка, а маркетплейс должен посчитать это автоматически и точно. Ваша задача - научить модель по данным об объявлении (текст + характеристики + фото) предсказывать четыре величины: вес, высоту, ширину и длину товара. Чем точнее предсказания, тем корректнее стоимость доставки и тем меньше неприятных сюрпризов на пункте выдачи. Задание: Автоматический точный расчёт стоимости доставки для продавцов и покупателей Авито подразумевает, что необходимо заранее предсказывать индивидуальные весогабаритные характеристики (ВГХ) товара по его описанию и фото. Для этого постройте модель, которая для каждого объекта из тестовой выборки предсказывает четыре непрерывные величины: target_weight - вес (кг), target_height - высота (см), target_width - ширина (см), target_length - длина (см). Данные: train.parquet - обучающая выборка, test.parquet - тестовая выборка, sample_submission.csv - шаблон отправки, train.zip и test.zip - изображения товаров (ровно 1 фото на товар). Все данные можно скачать из Облака Тренировочный набор train.parquet с колонками: item_id - уникальный ID товара, order_date - дата оформления заказа, item_condition - состояние товара, item_price - цена, ₽, category_name, subcategory_name, microcat_name - иерархия категорий (ур. 1/2/3), seller_id, buyer_id - ID продавца/покупателя, title, description - заголовок и описание объявления, image_name - имя изображения, соответствует объекту в train.zip. Таргеты: real_weight (кг), real_height (см), real_width (см), real_length (см). Тестовая выборка: test.parquet - те же признаки, без таргетов. Метрика В качестве метрики качества используется Macro Log-MAE. Считаем MAE в лог-шкале по каждому таргету и усредняем: LogMAE t = 1 N ∑ N i = 1 ∣ ∣ log ( 1 + y i , t ) − log ( 1 + ^ y i , t ) ∣ ∣ , Score = 1 4 ∑ t ∈ { weight,height,length,width } LogMAE t , где y i , t - истинное значение, ^ y i , t - предсказание.

Ответ нейросети

Краткий ответ

Задачу удобно решать как четыре отдельные регрессии:

  1. преобразовать каждый таргет:
    \[ z=\log(1+y); \]
  2. обучить модель на \(z\), используя категории, текст, цену, дату и признаки изображения;
  3. вернуть предсказание в исходную шкалу:
    \[ \hat y=\exp(\hat z)-1; \]
  4. посчитать среднее значение Log-MAE по четырём таргетам.

Так как метрика уже вычисляет MAE в логарифмической шкале, обучение на log1p(y) напрямую соответствует критерию соревнования.


Почему нужен логарифм

Для веса, высоты и длины значения могут сильно различаться. Ошибка в 1 кг для маленького товара гораздо существеннее, чем для большого.

Метрика использует:

\[ \left|\log(1+y)-\log(1+\hat y)\right|. \]

Поэтому вместо обучения на исходных значениях используем:

y_log = np.log1p(y)

После предсказания:

prediction = np.expm1(prediction_log)
prediction = np.clip(prediction, 0, None)

В обучающем файле таргеты называются real_..., а в отправке — target_..., поэтому нужно явно сопоставить их:

TARGET_MAP = {
    "target_weight": "real_weight",
    "target_height": "real_height",
    "target_width": "real_width",
    "target_length": "real_length",
}

Базовая модель

Для смешанных признаков удобно использовать CatBoostRegressor:

  • категориальные признаки обрабатываются без ручного One-Hot Encoding;
  • заголовок и описание можно передать как текстовые признаки;
  • числовые признаки используются напрямую;
  • для каждого таргета обучается отдельная модель.
import numpy as np
import pandas as pd

from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_absolute_error
from catboost import CatBoostRegressor

Загрузка и подготовка данных

train = pd.read_parquet("train.parquet")
test = pd.read_parquet("test.parquet")

TARGET_MAP = {
    "target_weight": "real_weight",
    "target_height": "real_height",
    "target_width": "real_width",
    "target_length": "real_length",
}

TEXT_COLS = ["title", "description"]

CAT_COLS = [
    "item_condition",
    "category_name",
    "subcategory_name",
    "microcat_name",
    "seller_id",
    "buyer_id",
]
def make_features(df):
    x = df.copy()

    # Признаки даты
    date = pd.to_datetime(x["order_date"], errors="coerce")
    x["order_year"] = date.dt.year.fillna(0).astype("int16")
    x["order_month"] = date.dt.month.fillna(0).astype("int8")
    x["order_dayofweek"] = date.dt.dayofweek.fillna(0).astype("int8")

    # Текстовые признаки
    for col in TEXT_COLS:
        x[col] = x[col].fillna("").astype(str)
        x[col + "_len"] = x[col].str.len()

    # Цена
    x["item_price"] = (
        pd.to_numeric(x["item_price"], errors="coerce")
        .replace([np.inf, -np.inf], np.nan)
        .fillna(0)
    )
    x["item_price_log"] = np.log1p(x["item_price"].clip(lower=0))

    # Категории и ID превращаем в строки
    for col in CAT_COLS:
        x[col] = x[col].fillna("__NA__").astype(str)

    # Это технические идентификаторы, а не содержательные признаки
    x = x.drop(
        columns=["item_id", "image_name", "order_date"],
        errors="ignore"
    )

    return x


X = make_features(train)
X_test = make_features(test)

# На всякий случай выравниваем наборы признаков
X_test = X_test.reindex(columns=X.columns, fill_value=0)

Добавление признаков из фотографий

Имя файла фотографии само по себе почти ничего не говорит о товаре. Поэтому изображение нужно преобразовать в числовой вектор — эмбеддинг.

Например, можно использовать предобученную ResNet18:

import os
import torch

from pathlib import PurePath
from zipfile import ZipFile
from PIL import Image
from torchvision.models import resnet18, ResNet18_Weights
device = "cuda" if torch.cuda.is_available() else "cpu"

weights = ResNet18_Weights.DEFAULT
preprocess = weights.transforms()

image_model = resnet18(weights=weights)
image_model.fc = torch.nn.Identity()
image_model = image_model.to(device)
image_model.eval()
def get_image_embeddings(image_names, zip_path, batch_size=64):
    image_names = [str(name) for name in image_names]
    result = []

    with ZipFile(zip_path) as archive:
        archive_names = set(archive.namelist())

        by_basename = {
            os.path.basename(name): name
            for name in archive_names
        }

        for start in range(0, len(image_names), batch_size):
            batch_names = image_names[start:start + batch_size]
            batch_images = []

            for name in batch_names:
                archive_name = name

                if archive_name not in archive_names:
                    archive_name = by_basename.get(
                        os.path.basename(name)
                    )

                if archive_name is None:
                    raise FileNotFoundError(name)

                with archive.open(archive_name) as file:
                    image = Image.open(file).convert("RGB")

                batch_images.append(preprocess(image))

            batch = torch.stack(batch_images).to(device)

            with torch.inference_mode():
                embedding = image_model(batch)

            result.append(embedding.cpu().numpy())

    return np.vstack(result)

Извлекаем признаки для тренировочных и тестовых изображений:

image_train = get_image_embeddings(
    train["image_name"],
    "train.zip"
)

image_test = get_image_embeddings(
    test["image_name"],
    "test.zip"
)

Добавляем их к таблице:

for i in range(image_train.shape[1]):
    X[f"image_{i}"] = image_train[:, i]
    X_test[f"image_{i}"] = image_test[:, i]

Если сначала нужно проверить только таблично-текстовый baseline, этот блок можно временно пропустить.


Обучение четырёх моделей

Используем один и тот же разбор на обучение и валидацию для всех таргетов.

indices = np.arange(len(train))

idx_train, idx_valid = train_test_split(
    indices,
    test_size=0.2,
    random_state=42
)
def create_model(iterations=2500):
    return CatBoostRegressor(
        iterations=iterations,
        depth=8,
        learning_rate=0.04,
        loss_function="MAE",
        eval_metric="MAE",
        l2_leaf_reg=8,
        random_seed=42,
        allow_writing_files=False,
        verbose=200
    )
scores = {}
best_iterations = {}

for target_name, real_name in TARGET_MAP.items():
    y = train[real_name].astype(float).to_numpy()
    y_log = np.log1p(np.clip(y, 0, None))

    model = create_model()

    model.fit(
        X.iloc[idx_train],
        y_log[idx_train],
        cat_features=CAT_COLS,
        text_features=TEXT_COLS,
        eval_set=(X.iloc[idx_valid], y_log[idx_valid]),
        early_stopping_rounds=150
    )

    prediction_log = model.predict(X.iloc[idx_valid])
    prediction = np.clip(np.expm1(prediction_log), 0, None)

    score = mean_absolute_error(
        y_log[idx_valid],
        np.log1p(prediction)
    )

    scores[target_name] = score

    best_iteration = model.get_best_iteration()
    if best_iteration is None or best_iteration < 0:
        best_iterations[target_name] = 2500
    else:
        best_iterations[target_name] = best_iteration + 1

    print(f"{target_name}: {score:.5f}")

Итоговая метрика:

macro_log_mae = np.mean(list(scores.values()))

print("Macro Log-MAE:", macro_log_mae)

Именно это значение нужно использовать для проверки качества на локальной валидации.


Обучение на всех данных и создание отправки

submission = pd.read_csv("sample_submission.csv")

assert len(submission) == len(test)

for target_name, real_name in TARGET_MAP.items():
    y = train[real_name].astype(float).to_numpy()
    y_log = np.log1p(np.clip(y, 0, None))

    final_model = create_model(
        iterations=best_iterations[target_name]
    )

    final_model.fit(
        X,
        y_log,
        cat_features=CAT_COLS,
        text_features=TEXT_COLS,
        verbose=False
    )

    prediction_log = final_model.predict(X_test)
    prediction = np.clip(np.expm1(prediction_log), 0, None)

    submission[target_name] = prediction

Проверяем результат и сохраняем файл:

prediction_columns = list(TARGET_MAP.keys())

assert np.isfinite(
    submission[prediction_columns].to_numpy()
).all()

assert (
    submission[prediction_columns].to_numpy() >= 0
).all()

submission.to_csv("submission.csv", index=False)

print(submission.head())

Главное

  • Обучать нужно на log1p(real_...).
  • Для каждого таргета нужна отдельная модель.
  • item_id и image_name не являются полезными числовыми признаками.
  • Фото сначала преобразуется в эмбеддинг.
  • После предсказания применяется expm1.
  • Итоговое качество — среднее из четырёх Log-MAE.

Без самих файлов данных нельзя назвать конкретный числовой score, но приведённый код вычислит его на валидационной выборке и создаст submission.csv.