А тут — непередаваемая атмосфера конференции и подборка постеров из сегодняшнего поста.
#YaACMRecSys
@RecSysChannel
#YaACMRecSys
@RecSysChannel
🔥13❤6👍4
Интересное с ACM RecSys 2024, часть 2
А мы продолжаем делиться классными докладами с ACM RecSys — оставайтесь с нами и приглашайте друзей подписываться, чтобы не пропустить самое интересное 👀
Ranking Across Different Content Types: The Robust Beauty of Multinomial Blending
Простая, но разумная продуктовая идея от Amazon Music: дать возможность продактам задавать пропорции по типу контента. Для этого есть две модели: одна ранжирует карусели, а другая — контент внутри каруселей. Когда карусели отранжированы, их группируют по типам контента, сэмплируют тип пропорционально весам, заданным продактам, и выбирают самую релевантную карусель из типа, выпавшего в сэмплировании. В А/Б тесте этот подход сравнили с системой, которая работает на MMR-like алгоритме и получили отличный рост метрик.
Раньше для ранжирования авторы использовали linear thompson sampling, теперь — нейронка, которая обучается в онлайн-режиме на сабсэмпле логов с задержкой в десятки секунд. Сейчас они активно пробуют sequential-модели, но пока не в проде.
AIE: Auction Information Enhanced Framework for CTR Prediction in Online Advertising
Довольно интересный фреймворк. Авторы добавили отшкалированный CPC как вес позитива в log loss, и получили рост метрик (выразившийся в деньгах) в А/Б тесте. К сожалению, автор не подсказал, какими были теоретические предпосылки — судя по всему сработала какая-то очень общая интуиция.
В оффлайне используют в основном AUC и csAUC, которые обычно нормально конвертируются в онлайн-метрики.
Enhancing Performance and Scalability of Large-Scale Recommendation Systems with Jagged Flash Attention
Постер о jagged flash attention — это когда вы не используете пэдлинг в историях пользователей, а вместо этого упаковываете её в два тензора: непрерывную историю и размеры историй.
Авторы обещают код в опенсорсе в ближайшее время. Сообщают об ускорении на инференсе, но не рассказали, на каких размерах батчей и длинах истории получены цифры. На графиках с ускорением обучения всегда пэдят до максимальной длины, а не до максимальной длины в батче, а значит, цифры завышены. Но в целом история очень полезная.
Sliding Window Training: Utilizing Historical Recommender Systems Data for Foundation Models
Исследователи в Netflix учат базовую модель для downstream-тасков. По сути это sasrec — предсказывают next item. На разных эпохах используют разные длины истории (фиксированные на всю эпоху). Для каждого пользователя выбирают одно рандомное окно указанной длины в эпоху. На вход подают просто ID, action type используют только в loss, где смешивают loss’ы на разный action type с разными весами. Истрия пользователя состоит из разных позитивов: клики, просмотры и т. п.
Авторы никак не дообучают модель в downstream-тасках, а просто подают на вход верхней модели полученные эмбеддинги. Lookahead и action type во входе модели не пробовали. Размерность эмбеда — 64. Loss представляет собой честный softmax по всей базе.
@RecSysChannel #YaACMRecSys
Находками делился❣ Николай Савушкин
А мы продолжаем делиться классными докладами с ACM RecSys — оставайтесь с нами и приглашайте друзей подписываться, чтобы не пропустить самое интересное 👀
Ranking Across Different Content Types: The Robust Beauty of Multinomial Blending
Простая, но разумная продуктовая идея от Amazon Music: дать возможность продактам задавать пропорции по типу контента. Для этого есть две модели: одна ранжирует карусели, а другая — контент внутри каруселей. Когда карусели отранжированы, их группируют по типам контента, сэмплируют тип пропорционально весам, заданным продактам, и выбирают самую релевантную карусель из типа, выпавшего в сэмплировании. В А/Б тесте этот подход сравнили с системой, которая работает на MMR-like алгоритме и получили отличный рост метрик.
Раньше для ранжирования авторы использовали linear thompson sampling, теперь — нейронка, которая обучается в онлайн-режиме на сабсэмпле логов с задержкой в десятки секунд. Сейчас они активно пробуют sequential-модели, но пока не в проде.
AIE: Auction Information Enhanced Framework for CTR Prediction in Online Advertising
Довольно интересный фреймворк. Авторы добавили отшкалированный CPC как вес позитива в log loss, и получили рост метрик (выразившийся в деньгах) в А/Б тесте. К сожалению, автор не подсказал, какими были теоретические предпосылки — судя по всему сработала какая-то очень общая интуиция.
В оффлайне используют в основном AUC и csAUC, которые обычно нормально конвертируются в онлайн-метрики.
Enhancing Performance and Scalability of Large-Scale Recommendation Systems with Jagged Flash Attention
Постер о jagged flash attention — это когда вы не используете пэдлинг в историях пользователей, а вместо этого упаковываете её в два тензора: непрерывную историю и размеры историй.
Авторы обещают код в опенсорсе в ближайшее время. Сообщают об ускорении на инференсе, но не рассказали, на каких размерах батчей и длинах истории получены цифры. На графиках с ускорением обучения всегда пэдят до максимальной длины, а не до максимальной длины в батче, а значит, цифры завышены. Но в целом история очень полезная.
Sliding Window Training: Utilizing Historical Recommender Systems Data for Foundation Models
Исследователи в Netflix учат базовую модель для downstream-тасков. По сути это sasrec — предсказывают next item. На разных эпохах используют разные длины истории (фиксированные на всю эпоху). Для каждого пользователя выбирают одно рандомное окно указанной длины в эпоху. На вход подают просто ID, action type используют только в loss, где смешивают loss’ы на разный action type с разными весами. Истрия пользователя состоит из разных позитивов: клики, просмотры и т. п.
Авторы никак не дообучают модель в downstream-тасках, а просто подают на вход верхней модели полученные эмбеддинги. Lookahead и action type во входе модели не пробовали. Размерность эмбеда — 64. Loss представляет собой честный softmax по всей базе.
@RecSysChannel #YaACMRecSys
Находками делился
Please open Telegram to view this post
VIEW IN TELEGRAM
👍6❤4🔥4
👍6❤5🔥5
Интересное с ACM RecSys 2024, часть 3
Конференция завершилась, ребята вернулись домой, но продолжают делиться с нами обзорами интересных и актуальных статей, а мы и рады их опубликовать! Сегодня в эфире — довольно подробный разбор статьи Text2Tracks: Generative Track Retrieval for Prompt-based Music Recommendation.
Авторы рассматривают задачу рекомендации музыки на основе текстовых запросов, например, “old school rock ballads to relax”, “songs to sing in the shower” и т. д. Исследуется эффективность модели на широких запросах, не подразумевающих конкретного артиста или трека. Рекомендовать трек по текстовому запросу можно разными путями. Например, задать вопрос языковой модели, распарсить ответ и найти треки через поиск. Это может привести к галлюцинациям или неоднозначности поиска — иногда совершенно разные треки могут иметь одно название. Кроме того, предсказание может занимать много времени и требовать больших вычислительных ресурсов.
Авторы предлагают дообучить модель типа encoder-decoder (flan-t5-base), которая по текстовому входу смогла бы генерировать идентификатор трека напрямую, вдохновившись подходом differentiable search index. Основной вопрос, на который дают ответ в статье — как лучше кодировать трек? Для этого сравнивают несколько подходов:
— Трек кодируется случайным натуральным числом, которое подаётся на вход в виде текста. Например “1001”, “111”
— Трек котируется как два числа: ID артиста и ID трека внутри артиста. То есть треки артиста 1 будут представляться как “1_1”, “1_2” … Для топ 50к артистов добавляют отдельные токены с словарь.
— Каждый трек описывается списком ID на основе иерархической кластеризации контентного (названия плейлистов с треком) или коллаборативных ембеддингов (word2vec). Для каждого кластера добавляется отдельный токен.
Эти стратегии значительно сокращают количество токенов, необходимых для представления трека по сравнению с текстовым описанием. Результат получился следующий: лучше всего себя показал второй подход (ID артиста + ID трека в нём). При этом хуже всего себя показали подходы с кластеризацией коллаборативных ембеддингов и ID трека в виде натурального числа.
В качестве основных бейзлайнов авторы используют popularity, bm25 и двухбашенный энкодер (all-mpnet-base-v2), который файнтюнят c multiple negatives ranking loss. Сравнивают модели на трёх датасетах: MPD 100k, CPCD и редакционные плейлисты Spotify. Исследователи показывают, что их модель значительно лучше бейзлайнов на всех датасетах. В будущем они планируют изучить возможности моделей с архитектурой decoder-only и использование пользовательской истории для персонализации рекомендаций.
@RecSysChannel #YaACMRecSys
Обзор подготовил❣ Пётр Зайдель
Конференция завершилась, ребята вернулись домой, но продолжают делиться с нами обзорами интересных и актуальных статей, а мы и рады их опубликовать! Сегодня в эфире — довольно подробный разбор статьи Text2Tracks: Generative Track Retrieval for Prompt-based Music Recommendation.
Авторы рассматривают задачу рекомендации музыки на основе текстовых запросов, например, “old school rock ballads to relax”, “songs to sing in the shower” и т. д. Исследуется эффективность модели на широких запросах, не подразумевающих конкретного артиста или трека. Рекомендовать трек по текстовому запросу можно разными путями. Например, задать вопрос языковой модели, распарсить ответ и найти треки через поиск. Это может привести к галлюцинациям или неоднозначности поиска — иногда совершенно разные треки могут иметь одно название. Кроме того, предсказание может занимать много времени и требовать больших вычислительных ресурсов.
Авторы предлагают дообучить модель типа encoder-decoder (flan-t5-base), которая по текстовому входу смогла бы генерировать идентификатор трека напрямую, вдохновившись подходом differentiable search index. Основной вопрос, на который дают ответ в статье — как лучше кодировать трек? Для этого сравнивают несколько подходов:
— Трек кодируется случайным натуральным числом, которое подаётся на вход в виде текста. Например “1001”, “111”
— Трек котируется как два числа: ID артиста и ID трека внутри артиста. То есть треки артиста 1 будут представляться как “1_1”, “1_2” … Для топ 50к артистов добавляют отдельные токены с словарь.
— Каждый трек описывается списком ID на основе иерархической кластеризации контентного (названия плейлистов с треком) или коллаборативных ембеддингов (word2vec). Для каждого кластера добавляется отдельный токен.
Эти стратегии значительно сокращают количество токенов, необходимых для представления трека по сравнению с текстовым описанием. Результат получился следующий: лучше всего себя показал второй подход (ID артиста + ID трека в нём). При этом хуже всего себя показали подходы с кластеризацией коллаборативных ембеддингов и ID трека в виде натурального числа.
В качестве основных бейзлайнов авторы используют popularity, bm25 и двухбашенный энкодер (all-mpnet-base-v2), который файнтюнят c multiple negatives ranking loss. Сравнивают модели на трёх датасетах: MPD 100k, CPCD и редакционные плейлисты Spotify. Исследователи показывают, что их модель значительно лучше бейзлайнов на всех датасетах. В будущем они планируют изучить возможности моделей с архитектурой decoder-only и использование пользовательской истории для персонализации рекомендаций.
@RecSysChannel #YaACMRecSys
Обзор подготовил
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥7❤5👍4
👍6❤4🤝3🔥1
Actions Speak Louder than Words: Trillion-Parameter Sequential Transducers for Generative Recommendations
У нейросетевых рекомендательных систем есть одна большая проблема — они плохо масштабируются, в то время как в NLP и CV скейлинг по размеру нейросетевых энкодеров очень хороший. Выделяют несколько причин этого явления: гигантский нестационарный словарь айтемов, гетерогенная природа признаков, а также очень большой объем данных.
В сегодняшней статье авторы предлагают переформулировать задачу рекомендации в генеративной постановке. Для начала, они представляют данные в виде последовательности событий. Вещественные фичи (счетчики и проч.) выкидываются, из взаимодействий с айтемами формируется единая последовательность, и затем в нее добавляются события изменения статической информации, такие как смена локации или изменение любого другого контекста.
Архитектура для генерации кандидатов выглядит довольно стандартно и похожа на SASRec или Pinnerformer: представляем пользователя в виде последовательности событий (item, action), и в тех местах, где следующим событием идет положительное взаимодействие с айтемом, предсказываем, что это за айтем.
А вот для ранжирования новизна достаточно серьезная: чтобы сделать модель target-aware (см. Deep Interest Network от Alibaba), понадобилось сделать более хитрую последовательность, в которой чередуются токены айтемов и действий: item_1, action_1, item_2, action_2, …. Из айтем-токенов предсказывается, какое с ними произойдет действие. Еще говорят, что на практике можно решать в этом месте любую многоголовую мультизадачу. Важно отметить, что авторы не учат единую модель сразу на генерацию кандидатов и ранжирование, а обучают две отдельные модели.
Другое нововведение — отказ от софтмакса и FFN в трансформере. Утверждается, что софтмакс плох для выучивания «интенсивности» чего-либо в истории пользователя. Те вещественные признаки, которые были выкинуты авторами, в основном её и касались. Например, сколько раз пользователь лайкал автора видеоролика, сколько раз скипал и т. д. Такие признаки очень важны для качества ранжирования. То, что отказ от софтмакса эту проблему решает, видно по результатам экспериментов — действительно есть значительное улучшение результатов ранжирования при такой модификации.
В итоге HSTU (Hierarchical Sequential Transduction Unit, так авторы окрестили свою архитектуру) показывает отличные результаты как на публичных, так и на внутренних датасетах. Еще и работает гораздо быстрее, чем прошлый DLRM подход за счет авторегрессивности и нового энкодера. Результаты в онлайне тоже очень хорошие — на billion-scale платформе short-form video (предполагаем, что это рилсы) получили +12.4% относительного прироста целевой метрики в A/B-тесте. Тем не менее, итоговая архитектура, которую авторы измеряют и внедряют, с точки зрения количества параметров не очень большая, где-то сотни миллионов. А вот по размеру датасета и длине истории скейлинг получился очень хороший.
@RecSysChannel
Разбор подготовил❣ Кирилл Хрыльченко
У нейросетевых рекомендательных систем есть одна большая проблема — они плохо масштабируются, в то время как в NLP и CV скейлинг по размеру нейросетевых энкодеров очень хороший. Выделяют несколько причин этого явления: гигантский нестационарный словарь айтемов, гетерогенная природа признаков, а также очень большой объем данных.
В сегодняшней статье авторы предлагают переформулировать задачу рекомендации в генеративной постановке. Для начала, они представляют данные в виде последовательности событий. Вещественные фичи (счетчики и проч.) выкидываются, из взаимодействий с айтемами формируется единая последовательность, и затем в нее добавляются события изменения статической информации, такие как смена локации или изменение любого другого контекста.
Архитектура для генерации кандидатов выглядит довольно стандартно и похожа на SASRec или Pinnerformer: представляем пользователя в виде последовательности событий (item, action), и в тех местах, где следующим событием идет положительное взаимодействие с айтемом, предсказываем, что это за айтем.
А вот для ранжирования новизна достаточно серьезная: чтобы сделать модель target-aware (см. Deep Interest Network от Alibaba), понадобилось сделать более хитрую последовательность, в которой чередуются токены айтемов и действий: item_1, action_1, item_2, action_2, …. Из айтем-токенов предсказывается, какое с ними произойдет действие. Еще говорят, что на практике можно решать в этом месте любую многоголовую мультизадачу. Важно отметить, что авторы не учат единую модель сразу на генерацию кандидатов и ранжирование, а обучают две отдельные модели.
Другое нововведение — отказ от софтмакса и FFN в трансформере. Утверждается, что софтмакс плох для выучивания «интенсивности» чего-либо в истории пользователя. Те вещественные признаки, которые были выкинуты авторами, в основном её и касались. Например, сколько раз пользователь лайкал автора видеоролика, сколько раз скипал и т. д. Такие признаки очень важны для качества ранжирования. То, что отказ от софтмакса эту проблему решает, видно по результатам экспериментов — действительно есть значительное улучшение результатов ранжирования при такой модификации.
В итоге HSTU (Hierarchical Sequential Transduction Unit, так авторы окрестили свою архитектуру) показывает отличные результаты как на публичных, так и на внутренних датасетах. Еще и работает гораздо быстрее, чем прошлый DLRM подход за счет авторегрессивности и нового энкодера. Результаты в онлайне тоже очень хорошие — на billion-scale платформе short-form video (предполагаем, что это рилсы) получили +12.4% относительного прироста целевой метрики в A/B-тесте. Тем не менее, итоговая архитектура, которую авторы измеряют и внедряют, с точки зрения количества параметров не очень большая, где-то сотни миллионов. А вот по размеру датасета и длине истории скейлинг получился очень хороший.
@RecSysChannel
Разбор подготовил
Please open Telegram to view this post
VIEW IN TELEGRAM
arXiv.org
Actions Speak Louder than Words: Trillion-Parameter Sequential...
Large-scale recommendation systems are characterized by their reliance on high cardinality, heterogeneous features and the need to handle tens of billions of user actions on a daily basis. Despite...
❤16🔥11💯3🤯2👍1
LiGNN: Graph Neural Networks at LinkedIn
Один из интуитивных подходов к представлению данных в рекомендательных системах — графы. Например, двудольный гетерогенный граф, где вершины — пользователи и айтемы, а рёбра — факты их взаимодействий.
В теории, использование графовой структуры вводит некий inductive bias и может помочь ML-модели в выучивании закономерностей, однако на практике очень сложно внедрить графы в продакшен из-за ряда проблем: distribution shift, cold start, dynamic vocabulary. В сегодняшней статье ребята из LinkedIn рассказывают, как внедряли графы в свою инфраструктуру, с какими сложностями столкнулись и что усвоили.
На первом рисунке — схема их гетерогенного графа для семплирования подграфов. Он включает в себя несколько разнородных сущностей: в качестве вершин — пользователи, сообщения, вакансии, группы, компании. А рёбра — три типа взаимодействий: вовлечённость, родство и наличие атрибута.
Архитектура ML-модели представлена на втором рисунке и состоит из трёх частей:
- Graph Engine — алгоритм для семплирования подграфов на базе открытой библиотеки DeepGNN от Microsoft. Для семплирования используют Personalized Page Rank (PPR).
- Encoder — помогает получить агрегированные представления вершин графа, опирается на GraphSage;
- Decoder — обычный MLP, вычисляет финальную релевантность между двумя вершинами (source и target).
За время работы над LiGNN команда смогла в 7 раз ускорить обучение графовых нейросетей, частично побороть cold start и запуститься в near-realtime сеттинге. Внедрение такой архитектуры как в ранжирование, так и в кандидатогенерацию повысило продуктовые метрики: +1% откликов на вакансии, +2% CTR объявлений, +0,5% еженедельно активных пользователей, +0,2% продолжительности взаимодействия с платформой и +0,1% еженедельно активных пользователей благодаря рекомендациям.
Посмотреть, как работает LiGNN, можно в приложениях LinkedIn: сейчас он развёрнут в доменах Feed, Jobs, People Recommendation и Ads.
@RecSysChannel
Разбор подготовил❣ Владимир Байкалов
Один из интуитивных подходов к представлению данных в рекомендательных системах — графы. Например, двудольный гетерогенный граф, где вершины — пользователи и айтемы, а рёбра — факты их взаимодействий.
В теории, использование графовой структуры вводит некий inductive bias и может помочь ML-модели в выучивании закономерностей, однако на практике очень сложно внедрить графы в продакшен из-за ряда проблем: distribution shift, cold start, dynamic vocabulary. В сегодняшней статье ребята из LinkedIn рассказывают, как внедряли графы в свою инфраструктуру, с какими сложностями столкнулись и что усвоили.
На первом рисунке — схема их гетерогенного графа для семплирования подграфов. Он включает в себя несколько разнородных сущностей: в качестве вершин — пользователи, сообщения, вакансии, группы, компании. А рёбра — три типа взаимодействий: вовлечённость, родство и наличие атрибута.
Архитектура ML-модели представлена на втором рисунке и состоит из трёх частей:
- Graph Engine — алгоритм для семплирования подграфов на базе открытой библиотеки DeepGNN от Microsoft. Для семплирования используют Personalized Page Rank (PPR).
- Encoder — помогает получить агрегированные представления вершин графа, опирается на GraphSage;
- Decoder — обычный MLP, вычисляет финальную релевантность между двумя вершинами (source и target).
За время работы над LiGNN команда смогла в 7 раз ускорить обучение графовых нейросетей, частично побороть cold start и запуститься в near-realtime сеттинге. Внедрение такой архитектуры как в ранжирование, так и в кандидатогенерацию повысило продуктовые метрики: +1% откликов на вакансии, +2% CTR объявлений, +0,5% еженедельно активных пользователей, +0,2% продолжительности взаимодействия с платформой и +0,1% еженедельно активных пользователей благодаря рекомендациям.
Посмотреть, как работает LiGNN, можно в приложениях LinkedIn: сейчас он развёрнут в доменах Feed, Jobs, People Recommendation и Ads.
@RecSysChannel
Разбор подготовил
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
❤20🔥8👍5
Joint Modeling of Search and Recommendations Via an Unified Contextual Recommender (UniCoRn)
В ещё одном интересном докладе с ACM RecSys разработчики из Netflix делятся опытом объединения моделей для персонализированного поиска и рекомендаций. В статье есть несколько предпосылок. Во-первых, обслуживать одну модель в продакшене проще, чем несколько. Во-вторых, качество объединённых моделей может быть выше.
Представленная архитектура обучается на трёх задачах: персональные рекомендации, персонализированный поиск и рекомендации к текущему видео. Для этого в нейросетевой ранкер подаётся поисковой запрос, ID текущей сущности (видео), ID пользователя, страна и ID задачи, которая решается (поиск или одно из ранжирований). Также в ранкер подаётся эмбеддинг истории действий пользователя, полученный так называемой "User Foundation Model", детали которой не раскрываются ни в тезисах с конференции, ни в ответе на прямой вопрос после устного доклада.
Чтобы заполнить эмбеддинги сущностей, которые отсутствуют (например, поисковые запросы в задаче рекомендаций), авторы провели серию экспериментов, по итогам которых решили, что в задаче поиска лучше вместо контекста подставлять отдельное нулевое значение, а в задаче рекомендаций — использовать название текущего видео вместо строки запроса.
Авторы отметили, что до внедрения этого подхода на этапе, когда пользователь вводил несколько первых букв в поисковом запросе, показывались результаты, которые не соответствовали интересам пользователя, так как поиск не был полностью персонализированным. Сейчас проблему удалось решить. Также в докладе подтверждают, что логика отбора кандидатов для поиска и рекомендаций оказалась ожидаемо разной.
Результаты — рост на 7% в офлайн-качестве в поиске и на 10% — в рекомендациях. Это, по всей видимости, достигается за счёт регуляризации, возникающей при обучении на несколько задач и за счёт перехода к полной персонализации в поиске.
@RecSysChannel
Разбор подготовил❣ Владимир Цепулин
В ещё одном интересном докладе с ACM RecSys разработчики из Netflix делятся опытом объединения моделей для персонализированного поиска и рекомендаций. В статье есть несколько предпосылок. Во-первых, обслуживать одну модель в продакшене проще, чем несколько. Во-вторых, качество объединённых моделей может быть выше.
Представленная архитектура обучается на трёх задачах: персональные рекомендации, персонализированный поиск и рекомендации к текущему видео. Для этого в нейросетевой ранкер подаётся поисковой запрос, ID текущей сущности (видео), ID пользователя, страна и ID задачи, которая решается (поиск или одно из ранжирований). Также в ранкер подаётся эмбеддинг истории действий пользователя, полученный так называемой "User Foundation Model", детали которой не раскрываются ни в тезисах с конференции, ни в ответе на прямой вопрос после устного доклада.
Чтобы заполнить эмбеддинги сущностей, которые отсутствуют (например, поисковые запросы в задаче рекомендаций), авторы провели серию экспериментов, по итогам которых решили, что в задаче поиска лучше вместо контекста подставлять отдельное нулевое значение, а в задаче рекомендаций — использовать название текущего видео вместо строки запроса.
Авторы отметили, что до внедрения этого подхода на этапе, когда пользователь вводил несколько первых букв в поисковом запросе, показывались результаты, которые не соответствовали интересам пользователя, так как поиск не был полностью персонализированным. Сейчас проблему удалось решить. Также в докладе подтверждают, что логика отбора кандидатов для поиска и рекомендаций оказалась ожидаемо разной.
Результаты — рост на 7% в офлайн-качестве в поиске и на 10% — в рекомендациях. Это, по всей видимости, достигается за счёт регуляризации, возникающей при обучении на несколько задач и за счёт перехода к полной персонализации в поиске.
@RecSysChannel
Разбор подготовил
Please open Telegram to view this post
VIEW IN TELEGRAM
❤13💯9🔥6👍2