Может быть отсылка к экзистенциальному кризису звучит слишком громко, но лично для меня проблема поиска и выбора (или выбора и поиска, это имеет значение) как в мире интернета так и в мире простых вещей по мучениям иногда приближается к нему. Выбор фильма на вечер, книги неизвестного автора, сосисок в магазине, нового утюга — дикое количество вариантов. Особенно когда не очень знаешь чего хочешь. Да и когда знаешь, но не можешь попробовать — тоже не праздник — мир разнообразен и все сразу не перепробуешь.
Рекомендательные системы сильно помогают в выборе, но не везде и не всегда так как хотелось бы. Часто не учитывается семантика содержания. Кроме того, во весь рост встает проблема "длинного хвоста", когда рекомендации сосредоточены только на самых популярных позициях, а интересные, но не очень популярные в массе вещи ими не охвачены.
Cвой эксперимент в этом направлении я решил начать с поиска интересных текстов взяв для этого довольно небольшое, но пишущее сообщество авторов, которые еще остались на блоговой платформе Живой Журнал. О том как сделать собственную рекомендательную систему а в результате получить еще и помощник в выборе вина на вечер — под катом.
Почему Живой Журнал?
Ну, во-первых, это блог-платформа и часто там пишут интересные вещи. Даже несмотря на миграцию пользователей в фейсбук в ЖЖ еще осталось по моим оценкам порядка 35-40 тысяч активных блогов. Вполне посильный объем для экспериментов без привлечения инструментов big data. Ну и сам процесс краулинга весьма прост, в отличии от работы с тем же фейсбуком.
В итоге хотелось получить инструмент рекомендаций интересных авторов. В процессе работы над проектом хотелки ширились и множились и не все получилось сразу охватить. О том что удалось и какие есть дальнейшие планы я и расскажу в этой статье.
Краулинг
Как уже говорилось краулинг блогов на платформе ЖЖ дело весьма простое — можно легко получить тексты записей, картинки (если вдруг они нужны) и даже комментарии к постам с их структурой. Надо сказать что именно система иерархического комментирования как на Хабре и удерживает некоторых авторов на этой платформе.
Итак, несложный краулер на Perl'е, код которого можно взять у меня на github'е за пару недель неспешной работы выгрузил мне тексты и комментарии из примерно где-то 40 тысяч блогов за период лето 2017 и лето 2016. Эти два периода выбраны для параллельного сравнения активности в ЖЖ год от года.
Итак, исходные данные для исследования получились примерно такие:
— Период анализа: 6 месяцев
— Авторов: примерно 45 тыс.
— Публикаций: примерно 2.5 млн.
— Слов: примерно 740 млн.
Аналитическая модель и обработка
Так как я не data scientist (хотя и смотрю конкурсы на kaggle время от времени) то и алгоритмы выбирались весьма простые — считать близость авторов как косинусное расстояние между векторами, характеризующими их тексты.
Было несколько подходов к построению векторов с характеристиками текстов авторов:
1. Очевидная — строим вектора с метрикой TF-IDF по словам собранного текстового корпуса. Недостаток — векторное пространство получается очень уж многомерным (~60 тыс.) и сильно разреженным.
2. Хитрая — кластеризовать текстовый корпус с помощью word2vec и взять вектора как проекцию текстов на получившиеся кластеры. Достоинство — относительно короткие вектора по числу кластеров (обычно ~1000). Недостаток — надо обучать word2vec, подбирать число кластеров для кластеризации, думать как проецировать текст на кластеры, заморочено.
3. И тоже неплохая — строить вектора с gensim doc2vec. Практически то же самое что и второй вариант, но вид с боку и все проблемы кроме обучения уже решены. Но на python. А мне этого не хотелось.
В итоге была выбрана первая модель дополненная методами LSA (Латентно-семантический анализ) в виде SVD разложения полученной матрицы «документы» — «термы». Метод много раз описывался на Хабре, останавливаться не буду.
Для лемматизации термов, а точнее приведения русских слов к базовой форме, использовалась утилита mystem от Яндекса и обертка вокруг нее, позволяющая объединять несколько документов в единый файл для скармливания его на вход утилиты. Можно было вызывать ее на каждый документ, что я и делал раньше, но это не очень эффективно. В корпус отбирались существительные, прилагательные, глаголы и наречия длинной больше 3-х символов и с частотой употребления более 100 по всему корпусу.
Вот, например, топ существительных, приведенных к базовой форме:
1. Человек
2. Время
3. Россия
4. День
5. Страна
SVD разложение делалось с помощью внешней утилиты SVDLIBC, которую пришлось пропатчить на предмет использования очень больших матриц, так как она падала при попытке создать «плотную матрицу» с количеством элементов больше чем максимальное значения типа int. Но утилита хороша тем что она умеет работать в разреженными матрицами и вычислять только необходимое количество сингулярных векторов, что существенно экономит время. Кстати, для Perl есть модуль PDL::SVDLIBC основанный на коде этой утилиты.
Выбор длины вектора (количества сингулярных векторов в разложении) делался «экспертным анализом» (читай на глаз) на тестовом множестве журналов. В итоге остановился на n=500.
Результат анализа — матрица коэффициентов схожести авторов.
Визуализация результата
На начальном этапе хотелось глазами оценить структуру тематических сообществ чтобы понять что с этим делать дальше.
Я попытался использовать кластеризацию и визуализацию в виде самоорганизующихся карт (SOM) из аналитического пакета Deductor подав на вход вектора из 500 значений после SVD. Результат заставил себя ждать (многопоточность? нет, не слышали) и не порадовал — прослеживалось три больших кластера и достаточно плотным размещением авторов по всему полю карты.
Было очевидно что матрицу расстояний можно преобразовать в граф сохраняя ребра с значением коэффициента близости больше определенного порога. Написав конвертер матрицы в граф формата GDF (он показался мне наиболее подходящим для визуализации разных параметров), скормив этот граф пакету визуализации графов Gephi и поэкспериментировав с разными layout'ами и параметрами отображений получил от такую картинку:
При клике на картинку и вот тут можно помедитировать на нее в разрешении 3000x3000. И если вы иногда заходите в блоги ЖЖ возможно вы найдете там знакомые имена.
Картинка эта вполне соответствует моим представлением о тематической структуре авторов хотя и не идеальна (слишком плотная). Осталось малое — дать себе и другим возможность получать рекомендации на основе персональных авторских предпочтений: задал имя автора и получил рекомендации похожих на него.
С этой целью была написана небольшая визуализация областей графа прилегающих к интересующему автору на d3js с использованием force layout. Для каждого автора (а их чуть больше 40 тыс. как я уже говорил) был сгенерирован json-файл с описанием близлежащих узлов графа, «теговая аннотация» на основе отсортированного списка популярных слов для этого автора и аналогичная аннотация его группы, в которую входят близлежащие узлы. Аннотации должны дать некоторое понимание о чем же пишет автор и какие темы актуальны у похожих на него блогеров.
Получилась вот такая мини-поисковая система similarity.me с рекомендациями и графической картой:
Что можно улучшить?
- Адекватность — сейчас идет процесс сбора данных, так что охват постов будет больше и семантическая близость авторов на карте должна стать более адекватна. Кроме этого планирую провести дополнительный предотбор авторов — исключить «полуживых», любителей репостов, автоматические публикаторы — это позволить немного проредить граф от мусора.
- Точность — есть мысль использовать нейросети для формирования векторов документов и поэкспериментировать с различными расстояниями кроме косинусного
- Визуализация — попробовать наложить тематическую структуру на структуру связей между авторами с помощью гексагональных проекций и все тех же самоорганизующихся карт. Ну и добиться более четкой кластеризации общей карты.
- Аннотирование — попробовать алгоритмы аннотирования с составлением словосочетаний.
А что насчет винишка?
Заглавная картинка — это как раз винная карта по вкусовым характеристикам ))
Побочным результатом исследования стала мысль «а не использовать этот подход для рекомендаций каких-нибудь товаров по их описаниям». И для этого идеально подошли дегустационные заметки винного критика Дениса Руденко в его блоге в ЖЖ «Ежедневный Винный Телеграф». К сожалению он давно забросил их публикацию, но накопившиеся там более 2000 описаний вин — отличный материал для такого эксперимента.
Особой предобработки текстов не делалось. Скормив их системе получил вкусовую карту 700 лучших вин с рекомендацией похожих:
На ней можно подсвечивать вина из самых популярных сортов винограда и по их субъективным (с точки зрения дегустатора) вкусам.
В планах немного оптимизировать рекомендации за счет преобразования некоторых прилагательных в существительные, например «черничный» — «черника». И разместить на карте все более чем 2000 продегустированных вин.
Вот как-то так можно бороться с проблемой выбора (если вдруг она актуальна для вас) расширив эти подходы и на другие области.
Автор: RomanL