Как мы создали телеграм-канал, работающий целиком на нейронных сетях

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

Пример поста из нашего канала

Пример поста из нашего канала

Введение

Приветствую всех любителей Хабра!

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

В данной статье речь пойдет о том, как мы (для этого был привлечен еще один разработчик) пытались построить автономный телеграм-канал, работающий целиком и полностью с использованием нейронных сетей.

Как определить: является ли новость интересной?

Без сомнений, киллер-фичей (ради чего весь этот канал и задумывался) является идея того, что все новости в телеграм-канале будут интересные. Как подобное можно реализовать?

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

Так или иначе, мы напарсили датасет своими руками.

Парсинг датасета и его разметка

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

Датасет, полученный в ходе парсинга

Датасет, полученный в ходе парсинга

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

Для обучения модели необходимо датасет разметить — сказать, какая новость интересна, а какая — нет. После долгих споров о том, каким образом необходимо это делать, мы сошлись на том мнении, что нужно использовать только знание о количестве форвардов — сколько раз люди переслали конкретную новость своим друзьям или знакомым.

При таком подходе возникает серьезная проблема: в 2ух разных, но с одной тематикой источниках, большом и малом, одна и та же новость соберет разное количество форвардов. Хотя значение форвард / просмотр должно быть примерно одинаковым, если наберется сколько-нибудь значимая статистика.

Очевидное решение этой проблемы — скейлить форварды. Для этого мы применили RobustScaler.

Принцип работы RobustScaler

Принцип работы RobustScaler

В итоге, мы получили следующий датасет:

Датасет после RobustScaler

Датасет после RobustScaler

Но мы все еще не сказали, какая новость интересная! Методом проб и ошибок (уже во время обучения модели), мы приняли, что новость будет интересной в том случае, если ее заскейленное значение форвардов будет выше 60-го перцентиля. По этому принципу мы и разметили датасет: 1 — новость интересная, 0 — новость неинтересная.

Обучение бинарного классификатора

Для обучения модели необходимо подготовить датасет или, проще говоря, его очистить. Опустим это действо, упомяну лишь, что мы это сделали.

Чтобы обучать модель, классифицирующую текст, нужно перевести текст в вектора чисел. Для этого мы использовали уже готовую модель построения эмбеддингов от сбера. Кстати, почитать об этой модели можно здесь, а саму модель можно найти здесь. Касаемо используемой аппаратуры — мы, как классические представители класса непрофессиональных ML-инженеров (я работаю разработчиком компиляторов, а второй разработчик пишет бэк на Go), использовали kaggle.

В итоге, потратив несколько часов ГПУ, мы получили датасет размерностью 1024 (размер эмбеддинга) + 1 (метка интересности) = 1025. Обладая таким датасетом, уже можно приступить к обучению модели.

Структура решения ML-задачи

Структура решения ML-задачи

Как показано на картинке выше, решение задачи машинного обучения — процесс итеративный. Мы пробовали самые разные модели классификации: от логистической регресии до catboost. Все эти модели не давали сколько-нибудь хороших результатов по метрикам F1 и ROC-AUC. В одной из попыток мы попробовали MLPClassifier — эта (фактически, нейросетевая) модель показала лучшие результаты среди всех использованных моделей машинного обучения.

Метрики MLPClassifer'а на датасете новостей из

Метрики MLPClassifer’а на датасете новостей из «Медузы»

В данном случае мы видим, что метрика ROC-AUC — метрика, не зависящая от сбалансированности классов в датасете — довольно высока. Но мы обратили внимание и на другие метрики: recall и precision. Мы не накрываем достаточной доли интересных новостей, чтобы говорить, что наша модель действительно хорошо работает. Да и применив модель на практике, мы получили откровенный шлак.

Но мы поняли важную вещь — нужно идти в сторону нейросетей.

Сперва мы выбрали модель rubert-tiny, а в дальнейшем более сильную модель rubert-base, эти модели были созданы специально для обучения классификаторов. Обучали мы rubert-base 15 часов и получили следующие результаты:

Метрики ruber-base на всем датасете

Метрики ruber-base на всем датасете

Выше представлены самые лучшие конечные результаты: те, на которых до сих пор работает наш канал. Подобраны самые лучшие конфигурации: по источникам, на которых модель обучалась, по количеству эпох и прочих гиперпараметрах. Перебор всех параметров занял настолько много времени, что нам не хватало бесплатного допуска к видеокартам в kaggle на 2х аккаунтах, и мы решили собрать собственный компьютер, предварительно приобрев у моего знакомого видеокарту NVIDIA GeForce GTX 1080ti.

Внедряя модель в канал мы позаботились и о том, чтобы новости шли не слишком часто: мы установили высокий порог входа — вероятность того, что новость интересная, должна превышать 90%. Таким образом мы увеличили precision — мы будем уверены в том, что новость, в среднем, будет интересной.

Оформление постов в канале

Мы научились находить интересные новости, но теперь нам нужно правильно их упаковать. Приводить метрики оценивания моделей генерации текста я уж не буду, ограничимся лишь плоскостью: (какая модель, хорошо ли работала).

Заголовки новостей

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

Суммаризация и перефразирование новостей

Мы согласились с форматом не слишком длинных новостей, чтобы не читать много воды (откуда-то в нашем тандеме взялась мысль о том, что основную идею поста можно уместить в 5–7 строчек). Также, чтобы быть «полноценным каналом», мы решили перефразировать приходящие новости, поскольку заметили, что абсолютно все телеграм-каналы воруют друг-у-друга новости, предварительно их перефразировав.

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

Касаемо перефраза: мы вдохновлялись статьей одного автора, он также является создателем многих других нейросетевых моделей. По результатам, отраженным в его статье, оказалось, что наилучшим подходом к перефразу будет использование 2х переводчиков «туда» и «обратно». Так мы и сделали. Мы взяли ровно те же модели (на английский и на русский), что были приведены в его статье.

Поначалу все работало очень хорошо, но данный метод перефразирования не выдержал проверку временем. К нам пришла новость, содержащая ту заветную фамилию «Пасечник». Мы запостили новость с явной, фактической ошибкой.

Проблема оказалась в том, что переводчик от Facebook (Meta) в некоторых случаях не знает, как правильно определять фамилии в тексте. «Леонид Пасечник» в наших постах становился никем иным, как «Леонидом Пчеловодом». Можно привести еще множество подобных забавных примеров. Необычные фамилии или названия мест превращались в совершенно другое слово. Разумеется, в новостном канале такое абсолютно недопустимо.

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

На данный момент мы используем иной подход к решению задачи перефраза.

Разбиение текста на абзацы

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

На помощь пришла статья английского автора. Вот ссылка на ее перевод на русский язык.

Для того, чтобы воплотить в жизнь описанный в статье метод, необходимо использовать эмбеддинг текста. Вспоминая написанное выше, вы могли догадаться, что мы стали использовать эмбеддинги от сбера. Сразу скажу, что метод, описанный в статье, не слишком хорошо работает в нашем случае, поскольку для его использования необходимо иметь большой корпус текста, а мы имеем дело, в основном, с текстом в 3–5 предложений.

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

Взвешенные суммы каждой строки. Красными точками показаны минимумы и максимумы. По оси абсцисс — номер предложения, по оси ординат — взвешенная сумма

Взвешенные суммы каждой строки. Красными точками показаны минимумы и максимумы. По оси абсцисс — номер предложения, по оси ординат — взвешенная сумма

Резюме

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

Еще один пример поста из нашего канала

Еще один пример поста из нашего канала

Если у тебя, читатель, есть какие-то вопросы или замечания, можешь смело писать в комментарии к этой статье. Мы с радостью выслушаем объективную критику и учтем ее в будущем.

А вот и ссылка на наш канал.

До скорых встреч!

© Habrahabr.ru