Каждый месяц в блоге Selectel на Хабре появляется 35-40 публикаций. Сбор статистики по ним мы давно автоматизировали, но до последнего времени не охватывали sentiment-анализ, то есть оценку тональности комментариев средствами машинного обучения.
У нас есть своя ML-платформа, серверное железо и опыт в развертывании IT-инфраструктуры. Вполне логично, что в какой-то момент возник вопрос: что, если проанализировать эмоциональный окрас комментариев в блоге на Хабре с помощью LLM?
Под катом рассказываем, что из этого получилось.
Используйте навигацию, если не хотите читать текст полностью:
→ Выбираем большую языковую модель
→ Оцениваем пригодность модели
→ Автоматизируем сбор данных
→ Что еще за DAVM
→ Создаем и запускаем платформу аналитики
→ Формируем чарты и дашборды
→ Автоматизируем обновление данных
→ О чем пишут в комментариях
Выбираем большую языковую модель
Sentiment-анализ в нашем случае — задача мультиклассовой классификации. Отправляемся на Hugging Face в раздел Models (отсюда с помощью библиотеки Transformers позже будем вытаскивать веса модели). Во вкладке Tasks выбираем «Text Classification», во вкладке Languages — «Russian», а в поле для ввода имени LLM пишем Sentiment. В результате получаем список моделей, которые, судя по названию и описанию, были файнтюнены специально на предварительно размеченных датасетах под определение эмоционального окраса текста.
Здесь нужно сделать оговорку. Если в обучающей выборке модели объект (текст, комментарий и т. д.) отнесен сразу к нескольким тегам, речь будет идти о мультилейбле, а не о мультиклассе (positive, neutral, negative). Это может быть полезно, если нам нужна не просто оценка эмоционального окраса, а выделение комментариев, авторы которых, например, одновременно пытаются оскорблять и запугивать.
Выбираем 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.
Но мы пока возьмем эту модель в качестве базового решения и будем строить с ее помощью аналитику комментариев на Хабре. В конце концов, мы можем взять данные, подставить их в конкретную модель и получить оценку — то, что мы изначально хотели.
Автоматизируем сбор данных
Теперь начинаются «боль и страдания» — базовая автоматизация бесконечно большого количества вещей, которые мы точно не хотим делать руками. В нашем случае речь о парсинге данных. Раскладывать по ячейкам комментарии к каждой статье — это, мягко говоря, не совсем аналитическая задача.
Есть смысл разбить сбор данных на три последовательных этапа:
- начальный сбор данных,
- прогон комментариев через LLM и загрузку информации в отдельную базу данных,
- настройку пайплайна для новых статей/комментариев.
Разберем каждый этап по порядку.
Начальный сбор данных
Когда публикаций и комментариев немного, можно собрать все данные вручную. Но это точно не наш случай. Мы считерим и пройдемся по нашему блогу Selenium-ботом, который соберет информацию о количестве страниц со статьями, ссылки на все статьи и комментарии к каждой из них.
Полный код бота можно посмотреть в репозитории. Если кратко, скрипт содержит Webdriver_manager (чтобы не приходилось вручную указывать версию браузера и его директорию), отвечает за нахождение количества страниц в блоге, их сбор и скролл секций с комментариями.
Этот скрипт — исключительно утилитарный элемент, который создавался под конкретную задачу. Его можно сделать аккуратнее. Но все равно смело пишите в комментариях, что о нем думаете. Эти комментарии мы тоже соберем и оценим. ?
Прогон комментариев через LLM
Есть два пути. Выбор конкретного зависит от того, как вам удобнее работать с базой данных и чего вы от нее хотите.
- Вариант 1. Создать облачную базу данных, управляемую PostgreSQL, и прогнать комментарии через LLM в Data Science Virtual Machine. Этот вариант подходит, если у вас есть какие-то специфические требования к БД.
- Вариант 2. Запустить Data Analytics Virtual Machine. В образ вшита PostgreSQL, поднятая как Docker-образ, в рамках Superset. Это вариант подойдет, если вы хотите быстро создать прототип и не тратить много времени на базу данных.
Мы пошли по второму пути, поэтому далее опишем именно его.
Настройка пайплайна для новых статей и комментариев
Этот шаг отчасти можно назвать опциональным. Он необходим, если в будущем вы планируете встраивать более качественные модели.
Итак, запускаем DAVM и переходим в Prefect Flow, который:
- идет в объектное хранилище Selectel и забирает файл с новыми статьями/комментариями,
- прогоняет комментарии через LLM,
- загружает результаты в базу данных.
Что еще за DAVM
DAVM, или Data Analytics Virtual Machine, — это не просто виртуалка с Ubuntu, драйверами и библиотеками, а небольшая платформа аналитики данных. В ней собрано несколько инструментов:
- Jupyter Hub для одновременной работы нескольких пользователей,
- Keycloak для управления авторизацией,
- Prefect в качестве оркестратора,
- Superset для BI-аналитики,
- предустановленные библиотеки,
- туториалы и примеры, чтобы было проще разобраться с самой платформой.
Как устроена виртуальная машина 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-инженера Антона Алексеева:
Создаем и запускаем платформу аналитики
Чтобы приступить к работе, открываем панель управления Selectel, переходим в раздел Облачная платформа и создаем новый сервер. Здесь в качестве источника обязательно выбираем Data Analytics VM (Ubuntu 22.04 LTS 64-bit). Подбираем параметры видеокарты, добавляем белый IP и «подкидываем» SSH-ключ.
Конфигурация Data Analytics VM в панели управления.
После этого запускаем саму виртуальную машину. Это не самый тривиальный шаг, поэтому в документации есть простая пошаговая инструкция по запуску DAVM.
Окно авторизации в DAVM.
После входа видим все наши приложения, ссылки на туториалы и ноутбуки. Кстати, вся базовая информация о DAVM, схема платформы, инструкции, руководства и описания доступны в Welcome notebook.
Главная страница DAVM.
Keycloak позволяет создать несколько пользователей, каждый из которых будет работать в отдельном инстансе Jupyter Lab со своими файлами. Если добавить к этому MIG, они будут использовать еще и разные партиции GPU. Так можно исключить ситуации, когда пользователи каким-либо образом аффектят друг друга в рамках одной платформы.
При работе с DAVM нас будет интересовать в основном раздел Superset. На его главной странице находятся дашборды, чарты и прочее. Дашборды собираются из отдельных чартов, чарты опираются на датасеты, датасеты собираются в основном из SQL-запросов и подключений к базе данных. Именно здесь мы и запускаем обработку данных.
Сами данные заботливо собраны в файлы json с помощью все того же скрипта, с помощью которого можно собирать комментарии из блога на Хабре. Вручную просмотреть содержимое файлов json можно в разделе JupyterHub.
Выглядит json вот так: есть ссылка на статью, ее заголовок, комментарии, голоса за комментарии, время появления и оценка тональности.
Вот еще один комментарий к той же статье, что на скриншоте выше:
Здесь же можно найти дополнительную информацию, например охват в хабах. Впрочем, разберем все на отдельном примере.
Формируем чарты и дашборды
Заходим в DAVM и переходим в JupyterHub. Здесь импортируем все пакеты, необходимые для работы, вытаскиваем список файлов json, загружаем нашу языковую модель на GPU и запускаем ее. LLM начинает классифицировать комментарии в блоге. Наблюдать за ее работой, как уже сказано выше, удобнее через nvitop.
Когда все комментарии размечены, формируются датасеты. После этого идем в PostgreSQL и запускаем скрипты, чтобы создать базу данных, таблицы со связями и так далее. Отчасти это тоже «путь мук и страданий», потому что здесь нет универсального решения на все случаи жизни в духе «запустил скрипт и все само сделалось».
В любом случае, инициация такого пайплайна вынуждает подготовить базу данных и продумать модель, чтобы и связи между таблицами были, и все атрибуты находились на своих местах. Можно сказать, что это итеративный процесс: начинаешь чуть ли не с плейн-таблицы, а потом потихонечку раскидываешь атрибуты, продумываешь связи, индексы и так далее.
Важный момент: в самом коде мы не держим никакие данные для доступов. Мы предварительно загрузили их Prefect, а теперь можем в коде на них ссылаться.
Следующий шаг — подключаемся к PostgreSQL, создаем базу данных и таблицы, а затем загружаем туда сформированные датасеты. После возвращаемся в Superset и формируем новый датасет на основе базы данных, схемы и таблиц, а затем создаем чарты.
Готовые чарты в разделе Superset.
Чарты будут собраны во вкладке Charts. Если мы откроем любой из них, то увидим, что у нас две таблицы формируют датасет. Визуализацию чарта можно выбрать из предложенных вариантов: графиков, диаграмм, таблиц и прочих вариантов.
Каждый чарт можно настроить так, чтобы он отображал нужные параметры. Например, показывал зависимость между оценкой комментария (оценкой его тональности языковой моделью) и его рейтингом. Дополнительно можно вывести названия комментируемых статей, ссылки на них, время появления комментариев, распределение по хабам и так далее. Вот наглядные примеры:
Примеры визуализации чартов.
Удобство последнего чарта в том, что в нем соотносятся длина комментария, его тональность и рейтинг. Каждый пузырек здесь — отдельный комментарий.
- Положение пузырька на шкале Х показывает тональность комментария (чем левее, тем больше негатива; чем правее, тем, соответственно, больше позитива).
- Размер пузырька отражает длину комментария.
- Положение по оси 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+ продуктов. Уроки, которые я извлекла из перезапуска дизайн-системы». А вот и сам комментарий:
А вот подборка самого-самого негатива (вы же за этим статью открыли?):
«Майские мини-ПК: мощные, дорогие и не очень. Весенние новинки 2023 года».
«Motorola Razr 2019 с гибким экраном: опыт использования и личное мнение о девайсе».
«Intel® Pentium® Pro — 25 лет: ближайший общий предок».
Безусловно, в блоге можно найти и куда более негативные комментарии, если проверить чарты вручную или обучить LLM на более релевантном датасете. Впрочем, отыскать в числе «самых злых» комментариев (score — ниже -0,9) интересные экземпляры совсем не сложно:
«Накопители на магнитной ленте начинают и выигрывают: технология продолжает совершенствоваться десятилетия спустя».
«Raspberry Pi Pico за $4 — на что способна новая плата от разработчиков “малинок”».
«Анализ производительности накопителя Intel Optane SSD 750ГБ».
А теперь — добро пожаловать в комментарии. Эта статья как никакая другая подходит для того, чтобы оторваться по полной. Через некоторое время мы отдельно проанализируем комментарии этой публикации. Авторам самого позитивного и самого негативного по мнению LLM подарим наших плюшевых Тирексов. Возможно, для этого вам придется поделиться с нами своими контактами.
Комментарии (16)
fenrir1121
07.06.2024 09:44+68Огромное спасибо! Отвратительная статья, сплошная вода...
Автор большой молодец, пиши еще!
Учись писать материал так, чтобы его было ИНТЕРЕСНО ЧИТАТЬ, а не пробегать глазами.Ни одного положительного момента в статье. Все очень понравилось, обязательно поделюсь материалом с коллегами!
Galim999
07.06.2024 09:44+6Это самое отвратительное. ужасно отвратительный. полностью отвратительный. ужас. ужас. ужас. ужас. ужас. ужас. ужас. ужас. ужас. кошмар. кошмар. кошмар. кошмар. кошмар.
strokoff
07.06.2024 09:44+37Не рад был прочитать. Спасибо небольшое! Пожалуйста, не пишите ещё. Было очень не интересно читать, обязательно не расскажу своим друзьям про вашу не самую хорошую статью. Эмоции сугубо отрицательные испытать не удалось, автор полный профан и лох мог бы быть, но всё-таки качество материала оставляет желать всего наилучшего. Подписался, чтобы в будущем вероятно оставить свое негативное мнение, на тему крутости автора статьи.
metalidea
07.06.2024 09:44+16А анализ комментариев из статьи про анализ комментариев из статьи про анализ комментариев будет?
TestNickname
07.06.2024 09:44Забавно было читать наезды на nvidia-smi который вполне себе умеет 95% необходимого для мониторинга GPU. Включая автоапдейты.
latitov
07.06.2024 09:44+1Отличная статья! Следующий шаг, после сентимент анализа комментариев - сентимент-анализ статей, перед публикацией :)
Sakhar
07.06.2024 09:44+2Думаю, что высокий скор неверно интерпретировать как "наиболее негативный комментарий". Скоре как то, что модель наиболее уверенна, что он негативный - это все же учили как классификацию, а не как регресссию
Psychosynthesis
07.06.2024 09:44+2Классно, надеюсь теперь в вашем блоге появится что-то кроме бесполезного мусора.
Olegun
Куета!