А помните, был такой коронавирус? Затронул он в том или ином виде практически всех: кто-то болел, кто-то вакцинировался, а кто-то с вакцинацией боролся. Вот о последних и пойдёт речь. Надо помнить, что антивакцинаторы — явление не новое, появились они задолго до коронавируса и боролись они как с конкретными вакцинами, так и с вакцинацией как явлением. Думаю, можно смело сказать, что существуют они столько же, сколько и вакцины. Но можно с уверенностью сказать, что наибольшее внимание они привлекли как раз во время пандемии COVID-19. Впрочем в этот раз они боролись не только с вакцинами, но и с ограничительными мерами, социальной дистанцией и QR-кодами. Их даже называли одной из главных проблем здравоохранения! Антивакцинаторы объединялись в сообщества в социальных сетях, делились мнением и координировали свои действия. Пожалуй самым активным сообществом антивакцинаторов может похвастаться Telegram. Вот на эти сообщества и посмотрим: этот пост посвящён тематическому моделированию антивакцинаторских каналов в Telegram, мы рассчитываем понять, что именно они обсуждают. Для начала пару слов о том, что такое тематическое моделирование. Тематическое моделирование (оно же topic modeling) — задача и процесс построения тематической модели, задачей которой является распределение каждого документа из некоторой коллекции по темам на основе их содержания. Входом модели является коллекция документов, а выходом — набор векторов, каждый из которых содержит оценки принадлежности документа каждой из тем. Подробнее про тематическое моделирование можно узнать, например, здесь, осознать необъятность темы можно в курсе К. В. Воронцова. В своё время, в сети уже выкладывали дамп постов антивакцинаторских каналов в Telegram. Этот дамп и будем использовать для экспериментов. Наш датасет представляет собой набор JSON-файлов, свой файл на каждый канал. Внутри файла — массив постов, а пост это объект с полями. Кстати, посты бывают разными: есть служебные посты, о том, что кто-то присоединился к каналу или о том что заголовок канала изменён, а есть посты с сообщением пользователя. Нам нужны вторые, так как они содержат мнение участников каналов по интересующей нас теме. У таких постов поле type имеет значение "message", посты с другим значением в этом поле можно смело отбросить. Давайте посмотрим на один пост. JSON-объект поста позволяет получить нам идентификатор отправителя, дату отправки и текст сообщения. Если на пост кто-то ответил или откуда-то переслал, это тоже можно узнать. К некоторым постам есть вложения, тогда можем узнать его тип, формат и размер кадра, если вложение это видео. Но самих вложений в дампе нет. Итак, займёмся предобработкой данных. Цель предобработки в двух словах можно определить как "убрать ненужное, оставить нужное и в нужном виде". Пока обойдёмся скриптами на Python, Jupyter нам понадобится позднее. В первую очередь, отфильтруем посты и поля в постах: уберём все служебные посты, а из самих постов надо убрать смайлы, непечатаемые и диакритические символы. Наконец, нужно добавить к каждому посту идентификатор канала, которому он принадлежит. Сейчас посты разных каналов разложены по отдельным файлам, но нам удобнее слить их все в один датасет. Идентификатор родительского канала позволит анализируя посты группировать их или фильтровать по каналам. Теперь пост выглядит уже лучше, там только то что нужно. Продолжаем смотреть на посты и видим, что в тексте встречается форматирование. Можно сделать текст полужирным, курсивом, вставить ссылку. В итоге текст сообщения это на самом деле не текст, а массив фрагментов, каждый со своим форматированием. Например, так выглядит пост с форматированием: Нам форматирование без надобности, уберём его, сформировав из всех фрагментов цельную строку. Финальный этап предобработки — приведение текста к нормальному виду. Слова нужно нормализовать, чтобы все встречающиеся варианты одного слова приобрели одинаковую форму, также надо привести их к нижнему регистру, убрать пунктуацию и стоп-слова. Тут можно вспомнить, что выделяют два способа нормализация слов: стемминг и лемматизацию. Первый подход заключается в усечении слова до его основы путем отбрасывания приставок, окончаний и тому подобного, что мы помним ещё со школы. Проблемой этого подхода является то, что основа слова не обязательно совпадает с его корнем, да и вообще может быть каким-то огрызком слова, понять по которому о чём речь затруднительно. Второй подход заключается в приведении слова к словарной форме с применением всех развесистых морфологических методов. Следствием здесь является то, что работает лемматизация довольно медленно. Принято считать, что для русского языка стемминг теряет больше полезной информации, что не лучшим образом сказывается на качестве модели, и лучше использовать лемматизацию. Воспользуемся этим советом. Теперь уберем стоп-слова. Убирать их удобнее уже после лемматизации, так как стоп-слова в исходном тексте тоже будут приведены к словарной форме и в списке стоп-слов можно иметь только одну форму слова, а не все. Наконец, можно отбросить все посты, где меньше 10 слов. Скорее всего в таких постах не очень много информации, а время на их обработку потратим. Кстати, отбрасывать посты по числу слов можно два раза: до и после очистки стоп-слов. Если в посте меньше 10 слов изначально, то тратить время и ресурсы на лемматизацию смысла нет — больше слов всё равно не станет. Посмотрим на наш очищенный и обработанный пост, прежде чем идти дальше. Теперь нужно применить эти шаги ко всем нашим постам. Приводить исходный код не будем, так как он в целом тривиален, а конкретные параметры нормализации и очистки данных и порядок шагов зависят от предпочтений и конкретной решаемой задачи. Лучше поговорим о проблемах. Первая неожиданная проблема оказалась в том, что словарь стоп-слов для русского языка из пакета nltk, весьма популярного для обработки естественного языка на Python, оказался очень маленьким и некоторые очевидные стоп-слова (например "наш" или "твой") он почему-то стоп-словами не считает. В итоге таких слов в постах оказывается много и они мешают дальнейшей работе. Поэтому пришлось расширить список стоп-слов, поискав такие списки в Сети и объединив несколько. Стало гораздо лучше. Второй проблемой, ожидаемой, кстати, стало то, что всё это работает медленно. Действительно, мы берем один пост, фильтруем поля, проводим лемматизацию, отбрасываем стоп-слова, решаем, сохранять ли пост, а затем переходим к следующему. При этом посты уже разложены по разным файлам, а обработка постов одного канала не зависит от постов другого (вообще говоря, обработка поста от других постов тоже не зависит, но параллелить на уровне отдельных постов вряд ли имеет смысл). Поэтому мы можем обрабатывать параллельно на уровне каналов. К счастью, Python уже позволяет распределять на несколько процессов выполнение некоторой функции, причём управление очередью задач и распределением задач на процессы он берет на себя. Эту проблему тоже решили, теперь предобработка занимает примерно 3 часа на моей машине от начала и до конца. Хотя это всё равно не очень комфортно, но вооружившись достаточным количеством чашек чая всё же можно дождаться окончания процесса. Итак, данные загрузили и предобработали. Теперь можно спокойно переходить к их исследованию. Для начала общая статистика. Всего в датасете собраны посты от 53 каналов, всего около миллиона постов. Посты написали 53 тысячи пользователей. Телеграм позволяет писать посты не только от своего имени, админы каналов могут писать посты от имени канала. Таких постов в датасете около 8 тысяч. Здесь нужно оговориться, поскольку мы располагаем именно постами, то мы знаем только о тех участниках каналов, которые что-то писали. Если кто-то никогда ничего не писал (или писал, но его посты были отфильтрованы как слишком короткие), то в наш датасет он не попал. Дальнейшее исследование будем строить по простому принципу: будем задавать данным вопрос и искать на него ответ. В этом нам поможет простой код. Отберём посты, написанные людьми, посчитаем их количество и поделим на число пользователей. Итак, в среднем пользователь  пишет 18 постов. Здесь тоже код простой. Сгруппируем посты по автору, посчитаем количество постов, а затем посчитаем количество тех, у кого пост только один. Только один пост написали почти 20 тысяч участников, это около 36% ото всех. Мы понимает, что пользователи пишут разное количество сообщений. Кто-то более активен, кто-то менее. Поэтому интересно... Если взять max() для user_posts, то увидим, что самый активных участник написал почти 11 тысяч постов! Такое количество постов означает, что новый он пишет примерно каждые полтора часа (это если предположить, что он никогда не спит, если допустить что отдых требуется и ему, то посты писать он должен чаще). А что он там пишет? Посмотрим на несколько его сообщений: сильный потрясение испытать сторонник единый... мэр екатеринбург высказаться манипуляция связь... брат поддать голос завтра трансформера орать... Из-за всей предобработки понять смысл постов может быть немного сложно, но похоже что самый активных участник просто пересказывает новости. Кстати, наш рекордсмен пишет только в один канал, но при этом он там не единственный участник, помимо него там еще больше ста тысяч сообщений, так что этот канал — не только для анонсов, но и для обсуждений. Как мы уже говорили, в среднем пользователь пишет 18 постов, но вместе с тем, почти 36% написали только один пост. А как распределяется количество пользователей по числу сообщений? Мы ожидаем, что большое количество сообщений написало мало пользователей, то есть чем большее число сообщений мы оцениваем, тем меньшее количество пользователей столько написало. Посмотрим на графике, так ли это. И действительно, по графику мы видим, что наше предположение подтверждается. Теперь переходим к наполнению постов. Пока что по-прежнему будем рассматривать числовую статистику, связанную с постами, но теперь будет опираться на их содержание. В частности, нас интересует количество слов в одном посте, поэтому выполним небольшую подготовку. Разделим текст поста на слова и посчитаем число слов, сохранив результат в отдельной колонке. Здесь нам поможет такой однострочник: Здесь всё просто: Итого 38 слов. Сразу же возникает следующий вопрос... Здесь тоже тривиальные код: В самом длинном посте 780 слов. Давайте посмотрим на него. i am a funeral director running his own funeral home for the doubters this is me i have been in the trade 15 years... Пост довольно длинный, поэтому приводить его целиком не будем. Судя по тексту, это рассказ владельца похоронного бюро об умерших от коронавируса. Так как стоп-слова у нас были только для русского языка, в этом посте все стоп-слова остались. Возможно, это и стало причиной его рекорда. По уже сложившейся традиции посмотрим, как распределяется количество постов в зависимости от длины. Ожидаем, что чем больше слов, тем реже встречается такой пост. Так и есть, график подтверждает наше предположение. Любопытно, что в районе 400 слов мы видим всплеск — таких постов очень много. Каналы неравномерны по количеству пользователей и сообщений. Сравним выводы, сделанные по всему датасету с самый активным каналом. На этот раз обойдемся без подробностей, сразу посмотрим на таблицу с результатом.  В целом Самый активный канал Доля постов от всех постов 100% 11.52% Среднее число постов у одного пользователя 18 22.71 Доля пользователей с одним постом 35.87% 39.1% Количество постов самого активного пользователя 11153 11153 Средняя длина поста 38.33 46.61 Длина самого длинного поста 779 654 Доля постов с медиа-объектом 7.54% 4.89% Время между постами  8 минут Итак, мы видим, что на самый активный канал приходится почти 10% всех постов датасета. По многим параметрам самый активный канал похож на весь датасет: у них схожее число постов у среднего пользователя, схожая доля пользователей с одним постом. Самый активный пользователь у них вообще один и тот же. Вот самый длинный пост у канала короче, почти на 120 слов. Любопытно, что в самый активный канал новый пост пишут каждые 8 минут. Видимо, не спят. И, наконец, последний вопрос, перед тем как перейдём к тематическому моделированию. Какова связность каналов? Насколько вообще типичным является участие пользователя в нескольких каналах? Для начала снова небольшая подготовка. Посчитаем, в каком количестве каналов состоит каждый пользователь. Так как нас интересуют только те пользователи, кто участвует в нескольких каналах, тех, кто состоит только в одном можно проигнорировать. Но лучше сначала посчитать долю таких пользователей, чтобы знать, какую часть пользователей мы отбросили и насколько вообще участие в нескольких группах распространено. Итак, 73% пользователей участвуют только в одном канале. А сколько пользователей хоть раз написали более чем в один канал? Здесь мы получаем, что в несколько каналов когда-либо писали 14 тысяч человек. Посмотрим, какое среднее и максимальное количество каналов, в котором состоит один пользователь. Поскольку тех, кто состоит только в одном канале большинство, пропустим их. Итак, в среднем один пользователь участвует в трёх каналах, а самый социально активный — в 30. Однако если мы посмотрим на общее число сообщений этого пользователя-рекордсмена, то обнаружим, что он написал только 325 постов, то есть 9-10 постов в одном канале. Видимо, ему важнее была широта охвата. Построим график зависимости числа каналов от числа пользователей, которые в этих каналах участвуют. Мы ожидаем, что чем большего числа каналов мы требуем, тем меньше пользователей участвует в таком количестве каналов. График подтверждает наше предположение. Есть, однако, и исключения: участие в 20 каналах более распространено, чем в 18, а участие в 15 каналах более распространено, чем участие в 14. Теперь проанализируем связность с точки зрения общих пользователей у каналов. Построим матрицу где строки и столбцы соответствуют каналам, а значения элементов матрицы — доле пользователей одного канала среди пользователей другого. Теперь посчитаем средние значения по каждой строке и каждому столбцу, что позволяет узнать среднюю долю участников других каналов в каждом канале и среднюю долю каждого канала в других. Благодаря этому мы видим, что пользователи канала 1343694760 создают примерно 25% активности в остальных каналах. Можно утверждать, что этот канал является одним из ключевых в сообществе антивакцинаторов. Посмотрим на первые несколько постов этого канала.  date text 41071 01.01.2021 01:45 медсестра сша прямой эфир сделать укол вакцина... 41072 01.01.2021 01:53 фельдшер марина латвия записать выложить сеть ... 41073 01.01.2021 01:57 заголовок летальный инфаркт случиться мужчина ... 41074 01.01.2021 01:59 воз поменять определение коллективный иммуните... 41075 01.01.2021 02:11 тестирование киви короновирус результат положи... Судя по текстам постов, этот канал специализируется на новостях. Вероятно, обсудив и поделившись новостями в этом канале, пользователи разносят их по другим, чем и объясняется значительная доля пользователей этого канала в других. Наконец, можно переходить к тематическому моделированию. Для начала определимся, что мы будем моделировать. Мы возьмём весь датасет, посты в самом активном канале и посты самого активного пользователя. То есть, мы выполним три сеанса моделирования. Это поможет нам проанализировать, как меняются найденные темы при изменении данных. Кроме того, мы рассмотрим два метода моделирования — LSA и LDA, чтобы сравнить их между собой. Использовать будем реализации алгоритмов из библиотеки Gensim. Хорошую инструкцию по тематическому моделированию с помощью Gensim можно найти по ссылке. Начнём с моделирования всего датасета. Но чтобы моделировать, надо сначала получить векторное представление документов (в нашем случае — постов), а для этого надо собрать словарь, в этом нам поможет следующий код. Сразу же во время построения словаря отфильтруем его. Нам не очень интересны слова, которые встречаются слишком редко или слишком часто. В первом случае слово встречается так редко, что судить о его влиянии на тему документа не имеет смысла, это будет предположение, почти ничем не подкреплённое. Во втором случае, слово настолько распространено, что может встречаться в документах любой темы, поэтому само это слово определить тему не поможет. Отбросим 1% самых частых слов (no_above=0.99) и слова, которые встречаются только в 100 документах или реже (no_below=100). Теперь на основе текстов постов построим корпус их векторных представлений. Всё готово, можно переходить к моделированию. Начнём с LSA. Забегая вперёд скажу, что этот алгоритм работает несколько быстрее, поэтому начав с него мы раньше поймём, что все работает. Создадим модель Гиперпараметром модели является количество тем, которые мы хотим выделить в корпусе документов. Есть разные способы выбора оптимального количества тем, но сейчас будем искать 7 тем. После того как модель построена, мы можем получить список тем и присвоенных им ключевых слов. Посмотрим, какие слова, по мнению LSA, в большей мере описывают найденные темы.  0 1 2 3 4 5 6 0 вакцина вакцина вакцина the рф • *** 1 вакцинация вакцинация молитва of ст group ребёнок 2 ребёнок бог рф and ребёнок channel россия 3 россия молитва господь to вакцинация rising сша 4 страна святой святой in право up трамп Мы видим, что все темы так или иначе связаны с вакцинацией. Темы 1 и 2 имеют преимущественно религиозное содержание, тема 0 посвящена детям и стране. Темы 3 и 5 объединяют англоязычные посты. Любопытно, что в тему 5 попало ключевое слово, которое на самом деле не слово, а символ форматирования. Тема 4 посвящена правовым вопросам (слово "ст" это, очевидно, "статья"). Тема 6 любопытна тем, что она посвящена международной политике — она упоминает Трампа и США. Кстати, первое слово в этой теме это действительно слово, но очень нехорошее, поэтому пришлось его убрать. Сравним полученные LSA темы с темами по версии LDA. Модель создаём аналогичным образом. Посмотрим на таблицу слов, выбранных LDA как ключевые.  0 1 2 3 4 5 6 0 сделать бог вакцина ребёнок ребёнок вакцина россия 1 свой ребёнок вакцинация знать знать ребёнок вакцина 2 страна делать ребёнок вакцинация страна народ новый 3 россия вакцина знать вакцина власть страна мир 4 маска новый говорить закон россия вакцинация вопрос Точно также, мы видим тут религиозные мотивы (тема 1), мотивы семьи и детей (темы 0 и 5). В то же время, LDA чаще выбирает в качестве ключевого слова слово "страна", оно встречается почти в каждой теме. Кроме того, в случае LDA впервые появляется слово "маска". Теперь проведём тематическое моделирование наиболее активного канала. Воспользуемся ранее проведённым расчётом количества постов в каналах и уже найденным самым активным каналом. Здесь нужно также подготовить словарь и корпус векторных представлений документов. Это нужно сделать с нуля, так как весь датасет и один из входящих в его состав каналов, скорее всего, имеют разные частотные характеристики, сами наборы используемых слов тоже, скорее всего, отличаются. Код формирования словаря и корпуса, а также обучения моделей аналогичен случаю всего датасета, поэтому сразу перейдём к таблицам с ключевыми словами.  0 1 2 3 4 5 6 0 россия бог русский русский ребёнок сталин вакцина 1 русский господь россия ребёнок россия проект церковь 2 страна святой ребёнок россия церковь ссср русский 3 бог помиловать народ церковь православный русский вакцинация 4 народ молитва вакцина страна вакцина лондон православный Поскольку мы моделировали только один канал, темы здесь значительно уже, чем для всего датасета. Мы видим, что темы имеют явную религиозную и патриотическую направленность, оставляя вопросы вакцинации на второстепенных местах. Примечательна тема 5, упоминающая Сталина и СССР. В то же время, ключевые слова у разных тем часто совпадают, то есть выделить независимые темы тут оказалось сложнее. Теперь тот же канал, но LDA.  0 1 2 3 4 5 6 0 русский русский россия ребёнок россия россия слово 1 народ россия вакцина страна православный народ страна 2 власть ребёнок ребёнок новый русский русский новый 3 россия церковь самый россия бог москва россия 4 страна бог бог власть знать знать русский В случае LDA темы также имеют ярко выраженную религиозно-патриотическую окраску. При этом, некоторые слова довольно часто повторяются в качестве ключевых для разных тем. Теперь проведём моделирование наиболее активного пользователя, посмотрим о чем он пишет с точки зрения тем. Снова подготовим словарь и корпус с нуля, чтобы они учитывали филологические особенности этого пользователя. Посмотрим на ключевые слова, выбранные LSA.  0 1 2 3 4 5 6 0 россия русский  россия ребёнок сорок трамп 1 русский россия канал церковь семья  сша 2 страна народ русский православный сорок канал вакцина 3 новый православный православный  россия движение власть 4 народ  народ ребёнок церковь ребёнок ребёнок Так как здесь моделировали одного пользователя, темы ещё уже, чем в других случаях. Мы видим, что темы носят ярко выраженную религиозную направленность. Среди ключевых слов стали появляться непечатаемые символы (эмодзи?), им соответствуют пустые ячейки таблицы. При этом, из-за того что все посты пользователя примерно об одном и том же, значительной разницы между темами нет. Теперь посмотрим на ключевые слова для тем, отобранные LDA:  0 1 2 3 4 5 6 0 россия россия вакцина русский россия россия россия 1 сорок новый россия россия страна власть власть 2 русский вопрос новый сорок новый народ русский 3 ребёнок церковь канал православный русский сорок москва 4 семья президент русский народ сша мир свой Также, как и для LSA, темы сильно связаны друг с другим, много общих ключевых слов. Про чипирование и 5G в контексте вакцинации, наверно, знают, все. Если мы вернёмся к тому посту, на котором тестировали предобработку, то обнаружим, что в нём упоминается великий и ужасный Билл Гейтс, олицетворяющий всё плохое, что связано с вакцинацией. Но по результатам тематического моделирования мы видим, что ни один метод ни на самом датасете ни на его подмножестве ни 5G ни Билла Гейтса не выявил. Возможно, они не настолько распространены, как мы привыкли думать. Попробуем посмотреть, в какой мере темы теорий заговора, мистики, чипирования и тому подобного были распространены в датасете. Сформируем набор "странных" тем и подберем для них ключевые слова. Посмотрим на темы: Тема Ключевые слова Чипирование чип* 5g 5g; 5г; 5ж; 5 g; 5 г; 5 ж Билл Гейтс гейтс* Факцина факцин*; жиж* Сорос сорос* Ротшильды ротшильд* Рокфеллеры рокфел* Катастрофы катастроф*; катаклизм*; авари* Астрал астрал* Климатическое оружие haarp; harp; (климатич* && оружи*) Заговор заговор* Сокращение населения (сокращени* && населен*); (сокращени* && популяц*); (сократить && населен*); (сократить* && популяц*) Посчитаем количество постов, в которых упоминались эти ключевые слова. Построим график количества таких постов по теме. Как видим, на первом месте чипирование, на втором сам Билл Гейтс. 5G только на шестом месте, сразу за постами, называющими вакцину "факциной" или "жижей". Сокращение населения волнует участников сообщества заметно меньше, чем 5G, и лишь немногим больше, чем коварство Сороса (Соросу, должно быть, обидно: раньше главным злодеем был он, а теперь Билл Гейтс почти в два раза более популярен). Астрал и климатическое оружие плетутся в самом конце, но нельзя сказать, что эти темы совсем не обсуждают: климатическому оружию посвящено 480 постов. На самую популярную "странную" тему, то есть на Билла Гейтса, приходится почти 1% всех постов датасета. Всего же "странные" посты составляют чуть меньше 6% ото всех. Подведём итоги. Из анализа активности пользователей в каналах видим, что сообщество антивакцинаторов довольно активно, большинство пользователей пишет более чем одно одно сообщение, а количество сообщений наиболее активных участников исчисляется тысячами. Участие в нескольких каналах считается скорее нормой, чем исключением. Почти 27% участников сообщества писало в два канала или больше. Рекордсмен написал аж в 30 каналов, впрочем, наибольшим числом самих постов он похвастаться не может. Сравнивать разные методы тематического моделирования дело довольно неблагодарное — верных ответов нет и сравнить не с чем, но складывается впечатление, что алгоритм LSA формирует темы разной направленности, в то время как LDA требует больше времени, а ключевые слова полученных тем пересекаются гораздо сильнее. Ещё одно отличие в работе алгоритмов в том, что при разных запусках LSA выдаёт одинаковый результат, а вот у LDA наборы ключевых слов меняются от запуска к запуску. С точки зрения упомянутых в постах тем, наиболее распространены темы вакцинации как таковой, взаимодействия человека с государством по вопросам вакцинации, темы права, религии и семьи. Темы, связанные с заговорами, чипированием и тому подобным упоминаются в 6% постов и не попали в ключевые слова по результатам тематического моделирования. Вероятно, это связано с тем, что постов очень много и все каналы разные. Какие-то каналы больше посвящены гражданскому сопротивлению и "странные" темы в них представлены в меньшей степени, а какие-то целиком посвящены заговорам. Но так как мы анализировали все каналы в целом, мы видим средние темы по всему датасету. Если выполнить тематическое моделирование каждого канала отдельно, чипирование может попасть в ключевые слова для некоторых из них, наиболее сконцентрированных на этих темах. В заключение хочу сказать, что данная статья написана на основе моего выпускного проекта в OTUS, поэтому сейчас хочу порекомендовать бесплатный интенсив от OTUS на котором вас ждет глубокое погружение в область рекомендательных систем. В первый день интенсива вы изучите наиболее популярные подходы, которые существуют для формирования рекомендаций, и реализуете один из них своими руками. Во второй день - ознакомитесь с более продвинутыми методами на основе матричных разложений, а также изучите готовые инструменты и библиотеки для построения рекомендательных систем. Зарегистрироваться на интенсив  Интересующийся