Почему поиска по сходству недостаточно

1. Рост числа LLM и RAG

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

Среди многих моделей использования LLM, например. тонкая настройка, обучение в несколько этапов и т. д. Поисковая дополненная генерация (RAG) продемонстрировала свои уникальные преимущества.

RAG был представлен Facebook AI в статье под названием Поисковая дополненная генерация для наукоемких задач НЛП, которая была представлена ​​на NeurIPS 2020. Диаграмма ниже наглядно объясняет концепцию RAG.

Вкратце, RAG включает в себя 2 этапа:

  1. Компонент поиска: использует систему поиска информации (часто построенную на базе крупных массивов) для извлечения соответствующих документов или отрывков, которые могут быть полезны для данного запроса.
  2. Генераторный компонент. Обычно это крупномасштабная языковая модель, которая генерирует текст, похожий на человеческий, на основе предоставленных входных данных и полученных документов.

Чтобы улучшить понимание контекста, объяснимость и актуальность в НЛП/НЛУ, RAG, безусловно, во многих местах доказал свою уникальную ценность по сравнению с подходом тонкой настройки LLM. Фактически, в одном из моих предыдущих постов было более подробное объяснение решения на основе RAG.



RAG предлагает мощный подход для объединения поисковых и генеративных моделей. Однако, как и все модели, он имеет свои особенности:

1. Точность извлечения. Эффективность RAG во многом зависит от точности компонента извлечения. Если механизм поиска выбирает ненужные документы или пропускает важные, сгенерированный результат может быть неоптимальным.

2. Зависимость данных. Модель зависит от качества и актуальности внешнего набора данных, используемого для поиска. Отсутствие полного или актуального набора данных может ограничить производительность системы.

3. Избыточность. Существует риск избыточности, если полученные документы содержат дублирующую информацию. Модель может повторять или переоценивать определенные аспекты сгенерированных результатов.

4. Ограничение размера. LLM обычно ограничивает размер запроса, предоставляемого для создания контента.

5. Осведомленность о домене. Адаптация RAG к конкретным доменам может потребовать настройки компонента поиска для конкретного домена или переобучения всей системы на данных, специфичных для домена.

Поскольку многие реализации RAG полагаются на встраивание текста для извлечения контента на основе его семантики, я продемонстрирую подход к повышению точности поиска, который использует совместную фильтрацию по графу знаний.

2. Обзор встраивания текста

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



Чего я не упомянул в посте, так это ограничений вложений, генерируемых LLM. При применении в реальных приложениях мы часто можем обнаружить, что ответы на вопрос не совсем релевантны, когда их можно получить только путем поиска наиболее похожих вложений текста. Некоторые из причин (на примере GPT):

  1. Контекстные внедрения. Одна из сильных и потенциальных слабых сторон GPT заключается в том, что он генерирует контекстные внедрения. Это означает, что представление слова или фразы может меняться в зависимости от окружающего контекста. При поиске с использованием внедрений из определенного контекста они могут не полностью совпадать с внедрениями, созданными из другого контекста, даже если термин тот же.
  2. Высокая размерность. Внедрения GPT являются многомерными и отражают широкий спектр лингвистических нюансов. Хотя такое разнообразие полезно, иногда оно может отдавать приоритет некоторым измерениям, которые вносят шум или нерелевантность в поиск.
  3. Смещения обучающих данных. Вложения, созданные GPT, отражают предвзятость и распределение обучающих данных. Если определенные темы или контексты были недостаточно представлены в обучающих данных, их встраивание могло быть не таким точным, что приводило к неоптимальным результатам поиска.
  4. Семантические совпадения. GPT может создавать встраивания, семантически близкие для терминов или фраз, которые люди воспринимают как разные. Это может привести к получению результатов, которые кажутся нерелевантными.

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

1 — Для вопроса получите его встраивание текста и назовем его emb(q).

2. Выполните поиск в хранилище векторной графики, чтобы найти содержимое, наиболее похожее на emb(q), и сохраните оценку сходства в sim(content ).

3 — Для возвращаемого содержимого примените совместную фильтрацию для расчета веса и корректировки показателя сходства.

4 — Возвращает содержимое, точность которого теперь повышена.

Позвольте мне показать вам, как это работает, используя Movie Graph.

3. Пример: рекомендации фильмов

3.1 Подготовьте график фильма

Чтобы начать эксперимент как можно проще, я собираюсь создать бесплатный экземпляр AuraDB из Neo4j для графа фильма.



После запуска экземпляра нажмите кнопку _Query, чтобы запустить Neo4j Browser. Вам нужно будет указать здесь пароль, сгенерированный во время создания экземпляра. После запуска браузера в текстовом поле введите:

:play movies

Откроется Учебное пособие по Movie Graph. Перейдите на вторую страницу и щелкните блок, где вы видите несколько строк кода. С помощью 3 простых шагов мы просто создаем Movie Graph.

Щелкнув значок базы данных слева, мы сможем увидеть созданные узлы и связи.

Более подробное руководство по Movie Graph и Neo4j можно найти на этом замечательном ресурсе.



3.2 Добавление текстовых вложений в названия и слоганы фильмов

В Movie Graph каждый узел Movie имеет два текстовых свойства: title и tagline. Давайте создадим встраивание текста для title и tagline, используя приведенный ниже код Python.

from neo4j import GraphDatabase
from openai.embeddings_utils import get_embedding
import openai 

"""
LoadEmbedding: call OpenAI embedding API to generate embeddings for each proporty of node in Neo4j
Version: 1.1
"""
OPENAI_KEY = "OPENAI-KEY"
EMBEDDING_MODEL = "text-embedding-ada-002"
NEO4J_URL = "neo4j+s://INSTANCE_ID.databases.neo4j.io:7687"
NEO4J_USER = "neo4j"
NEO4J_PASSWORD = "NEO4J_PASSWORD"
class LoadEmbedding:
    def __init__(self, uri, user, password):
        self.driver = GraphDatabase.driver(uri, auth=(user, password))
        openai.api_key = OPENAI_KEY
    def close(self):
        self.driver.close()
    def load_embedding_to_node_property(self, label, property):
        with self.driver.session() as session:
            result = session.run("MATCH (n:" + label + ") WHERE n." + property + " IS NOT NULL RETURN id(n) AS id, n." + property + " as " + property)
            # call OpenAI embedding API to generate embeddings for each proporty of node
            # for each node, update the embedding property
            count = 0
            for record in result:
                id = record["id"]
                text = record[property]
                # Below, instead of using the text as the input for embedding, we add label and property name in front of it
                embedding = get_embedding(label + " " + property + " - " + text, EMBEDDING_MODEL)
                # key property of Embedding node differentiates different embeddings
                cypher = "CREATE (e:Embedding) SET e.key=$key, e.value=$embedding"
                cypher = cypher + " WITH e MATCH (n) WHERE id(n) = $id CREATE (n) -[:HAS_EMBEDDING]-> (e)"
                session.run(cypher,key=property, embedding=embedding, id=id )
                count = count + 1
            
            print("Processed " + str(count) + " " + label + " nodes for property @" + property + ".")
            return count
if __name__ == "__main__":
    loader = LoadEmbedding(NEO4J_URL, NEO4J_USER, NEO4J_PASSWORD)
    loader.load_embedding_to_node_property("Movie", "title")
    loader.load_embedding_to_node_property("Movie", "tagline")
    print("done")
    loader.close()

Я надеюсь, что приведенный выше код достаточно ясен для понимания. Здесь я хочу подчеркнуть способ моделирования вложений.

Для фильма, например. Apollo 13 выше, он подключен к двум узлам внедрения: один для title, а другой для tagline. Основным соображением здесь является будущая масштабируемость, когда мы хотим хранить встраивания одного и того же контента из разных LLM и/или версий.

3.3 Создание векторного индекса

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

Введите следующий запрос Cypher в текстовое поле браузера Neo4j:

CALL db.index.vector.createNodeIndex('embedingIndex', 'Embedding', 'value', 1536, 'COSINE')

Определение параметра:

  • embeddingIndex: имя индекса
  • Внедрение: имя метки.
  • значение: имя свойства.
  • 1536: размерность векторов. Для модели GPT встраивание имеет 1536 измерений.
  • КОСИНУС: функция сходства.

3.4 Первый тест: поиск по сходству

Теперь проведем первый тест. Предположим, мы хотим порекомендовать фильмы, похожие на Матрицу. В нашем Movie Graph у нас есть слоган и заголовок, которые можно использовать для сопоставления по сходству.

// Find movie The Matrix
MATCH (m1:Movie{title:'The Matrix'}) -[:HAS_EMBEDDING]-> (e:Embedding{key:'tagline'})
WITH m1, e
// Find other movies which have high semantic similarity on tagline
CALL db.index.vector.queryNodes("embeddingIndex", 50, e.value) YIELD node, score
WITH node, score
WHERE score < 1.0   // exclude self
// From the returned Embedding nodes, find their connected Movie nodes
MATCH (m2:Movie)  -[:HAS_EMBEDDING]-> (node)
WHERE node.key = 'tagline'
RETURN m2.title, m2.tagline, score

Здесь мы используем db.index.vector.queryNodes, чтобы вернуть топ-50 наиболее похожих вложений слогана фильма, вычисляя косинусное сходство между встраиванием слогана фильма The Матрица(то есть «Добро пожаловать в реальный мир»), а не встраивание других слоганов. Ниже приведены первые 5 возвращенных записей:

╒═════════════════════════════════╤══════════════════════════════════════════════════════════════════════╤══════════════════╕
│m2.title                         │m2.tagline                                                            │score             │
╞═════════════════════════════════╪══════════════════════════════════════════════════════════════════════╪══════════════════╡
│"The Birdcage"                   │"Come as you are"                                                     │0.9422817826271057│
├─────────────────────────────────┼──────────────────────────────────────────────────────────────────────┼──────────────────┤
│"The Matrix Reloaded"            │"Free your mind"                                                      │0.9407751560211182│
├─────────────────────────────────┼──────────────────────────────────────────────────────────────────────┼──────────────────┤
│"Cast Away"                      │"At the edge of the world, his journey begins."                       │0.9367693662643433│
├─────────────────────────────────┼──────────────────────────────────────────────────────────────────────┼──────────────────┤
│"Ninja Assassin"                 │"Prepare to enter a secret world of assassins"                        │0.9366204738616943│
├─────────────────────────────────┼──────────────────────────────────────────────────────────────────────┼──────────────────┤
│"That Thing You Do"              │"In every life there comes a time when that thing you dream becomes th│0.9354331493377686│
│                                 │at thing you do"                                                      │                  │
├─────────────────────────────────┼──────────────────────────────────────────────────────────────────────┼──────────────────┤

Судя по всему, есть некоторые проблемы, поскольку результаты не кажутся очень релевантными, особенно. остальные две серии сериала получили более низкие оценки (2 и 14 места).

Одной из потенциальных причин является слоган «Матрицы» «Добро пожаловать в реальный мир», который на самом деле не дает особого контекста. Это довольно часто является проблемой.

3.5 Второй тест: объединение поиска на основе сходства с совместной фильтрацией

Теперь давайте попробуем посмотреть, какую еще информацию в Movie Graph можно использовать для корректировки оценок.

Одна из очевидных стратегий — просмотреть доступные взаимоотношения между фильмами, актерами и режиссерами. Фильмы, снятые одними и теми же режиссерами и/или в ролях одних и тех же актеров, должны быть более актуальными, чем другие фильмы.

Ниже приведен блок кода обновления.

// 1. Find movie The Matrix
MATCH (m1:Movie{title:'The Matrix'}) -[:HAS_EMBEDDING]-> (e:Embedding{key:'tagline'})
WITH m1, e

// 2. Find other movies which have high semantic similarity on tagline
CALL db.index.vector.queryNodes("embeddingIndex", 50, e.value) YIELD node, score
WITH m1, node, score
WHERE score < 1.0   // exclude self

// 3. From the returned Embedding nodes, find their connected Movie nodes
MATCH (m2:Movie)  -[:HAS_EMBEDDING]-> (node)
WHERE node.key = 'tagline'

// 4. For returned Movie nodes, count number of same directors and actors
WITH m1, m2, score, 
     COUNT {(m1) <-[:ACTED_IN]- () -[:ACTED_IN]-> (m2)} AS sameActorCount,
     COUNT {(m1) <-[:DIRECTED]- () -[:DIRECTED]-> (m2)} AS sameDirectorCount

// 5. Use sameActorCount and sameDirectorCount to calculate weights and apply to similarity score
WITH m1, m2, score, sameActorCount, sameDirectorCount,
     CASE WHEN sameActorCount > 0 THEN 1+log(1+sameActorCount) ELSE 1 END AS actorWeight,
     1+sameDirectorCount AS directorWeight
RETURN  m2.title, m2.tagline AS tagline, sameActorCount, sameDirectorCount, score, actorWeight * directorWeight * score AS rank 
ORDER BY rank DESC;

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

4. Перспективы будущего

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

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

Приятного встраивания!