Как оценить тональность у тысячи комментариев в блоге? Разворачиваем платформу аналитики

У нас есть своя ML-платформа, серверное железо и опыт в развертывании IT-инфраструктуры. Но что, если проанализировать эмоциональный окрас комментариев в блоге? Рассказываем о реализации и что из этого получилось.

Как оценить тональность у тысячи комментариев в блоге? Разворачиваем платформу аналитики

Используйте навигацию, если не хотите читать текст полностью:

Выбираем большую языковую модель

Sentiment-анализ в нашем случае — задача мультиклассовой классификации. Отправляемся на Hugging Face в раздел Models (отсюда с помощью библиотеки Transformers позже будем вытаскивать веса модели). Во вкладке Tasks выбираем «Text Classification», во вкладке Languages — «Russian», а в поле для ввода имени LLM пишем Sentiment. В результате получаем список моделей, которые, судя по названию и описанию, были файнтюнены специально на предварительно размеченных датасетах под определение эмоционального окраса текста.

Здесь нужно сделать оговорку. Если в обучающей выборке модели объект (текст, комментарий и т. д.) отнесен сразу к нескольким тегам, речь будет идти о мультилейбле, а не о мультиклассе (positive, neutral, negative). Это может быть полезно, если нам нужна не просто оценка эмоционального окраса, а выделение комментариев, авторы которых, например, одновременно пытаются оскорблять и запугивать.
<i>Выбираем LLM на Hugging Face.</i>
Выбираем LLM на Hugging Face.

Большинство отсортированных LLM крутятся вокруг разных модификаций bert-моделей. Поверх строится пулинг-слой, а дальше это все сходится к нескольким классам. Мы протестировали разные модели и остановились на cointegrated/rubert-tiny-sentiment-balanced.

Вытаскиваем модель и токенайзер, кладем ее на CUDA Device и пишем небольшой кусочек кода. Ну ладно, на самом деле мы просто берем готовый кусок кода из карточки модели и фиксим в нем одну строчку. 😆

def get_sentiment_fixed(text, return_type='label'): """ Calculate sentiment of a text. `return_type` can be 'label', 'score' or 'proba' """ with torch.no_grad(): inputs = tokenizer(text, return_tensors='pt', truncation=True, padding=True).to(model.device) proba = torch.nn.functional.softmax(model(**inputs).logits, dim=1).cpu().numpy()[0] if return_type == 'label': return model.config.id2label[proba.argmax()] elif return_type == 'score': return proba.dot([-1, 0, 1]) elif return_type == 'all': return {"label": model.config.id2label[proba.argmax()], "score": proba.dot([-1, 0, 1]), "proba": proba.tolist()} return proba
Все, что в дальнейшем будет происходить с GPU, мы отслеживаем через nvitop. Почему nvitop, а не nvidia-smi? Причина проста, как хозяйственное мыло. Можно, конечно, сделать watch -n 0.01 nvidia-smi, но нам милее, когда все раскрашено и само обновляется. Кроме того, nvitop удобно кастомизируется, а с помощью встроенной Python-библиотеки можно сделать свой дашборд через запросы в API.

Оцениваем пригодность модели

Сперва для примера возьмем выдуманный комментарий, чтобы оценить, как модель в принципе справляется со своей задачей. Фразу «Какая гадость эта ваша заливная рыба!» модель однозначно определяют как негативную. Уже неплохо.

text = 'Какая гадость эта ваша заливная рыба!' # classify the text print(get_sentiment(text, 'label')) # negative # score the text on the scale from -1 (very negative) to +1 (very positive) print(get_sentiment(text, 'score')) # -0.5894946306943893 # calculate probabilities of all labels print(get_sentiment(text, 'proba')) # [0.7870447 0.4947824 0.19755007] negative -0.5894944816827774 [0.78704464 0.4947824 0.19755016] {'label': 'negative', 'score': -0.5894944816827774, 'proba': [0.7870446443557739, 0.49478238821029663, 0.19755016267299652]}

Если мы заменим «гадость» на «радость», комментарий будет оценен как положительный.

text = 'Какая радость эта ваша заливная рыба!' # classify the text print(get_sentiment(text, 'label')) # positive # score the text on the scale from -1 (very negative) to +1 (very positive) print(get_sentiment(text, 'score')) # 0.8131021708250046 # calculate probabilities of all labels print(get_sentiment(text, 'proba')) # [0.1313372, 0.2358502, 0.9444394] positive 0.8131021708250046 [0.13133724 0.23585023 0.9444394 ] {'label': 'positive', 'score': 0.8131021708250046, 'proba': [0.1313372403383255, 0.2358502298593521, 0.9444394111633301]}

Далеко не всегда и не во всем с такой моделью можно согласиться. Это видно даже по оценкам, которые она выдает. У нее есть ограничения и в наборе данных, на котором она училась, и в качестве разметки, и в специфике предметной области.

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

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

Автоматизируем сбор данных

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

Есть смысл разбить сбор данных на три последовательных этапа:

  1. начальный сбор данных,
  2. прогон комментариев через LLM и загрузку информации в отдельную базу данных,
  3. настройку пайплайна для новых статей/комментариев.

Разберем каждый этап по порядку.

Начальный сбор данных

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

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

Этот скрипт — исключительно утилитарный элемент, который создавался под конкретную задачу. Его можно сделать аккуратнее. Но все равно смело пишите в комментариях, что он кривой и вообще можно было бы получше постараться, прежде чем показывать на Хабре такое. Эти комментарии мы тоже соберем и оценим. 🙂

Прогон комментариев через LLM

Есть два пути. Выбор конкретного зависит от того, как вам удобнее работать с базой данных и чего вы от нее хотите.

  • Вариант 1. Создать облачную базу данных, управляемую PostgreSQL, и прогнать комментарии через LLM в Data Science Virtual Machine. Этот вариант подходит, если у вас есть какие-то специфические требования к БД.
  • Вариант 2. Запустить Data Analytics Virtual Machine. В образ вшита PostgreSQL, поднятый как Docker-образ, в рамках Superset. Это вариант подойдет, если вы хотите быстро создать прототип и не тратить много времени на базу данных.

Мы пошли по второму пути, поэтому далее опишем именно его.

Настройка пайплайна для новых статей и комментариев

Этот шаг отчасти можно назвать опциональным. Он необходим, если в будущем вы планируете встраивать более качественные модели.

Итак, запускаем DAVM и переходим в Prefect Flow, который:

  1. идет в объектное хранилище Selectel и забирает файл с новыми статьями/комментариями,
  2. прогоняет комментарии через LLM,
  3. загружает результаты в базу данных.

Что еще за DAVM

DAVM, или Data Analytics Virtual Machine, — это не просто виртуалка с Ubuntu, драйверами и библиотеками, а небольшая платформа аналитики данных. В ней собрано несколько инструментов:

  • Jupyter Hub для одновременной работы нескольких пользователей,
  • Keycloak для управления авторизацией,
  • Prefect в качестве оркестратора,
  • Superset для BI-аналитики,
  • предустановленные библиотеки,
  • туториалы и примеры, чтобы было проще разобраться с самой платформой.
<i>Как устроена виртуальная машина DAVM.</i>
Как устроена виртуальная машина DAVM.

Все, что есть в DAVM, можно настраивать и менять по своему усмотрению. А если вдруг что-то перестало работать, платформу можно пересоздать, особенно если все данные хранятся вне ее: например, в облачной базе данных или объектном хранилище.

Под капотом платформы — Ubuntu 22.04 и столько GPU, сколько вы захотите получить из доступных конфигураций. Внутри установлен Docker, в нем в контейнерах запущены все компоненты: Prefect, Superset, Keycloak, Jupyter Hub. Отдельно работает PostgreSQL, на который «нацелен» Superset. То есть можно загрузить данные из любого источника через Prefect в PostgreSQL, а потом вытащить их оттуда в Superset. В качестве надежного хранилища можно использовать не эту же виртуалку (хотя и так тоже можно), а хранилище S3 или DBaaS, в том числе с PostgreSQL.

Для изолированного запуска Jupyter Labs на одной виртуалке можно использовать технологию Multi Instance GPU (MIG). Она позволяет разделить видеокарту на несколько частей (партиций). В DAVM уже встроена команда для запуска нужного процесса на определенной партиции. Но эта команда сработает только в том случае, если GPU настроить соответствующим образом, то есть применить конфигурацию MIG.

Чтобы разобраться во всех тонкостях шеринга GPU, рекомендуем обратиться к статьям нашего DevOps-инженера Антона Алексеева:

Практическое пособие по работе с MIG,

ШерингGPU с помощью MIG и TimeSlicing,

Динамический шеринг GPU в Kubernetes с помощью MIG, MPS и TimeSlicing.

Создаем и запускаем платформу аналитики

Чтобы приступить к работе, открываем панель управления Selectel, переходим в раздел Облачная платформа и создаем новый сервер. Здесь в качестве источника обязательно выбираем Data Analytics VM (Ubuntu 22.04 LTS 64-bit). Подбираем параметры видеокарты, добавляем белый IP и «подкидываем» SSH-ключ.

<i>Конфигурация Data Analytics VM в панели управления.</i>
Конфигурация Data Analytics VM в панели управления.

После этого запускаем саму виртуальную машину. Это не самый тривиальный шаг, поэтому в документации есть простая пошаговая инструкция по запуску DAVM.

<i>Окно авторизации в DAVM.</i>
Окно авторизации в DAVM.

После входа видим все наши приложения, ссылки на туториалы и ноутбуки. Кстати, вся базовая информация о DAVM, схема платформы, инструкции, руководства и описания доступны в Welcome notebook.

<i>Главная страница DAVM.</i>
Главная страница DAVM.
Keycloak позволяет создать несколько пользователей, каждый из которых будет работать в отдельном инстансе Jupyter Lab со своими файлами. Если добавить к этому MIG, они будут использовать еще и разные партиции GPU. Так можно исключить ситуации, когда пользователи каким-либо образом аффектят друг друга в рамках одной платформы.

При работе с DAVM нас будет интересовать в основном раздел Superset. На его главной странице находятся дашборды, чарты и прочее. Дашборды собираются из отдельных чартов, чарты опираются на датасеты, датасеты собираются в основном из SQL-запросов и подключений к базе данных. Именно здесь мы и запускаем обработку данных.

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

<i>Выглядит json вот так: есть ссылка на статью, ее заголовок, комментарии, голоса за комментарии, время появления и оценка тональности.</i>
Выглядит json вот так: есть ссылка на статью, ее заголовок, комментарии, голоса за комментарии, время появления и оценка тональности.

Вот еще один комментарий к той же статье, что на скриншоте выше:

Как оценить тональность у тысячи комментариев в блоге? Разворачиваем платформу аналитики

Здесь же можно найти дополнительную информацию, например охват в хабах. Впрочем, разберем все на отдельном примере.

Формируем чарты и дашборды

Заходим в DAVM и переходим в JupyterHub. Здесь импортируем все пакеты, необходимые для работы, вытаскиваем список файлов json, загружаем нашу языковую модель на GPU и запускаем ее. LLM начинает классифицировать комментарии в блоге. Наблюдать за ее работой, как уже сказано выше, удобнее через nvitop.

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

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

Важный момент: в самом коде мы не держим никакие данные для доступов. Мы предварительно загрузили их Prefect, а теперь можем в коде на них ссылаться.

Следующий шаг — подключаемся к PostgreSQL, создаем базу данных и таблицы, а затем загружаем туда сформированные датасеты. После возвращаемся в Superset и формируем новый датасет на основе базы данных, схемы и таблиц, а затем создаем чарты.

<i>Готовые чарты в разделе Superset.</i>
Готовые чарты в разделе Superset.

Чарты будут собраны во вкладке Charts. Если мы откроем любой из них, то увидим, что у нас две таблицы формируют датасет. Визуализацию чарта можно выбрать из предложенных вариантов: графиков, диаграмм, таблиц и прочих вариантов.

Каждый чарт можно настроить так, чтобы он отображал нужные параметры. Например, показывал зависимость между оценкой комментария (оценкой его тональности языковой моделью) и его рейтингом. Дополнительно можно вывести названия комментируемых статей, ссылки на них, время появления комментариев, распределение по хабам и так далее. Вот наглядные примеры:

<i>Примеры визуализации чартов.</i>
Примеры визуализации чартов.

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

  • Положение пузырька на шкале Х показывает тональность комментария (чем левее, тем больше негатива; чем правее, тем, соответственно, больше позитива).
  • Размер пузырька отражает длину комментария.
  • Положение по оси Y отображает рейтинг комментария.

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

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

Автоматизируем обновление данных

Мы не хотим каждый день ходить и руками складывать данные — метки и оценки комментариев — в наши файлы json. А именно эти данные попадают на S3-бакет, забираются оттуда и прокручиваются через LLM. Благодаря этому обновляются дашборды.

Чтобы автоматизировать процесс, пишем скрипт для Prefect, из секретов забираем данные для входа в S3 и в Kserve. Это замечательный инструмент, который позволяет за два-три часа превратить нашу модель в inference service. Можно и быстрее, но для этого придется реже пить чай и повторять операцию регулярно.

После запуска скрипта в разделе Prefect мы можем наблюдать, как стартовал новый флоуран, пошел в S3, забрал файлы json и начал их отрабатывать. На скриншоте ниже как раз комментарии пропускаются через inference service. На выходе мы имеем, соответственно, response.

Как оценить тональность у тысячи комментариев в блоге? Разворачиваем платформу аналитики

Когда все отработает, флоуран в Prefect будет помечен как completed. Можно задать логику, по которой новые файлы в S3 будут пропускаться через inference service. А если нагрузка на него вырастет, он просто масштабируется.

Немного об эндпоинте

Файлы json из S3 необходимо пропустить через эндпоинт Kserve, который, в свою очередь, тоже забирается из секретов. Сам эндпоинт — это результат работы нескольких файлов.

Первый — скрипт на Python. Здесь используем библиотеку kserve и пишем кастомный inference service, в котором имплементируем методы load и predict. В load забираем команды, которые были в ноутбуках, а в predict вытаскиваем атрибут sequence, пропускаем через токенайзер и получаем из модели нужные данные. Дальше формируем из этого результаты и отдаем их.

import kserve import torch from transformers import AutoModeForSequenceClassification, AutoRikenizer from kserve import ModelServer import logging class KServeBERTSentimentModel(kserve.Model): def __init__(self, name: str): super().__init__(name) KSERVE_LOGGER_NAME = 'kserve' self.logger = logging.get.Logger(KSERVE_LOGGER_NAME) self.name = name self.ready = False def load(self): # Biuld tokenizer and model name = "cointegrated/rubert-tiny-sentiment-balanced" self.tokenizer = AutoTokenizer.from_pretrained(name) self.model = AutoModeForSequenceClassification.from_pretrained(name) if torch.cuda.is_available(): self.model.cuda() self.ready = True def predict(self, request: Dict, headers: Dict) -> Dict: sequence = request["sequence"] self.logger.info(f"sequence:-- {sequence}") input = self.tokenizer(sequence, return_tensors='pt', truncation=True, padding=True).to(self.model.device) # run prediction with torch.no_grad: predictions = self.model(**inputs)[0] scores = torch.nn.Softmax(dim=1)(predictions) results = [{"label": self.model.config.id2label[item.argmax().item()], "score": item.max().item()} for item in scores] self.logger.info(f"results:-- {results}") # return dictionary, which will be json serializable return {"predictions": results}

Второй — Docker-файл, в котором мы ставим все необходимые зависимости: kserve, torch и transformers. Запускаем скрипт и на этом весь наш Docker-файл заканчивается.

# Use the official lightweight Python image. # https://hub.docker.com/_/python FROM python:3.8-slim ENV APP_HOME /app WORKDIR $APP_HOME #Install production dependencies. COPY requiremetns.txt ./ RUN pip install --no-cache-dir -r ./requirements.txt # Copy local code to container image COPY Kserve_BERT_Sentiment_ModelServer.py ./ CMD ["python", "KServe_BERT_Sentiment_ModelServer.py"]

Последний файл можно посмотреть в UI Kserve в самой ML-платформе: Learning – Inference – Kserve. Во вкладке YAML описан inference service: указан Docker-образ, собранный из Docker-файла, имя kserve-контейнера и парочка вещей вроде bert-sentiment.

Во вкладке Logs можно посмотреть, как разворачивается inference service: как отрабатывает метод load, загружаются токенайзеры и веса модели, с какого момента LLM становится готовой для сервинга.

В этом же UI во вкладке Overview можно найти детали по эндпойнту, который нам необходимо использовать. Тут мы можем самостоятельно развернуть свой KServe (или KubeFlow) и работать с ним, но в нашем случае KServe уже разворачивается и настраивается в рамках нашей ML-платформы.

О чем пишут в комментариях

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

Впрочем, с оценкой тональности стоит быть осторожным. Помните, что выбранная LLM не совершенна? В идеальном мире для ее файнтюнинга мы бы сформировали датасеты из комментариев на Хабре. Тогда итог анализа получился бы, вероятно, еще более качественным.

Но даже так интересно взглянуть на самые негативные и самые позитивные комментарии в блоге Selectel по мнению LLM. Максимальную оценку модель поставила комментарию к статье «Как сделать консистентный UX для 40+ продуктов. Уроки, которые я извлекла из перезапуска дизайн-системы». А вот и сам комментарий:

Как оценить тональность у тысячи комментариев в блоге? Разворачиваем платформу аналитики

А вот подборка самого-самого негатива (вы же за этим статью открыли?):

Безусловно, в блоге можно найти и куда более негативные комментарии, если проверить чарты вручную или, как я сказал выше, обучить LLM на более релевантном датасете. Впрочем, отыскать в числе «самых злых» комментариев (score — ниже -0,9) интересные экземпляры совсем не сложно:

Расскажите, работали ли вы с подобными инструментами? Поделитесь своими впечатлениями и предложениями по улучшению анализа в комментариях!
99
22
2 комментария

как всегда самые негативные комментарии не содержат конструктива

1

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

1