Расширенная RAG 01 поиск от малого к большому
Расширенный поиск RAG 01 от малого к большому
Child-Parent RecursiveRetriever и извлечение окна предложения с помощью LlamaIndex
Системы RAG (Retrieval-Augmented Generation) извлекают соответствующую информацию из заданной базы знаний, позволяя при этом генерировать фактическую, контекстно согласованную и предметно-специфичную информацию. Однако RAG сталкивается с множеством проблем, когда дело доходит до эффективного извлечения соответствующей информации и генерации качественных ответов. В этой серии блоговых записей / видео я расскажу о передовых техниках RAG, направленных на оптимизацию рабочего процесса RAG и решение проблем в наивных системах RAG.
Первая техника называется извлечение от меньшего к большему. В основных конвейерах RAG мы встраиваем большой текстовый фрагмент для извлечения, и этот точно такой же текстовый фрагмент используется для синтеза. Но иногда встраивание / извлечение больших текстовых фрагментов может казаться неоптимальным. В большом текстовом фрагменте может содержаться много заполнительного текста, скрывающего семантическое представление и приводящего к худшему извлечению. Что, если мы могли бы встраивать / извлекать на основе более мелких, более целевых фрагментов, но всё равно иметь достаточно контекста для синтеза ответа с использованием LLM? В частности, отделение текстовых фрагментов, используемых для извлечения, от текстовых фрагментов, используемых для синтеза, может быть выгодным. Использование более мелких текстовых фрагментов повышает точность извлечения, а более крупные текстовые фрагменты предоставляют больше контекстной информации. Концепция извлечения от меньшего к большему заключается в использовании более мелких текстовых фрагментов в процессе извлечения, а затем предоставлении большого текстового фрагмента, к которому относится извлеченный текст, большой языковой модели.
Существуют две основные техники:
- Меньшие дочерние фрагменты, относящиеся к большим родительским фрагментам: Сначала извлекаем меньшие фрагменты во время извлечения, затем ссылаемся на идентификаторы родительского фрагмента и возвращаем большие фрагменты.
- Извлечение окна предложения: Извлекаем одно предложение во время извлечения и возвращаем окно текста вокруг предложения.
В этой блог-записи мы рассмотрим реализацию этих двух методов в LlamaIndex. Почему я не делаю это в LangChain? Потому что уже существует много ресурсов о передовом RAG с LangChain. Я предпочитаю не дублировать усилий. Кроме того, я использую как LangChain, так и LlamaIndex. Лучше понимать больше инструментов и гибко их использовать.
- Линейная регрессия, ядро-трюк и линейное-ядро.
- Птичий взгляд на линейную алгебру мера карты – определитель
- Рекомендательные системы на основе неявной обратной связи с использованием TensorFlow Recommenders
Вы можете найти весь код в этом ноутбуке.
Обзор базового RAG
Начнем с базовой реализации RAG с 4 простыми шагами:
Шаг 1. Загрузка документов
Мы используем PDFReader для загрузки файла PDF и объединяем каждую страницу документа в один объект Document.
loader = PDFReader()docs0 = loader.load_data(file=Path("llama2.pdf"))doc_text = "\n\n".join([d.get_content() for d in docs0])docs = [Document(text=doc_text)]
Шаг 2. Разбиение документов на текстовые фрагменты (узлы)
Затем мы разбиваем документ на текстовые фрагменты, которые в LlamaIndex называются “узлами”, где мы задаем размер фрагмента равным 1024. Идентификаторы узлов по умолчанию представлены случайными текстовыми строками, после чего мы форматируем идентификатор узла в определенном формате.
node_parser = SimpleNodeParser.from_defaults(chunk_size=1024)base_nodes = node_parser.get_nodes_from_documents(docs)for idx, node in enumerate(base_nodes):node.id_ = f"node-{idx}"
Шаг 3. Выбор модели эмбеддинга и LLM
Нам нужно определить две модели:
- Модель эмбеддинга используется для создания векторных эмбеддингов для каждого из текстовых фрагментов. Здесь мы вызываем модель FlagEmbedding от Hugging Face.
- LLM: запрос пользователя и соответствующие текстовые фрагменты передаются в LLM, чтобы он мог генерировать ответы с соответствующим контекстом.
Мы можем объединить эти две модели вместе в ServiceContext и использовать их позже на этапах индексации и запроса.
embed_model = resolve_embed_model(“local:BAAI/bge-small-en”)llm = OpenAI(model="gpt-3.5-turbo")service_context = ServiceContext.from_defaults(llm=llm, embed_model=embed_model)
Шаг 4. Создание индекса, восстановителя и поисковой системы запросов
Индекс, восстановитель и поисковая система запросов – это три основных компонента для задания вопросов о данных или документах:
- Индекс – это структура данных, которая позволяет нам быстро получать связанную информацию для пользовательского запроса из внешних документов. Индекс Vector Store берет текстовые фрагменты/узлы и создает векторные вложения текста каждого узла, готовые для запроса LLM.
base_index = VectorStoreIndex(base_nodes, service_context=service_context)
- Восстановитель используется для получения и извлечения соответствующей информации, исходя из пользовательского запроса.
base_retriever = base_index.as_retriever(similarity_top_k=2)
- Поисковая система запросов создается поверх индекса и восстановителя, предоставляя общий интерфейс для задания вопросов о ваших данных.
query_engine_base = RetrieverQueryEngine.from_args( base_retriever, service_context=service_context)response = query_engine_base.query( "Можете ли вы рассказать мне о ключевых концепциях для тонкой настройки безопасности")print(str(response))
Расширенный метод 1: Меньшие дочерние фрагменты, ссылающиеся на большие родительские фрагменты
В предыдущем разделе мы использовали фиксированный размер фрагмента, равный 1024, как для извлечения, так и для синтеза. В этом разделе мы рассмотрим, как использовать более маленькие дочерние фрагменты для извлечения и ссылаться на более большие родительские фрагменты для синтеза. Первый шаг – создание более маленьких дочерних фрагментов:
Шаг 1: Создание более мелких дочерних фрагментов
Для каждого текстового фрагмента размером 1024 мы создаем еще более мелкие текстовые фрагменты:
- 8 текстовых фрагментов размером 128
- 4 текстовых фрагмента размером 256
- 2 текстовых фрагмента размером 512
Мы добавляем исходный текстовый фрагмент размером 1024 в список текстовых фрагментов.
sub_chunk_sizes = [128, 256, 512]sub_node_parsers = [ SimpleNodeParser.from_defaults(chunk_size=c) for c in sub_chunk_sizes]all_nodes = []for base_node in base_nodes: for n in sub_node_parsers: sub_nodes = n.get_nodes_from_documents([base_node]) sub_inodes = [ IndexNode.from_text_node(sn, base_node.node_id) for sn in sub_nodes ] all_nodes.extend(sub_inodes) # также добавляем исходный узел в узел original_node = IndexNode.from_text_node(base_node, base_node.node_id) all_nodes.append(original_node)all_nodes_dict = {n.node_id: n for n in all_nodes}
Когда мы взглянем на все текстовые фрагменты ‘all_nodes_dict’, мы можем видеть, что к каждому из исходных текстовых фрагментов, например ‘node-0’, связаны множество более мелких фрагментов. Фактически, все более мелкие фрагменты ссылаются на большой фрагмент в метаданных с указанием параметра index_id на идентификатор индекса большого фрагмента.
Шаг 2: Создание индекса, восстановителя и поисковой системы запросов
- Индекс: Создание векторных вложений для всех текстовых фрагментов.
vector_index_chunk = VectorStoreIndex( all_nodes, service_context=service_context)
- Восстановитель: главное здесь – использовать RecursiveRetriever, чтобы просматривать связи между узлами и извлекать узлы на основе “ссылок”. Этот восстановитель рекурсивно исследует связи от узлов к другим восстановителям/поисковым системам запросов. Для полученных узлов, если любой из них является IndexNode, то он будет исследовать связанный восстановитель/поисковую систему запросов и этот восстановитель будет запрашивать их.
vector_retriever_chunk = vector_index_chunk.as_retriever(similarity_top_k=2)retriever_chunk = RecursiveRetriever( "vector", retriever_dict={"vector": vector_retriever_chunk}, node_dict=all_nodes_dict, verbose=True,)
Когда мы задаем вопрос и извлекаем наиболее релевантные текстовые фрагменты, на самом деле извлекаем текстовый фрагмент с идентификатором узла, указывающим на родительский фрагмент, и, таким образом, извлекаем родительский фрагмент.
- Теперь с помощью тех же шагов, что и раньше, мы можем создать поисковый движок в качестве общего интерфейса для задания вопросов о наших данных.
query_engine_chunk = RetrieverQueryEngine.from_args( retriever_chunk, service_context=service_context)response = query_engine_chunk.query( "Можете рассказать мне о ключевых концепциях для настройки безопасности")print(str(response))
Улучшенный метод 2: Извлечение по окну предложений
Для достижения еще более детализированного извлечения вместо использования более мелких дочерних фрагментов мы можем разобрать документы на одно предложение в каждом фрагменте.
В этом случае одиночные предложения будут аналогичны концепции “дочернего” фрагмента, которую мы упомянули в методе 1. “Окно” предложений (5 предложений с обоих сторон исходного предложения) будет аналогично концепции “родительского” фрагмента. Другими словами, мы используем одиночные предложения во время извлечения и отправляем извлеченное предложение вместе с окном предложений в LLM.
Шаг 1: Создание парсера узлов окна предложений
# create the sentence window node parser w/ default settingsnode_parser = SentenceWindowNodeParser.from_defaults( window_size=3, window_metadata_key="window", original_text_metadata_key="original_text",)sentence_nodes = node_parser.get_nodes_from_documents(docs)sentence_index = VectorStoreIndex(sentence_nodes, service_context=service_context)
Шаг 2: Создание поискового движка
При создании поискового движка мы можем заменить предложение окном предложений с использованием MetadataReplacementPostProcessor, чтобы окно предложений было отправлено в LLM.
query_engine = sentence_index.as_query_engine( similarity_top_k=2, # the target key defaults to `window` to match the node_parser's default node_postprocessors=[ MetadataReplacementPostProcessor(target_metadata_key="window") ],)window_response = query_engine.query( "Можете рассказать мне о ключевых концепциях для настройки безопасности")print(window_response)
Извлечение по окну предложений смогло ответить на вопрос “Можете рассказать мне о ключевых концепциях для настройки безопасности”:
Здесь вы можете видеть фактическое извлеченное предложение и окно предложений, которое обеспечивает больше контекста и деталей.
Заключение
В этом блоге мы исследовали, как использовать извлечение от малого к большому для улучшения RAG, сфокусировавшись на рекурсивном извлечении дочерних и родительских фрагментов и извлечении по окну предложений с использованием LlamaIndex. В будущих блог-постах мы погрузимся глубже в другие хитрости и советы. Ожидайте больше на этом захватывающем пути в продвинутые техники RAG!
Ссылки:
- https://docs.llamaindex.ai/en/latest/examples/node_postprocessor/MetadataReplacementDemo.html
- https://docs.llamaindex.ai/en/stable/examples/retrievers/recursive_retriever_nodes.html
By Sophia Yang on November 4, 2023
Connect with me on LinkedIn, Twitter, and YouTube and join the DS/ML Book Club ❤️