Сейчас в открытом доступе мало крупных датасетов сервисов коротких видео, но это уникальный формат для рекомендательных алгоритмов. В отличие от музыки или длинных видео они не могут потребляться в фоновом режиме, а каждый показанный ролик получает от пользователя реакцию. Даже если он не оставит лайк, досмотр видео до конца или пропуск уже считаются обратной связью. Именно поэтому мы выложили в открытый доступ датасет VK-LSVD. С его помощью инженеры и ученые смогут развивать и совершенствовать рекомендательные алгоритмы.

Что именно внутри: сигналы, контекст показа и контентные эмбеддинги

VK-LSVD — это полгода активности в сервисе коротких роликов (январь–июнь 2025): свыше 40 млрд событий, ~10 млн анонимных пользователей и ~19,6 млн роликов. Если сложить время просмотра роликов из датасета, получится ~858 млрд секунд, это чуть больше 27 000 лет экранного времени. Датасет содержит  ~1,17 млрд отметок «Нравится», ~11,86 млн «Не интересно», ~84,6 млн переходов к авторам, ~481 млн открытий комментариев и ~262,7 млн расшариваний.

Вся эта телеметрия идёт вместе с контекстом: Place (откуда пришли — лента, поиск, профиль и т.д., 24 варианта), Platform (Android, iOS, Web, Smart TV и пр., 11 вариантов) и Agent (конкретный клиент/браузер — VK App, Chrome, Safari и т.д., 29 вариантов). Для сохранения конфиденциальности все данные обезличены. 

У каждого клипа есть длительность 5–180 с, анонимный ID автора и 64-мерный контентный эмбеддинг, собранный из визуала, звука и текста.  При построении закладывалась возможность понижения размерности эмбедда. Для этого на контентных признаках введен порядок: чем меньше индекс контеного признака, тем он более информативный и менее шумный. Это решение помогает находить баланс между скоростью и точностью. 

Сам набор решает несколько задач. Во-первых, он позволяет начинающим специалистам получить опыт работы с реальными большими данными, реализовать и тестировать бэйзлайны. Во-вторых, позволяет систематически сравнивать разные алгоритмы, когда отличия в результатах объясняются не разной разметкой и  генерацией обучающей выборки, а архитектурой и фичами. В-третьих, можно учить гибриды «контент + поведение» и проверять, действительно ли контентные признаки помогают в некоторых сценариях.

Как работать с VK-LSVD

Датасет организован по неделям и временным срезам: обучающая часть train/week_00…week_24.parquet, валидация validation/week_25.parquet. Порядок строк внутри файлов хронологический, повторных показов одной и той же пары user–item нет. Для  категориальных полей и идентификаторов используются целочисленные значения для обезличивания. Схема полей во взаимодействиях включает user_id, item_id, контекст показа place, platform, agent, отклики like, dislike, share, bookmark, click_on_author, open_comments и timespent в секундах 0–255. Пользовательская мета: возраст 18–70, пол, укрупненный регион и ранг популярности для сэмплинга. По клипам: автор, длительность (секунды), ранг популярности и отдельный файл контентных эмбеддингов item_embeddings.npz с 64-мерными векторами float16, у которых компоненты специально упорядочены.

Скачиваем и читаем:

# pip install polars numpy huggingface_hub pyarrow
from huggingface_hub import hf_hub_download
import polars as pl
import numpy as np
from pathlib import Path

# берём готовый небольшой срез для экспериментов 
SUBSAMPLE = "up0.001_ip0.001"  # можно заменить на другой из README
EMB_DIM = 32                   # берём префикс эмбеддинга (1..64)

# готовим список файлов этого среза
train_files = [f"subsamples/{SUBSAMPLE}/train/week_{i:02}.parquet" for i in range(25)]
val_files   = [f"subsamples/{SUBSAMPLE}/validation/week_25.parquet"]
meta_files  = [
    "metadata/users_metadata.parquet",
    "metadata/items_metadata.parquet",
    "metadata/item_embeddings.npz",
]

# скачиваем в локальную папку
LOCAL_DIR = Path("VK-LSVD")
for file in train_files + val_files + meta_files:
    hf_hub_download(
        repo_id="deepvk/VK-LSVD",
        repo_type="dataset",
        filename=file,
        local_dir=str(LOCAL_DIR),
    )

# читаем train лениво и собираем потоково 
train = pl.concat([pl.scan_parquet(LOCAL_DIR / f) for f in train_files]).collect(engine="streaming")
val   = pl.read_parquet(LOCAL_DIR / val_files[0])

# проверим инварианты набора
assert train.select(["user_id", "item_id"]).unique().height == train.height, "Повторяющиеся user-item в train"
assert val.select(["user_id", "item_id"]).unique().height == val.height, "Повторяющиеся user-item в val"

print(train.head())
print(train.schema)  # увидите uint32/uint8/bool по описанию

Если нужно идти не через подсэмпл, а через весь датасет, меняем пути на interactions/train/week_XX.parquet и interactions/validation/week_25.parquet.

Подключаем метаданные пользователей и клипов:

users_meta = pl.read_parquet(LOCAL_DIR / "metadata/users_metadata.parquet")
items_meta = pl.read_parquet(LOCAL_DIR / "metadata/items_metadata.parquet")

# оставляем только тех пользователей/клипы, что встречаются в train
train_users = train.select("user_id").unique()
train_items = train.select("item_id").unique()

users_meta = users_meta.join(train_users, on="user_id", how="inner")
items_meta = items_meta.join(train_items, on="item_id", how="inner")

print(users_meta.head(), items_meta.head())

Контентные эмбеддинги

Эмбеддинги обучены только на контенте, без коллаборативных сигналов. Компоненты упорядочены, т.е можно выбрать первые n координат и ускориться с минимальной потерей качества. Пример чтения контентных эмбеддингов ниже:

npz = np.load(LOCAL_DIR / "metadata/item_embeddings.npz")
item_ids = npz["item_id"]               # uint32
emb_full = npz["embedding"]             # (N, 64) float16
emb = emb_full[:, :EMB_DIM]             # используем префикс

# синхронизируем порядок с items_meta
keep = np.isin(item_ids, items_meta.select("item_id").to_numpy().ravel())
item_ids = item_ids[keep]
emb = emb[keep]

# добавим эмбеддинг к метаданным клипов
items_meta = items_meta.join(
    pl.DataFrame({"item_id": item_ids, "embedding": list(emb)}),
    on="item_id",
    how="inner",
)
print(items_meta.select(["item_id", "duration", "embedding"]).head())

Конфигурируемые подмножества

Есть готовые нарезки по пользователям/айтемам и утилита, чтобы собрать свой срез под бюджет и железо. Логика такая: urX — X-доля случайных пользователей, ipX — X-доля самых популярных айтемов по train_interactions_rank, меньше ранг — больше взаимодействий, отрицательное X берёт низ распределения (-0.9 → нижние 90%). Аналогично можно отобрать непопулярных пользователей через их ранг.

def get_sample(entries: pl.LazyFrame | pl.DataFrame, split_column: str, fraction: float) -> pl.LazyFrame:
    lf = entries.lazy() if isinstance(entries, pl.DataFrame) else entries
    if fraction >= 0:
        q = lf.select(pl.col(split_column).quantile(fraction, interpolation="midpoint")).collect().item()
        return lf.filter(pl.col(split_column) <= q)
    else:
        q = lf.select(pl.col(split_column).quantile(1 + fraction, interpolation="midpoint")).collect().item()
        return lf.filter(pl.col(split_column) >= q)

# пример: 1% случайных пользователей + 1% самых популярных айтемов
users_lf = pl.scan_parquet(LOCAL_DIR / "metadata/users_metadata.parquet")
items_lf = pl.scan_parquet(LOCAL_DIR / "metadata/items_metadata.parquet")
users_sample = get_sample(users_lf, "user_id", 0.01).select("user_id")
items_sample = get_sample(items_lf, "train_interactions_rank", 0.01).select("item_id")

interactions_val = pl.scan_parquet(LOCAL_DIR / "subsamples" / SUBSAMPLE / "validation" / "week_25.parquet")
interactions_sample = (
    interactions_val
    .join(users_sample, on="user_id", how="inner", maintain_order=True)
    .join(items_sample, on="item_id", how="inner", maintain_order=True)
).collect(engine="streaming")

print(interactions_sample.head(), interactions_sample.height)

Чтобы собрать дно по популярности на 90% и по пользователям, и по айтемам (up-0.9_ip-0.9), заменяем выборки на get_sample(..., "train_interactions_rank", -0.9) и делаем join по тем же ключам.

Мини-шаблон пайплайна

С учетом глобального временного разреза работаем в хронологическом порядке: любые признаки из train, проверка гипотез на validation. Базовый скелет для воспроизводимых экспериментов выглядит так: загрузить train-недели лениво и собрать потоково; оставить нужную долю пользователей/айтемов; подсоединить мету и префикс эмбеддингов нужной длины; сгенерировать целевую переменную под задачу; зафиксировать параметры и seed; сохранить использованный список недель и размерность эмбеддинга.

# фильтрация train по выбранной подвыборке пользователей/айтемов
train_lf = pl.concat([pl.scan_parquet(LOCAL_DIR / f) for f in train_files])
train_small = (
    train_lf
    .join(users_sample, on="user_id", how="inner", maintain_order=True)
    .join(items_sample, on="item_id", how="inner", maintain_order=True)
).collect(engine="streaming")

# цели под разные задачи
y_cls = train_small.select(pl.col("like").cast(pl.Int8).alias("label_like"))      # бинарная классификация
y_reg = train_small.select(pl.col("timespent").cast(pl.Int16).alias("y_timespent"))  # регрессия по времени

# добавим длительность клипа и контентный префикс
train_with_items = train_small.join(items_meta.select(["item_id", "duration", "embedding"]), on="item_id", how="left")
print(train_with_items.head())

# сохраняем подготовленный срез на диск
OUT = Path("prepared")
OUT.mkdir(exist_ok=True)
train_with_items.write_parquet(OUT / "train_ready.parquet")
val.write_parquet(OUT / "val_raw.parquet")

# логи эксперимента
config = {
    "subsample": SUBSAMPLE,
    "train_weeks": list(range(25)),
    "val_week": 25,
    "embedding_prefix": EMB_DIM,
    "joins": ["users_metadata", "items_metadata", "item_embeddings"],
    "targets": ["like(binary)", "timespent(regression)"],
}
print(config)

На этом месте уже есть воспроизводимый вход для любых архитектур: классические табличные модели, двухбашенные с retrieval, ранжирование по implicit-сигналам, всё это подключается поверх train_ready.parquet. Можно использовать для воспроизводимых тестов различных моделей с разными нарезками (urX/ipX/upX), и выбранной размерности эмбеддинга эмбеддинга.

Следующий шаг

Дальше по плану расширение датасета и публичные бенчмарки. Сейчас проходит открытое соревнование на базе VK-LSVD. Если хочется стартовать быстро, берите репозиторий, собирайте бейзлайн и фиксируйте протокол воспроизводимости и обучайте классные модельки.

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


  1. MAXH0
    26.11.2025 10:00

    Есть ли шансы на то, что со временем алгоритмы победят менеджмент, и в перестанут продвигать блатных в ленту (ну помимо рекламы)?