Это продолжение перевода книги Кит Борн. Раскрытие потенциала данных с помощью генеративного ИИ и технологии RAG. Мы обсудим компонент Извлечение (retrieval) в рамках Генерации, дополненной поиском (RAG). Рассмотрим четыре темы, связанные с поиском сходства: индексация, метрики расстояния, алгоритмы сходства и сервисы векторного поиска. В этой главе вы изучите:
- Метрики расстояния, алгоритмы сходства и векторный поиск
- Векторное пространство
- Лаборатория кода 8.1: Метрики семантического расстояния
- Разные парадигмы поиска: разреженный, плотный, гибридный
- Лаборатория кода 8.2: Гибридный поиск с пользовательской функцией
- Лаборатория кода 8.3: Гибридный поиск с использованием LangChain’s EnsembleRetriever
- Алгоритмы семантического поиска
- Улучшение поиска с помощью методов индексации
- Варианты векторного поиска
Скачать заметку в формате Word или pdf
Предыдущая глава Содержание Следующая глава
Код для этой главы размещен в репозитории GitHub.
К концу этой главы вы получите полное понимание того, как работает поиск сходства, основанный на векторах, и почему он важен для компонента Извлечение в системах RAG.
Метрики расстояния, алгоритмы сходства и векторный поиск
Начнем с различий между метриками расстояния, алгоритмами сходства и векторным поиском. Алгоритм сходства может использовать разные метрики расстояния. Векторный поиск может использовать разные алгоритмы сходства. Эти три понятия различны и играют разные роли, формируя компонент извлечения системы RAG. Понимание этих различий необходимо для правильной реализации и оптимизации решения для извлечения данных. Эти понятия можно представить как иерархию:
Рис. 8.1. Иерархия векторного поиска, алгоритма сходства и метрики расстояния
Здесь показаны только два варианта для каждого уровня: у каждого векторного поиска есть два варианта алгоритмов сходства, а у каждого алгоритма сходства — два варианта метрик расстояния. Однако на практике вариантов гораздо больше на каждом уровне.
Ключевая идея в том, что эти термины часто используются как взаимозаменяемые или рассматриваются вместе, как если бы это было одно и то же, но на самом деле они представляют собой совершенно разные части механизма поиска по сходству. Если их путать, становится значительно сложнее понять общие концепции, лежащие в основе поиска сходства.
Теперь, когда мы это прояснили, давайте обсудим еще одну концепцию, которая поможет лучше понять основы работы поиска сходства, — векторное пространство.
Векторное пространство
Концепция векторного пространства тесно связана с поиском сходства в векторах, так как поиск проводится в рамках векторного пространства, представленном векторами. Технически, векторное пространство — это математическая структура, представляющая собой совокупность векторов в многомерном пространстве. Размерность векторного пространства соответствует количеству признаков или атрибутов, связанных с каждым вектором. В этом пространстве векторы текста, которые наиболее схожи, имеют близкие эмбеддинги и, следовательно, находятся ближе друг к другу.
Часто векторное пространство называют также пространством эмбеддингов или латентным пространством.
Представление векторного пространства может быть полезным для визуализации работы алгоритмов, находящих ближайшие векторы к эмбеддингу пользовательского запроса. Несмотря на то что векторы могут иметь тысячи измерений, для упрощения можно представить их в двумерном пространстве. Границы этого пространства определяются векторами, содержащимися в нем, а маленькие точки представляют каждый из этих векторов.
На рис. 8.2 показаны кластеры точек, которые отражают семантическое сходство между различными данными. Поиск в рамках пользовательского запроса обозначен X. Точки данных, ближайшие к запросу (X), становятся результатами поиска по сходству, организованного ретривером. Мы берем все ближайшие маленькие точки, и превращаем их в результат запроса (зеленые кружки).
Рис. 8.2. Двумерное представление эмбеддингов в векторном пространстве
Запрос (X) дает четыре результата. В 2D-пространстве кажется, что есть маленькие точки данных, которые расположены ближе к запросу (X), чем результаты запроса. Почему так? Вы можете вспомнить, что эти точки изначально находились в 1,536-мерном пространстве. Если представить добавление еще одного измерения (например, высоты), в котором точки «выходят» прямо к вам из этой страницы, то результаты запроса (большие точки) могут на самом деле быть ближе, потому что они находятся значительно выше, чем точки данных (маленькие точки), которые кажутся ближе в проекции на плоскость.
Если смотреть прямо сверху вниз, некоторые точки данных (маленькие точки) могут визуально выглядеть ближе. Однако с математической точки зрения результаты запроса (большие точки) будут действительно ближе, если учитывать все измерения. Если расширить это пространство до всех 1,536 измерений, такая ситуация становится еще более вероятной.
Семантический поиск против поиска по ключевым словам
Как мы уже не раз говорили, векторы фиксируют смысл данных в математическом представлении. Чтобы найти точки данных, схожие по смыслу с пользовательским запросом, мы можем выполнять поиск и извлечение ближайших объектов в векторном пространстве, подобном тому, что мы только что рассмотрели. Это называется семантическим или векторным поиском.
В отличие от поиска по ключевым словам, семантический поиск находит документы с похожим семантическим значением, а не просто с совпадающими словами. Люди могут выражать одну и ту же идею множеством различных способов! Семантический поиск способен уловить этот аспект языка, так как он присваивает схожие математические значения похожим концепциям. В то время как поиск по ключевым словам сосредоточен на точном совпадении слов и часто частично или полностью игнорирует похожие семантические значения.
С технической точки зрения семантический поиск использует значение документов, закодированное в их векторных представлениях. Для любителей математики в этом есть своеобразная красота: математическое решение используется для преодоления языковых вызовов!
Пример семантического поиска
Рассмотрим простой пример семантического сходства, например, отзыв о товаре — одеяле, который один из клиентов оставил онлайн:
Этот плед отлично согревает и поддерживает уютную температуру для меня!
Другой клиент оставил такой отзыв:
С этим покрывалом я чувствую себя намного теплее и уютнее!
Они выражают относительно схожие идеи с точки зрения семантики. Однако при поиске по ключевым словам они будут оценены как менее схожие, чем при семантическом поиске.
Теперь добавим третье предложение для сравнения, представляющее случайный комментарий из интернета:
Тейлор Свифт в 2024 году было 34 года.
Семантика этого случайного комментария сильно отличается от двух предыдущих предложений.
Но не будем полагаться только на слова, давайте проверим это математически! В следующем коде мы рассмотрим несколько наиболее распространенных метрик расстояния, которые используются в качестве основного элемента семантического поиска.
Лаборатория кода 8.1: Метрики семантического расстояния
Практикум посвящен различным способам расчета расстояния между векторами. Мы будем использовать новый блокнот, который содержит отдельный код, не связанный с тем, что мы использовали ранее. В рамках практикума мы
- установим и импортируем необходимые пакеты,
- создадим эмбеддинги для комментариев, приведенных выше,
- рассмотрим три типа формул для расчета метрик расстояния, которые являются наиболее распространенными в NLP, генеративном ИИ и системах RAG.
Для начала установим библиотеку с открытым исходным кодом sentence_transformers, которая будет использоваться для настройки алгоритма эмбеддингов:
1 |
%pip install sentence_transformers -q --user |
sentence_transformers предоставляет способ вычисления плотных векторных представлений для предложений и абзацев. Это позволяет использовать предварительно обученную модель эмбеддингов. Далее мы импортируем популярную библиотеку NumPy, которая предоставляет математические операции, необходимые для анализа расстояний, и только что установленную библиотеку sentence_transformers:
1 2 |
import numpy as np from sentence_transformers import SentenceTransformer |
В следующей строке мы определяем трансформерную модель, которую будем использовать:
1 |
model = SentenceTransformer('paraphrase-MiniLM-L3-v2') |
paraphrase-MiniLM-L6-v2 — одна из младших моделей, доступных в этом пакете. Она совместима с различными компьютерными средами, в которых вы можете запускать этот код. Если вы хотите более мощную модель, попробуйте all-mpnet-base-v2. У нее производительность семантического поиска примерно на 50% выше.
Теперь мы добавим ранее упомянутые комментарии в список, чтобы затем ссылаться на них в коде.
1 2 3 4 5 |
sentence = [ 'This blanket has such a cozy temperature for me!', 'I am so much warmer and snug using this spread!', 'Taylor Swift was 34 years old in 2024.' ] |
Затем мы кодируем предложения, используя модель SentenceTransformer:
1 2 3 |
embedding = model.encode(sentence) print(embedding) embedding.shape |
Функция model.encode принимает список строк и преобразует их в список эмбеддингов. На выходе мы видим математические представления (векторы) наших предложений:
Рис. 8.3. Результат кодирования трех предложений с использованием модели all-mpnet-base-v2
Что означает результат (3, 768)? У нас три вектора, каждый из которых имеет 768 измерений. Теперь мы знаем, что модель all-mpnet-base-v2 создает векторы в 768-мерном пространстве (модель paraphrase-MiniLM-L3-v2 создает векторы в 384-мерном пространстве).
Интересный факт. Вы, возможно, задумываетесь, можно ли использовать библиотеку sentence_transformers для генерации эмбеддингов для вашего векторного хранилища RAG так же, как мы использовали API эмбеддингов OpenAI. Ответ — «да»! Это бесплатная альтернатива API OpenAI, особенно если использовать более мощную модель all-mpnet-base-v2. Вы можете изучить рейтинги MTEB (Massive Text Embedding Benchmark), чтобы получить представление о качестве эмбеддингов, создаваемых этой моделью.
На данный момент модель all-mpnet-base-v2 занимает 94-е место из 303 моделей для задач извлечения данных. Модель ada от OpenAI находится на 65-м месте, а их «лучшая» модель, text-embedding-3-large, занимает 14-е место.
Кроме того, модели sentence_transformers можно дообучить на ваших данных, что может сделать их более эффективными для вашей системы RAG, чем любые платные API-сервисы эмбеддингов. И наконец, использование любой API-службы означает зависимость от ее доступности. Локальное использование модели sentence_transformers делает ее всегда доступной и на 100% надежной. Изучите MTEB, чтобы найти еще более подходящие модели, которые можно загрузить и использовать аналогичным образом.
Теперь у нас есть среда для изучения методов измерения расстояний. Существует множество способов расчета расстояния между векторами. Наиболее распространенные метрики расстояния в NLP — это евклидово расстояние (L2), скалярное произведение и косинусное расстояние. Начнем с евклидова расстояния (L2).
Евклидово расстояние (L2)
Евклидово расстояние измеряет кратчайшее расстояние между двумя векторами. Важно помнить, что мы ищем объекты, которые ближе друг к другу, поэтому меньшее значение указывает на большую схожесть. Мы определяем функцию euclidean_distance:
1 2 |
def euclidean_distance(vec1, vec2): return np.linalg.norm(vec1 - vec2) |
Внутри euclidean_distance сначала выполняется поэлементное вычитание между двумя векторами, а затем с помощью функции linalg.norm() из библиотеки NumPy вычисляется Евклидова норма (также известная как L2-норма) для вектора. Эта функция берет квадратный корень из суммы квадратов элементов вектора (в нашем примере 768 элементов).
Мы вызываем эту функцию для каждого из эмбеддингов.
1 2 3 4 5 6 |
print("Euclidean Distance: Review 1 vs Review 2:", euclidean_distance(embedding[0], embedding[1])) print("Euclidean Distance: Review 1 vs Random Comment:", euclidean_distance(embedding[0], embedding[2])) print("Euclidean Distance: Review 2 vs Random Comment:", euclidean_distance(embedding[1], embedding[2])) |
Рис. 8.4. Результат вычисления Евклидова расстояния
Для Отзыва 1 и Отзыва 2 Евклидово расстояние составляет 0.9576993. Оба отзыва находятся значительно дальше от Случайного комментария. Это показывает, как математика используется для определения семантической схожести или различия текстов.
Но, как и в большинстве случаев в науке о данных, у нас есть несколько вариантов для расчета этих расстояний. Рассмотрим другой подход — скалярное произведение.
Скалярное произведение (также называемое внутренним произведением)
Скалярное произведение технически не является метрикой расстояния, так как измеряет величину проекции одного вектора на другой, что указывает на схожесть, а не на расстояние. Тем не менее, оно используется для целей, аналогичных другим метрикам, упомянутым здесь.
Поскольку речь идет о величине, а не о близости, более высокое положительное значение скалярного произведения указывает на большую схожесть. И наоборот, по мере уменьшения значения, вплоть до отрицательного, схожесть убывает.
Рассчитаем скалярное произведение для каждой пары комментариев:
1 2 3 |
print("Dot Product: Review 1 vs Review 2:", np.dot(embedding[0], embedding[1])) print("Dot Product: Review 1 vs Random Comment:", np.dot(embedding[0], embedding[2])) print("Dot Product: Review 2 vs Random Comment:", np.dot(embedding[1], embedding[2])) |
Рис. 8.5. Скалярные произведения
Мы использовали функцию из библиотеки NumPy. Положительная величина скалярного произведения указывает на относительно высокую степень схожести между Отзывом 1 и Отзывом 2. Когда мы сравниваем Отзыв 1 или Отзыв 2 с Случайным комментарием, результат отрицательный. Это указывают на несовпадение между двумя векторами. Эти результаты показывают, что Отзыв 1 и Отзыв 2 более похожи друг на друга, чем каждый из них на Случайный комментарий.
Косинусное расстояние
Косинусное расстояние измеряет разницу в направленности между векторами. Поскольку это еще одна метрика расстояния, более низкое значение указывает на схожесть векторов. Сначала мы создаем функцию для вычисления косинусного расстояния между двумя векторами:
1 2 3 |
def cosine_distance(vec1,vec2): cosine = 1 - abs((np.dot(vec1, vec2)/(np.linalg.norm(vec1)*np.linalg.norm(vec2)))) return cosine |
Обратите внимание, что формула для косинусного расстояния включает элементы из обеих ранее рассмотренных метрик. Сначала используется np.dot(vec1, vec2) для вычисления скалярного произведения между двумя векторами. Затем результат делится на произведение величин векторов, где для вычисления Евклидовой нормы используется та же функция NumPy, что и для Евклидова расстояния.
Однако в данном случае мы вычисляем Евклидову норму для каждого из векторов (а не разницу между ними, как в случае Евклидова расстояния), а затем перемножаем эти нормы. В итоге мы получаем косинусное сходство, которое затем вычитается в виде абсолютного значения из 1, чтобы получить косинусное расстояние.
Теперь вызываем эту функцию для всех пар комментариев:
1 2 3 4 5 6 |
print("Cosine Distance: Review 1 vs Review 2:", cosine_distance(embedding[0], embedding[1])) print("Cosine Distance: Review 1 vs Random Comment:", cosine_distance(embedding[0], embedding[2])) print("Cosine Distance: Review 2 vs Random Comment:", cosine_distance(embedding[1], embedding[2])) |
Рис. 8.6. Косинусное расстояние
Как и в случае с Евклидовым расстоянием, более низкое значение косинусного расстояния означает, что объекты ближе друг к другу, то есть более схожи. В данном случае расстояние между двумя отзывами указывает на большую близость и схожесть семантики по сравнению с каждым из отзывов и случайным комментарием. Однако значение 0.45958… говорит скорее об умеренной схожести между Отзывом 1 и Отзывом 2. Два других значения указывают на значительную разницу между векторами. Извините, Тейлор Свифт, но с математической точки зрения у нас есть достаточно доказательств, что вы не являетесь семантическим эквивалентом теплого пледа!
Существует множество других метрик расстояния и показателей сходства, которые можно использовать для текстовых эмбеддингов, включая сходство Лина, Жаккара, расстояние Хэмминга, Левенштейна и Манхэттенское. Рассмотренные три метрики используются в NLP наиболее часто.
Разные парадигмы поиска: разреженный, плотный, гибридный
Существует несколько типов векторов, и это различие важно, потому что для разных типов векторов требуются разные методы поиска. Рассмотрим различия между типами векторов.
Плотный поиск
Плотный поиск (dense search) использует векторное представление данных для выполнения поиска. Как мы уже обсуждали, этот тип поиска позволяет находить и возвращать семантически схожие объекты, основываясь на значении данных. В теории это звучит отлично, но… есть ограничения. Если используемая модель была обучена на совершенно другой области, точность запросов будет низкой. Результаты сильно зависят от данных, на которых модель обучалась.
Поиск ссылок на конкретные объекты (например, серийные номера, коды, идентификаторы или имена людей) также даст плохие результаты, поскольку в таких данных мало смысла, который можно отразить в эмбеддингах. Для таких задач лучше подходит поиск по строкам или ключевым словам. Этот подход называется ключевым или разреженным поиском.
Разреженный поиск
Разреженный поиск (sparse search) позволяет использовать сопоставление по ключевым словам во всем контенте. Его называют разреженным, потому что текст преобразуется в векторы, где подсчитывается, сколько раз каждое уникальное слово встречается в запросе и предложениях. Такие векторы в основном состоят из нулей, так как вероятность того, что любое предложение содержит все слова из словаря, крайне мала.
Примером может быть метод мешка слов (bag-of-words), где подсчитывается частота появления слов в запросе и векторе данных, а затем возвращаются объекты с наивысшей частотой совпадений. Хорошим примером алгоритма на основе ключевых слов является BM25. Эта популярная модель хорошо справляется с поиском по многим ключевым словам. BM25 подсчитывает слова в фразе и снижает вес часто встречающихся слов, в то время как редкие слова получают более высокий рейтинг. Это концепция, основанная на TF-IDF, который мы рассматривали в предыдущей главе.
Гибридный поиск
Гибридный поиск (hybrid search) позволяет использовать лучшие стороны плотного и разреженного поиска, объединяя их результаты. При гибридном поиске выполняются как векторный/плотный поиск, так и ключевой/разреженный, а затем их результаты комбинируются. Комбинация может осуществляться на основе системы оценивания, которая измеряет, насколько хорошо каждый объект соответствует запросу с учетом обоих методов поиска. Лучший способ продемонстрировать, как работает этот подход, — пройти через практикум. В следующем разделе мы познакомимся с BM25 для выполнения ключевого/разреженного поиска, а затем объединим его с существующим ретривером, чтобы создать гибридный поиск.
Лаборатория кода 8.2: Гибридный поиск с пользовательской функцией
В этом практикуме мы начинаем с блокнота из главы 5. Обратите внимание, что код из глав 6 и 7 здесь не используется, поскольку он содержит много лишнего. В этом практикуме есть дополнительный бонус: мы познакомимся с новыми элементами, которые будут использоваться в нескольких следующих главах:
- новый загрузчик документов для работы с PDF (вместо веб-страниц),
- новый, более крупный документ с большим объемом данных для поиска,
- новый текстовый сплиттер.
Сначала мы удалим код, который больше не понадобится. Далее мы сосредоточимся на векторизаторе BM25 для генерации разреженных векторов и их комбинировании с плотными векторами для создания гибридного подхода к поиску. Для плотных векторов мы будем использовать наш предыдущий векторизатор. Для разреженных векторов мы используем BM25. Затем выполним поиск по обоим наборам векторов, ранжируем результаты и предоставим гибридный итог. Код финального файла.
BM25 используется уже несколько десятилетий, но это по-прежнему эффективный алгоритм мешка слов (bag-of-words), основанный на TF-IDF. К тому же он очень быстрый. Как происходит ранжирование результатов от двух разных механизмов поиска? Плотный поиск по векторам использует косинусное сходство. Разреженный поиск основан на TF-IDF. Эти оценки не сопоставимы. Тем не менее, существуют алгоритмы, которые ранжируют ответы двух этих ретриверов. Мы будем использовать алгоритм Reciprocal Rank Fusion (RRF).
Цель этого практикума — создать функцию, которая имитирует ранжирование RRF, чтобы вы могли пройти через вычисления и понять их.
Поскольку мы заменяем обработку веб-страниц на парсинг PDF-документов, нам больше не нужен пакет, ориентированный на обработку веб-страниц. Начнем с удаления этой строчки кода:
1 |
%pip install beautifulsoup4 |
Для обработки PDF-файлов нам потребуется установить новый пакет, а также пакет, который позволит использовать модель BM25 с LangChain для генерации разреженных эмбеддингов:
1 2 3 |
%pip install PyPDF2 -q –user %pip install rank_bm25 |
После установки этих пакетов обязательно перезапустите ядро ноутбука! Далее удалите этот код из импортов:
1 2 |
from langchain_community.document_loaders import WebBaseLoader import bs4 from langchain_experimental.text_splitter import SemanticChunker |
Код для обработки веб-страниц нам больше не нужен. Мы также удаляем старый сплиттер текста, заменив его новым. Добавьте этот код в импорты:
1 2 3 4 |
from PyPDF2 import PdfReader from langchain.text_splitter import RecursiveCharacterTextSplitter from langchain.docstore.document import Document from langchain_community.retrievers import BM25Retriever |
PdfReader будет извлекать текст из PDF-документов. Сплиттер RecursiveCharacterTextSplitter заменит SemanticChunker. Document – новый класс для управления и обработки документов при работе с LangChain. BM25Retriever будет действовать как ретривер LangChain.
Теперь удалим код для парсинга веб-страниц:
1 2 3 4 5 6 7 8 9 10 |
loader = WebBaseLoader( web_paths=("https://kbourne.github.io/chapter1.html",), bs_kwargs=dict( parse_only=bs4.SoupStrainer( class_=("post-content", "post-title", "post-header") ) ), ) docs = loader.load() |
В конец ячейки, где определяются переменные для OpenAI, добавьте новый блок:
1 2 3 4 5 6 7 8 9 10 11 12 |
# variables _ = load_dotenv(dotenv_path='env.txt') os.environ['OPENAI_API_KEY'] = os.getenv('OPENAI_API_KEY') openai.api_key = os.environ['OPENAI_API_KEY'] embedding_function = OpenAIEmbeddings() llm = ChatOpenAI(model_name="gpt-4o-mini", temperature=0) user_query = "What are Google's environmental initiatives?" # new pdf_path = "google-2023-environmental-report.pdf" collection_name = "google_environmental_report" str_output_parser = StrOutputParser() |
Добавьте код для обработки PDF-документов:
1 2 3 4 5 |
# Load the PDF and extract text pdf_reader = PdfReader(pdf_path) text = "" for page in pdf_reader.pages: text += page.extract_text() |
Мы подробнее поговорим о загрузке документов в LangChain в главе 11, но здесь мы хотим познакомить вас с альтернативой загрузке веб-страниц. Обратите внимание, что файл google-2023-environmental-report.pdf должен быть доступен в той же папке, что и ваш ноутбук. Наш код извлекает текст из файла и объединяет его так, чтобы не терялось содержимое между страницами.
На данном этапе у нас есть очень большая строка, представляющая весь текст из PDF. Теперь нужно использовать сплиттер для разделения текста на блоки (чанки). Мы заменили SemanticChunker на RecursiveCharacterTextSplitter. Подробнее о сплиттерах LangChain мы поговорим в главе 11.
Сначала удалите старый сплиттер…
1 2 |
text_splitter = SemanticChunker(OpenAIEmbeddings()) splits = text_splitter.split_documents(docs) |
… а затем добавьте новый…
1 2 3 4 5 6 7 |
# Split character_splitter = RecursiveCharacterTextSplitter( separators=["\n\n", "\n", ". ", " ", ""], chunk_size=1000, chunk_overlap=200 ) splits = character_splitter.split_text(text) |
RecursiveCharacterTextSplitter — это часто используемый сплиттер, который также избавляет от затрат на использование API эмбеддингов OpenAI, связанных с SemanticChunker. Для нашего большого PDF-документа сплиттер создаст больше блоков для анализа в векторных пространствах и картах извлечения.
С новыми данными и новым сплиттером нам также нужно обновить код, связанный с ретривером. Начнем с подготовки документов:
1 2 3 4 |
documents = [ Document(page_content=text, metadata={"id": str(i)}) for i, text in enumerate(splits) ] |
Далее необходимо заменить код ретривера…
1 2 3 |
vectorstore = Chroma.from_documents( documents=splits,embedding=OpenAIEmbeddings()) retriever = vectorstore.as_retriever() |
… на…
1 2 3 4 5 6 7 8 9 10 11 |
chroma_client = chromadb.Client() vectorstore = Chroma.from_documents( documents=documents, embedding=embedding_function, collection_name=collection_name, client=chroma_client, ) # Create dense retriever dense_retriever = vectorstore.as_retriever(search_kwargs={"k": 10}) # Create sparse retriever sparse_retriever = BM25Retriever.from_documents(documents, k=10) |
Выполнение кода займет некоторое время. В нем мы настраиваем векторное хранилище Chroma DB управления документами, полученными из PDF, с добавлением метаданных ID. Исходный ретривер теперь называется dense_retriever, что точнее описывает его работу, так как он взаимодействует с плотными эмбеддингами. Новый ретривер, sparse_retriever основан на BM25, и он доступен в LangChain. В обоих случаях мы задаем параметр k = 10, чтобы получать 10 результатов. Объект vectorstore использует параметр collection_name, которую мы определили в секции #variables.
Обратите внимание, что мы не сохраняем разреженные эмбеддинги в Chroma DB векторном хранилище, как это делаем с плотными эмбеддингами. Вместо этого мы загружаем документы напрямую в ретривер, который хранит их в памяти во время выполнения кода. В более сложном приложении, скорее всего, потребуется обработка с сохранением эмбеддингов в постоянное векторное хранилище для последующего извлечения. Даже наша Chroma DB в этом коде является эфемерной, то есть ее данные будут утеряны при завершении работы ядра ноутбука.
Чтобы улучшить эту ситуацию, можно использовать метод vectorstore.persist(), который сохранит базу данных Chroma DB локально в виде файла SQLite. Эти техники подходят для более продвинутых приложений и не требуются в рамках этого практикума, но изучите их, если хотите создать более надежное векторное хранилище для своего конвейера RAG!
Вскоре мы представим функцию, которая выполняет гибридный поиск, чтобы вы могли пошагово разобраться, как это работает. Но для начала обсудим общий подход. Эта функция — попытка быстро воспроизвести алгоритм ранжирования, который использует LangChain в своем механизме гибридного поиска. Идея заключается в том, чтобы вы могли понять, что происходит «под капотом» при выполнении гибридного поиска с LangChain.
LangChain предоставляет механизм, выполняющий весь процесс гибридного поиска одной строкой кода! Это EnsembleRetriever, который выполняет гибридный поиск таким же образом, как и наша функция, но использует более сложный алгоритм RRF. Этот алгоритм берет на себя задачу определения порядка результатов, аналогично тому, как это делает наша функция.
В следующей функции мы рассмотрим каждый шаг и то, как он соотносится с алгоритмом RRF. Это самая крупная функция, с которой мы работали до сих пор, но затраченные усилия стоят того! Учтите, что это одна функция, которую можно увидеть целиком в коде. Давайте начнем с определения функции:
1 |
def hybrid_search(query, k=10, dense_weight=0.5, sparse_weight=0.5): |
Сначала мы будем отдельно учитывать веса для плотных и разреженных результатов. Это соответствует параметрам весов в EnsembleRetriever из LangChain, которые мы рассмотрим чуть позже. Такая настройка функции позволяет ей действовать точно так же, как этот тип ретривера. Кроме того, у нас есть значение k, которое указывает общее количество результатов, которые функция должна вернуть. По умолчанию k соответствует количеству результатов, задаваемых ретриверами при их инициализации в коде.
Первым шагом функции является извлечение top-k документов из обоих типов ретриверов.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
# Step 1: Retrieve the top-k documents from both dense search and sparse search. dense_docs = dense_retriever.get_relevant_documents(query)[:k] dense_doc_ids = [doc.metadata['id'] for doc in dense_docs] print("\nCompare IDs:") print("dense IDs: ", dense_doc_ids) sparse_docs = sparse_retriever.get_relevant_documents(query)[:k] sparse_doc_ids = [doc.metadata['id'] for doc in sparse_docs] print("sparse IDs: ", sparse_doc_ids) # Combine the document IDs and remove duplicates all_doc_ids = list(set(dense_doc_ids + sparse_doc_ids)) # Create dictionaries to store the reciprocal ranks dense_reciprocal_ranks = {doc_id: 0.0 for doc_id in all_doc_ids} sparse_reciprocal_ranks = {doc_id: 0.0 for doc_id in all_doc_ids} |
Мы начинаем процесс извлечения, получая top-k документов как из плотного поиска, так и из разреженного поиска. Точно так же, как в алгоритме RRF, мы сначала извлекаем лучшие документы из обоих типов поиска, основываясь на их механизмах оценки. Мы назначаем уникальные ID содержимому, чтобы сравнивать результаты между ретриверами, удалять дубликаты (путем преобразования в множество, которое автоматически исключает повторы), и создаем два словаря для хранения обратных рангов каждого документа.
Далее вычислим обратный ранг для каждого документа:
1 2 3 4 5 6 |
# Step 2: Calculate the reciprocal rank for each document in dense and sparse search results. for i, doc_id in enumerate(dense_doc_ids): dense_reciprocal_ranks[doc_id] = 1.0 / (i + 1) for i, doc_id in enumerate(sparse_doc_ids): sparse_reciprocal_ranks[doc_id] = 1.0 / (i + 1) |
Этот код вычисляет обратный ранг для каждого документа в результатах плотного и разреженного поиска и сохраняет его в созданных словарях. Для каждого документа рассчитывается его обратный ранг в каждом упорядоченном списке. Обратный ранг — это обратная величина позиции документа в списке (например, 1/rank). Он вычисляется как 1.0, деленное на позицию документа в соответствующих результатах поиска (начиная с 1). Обратите внимание, что при этом не используются оценки схожести. Как мы уже обсуждали ранее, наш семантический поиск ранжирует результаты на основе расстояния, а BM25 — на основе релевантности.
Алгоритм RRF не требует использования этих оценок, что упрощает объединение результатов из разных механизмов ранжирования. Нам не нужно нормализовать оценки из разных методов извлечения, чтобы они находились в одной шкале или были прямо сопоставимы. RRF использует позиции ранжирования, что упрощает комбинирование результатов.
Важно учитывать влияние этого подхода на поиск. Может возникнуть ситуация, когда с семантической точки зрения вы получите очень близкое значение (по расстоянию), но самый высокоранжированный результат из поиска по ключевым словам окажется не таким схожим. Использование RRF с равными весами приведет к тому, что эти результаты будут иметь одинаковые ранги и, следовательно, равное значение с точки зрения ранжирования, даже если вам хотелось бы, чтобы семантический результат имел больший вес.
Вы можете настроить это с помощью параметров dense_weight и sparse_weight, но что, если ситуация обратная? Это недостаток использования RRF и гибридного поиска в целом, поэтому важно тестировать и проверять, подходит ли это решение для ваших нужд.
Теперь мы суммируем обратные ранги каждого документа по упорядоченным спискам из плотного и разреженного поиска:
1 2 3 4 5 6 |
# Step 3: Sum the reciprocal ranks for each document. combined_reciprocal_ranks = {doc_id: 0.0 for doc_id in all_doc_ids} for doc_id in all_doc_ids: combined_reciprocal_ranks[doc_id] = dense_weight * dense_reciprocal_ranks[doc_id] + sparse_weight * sparse_reciprocal_ranks[doc_id] |
Метод RRF основан на идее, что документы, которые занимают высокие позиции в обоих методах извлечения, с большей вероятностью релевантны запросу. Используя обратные ранги, RRF придает больший вес документам, которые находятся на вершине ранжированных списков. Обратите внимание, что при вычислении сумм мы используем веса, указанные в параметрах. Это означает, что именно здесь можно сделать одну из категорий эмбеддингов (плотные или разреженные) более влиятельной в результатах поиска.
Следующая строка кода сортирует ID документов на основе их суммарных оценок обратного ранга в порядке убывания:
1 2 3 |
# Step 4: Sort the documents based on their combined reciprocal rank scores. sorted_doc_ids = sorted(all_doc_ids, key=lambda doc_id: combined_reciprocal_ranks[doc_id], reverse=True) |
Порядок убывания задается параметром reverse=True. Используется функция sorted() с ключем, который извлекает суммарный обратный ранг для каждого ID документа.
На следующем этапе мы итерируемся по отсортированным ID документов и извлекаем соответствующие документы из результатов плотного и разреженного поиска:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
# Step 5: Retrieve the documents based on the sorted document IDs. sorted_docs = [] all_docs = dense_docs + sparse_docs for doc_id in sorted_doc_ids: matching_docs = [doc for doc in all_docs if doc.metadata['id'] == doc_id] if matching_docs: doc = matching_docs[0] doc.metadata['score'] = combined_reciprocal_ranks[doc_id] doc.metadata['rank'] = sorted_doc_ids.index(doc_id) + 1 if len(matching_docs) > 1: doc.metadata['retriever'] = 'both' elif doc in dense_docs: doc.metadata['retriever'] = 'dense' else: doc.metadata['retriever'] = 'sparse' sorted_docs.append(doc) |
Мы используем это, чтобы указать источник ретривера, что позволяет лучше понять, как каждый из ретриверов влияет на результаты. Извлекаем документы на основе отсортированных ID документов. Полученный ранжированный список представляет собой результаты гибридного поиска, где документы, которые занимают высокие позиции как в плотном, так и в разреженном поиске, получают более высокие комбинированные оценки.
В завершение мы возвращаем результаты:
1 2 |
# Step 6: Return the final ranked and sorted list, truncated by the top-k parameter return sorted_docs[:k] |
Обратите внимание, что параметр k использовался для обоих ретриверов, что дало нам в два раза больше результатов, чем мы запрашиваем здесь. Этот шаг сокращает результаты до top-k, возвращая только верхние значения. На практике это означает, что если результат находится в нижней части списка ретриверов, например, на 8-м месте, но он присутствует в обоих результатах, это, скорее всего, повысит его позицию в итоговом top-k.
Далее необходимо учесть этот новый механизм ретривера в нашей цепочке LangChain. Обновите функцию rag_chain_with_source chain:
1 2 3 |
rag_chain_with_source = RunnableParallel( {"context": hybrid_search, "question": RunnablePassthrough()} ).assign(answer=rag_chain_from_docs) |
Это завершает изменения кода для использования гибридного поиска в RAG-конвейере. Дополнительное преимущество написания этой функции вручную заключается в том, что она позволяет вывести на экран данные, которые обычно недоступны при использовании EnsembleRetriever из LangChain.
Давайте воспользуемся этим и заменим ячейку, где мы вызываем RAG-конвейер. Вместо использования финального кода из предыдущих практикумов при обработке конвейера RAG используйте следующий код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
# User Query result = rag_chain_with_source.invoke(user_query) relevance_score = result['answer']['relevance_score'] final_answer = result['answer']['final_answer'] retrieved_docs = result['context'] print(f"\nOriginal Question: {user_query}\n") print(f"Relevance Score: {relevance_score}\n") print(f"Final Answer:\n{final_answer}\n\n") print("Retrieved Documents:") for i, doc in enumerate(retrieved_docs, start=1): doc_id = doc.metadata['id'] doc_score = doc.metadata.get('score', 'N/A') doc_rank = doc.metadata.get('rank', 'N/A') doc_retriever = doc.metadata.get('retriever', 'N/A') print(f"Document {i}: Document ID: {doc_id} Score: {doc_score} Rank: {doc_rank} Retriever: {doc_retriever}\n") print(f"Content:\n{doc.page_content}\n") |
Этот код включает элементы, которые мы использовали в предыдущих главах, такие как оценка релевантности, применяемая в нашем сценарии безопасности. Мы добавили вывод каждого результата ретривера и собранную по ним метаинформацию.
Пример вывода с первыми несколькими результатами:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Compare IDs: dense IDs: ['451', '12', '311', '344', '13', '115', '67', '346', '66', '262'] sparse IDs: ['150', '309', '298', '311', '328', '415', '139', '432', '91', '22'] Original Question: What are Google's environmental initiatives? Relevance Score: 5 Final Answer: Google's environmental initiatives include partnering with suppliers to reduce energy consumption and GHG emissions, engaging with suppliers to report and manage emissions, empowering individuals to take action through sustainability features in products, working together with partners and customers to reduce carbon emissions, operating sustainably at their campuses, focusing on net-zero carbon energy, water stewardship, circular economy practices, and supporting various environmental projects and initiatives such as the iMasons Climate Accord, ReFED, and The Nature Conservancy. They also work on sustainable consumption of public goods and engage with coalitions and sustainability initiatives to promote environmental sustainability. Retrieved Documents: Document 1: Document ID: 150 Score: 0.5 Rank: 1 Retriever: sparse Content: sustainability, and we're partnering with them... Document 2: Document ID: 451 Score: 0.5 Rank: 2 Retriever: dense Content: Empowering individuals: A parking lot full of electric vehicles lined up outside a Google office. Document 3: Document ID: 311 Score: 0.29166666666666663 Rank: 3 Retriever: both Content: In 2022, we audited a subset of our suppliers to verify compliance for the following environmental. |
Во время извлечения документов мы выводим на экран их ID, чтобы увидеть, сколько из них пересекаются между ретриверами. Затем для каждого результата мы выводим: ID документа, оценку ранжирования, ранг, источник ретривера (включая оба ретривера, если результат был найден в обоих). Обратите внимание, что вывод был обрезан, чтобы показать только первые 3 результата из 10, так как оригинальный вывод был значительно длиннее. Если вы выполните код в ноутбуке, вы сможете увидеть полный результат.
Если просмотреть 10 результатов, источники ретриверов распределились следующим образом: разреженный, плотный, оба, разреженный, плотный, разреженный, плотный, плотный, разреженный, разреженный. Это относительно равномерное распределение между различными механизмами поиска, включая один результат, который был найден обоими ретриверами, что продвинуло его выше в ранжировании. Оценки ранжирования составили: 0.50, 0.50, 0.29, 0.25, 0.25, 0.17, 0.12, 0.10, 0.10, 0.08.
Финальный ответ такой же, как и при использовании только плотного эмбеддинга:
Google’s environmental initiatives include empowering individuals to take action, working together with partners and customers, operating sustainably, achieving net-zero carbon emissions, focusing on water stewardship, and promoting a circular economy. They have reached a goal to help 1 billion people make more sustainable choices through their products and aim to collectively reduce 1 gigaton of carbon equivalent emissions annually by 2030. Google also audits suppliers for compliance with environmental criteria and is involved in public policy and advocacy efforts. Additionally, Google is a founding member of the iMasons Climate Accord, provided funding for the ReFED Catalytic Grant Fund to address food waste, and supported projects with The Nature Conservancy to promote reforestation and stop deforestation.
На данном этапе оценка того, какой вариант лучше, носит субъективный характер. Однако в главе 9 мы рассмотрим более объективный подход к оценке RAG. Пока давайте просто обратим внимание на несколько моментов. Версия гибридного поиска, похоже, охватывает более широкий спектр:
Пример гибридного поиска:
Google’s environmental initiatives include partnering with suppliers to reduce energy consumption and GHG emissions, engaging with suppliers to report and manage emissions, empowering individuals to take action through sustainability features in products, working together with partners and customers to reduce carbon emissions, operating sustainably at their campuses, focusing on net-zero carbon energy, water stewardship, circular economy practices, and supporting various environmental projects and initiatives such as the iMasons Climate Accord, ReFED, and The Nature Conservancy.
Пример плотного поиска:
Google’s environmental initiatives include empowering individuals to take action, working together with partners and customers, operating sustainably, achieving net-zero carbon emissions, focusing on water stewardship, and promoting a circular economy.
Можно сказать, что подход плотного поиска фокусируется на более точных деталях, но хорошо это или плохо — вопрос субъективный. Например, цель о достижении одного миллиарда людей не упоминается в гибридном поиске, но она присутствует в плотном поиске:
They have reached a goal to help 1 billion people make more sustainable choices through their products and aim to collectively reduce 1 gigaton of carbon equivalent emissions annually by 2030.
Гибридный поиск предложил более общий подход, отметив следующее:
They also work on sustainable consumption of public goods and engage with coalitions and sustainability initiatives to promote environmental sustainability.
Вы можете запустить этот код с другими вопросами и сравнить, как они обрабатываются различными подходами к поиску.
Мы проделали большую работу, чтобы настроить эту функцию, но теперь посмотрим, что предлагает LangChain, и полностью заменим нашу функцию их инструментами.
Лаборатория кода 8.3: Гибридный поиск с использованием LangChain’s EnsembleRetriever
Мы продолжаем работу с кодом из предыдущего практикума. Полный код для текущего практикума.
Начнем с импорта ретривера из LangChain:
from langchain.retrievers import EnsembleRetriever
Этот импорт добавляет EnsembleRetriever из LangChain, который используется как третий ретривер для объединения двух других ретриверов. Ранее в Лаборатории кода 8.2 мы устанавливали k = 10 для обоих ретриверов, чтобы получать достаточно ответов для сравнения. Ранее у нас был только один набор документов, определяемый как documents, но здесь мы переименовываем их в dense_documents и добавляем второй набор — sparse_documents:
1 2 3 4 |
dense_documents = [Document(page_content=text, metadata={"id": str(i), "source": "dense"}) for i, text in enumerate(splits)] sparse_documents = [Document(page_content=text, metadata={"id": str(i), "source": "sparse"}) for i, text in enumerate(splits)] |
Это позволило добавить к метаданным документов информацию об источнике: для плотных документов — dense, а для разреженных — sparse. Эти метаданные передаются в итоговые результаты, чтобы указывать источник каждого документа. Тем не менее, это не так эффективно, как подход, который мы использовали в нашей пользовательской функции, поскольку для контента из обоих источников не указывается, что он был найден в обоих. Это подчеркивает преимущество создания собственной функции. Далее добавляем новый тип ретривера, EnsembleRetriever, в нижнюю часть ячейки, где определены два других ретривера:
1 2 3 |
# initialize the ensemble retriever ensemble_retriever = EnsembleRetriever(retrievers=[dense_retriever, sparse_retriever], weights=[0.5, 0.5], c=0) |
EnsembleRetriever принимает два ретривера, веса для их акцентирования и параметр c. c — это константа, добавляемая к рангу, которая регулирует баланс между важностью высокоранжированных элементов и учетом низкоранжированных. По умолчанию c = 60, но мы устанавливаем c = 0, чтобы упростить сравнение результатов, поскольку в нашей функции параметра c не было.
Однако c может быть полезным, если вы хотите, чтобы больше ID из нижней части списка поднимались вверх.
Теперь можно удалить функцию hybrid_search. Удалите всю ячейку, начинающуюся с:
1 |
def hybrid_search(query, k=10, dense_weight=0.5, sparse_weight=0.5): |
Затем обновите ввод context из rag_chain_with_source, чтобы использовать новый ретривер:
1 2 3 4 |
rag_chain_with_source = RunnableParallel( {"context": ensemble_retriever, "question": RunnablePassthrough()} ).assign(answer=rag_chain_from_docs) |
После этого нужно изменить код вывода, так как у нас больше нет всей той метаинформации, которую мы добавляли в пользовательской функции:
1 2 3 4 5 6 7 8 9 10 11 |
# User Query result = rag_chain_with_source.invoke(user_query) retrieved_docs = result['context'] print(f"Original Question: {user_query}\n") print(f"Relevance Score: {result['answer']['relevance_score']}\n") print(f"Final Answer:\n{result['answer']['final_answer']}\n\n") print("Retrieved Documents:") for i, doc in enumerate(retrieved_docs, start=1): print(f"Document {i}: Document ID: {doc.metadata['id']} source: {doc.metadata['source']}") print(f"Content:\n{doc.page_content}\n") |
Результат:
Original Question: What are Google’s environmental initiatives?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
Relevance Score: 5 Final Answer: Google's environmental initiatives include being a founding member of the iMasons Climate Accord, providing funding for the ReFED Catalytic Grant Fund to address food waste, supporting projects with The Nature Conservancy for reforestation and deforestation prevention, engaging with suppliers to reduce energy consumption and emissions, auditing suppliers for environmental compliance, addressing climate-related risks, advocating for sustainable consumption of public goods, engaging with coalitions like the RE-Source Platform, and working on improving data center efficiency. Retrieved Documents: Document 1: Document ID: 344 source: dense Content: iMasons Climate AccordGoogle is a founding member and part… Document 2: Document ID: 150 source: sparse Content: sustainability, and we're partnering with them to develop decarbonization roadmaps… Document 3: Document ID: 309 source: dense Content: that enable us to ensure that those we partner with are responsible environmental stewards… |
Результат почти идентичен тому, что мы видели с нашей функцией, за исключением одного нюанса: в случае равенства оценок результаты плотного поиска имеют приоритет (в нашей функции приоритет отдавался разреженным результатам). Эта разница минимальна, и ее можно легко устранить, изменив веса. А вот изменение значения c может значительно повлиять на результаты.
Создание собственной функции дало нам больше гибкости и позволило увидеть и изменить внутреннюю логику функции. С EnsembleRetriever от LangChain изменить этапы поиска или ранжирования для лучшего соответствия вашим нуждам невозможно. Кроме того, возникают мелкие недостатки, например, проблема с метаданными source, где нельзя определить, был ли результат получен из обоих источников.
Сложно судить, какой подход лучше на основе такого небольшого примера. Реальность такова, что каждый проект требует индивидуального подхода, и вам придется решать, что работает в вашей ситуации. Если гибридный поиск важен, стоит рассмотреть векторные базы данных или сервисы поиска, которые предоставляют больше функций и гибкости для настройки гибридного поиска.
LangChain предоставляет веса для усиления одного из механизмов поиска, но пока вы можете использовать только встроенный механизм ранжирования RRF. Например, Weaviate позволяет выбрать между двумя разными алгоритмами ранжирования. Это еще один фактор, который нужно учитывать при выборе инфраструктуры для вашего конвейера RAG.
Теперь давайте поговорим об алгоритмах, использующих эти метрики расстояния.
Алгоритмы семантического поиска
Мы уже подробно обсудили концепцию семантического поиска. Теперь перейдем к рассмотрению различных подходов, которые можно использовать для выполнения семантического поиска. Это собственно алгоритмы поиска, которые используют метрики расстояния, такие как Евклидово расстояние, скалярное произведение и косинусное сходство, для работы с плотными эмбеддингами. Начнем с алгоритма k ближайших соседей (k-NN).
k-NN
Один из способов поиска похожих векторов — использование метода грубой силы (brute force). В этом подходе вычисляются расстояния между запросом и всеми векторами данных, после чего расстояния сортируются от ближайших до самых дальних, и возвращается заданное количество результатов.
Можно задать пороговое значение, чтобы обрезать результаты, или указать фиксированное число возвращаемых результатов, например, k = 5. Этот подход известен в классическом машинном обучении как алгоритм k-NN. Это простой алгоритм, но его производительность ухудшается по мере увеличения размера набора данных. Рост вычислительных затрат для этого алгоритма линейно зависит от объема данных, с которыми вы работаете. Временная сложность алгоритма выражается как O (n * d), где n — количество экземпляров в обучающем наборе данных, d — размерность данных.
Это означает, что если объем данных удвоится, время обработки запроса также удвоится. Для больших наборов данных, содержащих миллионы или даже миллиарды точек, сравнение методом грубой силы между каждой парой элементов становится невыполнимым.
Если у вас относительно небольшой набор данных, k-NN может быть подходящим вариантом, так как он считается более точным, чем другие методы, которые мы рассмотрим далее.
Что считать небольшим набором данных, зависит от ваших данных и размерности эмбеддингов. Например, k-NN успешно применяется для проектов с 25 000 – 30 000 эмбеддингов и размерностью 256. В таких случаях можно добиться улучшения метрик оценки извлечения (о которых мы поговорим в главе 9) на 2–6%, что достаточно, чтобы оправдать небольшой рост вычислительных затрат.
А как насчет всех метрик расстояния, которые мы только что обсудили? Где они используются в k-NN? Алгоритм k-NN может применять любую из этих метрик для определения сходства между вектором запроса и векторами в наборе данных. Наиболее часто используемая метрика расстояния в k-NN — это евклидово расстояние. Другие метрики, такие как расстояние Манхэттена (также известное как городские кварталы) или косинусное сходство, также могут применяться в зависимости от характера данных и решаемой задачи. Выбор метрики расстояния может значительно повлиять на производительность алгоритма k-NN. После вычисления расстояний между вектором запроса и всеми векторами в наборе данных алгоритм k-NN сортирует их и выбирает k ближайших соседей на основе выбранной метрики расстояния.
Если ваш набор данных стал слишком большим для k-NN, существуют другие алгоритмы, которые позволяют находить ближайшие векторы более эффективно. В общем, эти методы называются приближенные ближайшие соседи (Approximate Nearest Neighbors, ANN), которые мы рассмотрим далее.
ANN
ANN представляет собой семейство алгоритмов, разработанных для преодоления ограничений масштабируемости k-NN при сохранении удовлетворительных результатов. Алгоритмы ANN нацелены на поиск похожих к запросу векторов более эффективным способом, жертвуя частью точности ради повышения производительности.
В отличие от k-NN, который выполняет исчерпывающий поиск, вычисляя расстояния между вектором запроса и всеми векторами данных, алгоритмы ANN используют различные техники для сокращения пространства поиска и ускорения процесса извлечения. Эти техники включают индексацию, разбиение и методы аппроксимации, позволяющие ANN сосредотачиваться на подмножестве точек данных, которые с высокой вероятностью являются ближайшими соседями.
Ключевое отличие между k-NN и ANN заключается в компромиссе между точностью и эффективностью. Хотя k-NN гарантирует нахождение k ближайших соседей, его вычислительная сложность резко возрастает с увеличением объема данных. Напротив, алгоритмы ANN отдают приоритет эффективности, приближая ближайших соседей и принимая возможность пропуска некоторых из них в обмен на более быстрое время поиска.
Алгоритмы ANN часто используют структуры индексации, такие как иерархические деревья (например, KD-деревья, Ball-деревья), хэширование (например, хеширование с учетом локальности, Locality-Sensitive Hashing, LSH) или графовые методы (например, Hierarchical Navigable Small World, HNSW), чтобы организовать точки данных таким образом, чтобы облегчить эффективный поиск. Эти структуры индексации позволяют алгоритмам ANN быстро сужать пространство поиска и определять кандидатов на роль ближайших соседей, не сравнивая вектор запроса с каждой точкой данных. В следующем разделе мы подробно рассмотрим подходы к индексации для ANN.
Временная сложность алгоритмов ANN варьируется в зависимости от конкретного алгоритма и используемой техники индексации. Однако в целом алгоритмы ANN стремятся достичь сублинейного времени поиска, то есть времени, которое растет медленнее, чем объем набора данных. Это делает алгоритмы ANN более подходящими для больших наборов данных, где вычислительная стоимость k-NN становится чрезмерной.
Итак, что насчет метрик расстояния? Как и k-NN, алгоритмы ANN используют метрики расстояния для измерения сходства между вектором запроса и векторами в наборе данных. Выбор метрики расстояния зависит от характера данных и решаемой задачи. Среди распространенных метрик расстояния, используемых в ANN, можно выделить евклидово расстояние, расстояние Манхэттена и косинусное сходство. Однако, в отличие от k-NN, который вычисляет расстояния между вектором запроса и всеми векторами данных, алгоритмы ANN применяют структуры индексации и методы аппроксимации для сокращения количества вычислений расстояний. Эти техники позволяют алгоритмам ANN быстро определять подмножество кандидатов, которые, вероятно, находятся близко к вектору запроса. Затем метрика расстояния применяется к этому подмножеству для определения приблизительных ближайших соседей, вместо вычисления расстояний для всего набора данных. Минимизируя количество вычислений расстояний, алгоритмы ANN значительно ускоряют процесс извлечения данных, обеспечивая при этом удовлетворительные результаты.
Важно отметить, что выбор между k-NN и ANN зависит от конкретных требований приложения. Если точные ближайшие соседи критически важны, а набор данных относительно небольшой, k-NN может быть приемлемым вариантом. Однако при работе с большими наборами данных или при необходимости выполнения запроса в режиме, близком к реальному времени, алгоритмы ANN предоставляют практичное решение, обеспечивая баланс между точностью и эффективностью.
В общем, алгоритмы ANN предлагают более масштабируемую и эффективную альтернативу k-NN для поиска схожих векторов в больших наборах данных. Используя методы индексации и аппроксимации, алгоритмы ANN могут значительно сократить пространство поиска и время извлечения данных, что делает их подходящими для приложений, требующих быстрого и масштабируемого поиска по сходству.
Хотя важно понимать, что такое ANN, не менее важно знать, что настоящая ценность заключается в способах его усовершенствования. Давайте рассмотрим некоторые из этих методов далее.
Улучшение поиска с помощью методов индексации
Поиск ANN и k-NN являются фундаментальными решениями в компьютерных науках и машинном обучении, применяемыми в различных областях, таких как поиск изображений, рекомендательные системы и поиск по сходству. Хотя алгоритмы поиска играют ключевую роль в ANN и k-NN, методы индексации и структуры данных столь же важны для повышения эффективности и производительности этих алгоритмов.
Эти методы индексации оптимизируют процесс поиска, сокращая количество векторов, которые необходимо сравнить во время поиска. Они помогают быстро определить более компактное подмножество векторов-кандидатов, которые, вероятно, схожи с вектором запроса. Затем алгоритмы поиска (например, k-NN, ANN или другие алгоритмы поиска по сходству) могут работать с этим уменьшенным набором кандидатов, чтобы найти фактических ближайших соседей или схожие векторы.
Все эти методы нацелены на повышение эффективности и масштабируемости поиска по сходству, сокращая пространство поиска и обеспечивая более быстрый доступ к релевантным векторам. Однако каждый из них имеет свои преимущества и компромиссы в отношении времени индексации, времени поиска, использования памяти и точности. Выбор метода зависит от конкретных требований приложения, таких как размерность векторов, необходимый уровень точности и доступные вычислительные ресурсы.
На практике эти методы могут использоваться как отдельно, так и в комбинации для достижения наилучшей производительности в задаче поиска по векторам. Некоторые библиотеки и фреймворки, такие как Facebook AI Similarity Search (FAISS) и pgvector, предоставляют реализации нескольких методов индексации, включая PQ (квантование продукта), HNSW и LSH, предоставляя пользователям выбор подходящих методов для конкретного сценария.
Прежде чем углубиться в детали, давайте подведем промежуточный итог. Существуют метрики расстояния или сходства (например, косинусное сходство, евклидово расстояние и скалярное произведение), которые используются алгоритмами поиска. Эти алгоритмы поиска включают k-NN, ANN и другие. Алгоритмы поиска могут применять методы индексации, такие как LSH, KD-деревья, Ball-деревья, PQ и HNSW, для повышения их эффективности и масштабируемости.
Отлично, теперь давайте подробнее рассмотрим несколько методов индексации, которые дополняют алгоритмы поиска и улучшают общую эффективность поиска в ANN
LSH (хеширование с учетом локальности) — метод индексации, который с высокой вероятностью отображает похожие векторы в одни и те же хеш-ячейки. Цель LSH — быстро выявлять потенциальных кандидатов для поиска по сходству, сокращая пространство поиска. Это достигается за счет разделения пространства векторов на регионы с помощью хеш-функций, где похожие элементы с большей вероятностью попадают в одну и ту же ячейку. LSH предлагает компромисс между точностью и эффективностью. Используя LSH как этап предварительной обработки, можно значительно сократить множество векторов, подлежащих анализу поисковым алгоритмом. Это уменьшает вычислительные затраты и повышает общую производительность поиска.
Методы индексации на основе деревьев организуют векторы в иерархические структуры, основанные на их пространственных свойствах. Два популярных метода индексации такого рода — KD-деревья и Ball-деревья.
KD-деревья — это двоичные деревья разделения пространства, используемые для организации точек в k-мерном пространстве. Они рекурсивно делят пространство на подрегионы по измерениям векторов. Во время процесса поиска KD-деревья позволяют эффективно искать ближайших соседей, исключая нерелевантные ветви дерева.
Ball-деревья, напротив, делят точки данных на вложенные гиперсферы. Каждый узел дерева представляет гиперсферу, содержащую подмножество точек данных. Ball-деревья особенно эффективны для поиска ближайших соседей в пространствах высокой размерности.
Оба метода предоставляют возможность эффективно перемещаться среди кандидатов и ускоряют процесс поиска.
PQ (квантование продукта) — это техника компрессии и индексации, которая квантует векторы в набор подвекторов и представляет их с использованием кодовых книг. PQ обеспечивает компактное хранение и эффективное вычисление расстояний, приближая их между вектором запроса и квантованными векторами. PQ особенно эффективен для векторов высокой размерности и широко используется в таких приложениях, как поиск изображений и рекомендательные системы. Сжимая векторы и приближая расстояния, PQ снижает объем памяти и вычислительную сложность поиска по сходству.
HNSW (иерархически организованные навигационные малые миры) — это графовый метод индексации, который создает иерархическую структуру взаимосвязанных узлов для быстрого приближенного поиска ближайших соседей. Он строит многоуровневый граф, где каждый уровень представляет другой уровень абстракции, что позволяет эффективно перемещаться и находить приблизительных ближайших соседей. HNSW отличается высокой масштабируемостью и решает проблему высокой временной сложности поиска методом полного перебора в k-NN. Этот метод широко используется в продвинутых базах данных векторов и приобрел популярность благодаря своей высокой производительности и масштабируемости, особенно для данных высокой размерности.
Часть NSW в HNSW работает за счет поиска векторов, которые находятся в выгодном (близком к многим другим векторам) положении. Эти векторы становятся отправными точками для поиска. Количество связей между узлами можно задавать, что позволяет выбрать узлы, лучше всего подходящие для соединения с большим количеством других узлов. Во время запроса алгоритм поиска начинается со случайного начального узла и перемещается к ближайшему соседу к вектору запроса. Для каждого узла, приближающегося к цели, заново вычисляется расстояние от узла запроса до текущего узла, и выбирается следующий узел, ближайший из связей текущего узла. Этот процесс перескакивает через значительные части данных, что делает его намного быстрее.
Иерархическая (H) часть HNSW добавляет несколько уровней навигации по маленьким мирам. Это можно представить как путешествие, где вы сначала летите самолетом до ближайшего аэропорта, затем пересаживаетесь на поезд до города, а затем уже ищете конкретное место в гораздо меньшем наборе узлов.
Интересный факт. HNSW вдохновлен наблюдаемым феноменом в человеческих социальных сетях, где все люди тесно связаны, например, концепцией шести рукопожатий. Согласно этой концепции, любые два человека в среднем разделены шестью связями через знакомых. Эта идея была изначально вдохновлена рассказом Фридьеша Каринти 1929 года, где описывалась игра, в которой группа людей пыталась связать любого человека в мире с собой цепочкой из пяти других. Теоретически такая цепочка может соединить любых двух людей в мире максимум за шесть шагов.
Все эти методы индексации играют ключевую роль в повышении эффективности и производительности алгоритмов поиска ANN. LSH, индексация на основе деревьев, PQ и HNSW — это некоторые из наиболее известных методов индексации, используемых в сочетании с алгоритмами поиска. Благодаря использованию этих методов можно сократить пространство поиска, исключить нерелевантных кандидатов и ускорить общий процесс поиска. Методы индексации предоставляют способ организации и структурирования данных, что обеспечивает эффективный поиск и работу со сходством в пространствах высокой размерности.
Теперь, когда методы индексации добавлены в наш арсенал, нам все же нужно обсудить еще один важный аспект, прежде чем мы сможем начать их реализацию. ANN и k-NN — это не сервисы, которые можно просто подключить; это подходы к поиску, которые используются различными сервисами и программными пакетами. Поэтому следующим шагом будет изучение того, какие именно пакеты существуют и как вы можете их использовать. Давайте поговорим о векторном поиске!
Варианты векторного поиска
Векторный поиск — это процесс нахождения векторов, наиболее похожих на вектор запроса, в хранилище векторов. Способность быстро определять релевантные векторы имеет решающее значение для общей производительности системы, так как именно эти данные будут использоваться LLM для генерации ответов. Этот компонент соединяет исходный запрос пользователя с насыщенными данными, необходимыми для высококачественной генерации. На рынке существует множество решений и типов предложений для выполнения векторного поиска. Мы уже много говорили о компонентах и концепциях, которые делают векторный поиск эффективным. Теперь эти знания можно применить для выбора наилучшего варианта для конкретных потребностей проекта. Поскольку услуги в этой области быстро развиваются, а новые стартапы появляются каждый день, стоит потратить время на тщательное исследование перед выбором решения. В следующих нескольких подразделах мы рассмотрим некоторые варианты, которые помогут вам оценить широту доступных возможностей.
pgvector
Это расширение с открытым исходным кодом для поиска сходства векторов в PostgreSQL, одной из популярных систем управления реляционными базами данных. Оно позволяет хранить и искать векторы непосредственно в PostgreSQL, используя его надежные функции и экосистему. pgvector поддерживает различные метрики расстояния и алгоритмы индексации, включая L2-расстояние и косинусное расстояние. Это одно из немногих решений, поддерживающих как точный поиск k-NN, так и приближенный поиск с использованием алгоритмов ANN.
Варианты индексации включают Inverted File Index (IVF) и HNSW. IVF — это метод индексации, обычно используемый в сочетании с хранилищами векторов. IVF разработан для эффективного выделения подмножества векторов, которые, вероятно, схожи с заданным вектором запроса, что сокращает количество вычислений расстояний, необходимых во время поиска. IVF может использоваться вместе с HNSW, что еще больше повышает эффективность и скорость.
pgvector легко интегрируется с существующими инструментами и библиотеками PostgreSQL, что упрощает добавление возможностей векторного поиска в приложения, уже использующие PostgreSQL. Этот вариант подходит, если вы уже используете PostgreSQL и хотите добавить возможности векторного поиска без внедрения отдельной системы. Он также выигрывает от надежности, масштабируемости и поддержки транзакций PostgreSQL.
Elasticsearch
Популярный движок для поиска и аналитики с открытым исходным кодом, поддерживающий поиск по сходству векторов. Этот инструмент широко используется и имеет большую экосистему плагинов и интеграций. Elasticsearch использует алгоритмы ANN, в частности HNSW, для эффективного поиска по сходству векторов. Хотя он не использует k-NN явно, его функции поиска по сходству можно применять для нахождения k ближайших соседей.
Elasticsearch предлагает расширенные возможности поиска, включая полнотекстовый поиск, агрегирование и геопространственный поиск, а его распределенная архитектура обеспечивает высокую масштабируемость и отказоустойчивость. Elasticsearch хорошо интегрируется с LangChain и обладает надежной масштабируемостью, распределенной архитектурой и широким набором функций.
Этот инструмент подходит, если у вас уже есть опыт работы с Elasticsearch или если вам нужны его расширенные возможности поиска и аналитики. Однако он может потребовать больше усилий для настройки и конфигурации по сравнению с некоторыми другими хранилищами векторов.
FAISS
Это библиотека для эффективного поиска сходства и кластеризации плотных векторов, разработанная Facebook. Она известна своей высокой производительностью и способностью обрабатывать векторные наборы данных масштаба миллиарда. FAISS в значительной степени опирается на алгоритмы ANN для эффективного поиска сходства, предлагая широкий спектр методов индексирования и поиска, включая IVF, PQ и HNSW. FAISS можно использовать для выполнения k-NN поиска, извлекая k наиболее похожих векторов на заданный вектор-запрос. FAISS предлагает множество алгоритмов индексирования и поиска, включая методы, основанные на квантовании, для компактного представления векторов, а также поддерживает использование GPU для ускоренного поиска сходства. Библиотека может применяться как хранилище векторов и интегрируется с LangChain. FAISS является хорошим выбором, если у вас есть высокие требования к производительности и вы готовы работать с более низкоуровневой библиотекой. Однако ее использование может требовать большей ручной настройки и управления по сравнению с управляемыми сервисами.
Google Vertex AI Vector Search
Это полностью управляемый сервис поиска сходства векторов, предоставляемый GCP, который включает как хранилище векторов, так и возможности их поиска. Сервис использует алгоритмы ANN для обеспечения быстрого и масштабируемого поиска сходства векторов. Конкретные используемые алгоритмы ANN не раскрываются, но, вероятно, сервис применяет передовые методы, основанные на Google ScaNN (Scalable, Channel-Aware Nearest Neighbors). Его можно использовать для выполнения k-NN поиска, задав желаемое количество ближайших соседей. При использовании Vertex AI Vector Search векторы хранятся внутри самого управляемого сервиса. Он бесшовно интегрируется с другими сервисами Google Cloud, такими как BigQuery и Dataflow, что позволяет создавать эффективные конвейеры обработки данных. Сервис предоставляет функции, такие как онлайн-обновления, позволяющие добавлять или удалять векторы по мере необходимости без перестройки всего индекса. Сервис интегрируется с LangChain и обеспечивает масштабируемость, высокую доступность и простую интеграцию с другими сервисами Google Cloud. Если вы уже используете Google Cloud и хотите получить управляемое решение с минимальными настройками, Vertex AI Vector Search будет хорошим выбором. Однако он может быть дороже иных решений.
Azure AI Search
Это полностью управляемый поисковый сервис, предоставляемый Microsoft Azure. Он поддерживает поиск сходства векторов наряду с традиционным поиском на основе ключевых слов. Azure AI Search использует алгоритмы ANN для эффективного поиска сходства векторов. Конкретные используемые алгоритмы ANN не указаны, но сервис применяет передовые методы индексирования для быстрого извлечения похожих векторов. Его можно использовать для выполнения k-NN поиска, задавая количество ближайших соседей. Azure AI Search предлагает такие функции, как синонимы, автозаполнение и фасетная навигация. Он интегрируется с Azure Machine Learning для бесшовного развертывания моделей машинного обучения. Azure AI Search подходит, если вы уже используете сервисы Azure и ищете управляемое решение с расширенными возможностями поиска. Однако у него может быть более крутая кривая обучения по сравнению с некоторыми другими вариантами.
Approximate Nearest Neighbors Oh Yeah (ANNOY)
Это библиотека с открытым исходным кодом, разработанная Spotify для поиска по алгоритмам ANN. Она известна высокой скоростью индексирования и выполнения запросов, а также эффективной обработкой высокоразмерных векторов. ANNOY использует комбинацию случайных проекций и разбиения пространства на бинарные части для создания леса деревьев, что обеспечивает быстрый поиск сходства. ANNOY можно использовать для выполнения k-NN поиска, извлекая k приблизительных ближайших соседей. Простота и интуитивно понятный API делают библиотеку легкой для интеграции в существующие проекты. ANNOY хорошо подходит, если вы отдаете приоритет скорости и работаете с небольшим набором данных. Однако она может не справляться с масштабированием так же эффективно, как некоторые другие решения.
Pinecone
Это полностью управляемая векторная база данных, специально разработанная для приложений машинного обучения. Она обеспечивает высокопроизводительный поиск сходства векторов и поддерживает как плотные, так и разреженные представления векторов. Pinecone использует алгоритмы ANN для достижения высокой производительности поиска сходства векторов. Она поддерживает различные алгоритмы индексирования ANN, включая HNSW, для эффективного извлечения похожих векторов. Pinecone можно использовать для выполнения k-NN поиска, задавая количество ближайших соседей. Сервис предоставляет такие функции, как обновления в реальном времени, горизонтальное масштабирование и мульти-региональная репликация для обеспечения высокой доступности. У Pinecone простой API, и он бесшовно интегрируется с различными фреймворками и библиотеками машинного обучения. Pinecone — хороший выбор, если вам нужна специализированная векторная база данных с упором на задачи машинного обучения. Однако она может быть дороже, чем решения с открытым исходным кодом или самостоятельные варианты.
Weaviate
Это поисковый движок с открытым исходным кодом, обеспечивающий эффективный поиск сходства и исследование данных. Он поддерживает несколько алгоритмов индексирования векторов, включая HNSW, и предоставляет API на основе GraphQL для удобной интеграции. Weaviate использует алгоритмы ANN, в частности HNSW, для эффективного поиска сходства векторов. Он применяет структуру NSW, используемую в HNSW, для быстрого извлечения похожих векторов. Weaviate можно использовать для выполнения k-NN поиска, задавая желаемое количество ближайших соседей. Weaviate предоставляет такие функции, как управление схемами, проверка данных и обновления в реальном времени. Он может быть развернут как в самостоятельной конфигурации, так и в управляемом виде. Weaviate — хороший выбор, если вы предпочитаете решение с открытым исходным кодом с упором на исследование данных и графоподобные запросы. Однако его использование может потребовать больше настроек и конфигураций по сравнению с полностью управляемыми сервисами.
Chroma
Это инструмент, который вы использовали для поиска векторов в большинстве примеров кода в этой книге. Chroma — это встраиваемая векторная база данных с открытым исходным кодом, предназначенная для легкой интеграции с существующими инструментами и фреймворками. Chroma поддерживает алгоритмы ANN, включая HNSW, для быстрого и эффективного поиска сходства векторов. Ее можно использовать для выполнения k-NN поиска, извлекая k ближайших соседей к заданному вектору-запросу. Chroma предоставляет простой и интуитивно понятный API на Python для хранения и поиска векторов, что делает ее особенно удобной для рабочих процессов машинного обучения и анализа данных. Именно по этой причине она была выбрана для демонстрации в коде этой книги.
Chroma поддерживает различные алгоритмы индексирования, включая HNSW, и предлагает такие функции, как динамическая фильтрация и хранение метаданных. Она может использоваться как база данных в оперативной памяти или сохранять данные на диск для долговременного хранения. Chroma — это хороший выбор, если вам нужна легковесная и простая в использовании векторная база данных, которую можно встроить непосредственно в ваши Python-приложения. Однако она может не обладать таким уровнем масштабируемости и расширенных функций, как более зрелые решения для поиска векторов.