🤖 Дедубликация: как OpenAI и FastAPI спасут Habr от дублей
В статье рассказывается о том, как модели OpenAI помогают в задаче дедубликации текстов и similarity search. Рассмотрены различные подходы к решению проблемы: от концепции MinHash до реализации на эмбеддингах современных трансформенных моделей. В статье также описан пример создания микросервиса на FastAPI для поиска дубликатов постов.
OpenAI продолжает наводить суету. Количество пользователей ChatGPT перевалило за 100 миллионов, но хайп даже и не думает стихать – кажется, даже бабушки у подъезда шлют ИИ свои вопросы про повышение пенсии. Но сегодня пойдет речь не о правильных вопросах, а о том, как можно использовать модели OpenAI для задачи дедубликации.
Дедубликация (Deduplication), similarity search являются востребованными доменами машинного обучения. В команде новостного мониторинга Сбера я решал эти задачи для потока новостей. Сейчас я работаю в команде Data Science Самолета (это такой застройщик здорового человека) и оказалось, что здесь точек приложения для NLP даже больше чем в банке, в том числе и для мэтчинга.
В этой статье я постараюсь рассказать о подходах к дедубликации текстов, как в этом помогают модели OpenAI, а также приведу пример боевого микросервиса на FastAPI, который будет находить не очень свежие по содержанию посты на Хабре. Мне импонирует исторический подход, поэтому я сначала расскажу про темное прошлое, и постепенно доберемся до золотого века современных языковых моделей.
Старое, доброе, вечное
Давайте сначала разберемся, как вообще можно оценить, что два текста являются похожими. Ну, самое очевидное – посмотреть на общие слова, чем их больше, тем ближе два текста. Собственно эту идею использует мера Жаккара.
Однако в таком подходе есть свои минусы. Во-первых, он не оптимален с вычислительной точки зрения. Если у вас большой корпус документов, а документы состоят из тысяч слов, то квадратичная сложность может похоронить ваши ресурсы. Во-вторых, отсутствие общих слов еще не означает смыслового отличия.
Обойти первую проблему поможет связка minhash + lsh. Minhash-подход заключается в случайной перестановке слов (на самом деле кусочков слов – шинглов) и хешировании. То есть текст любой длины превращается в набор таких хешей (сигнатура): было 10 тыс. слов, а легким движением руки хеш-функции все превратится в сигнатуру длины 200. Ну а дальше вы уже сравниваете такие сигнатуры. Это пока не избавляет от квадратичной сложности, но зато поможет быстрее просчитывать близость. А вот LSH (local sensitive hashing) как раз позволит искать дубли не по всему корпусу, а по ограниченному подмножеству. В питоне есть уже готовая реализация этого метода в виде библиотеки datasketch. Что касается второй проблемы, то… придется читать дальше.
Эмбеддинг является краеугольным камнем всего NLP, да, прямо как четвертьфунтовый чизбургер в питании. Эмбеддинг – это векторное представление какой-то сущности: слова, предложения, абзаца и даже всего текста. На самом деле векторизовать можно все что угодно. Сигнатура документа из текста выше тоже, по сути, является вектором, но эмбеддингом лучше это не называть, т. к. эмбеддинг это «хороший» вектор, для которого операции (сумма, разность, скалярное произведение) являются осмысленными. Картинка ниже уже набила оскомину, но она хорошо иллюстрирует операции над эмбеддингами. Картинка иллюстрирует классический word2vec из далекого 2013 года.
Текущие векторные представления уже настолько круты, что, например, можно из вектора футболки с короткими рукавами вычесть вектор короткого рукава, потом прибавить вектор длинного рукава и наконец – получить картинку футболки с длинными рукавами.
В word2vec вы получаете вектора для отдельных слов, и они фиксированные. В предложениях «Ключ для замка» и «Эти мерзавцы лишили меня родового замка», слово «замок» будет иметь одно и то же векторное представление.
Для сравнения текстов из сотен и тысяч слов вам придется придумать способ объединения векторов в один. Можно, конечно, просто усреднить, но так можно потерять очень много информации. Другой подход – это складывать отдельные вектора с весами, а веса взять из матрицы TF-IDF.
Когда у вас есть вектора для документов, то найти их похожесть можно с помощью косинусной близости. Когда в 8 классе вы решали примеры на скалярное произведение, то как раз и высчитывали эту метрику. Чем она меньше, тем больше похожесть.
Мощь трансформеров
Вектора word2vec или их более современные аналоги типа fasttext страдают отсутствием контекстуальности. Также эти эмбеддинги существуют только на уровне отдельных слов (токенов), а нам нужен эмбединг всего документа.
Вот тут и приходят на помощь трансформеры. Я не буду рассказывать про их крутость и магию, происходящую под капотом, тем более читатель недоумевает и наверняка ждет, когда же я все-таки начну рассказывать про ChatGPT.
Самое главное, что из энкодера трансформера можно вытащить как отдельные эмбеддинги слов (при чем здесь они будут уже контекстно-зависимыми), так и векторное представление предложения.
На практике очень хорошо себя показал фреймворк SentenceTransformers. С его помощью можно векторизовать ваш документ несколькими строчками кода:
SentenceTransformers отличная штука, но, как и все хорошее, имеет свои изъяны. Главный из них – ограничение на длину последовательности. По умолчанию это 128 токенов. Можно в принципе увеличить это значение вплоть до 512, но этого все равно не хватит, чтобы векторизовать большой текст. Пример ниже показывает, что при этом происходит.
На сцену выходит OpenAI
Наконец-то мы добрались до основного блюда. Команда OpenAI помимо web-интерфейса, также открыла доступ к API своих моделек. В том числе можно задействовать и те, что создают текстовые эмбеддинги. Логика тут такая же, как и в SentenceTransformers, но мы предполагаем, что такие векторайзеры задействует гораздо больший контекст.
Если обратиться к документации, то можно увидеть, что максимальная длина входной последовательности существенно выше, чем у SentenceTransformers.
Пора посмотреть, как они справятся с задачей поиска дублей.
Создадим виртуальное окружение под этот проект
Устанавливаем необходимые библиотеки
Для работы с openai нам понадобится токен авторизации.
Посмотрим, как она справляется с длинными последовательностями.
Для похожих по смыслу новостей, но с разной лексикой и разным размером, модель выдает адекватное значение косинусного расстояния. Что ж, видимо, ребята из Калифорнии все-таки не зря едят свой хлеб со смузи.
По коням
Пора собирать работающий пайплайн. Необходимые ингридиенты:
Корпус уникальных текстов. Чтобы проверить наш пример на уникальность, мы будем сравнивать его с каждым документом из корпуса. Парсить ничего не будем, возьмем готовый датасет с huggingface.
Код для поиска потенциальных дублей.
API-интерфейс для работы с нашим микросервисом.
На вход мы будем подавать список примеров, на выходе оценка уникальности:
Датасет
Скачанный архив весит 3.5 ГБ. Это может оказаться серьезным вызовом для вашей оперативки, кроме того, поиск в большом корпусе будет занимать слишком много времени (сейчас пока пропустим возможные способы оптимизации этого процесса). Сейчас наша задача выкатить MVP (minimal viable product), поэтому ограничимся батчем в 10к постов.
В сете 22 колонки, но нам столько не понадобится. Наиболее важные для нас:
text_markdown – собственно сам текст поста.
lead_markdown – хедер поста (первые несколько предложений).
title – заголовок.
Для поиска дубликатов пока можно ограничиться только text_markdown, в дальнейшем можно будет задействовать и другие поля.
Получение эмбеддингов
Выше я указывал, что модель способна принимать на вход свыше 8k токенов, однако если ваш текст превысит это значение, то API упадет с ошибкой. Перед отправкой будем производить подсчет и обрезать наши тексты.
Эмбеддинги записываем в датасет и сохраняем.
База данных уникальных постов у нас есть. Правда, мы никак не проверили их на уникальность. Пока отправим эту задачу в бэклог.
Поиск дублей
Микросервис крутится
Все детальки микросервиса готовы, осталось соединить их вместе и передать FastAPI. Для удобства создадим пакет utils, в него поместим код для получения векторов и поиска по постам.
В search.py скопируем код для эмбеддингов и поиска, в app.py приложение FastAPI, оно предельно простое.
Done!
Чтобы запустить приложение, запускаем сервер uvicorn.
В случае успеха вы должны увидеть что-то похожее на это:
Крутость FastAPI не только в быстроте, но и в удобстве отладки. Перейдя по адресу http://127.0.0.1:8000/docs, вы должны попасть в swagger. Swagger – это фреймворк, предоставляющий возможность не только просматривать спецификацию RESTful API интерактивно, но и отправлять запросы и проверять ответы.
В список ITEM можно помещать своих кандидатов на дубли. Возьмем, для примера, хедер какого-нибудь поста. По-хорошему, стоит ждать, что модель должна нам вернуть полный текст в качестве дубликата.
К счастью, наши ожидания не оказались нашими проблемами.
Заключение
Дедубликация понятная и на первый взгляд довольно простая проблема. Но решая ее, можно открыть для себя множество крутых и ценных идей, которые будут полезны во множестве других задач. В статье я постарался раскрыть базовые подходы, без существенного погружения в детали, но надеюсь, что кого-то это вдохновит к собственным поискам. Спасибо за внимание!