[Из песочницы] Как я создаю сервис рекомендаций сообществ ВКонтакте

Лето заканчивалось, шёл особенно холодный август. Начинался мой 11 класс и я осознал, что сейчас последний шанс (спойлер: нет) на то, чтобы как-то улучшить свою профессиональную компетенцию. Уже несколько лет я усердно делал разные IT проекты, какие-то один, какие-то в коллективе. Но вот все сыны маминой подруги  уже делают что-то красивое. Возможно, бесполезное, но прекрасное внешне. Кто-то делает залипательную симуляцию частиц в виде гифок, кто-то погружается в машинное обучение и делает всякие стайл-трансферы. А я чем хуже? Я так же хочу!

Пример симуляции также есть под катом

g0htshdl9xzixux3j0-3aviwqxa.gif


Пример симуляции частиц от моего знакомого

Именно с этой мысли началось моё изучение темы машинного обучения. И в плане обучения для меня не было ничего нового, как и в любой другой сфере IT тут нужна практика. Но что делать, если мне не интересны всякие анализаторы тональности? Надо придумывать что-то своё.

В очередной раз листая ленту новостей ВКонтакте, я понял, что сообщества этой социальной сети являются настоящим кладом для data science. Если обработать текст, мемы в виде картинок или музыку из подборок, то можно получить огромный срез актуальной информации про современных людей: популярные жанры музыки, лексикон или же время суток наибольшей активности людей. Это поле для многих открытий.

Но насколько такая статистика будет нужна для обычных людей? Как будто мне не хватает своей музыки, или я не могу зайти в раздел «Популярное»? Значит, надо думать что-то практичное, что-то такое, что может понравится хоть какому-то значительному проценту пользователей.

Стоит отметить и то, что за пару месяцев до этого я прочитал классную статью про создание своего домашнего поисковика, которая меня очень задела. Как и автор, я испытывал большую тягу к огромным проектам, которые день и ночь обрабатывают огромное число в не менее огромной степени тысяч гигабайт информации.

arbxukqaqo48ycseasyl07jdxz8.png

И вот, возвращаемся в август, который стал чуть теплее, чем в начале статьи. Когда я осознал, что теперь у меня есть огромный источник информации, то я понял, что пора. Пришло время для собственной монструозной системы. Но главный вопрос оставался еще несколько дней — что же мне делать со всем этим? Что предложить пользователю? Не буду томить читателя, расскажу только то, что мне, как и некоторым моим друзьям очень сложно искать новые группы ВКонтакте, которые могли бы понравиться мне. Сейчас у каждого первого паблика название — случайный набор слов. Админы стараются сделать его наиболее абсурдным, наверное, это какая-то, понятная только для них, гонка.

И тут я решил написать сервис, который будет помогать пользователю с выбором сообществ, рекомендовать то, на что можно подписаться. Так и появилась моя идея.

Вступление было длинным не случайно, оно должно было передать мои эмоции и показать, что идея не появилась на ровном месте. Собственно, как и любые другие идеи на Хабре.

Мой сервис всё ещё в работе, она продолжается уже больше четырёх месяцев (если считать с момента первого удачного эксперимента). Но у меня уже есть опыт, которым я хочу поделиться с вами. Сейчас будет краткое, максимально сжатое описание проекта. Дальше я распишу некоторые основные пункты. А если статья особенно сильно зайдёт хабраюзерам, то выйдет и ее продолжение, в котором будет больше чисто технической информации и кода.

Всё состоит из трёх частей:

  • поисковый бот (если его так можно назвать)
  • движок обработки данных
  • сайт для пользователей (с панелью управления и мониторинга для админов)


В функционал бота входит поиск новых групп и «сдирание» текста и прочей информации в базу. Движок же занимается дальнейшей обработкой этой data, о чём напишу ниже. А сайт просто позволяет пользователям всем этим пользоваться.
Тут ничего нового. Я просто беру профиль какого-либо человека в VK и получаю от него список групп и его друзей. Всё это происходит при помощи VK API. И если этот АПИ справляется с получением списка групп пользователя и его друзей, то он не справляется с получением содержимого групп… Я просто врезаюсь в ограничение и всё. Тут я вспомнил, что какое-то время назад ВКонтакте пиарили свою крутую систему как раз для таких вещей. И имя этой системе Streaming API.

Streaming API — инструмент для получения случайной выборки записей из VK. На странице с описанием написано, что просто так можно получить до 1% от всей информации, для того, чтобы получить до сотни нужно написать в Поддержку и объяснить им свои намерения.

Казалось бы, всё замечательно. Но нет. Я, как и, наверное, многие, пропустил самое важное слово в описании выше. И это предлог «до». Вам никто и не собирается давать все 100% данных. Это просто красивая верхняя планка и всё. На деле получаем вот так:

gpvhpzq6dgcenzxvzgvmyz-l0hs.png
Надеюсь, что агент #365 не будет ненавидеть меня все 365 дней в году за этот скриншот

То есть, получить я смогу только 30К событий в день. А в это число входят и комментарии, и просто репосты. Нужно указать еще и какие-то слова-теги, будут приходить сообщения только с ними. Какая-то часть оставшихся постов просто не интересует меня, так как находится на стене у пользователей. Остаётся совсем чуть-чуть. Для справки, в моей текущей реализации я могу получать до 8.5М записей за несколько дней неполного аптайма (суммарно — около 10 часов, но точных замеров не было).

Тут я должен сказать про одно правило, которое я выявил из всего этого эксперимента. Никогда не судите о группе по одному посту. Особенно, если вы — искусственный интеллект, восприимчивый к таким шумам. Значит, нужно хотя бы несколько постов для создания объективной характеристики паблика. Теперь прикинем, что некоторые группы с реально качественным контентом выпускают его раз в несколько недель. И даже тогда я могу пропустить его из-за неидеальности Streaming API. А если и получу, то как долго мне собирать содержимое по крупицам?

Я решил, что слишком долго и пошёл другим путём. Раз уж я не могу получить аккуратный ответ от ВКонтакте в JSON формате, то я буду парсить стены сообществ. Да, задача чуть-чуть усложняется, а ее решение замедляется, но у меня нет альтернативы. Вот так я и начал писать первый блок моей системы. Написал я его, кстати, на Java с использованием Jsoup, библиотеки, которая позволяет очень удобно извлекать содержимое из HTML текста. Не забыл и про обработку даты публикации последнего поста, мне не нужны мёртвые сообщества, я их попросту не индексирую. Также отбрасываются и посты, помеченные рекламными. Не все администраторы делают такие пометки, но эту проблемы решить не так просто, я не смог составить адекватный фильтр рекламы и из-за этого, пока что, отказываюсь от такой фильтрации.


Наверное, это самая интересная часть проекта, но я не буду описывать всё в деталях в этой публикации. Если кому-то будут интересны детали, то спрашивайте их у меня всеми возможными способами.

Самый простой из всех способов представления текста в понятном для нейронной сети формате это bag-of-words.

uxc-ktsdoepwc2ykzlf0cojafui.png

Процесс векторизации и подробнее о BOW

Я заранее подготавливаю словарь всех частых слов (не забывая исключить прям совсем частые, такие как «а», «что», «который» и прочие; они не выделяют свой текст на фоне других), в нём у каждого слова есть свой номер. Затем, когда мне надо обработать текст нейросетью, я получаю номер каждого слова из словаря (если оно там есть) и получаю вектор (aka массив в программировании). Это такой упорядоченный набор чисел, в котором на месте каждого номера слова из текста стоит единица (см картинку выше). Получается вполне понятный для сети тип данных. У меня длина каждого вектора это 30000, примерно столько адекватных слов я собрал на первых этапах разработки.


Еще важно не забыть про то, что, например, слова «хабр» и »(в) хабре» являются почти одним и тем же для понимания. Но для алгоритма, описанного выше, это совершенно разные слова. Для того, чтобы исправить это, я использую морфологический анализатор JMorphy2. Это порт оригинального PyMorphy2 под Java. Он может делать много классных вещей, например, изменять форму слова (падеж, род, число и прочее). Мне же он нужен для получения начальной формы слова. Как известно, начальные формы «одинаковых» слов одинаковы. И это решает проблему выше.

ноль 6929
маслов 21903
дракончик 25126
цветковый 11441
будда 7374
автобус 1925
супер 1626
плойка 23128
награждение 6241
истеричный 25584


Пример списка слов в словаре и их номеров (разделены пробелом)

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

Сервис ориентирован на русскоговорящую аудиторию. И для простоты сейчас обрабатывается только русский язык. Все символы (буквы в их числе) из других алфавитов выбрасываются, как и знаки препинания, цифры, эмодзи… Опять же, упрощение. Еще надо не забывать отфильтровать слова из языков, которые частично используют русский алфавит, но добавляют туда свои буквы (украинский, например).

Но продолжу с того момента, как какой-нибудь пост из ВКонтакте уже превратился в вектор (я называю это векторизацией). Тут подключается следующее звено: нейронная сеть. Я решил использовать именно её так как мне это было интересно и удалось найти подходящую архитектуру для моей задачи. В этом мне помогла первая статья из цикла «Автоэнкодеры в Keras». И да, я решил использовать самый обычный автоэнкодер, так как это выгодно с точки зрения скорости работы и обучения. Но давайте обо всём по порядку.

Как и для всех других автоэнкодеров тут надо создать две нейронные сети (encoder и decoder) и объединить их в одну. Я сделал это следующим образом:

from keras.layers import Input, Dense, Flatten, Reshape
from keras.models import Model
# Размерность кодированного представления
encoding_dim = 64

# Энкодер
inp = Input(shape=(30000, )) # 30000 - размерность
# Кодированное полносвязным слоем представление
encoded = Dense(encoding_dim, activation='linear')(inp)

# Декодер
# Раскодированное другим полносвязным слоем
input_encoded = Input(shape=(encoding_dim,))
decoded = Dense(30000, activation='sigmoid')(input_encoded)

# Другие модели можно так же использовать как и слои
encoder = Model(inp, encoded, name="encoder")
decoder = Model(input_encoded, decoded, name="decoder")
autoencoder = Model(inp, decoder(encoder(inp)), name="autoencoder")


Но зачем же нужны две сети? Да и вообще, автор, ты не описал, зачем это всё!

Спокойно, сейчас всё будет. Обучать, например, только энкодер невозможно — будет просто непонятно, насколько правильное предсказание он сделал. И для этого мы тренируем вторую сеть, которая будет сразу же декодировать вывод первой (декодер). Еще используются одинаковые входные и выходные данные. Связка двух сетей (как раз она называется autoencoder) учится получать из данных на входе тоже самое на выходе. Но все данные проходят через узкое «бутылочное горлышко» в виде 64 нейронов. При этом отбрасывается самая ненужная информация. Таким образом, нейронные сети учатся максимально качественно передавать важную информацию о тексте и выбрасывать все шумы. Затем я просто убираю декодер и всё. Можно получить более качественный результат, но тогда надо или увеличивать размерность выходного слоя энкодера/входного декодера. Тогда надо будет хранить в базе данных больше значений, она будет больше весить + все операции над длинными векторами будут дольше (об этом позже). Или можно добавить слои/нейроны, но тогда обучени и векторизация будут дольше.

Сам энкодер позволяет «сжимать размерность вектора». Помните тот вектор нулей и единиц? Так вот, энкодер позволяет изменить его размер с 30К до 64 без особой потери важной информации. После этого шага можно нормально сравнивать два вектора для выяснения их схожести…

Но мы же смотрим на работу сервиса по рекомендации сообществ VK, а не отдельных записей. Это значит, что нам надо как-то получить вектор всего паблика. Это делается очень легко, математика на уровне пятого класса. Это немного грубый метод, но он работает. Я просто беру и складываю все векторы записей из одного сообщества (для примера возьмём три маленьких вектора {1, 2, 3}, {2, 3, 4}, {0, 4, 2}, получится вектор {3, 9, 9}). И делю каждый его элемент на количество векторов (у нас получится вектор {1, 3, 3}). Всё, мы объединили все записи группы в одну. В дальнейшем надо придумать что-то более хитрое, чтобы можно было отбрасывать шумы в виде постов с рекламой, например. Но сейчас и этого хватает.

Переходим к самой математической части, но так как ее все почему-то боятся, то я распишу ее максимально сильно. Начнём с векторов в математическом смысле. Вектор — направленный отрезок. Это такая штука, у которой есть координаты начала (удобнее всего брать их нулями) и координаты конца. Именно последние и записываются в фигурных скобках. Например, координаты конца вектора {1, 0, 1} ютьюб это точка с координатами (1, 0, 1). Но мы рассмотрим два двухмерных вектора, $inline$\vec{a}$inline${5, 2} и $inline$\vec{b}$inline${5, 0}. Построим их в одной системе координат:

6aaxhnmmwy42zoz5uan5jarnd8i.png

Пусть вектор $inline$\vec{a}a$inline$ розовый, $inline$\vec{b}a$inline$ — жёлтый. Тогда по математическому факту девятого класса косинус угла между ними равен отношению их скалярного произведения к произведению их модулей.

Скалярное произведение <$inline$\vec{a}$inline$, $inline$\vec{b}$inline$> равно сумме произведений соответствующих элементов, у нас $inline$<\vec{a}, \vec{b}>=5×5+2×0=25$inline$.

Модуль вектора находится по такой формуле:

$$display$$|\vec{a}| = \sqrt{a^2_x + a^2_y}$$display$$


Где $inline$a_x$inline$ и $inline$a_y$inline$ это первое и второе значение вектора a, соответственно. Значит:
$inline$|a| = \sqrt{5^2 + 2^2} = \sqrt{29}$inline$
$inline$|b| = \sqrt{5^2 + 0^2} = 5$inline$

Соединив всё по формуле получим:
$inline$cos (\vec{a}, \vec{b}) = \frac{25}{\sqrt{29}*5}\approx 0.93$inline$
Правильность вычислений можно проверить через тригонометрические функции образовавшегося прямоугольного треугольника. В проекте все вычисления происходят по таким формулам, вот только координат конца вектора у меня не две, а шестьдесят четыре.

Что даёт эта информация? Как оказалось, чем больше значение косинуса (чем меньше угол), тем более похожи тексты, которые соответствуют векторам. Таким образом, задача поиска группы, наиболее похожей на группу А сводится к нахождению косинуса угла между вектором этой группы и всеми другими. Затем движок оставляет все группы, у которых значение косинуса в паре с А будет больше, скажем, 0.99. На этом этапе можно просто вывести результат, так раньше было и у меня. Но этот процесс очень долгий уже при 100К сообществ, а что будет, скажем, при 1М?

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


Я не буду расписывать всё о сайте, так как это самая простая и скучная часть. Никогда ранее не писал сайты с нуля, всегда использовал различные готовые движки. Но в этом проекте я понял, что будет проще сделать самопис. Итак, движок сайта написан на Python 3 с использованием Flask. И используется шаблонизатор Ninja2, который позволяет более удобно подставлять динамические значения в статический HTML (и js) код. Не забыл и про авторизацию через ВКонтакте, так как это наиболее оптимальный вариант. Дизайнер, как и верстальщик, из меня просто ужасный, если кто-то хочет присоединиться к проекту — добро пожаловать.

b3ptt2glf6ex1ggakahmil_9e_c.png
Первая строка результатов с сайта


Я столкнулся с некоторыми неприятными ситуациями, которые успешно решил. Выше написана проблема с API VK и ее решение было особенно неприятным для сервиса, так как скорость упала очень сильно. Если раньше сотню постов я получал одним запросом, то теперь мне надо сделать несколько прогрузок большого HTML код, распарсить его и только после этого обработать. Сейчас есть проблема с ограничением на получение пользователей, их друзей и групп, но этот лимит не очень мешает на этом этапе. Дальше придётся решить его так же, как и первый.

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

k_dofkqnhrqjioy1mplfkzktysy.png

Но тут не текст и нужны по настоящему серьезные вычислительные мощности. Например, это топовая видеокарта, но сейчас у меня ее нет, а брать сервер для всего этого не хочу (еще слишком рано). Но в целом, у меня уже есть наработки архитектуры нейронной сети для этой задачи. Я собираюсь использовать какую-нибудь нейронку для классификации изображений, «отрезав» от нее верхнюю часть, которая отвечает за саму классификацию объектов. Останется всё то, что будет составлять карту признаков картинки. Эту карту я, возможно, сожму еще одним энкодером и всё, все последующие операции аналогичны «текстовым».

Остаётся еще один неразрешённый вопрос касательно того, как много запросов к сайту ВКонтакте я могу делать за единицу времени. Или за день. Сейчас я еще не упёрся в это ограничение, но это может произойти в самый неподходящий момент.


Мне срочно нужна красивая панель управления и статистики. Она уже есть в начальном состоянии, но ее надо допилить. Из нее хочу управлять запуском/остановкой микросервисов (а именно из них и состоит движок), размером очередей, скоростью обработки и всем таким. Ну и статистика, кому бы не хотелось посмотреть на свои цифры? Конечно же, мне надо всё оптимизировать и делать пригодным для пользователей, в частности, надо переделать внешнюю часть сайта, так как она не соответствует моим стандартам удобства.
У меня получилось встать на путь создания сервиса с интересной (по крайней мере, для меня) структурой, которую я собираюсь использовать для одного из конкурсов, позволяющих поступить в самый лучший ВУЗ России (я не скажу, что это за первый неклассический). Думаю, что если еще поработать, то можно выжать из него и что-то более интересное, например, анализатор качества публикаций, сделать сервис аналитики для администрации сообщества или еще что-нибудь такое же.

Со многими вещами из текста выше я сталкивался в первый раз. Это значит, что что-то я мог делать неверно. Если мои читатели знают, что можно улучшить/исправить, где у меня могут быть другие проблемы и так далее — пожалуйста, напишите об этом в комментарии. И прошу дать критику по поводу самого качества статьи, чтобы я смог улучшить его в следующие разы. Спасибо.

© Habrahabr.ru