Овладение поиском в Arxiv Пошаговое руководство по созданию чат-бота для вопросов и ответов с использованием Haystack

Мастерство в поиске на Arxiv пошаговое руководство по созданию чат-бота для вопросов и ответов с использованием Haystack

Введение

Вопрос-ответ на основе пользовательских данных является одним из наиболее востребованных случаев использования больших языковых моделей. Человекоподобные навыки беседы LLM, объединенные с методами векторного поиска, значительно облегчают извлечение ответов из больших документов. С некоторыми вариациями мы можем создавать системы для взаимодействия с любыми данными (структурированными, неструктурированными и полуструктурированными), хранящимися в виде векторов в базе данных. Этот метод расширения LLM с помощью извлеченных данных на основе оценок сходства между векторными представлениями запроса и векторными представлениями документов называется RAG или улучшенная генерация на основе поиска. Этот метод может упростить многие вещи, такие как чтение статей из arXiv.

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

В этой статье мы создадим чат-бот RAG для статей arXiv, используя инструмент с открытым исходным кодом под названием Haystack.

Цели обучения

  • Понять, что такое Haystack и какие компоненты он содержит для создания приложений на основе LLM.
  • Создать компонент для извлечения статей из arXiv с использованием библиотеки “arxiv”.
  • Изучить, как создавать индексационные и запросные конвейеры с помощью узлов Haystack.
  • Научиться создавать интерфейс чата с помощью Gradio, координировать конвейеры для извлечения документов из векторного хранилища и генерировать ответы с помощью LLM.

Эта статья была опубликована в рамках Data Science Blogathon.

Что такое Haystack?

Haystack — это открытая платформа NLP, которая объединяет в себе все необходимое для создания масштабируемых приложений, основанных на LLM. Haystack предоставляет высокомодульный и настраиваемый подход к созданию готовых к производству NLP-приложений, таких как семантический поиск, вопросно-ответная система RAG и другие. Он основан на концепции конвейеров и узлов; конвейеры обеспечивают очень гибкое построение цепочки узлов для создания эффективных NLP-приложений.

  • Узлы: Узлы являются основными строительными блоками Haystack. Каждый узел выполняет отдельную задачу, такую как предварительная обработка документов, извлечение из векторных хранилищ, генерация ответов на основе LLM и другие.
  • Конвейер: Конвейер позволяет связывать узлы между собой, чтобы строить цепочки узлов. Это упрощает создание приложений с помощью Haystack.

Haystack также содержит встроенную поддержку ведущих векторных хранилищ, таких как Weaviate, Milvus, Elastic Search, Qdrant и других. Более подробную информацию можно найти в общедоступном репозитории Haystack: https://github.com/deepset-ai/haystack.

Таким образом, в этой статье мы будем использовать Haystack для создания чат-бота Q&A для статей Arxiv с использованием интерфейса Gradio.

Gradio

Gradio – это открытое решение от Huggingface для настройки и обмена демонстрацией любого приложения машинного обучения. Оно работает на основе Fastapi на серверной части и Svelte для компонентов веб-интерфейса. Оно позволяет создавать настраиваемые веб-приложения на Python. Идеально подходит для создания и обмена демонстрационными приложениями моделей машинного обучения или концептами. Дополнительную информацию можно найти на официальной странице Gradio в GitHub: https://github.com/gradio-app/gradio. Чтобы узнать больше о создании приложений с помощью Gradio, см. статью “Let’s Build Chat GPT with Gradio” [https://github.com/gradio-app/gradio].

Создание чат-бота

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

У нас есть две конвейеры: конвейер индексации и конвейер запросов. Когда пользователь вводит идентификатор статьи в Arxiv, он попадает в компонент Arxiv, который извлекает и загружает соответствующую статью в указанный каталог и запускает конвейер индексации. Конвейер индексации состоит из четырех узлов, каждый из которых отвечает за выполнение одной задачи. Так что давайте посмотрим, что делают эти узлы.

Конвейер индексации

В конвейере Haystack вывод предыдущего узла используется в качестве входных данных для текущего узла. В индексирующем конвейере начальным входом является путь к документу.

  • PDFToTextConverter: Библиотека Arxiv позволяет загружать статьи в формате PDF. Но нам нужны данные в текстовом формате. Поэтому этот узел извлекает тексты из PDF.
  • Preprocessor: Извлеченные данные должны быть очищены и обработаны перед хранением их в векторной базе данных. Этот узел отвечает за очистку и разделение текстов.
  • EmbeddingRetriver: Этот узел определяет хранилище векторов, где данные должны быть сохранены, и модель вложения, используемую для получения вложений.
  • InMemoryDocumentStore: Это хранилище векторов, в котором хранятся вложения. В данном случае мы использовали стандартное хранилище документов In-memory от Haystack. Но вы также можете использовать другие хранилища векторов, такие как Qdrant, Weaviate, Elastic Search, Milvus и т. д.

Конвейер запросов

Конвейер запросов запускается, когда пользователь отправляет запросы. Конвейер запросов извлекает “k” ближайших документов к вложениям запроса из векторного хранилища и генерирует ответ LLM. Здесь также есть четыре узла.

  • Retriever: Извлекает “k” ближайших документов к вложениям запроса из векторного хранилища.
  • Sampler: Фильтрует документы на основе совокупной вероятности оценок сходства между запросом и документами с использованием выборки верхних p.
  • LostInTheMiddleRanker: Этот алгоритм переупорядочивает извлеченные документы. Он помещает наиболее релевантные документы в начало или конец контекста.
  • PromptNode: PromptNode ответственен за генерацию ответов на запросы из предоставленного контекста для LLM.

Так что это был рабочий процесс нашего чат-бота Arxiv. Теперь давайте перейдем к части с кодированием.

Настройка Dev Env

Перед установкой зависимостей создайте виртуальное окружение. Вы можете использовать Venv и Poetry для создания виртуального окружения.

python -m venv my-env-namesource bin/activate

Теперь установите следующие зависимости разработки. Для загрузки статей Arxiv нам понадобится установленная библиотека Arxiv.

farm-haystackarxivgradio

Теперь мы импортируем библиотеки.

import arxivimport osfrom haystack.document_stores import InMemoryDocumentStorefrom haystack.nodes import (    EmbeddingRetriever,     PreProcessor,     PDFToTextConverter,     PromptNode,     PromptTemplate,     TopPSampler    )from haystack.nodes.ranker import LostInTheMiddleRankerfrom haystack.pipelines import Pipelineimport gradio as gr

Построение компонента Arxiv

Этот компонент будет отвечать за загрузку и хранение файлов PDF Arxiv. Вот как мы определяем компонент.

class ArxivComponent:    """    Этот компонент отвечает за извлечение статей ArXiv на основе идентификатора ArXiv.    """    def run(self, arxiv_id: str = None):        """        Извлекает и сохраняет статью ArXiv для заданного идентификатора ArXiv.        Args:            arxiv_id (str): Идентификатор ArXiv статьи, которую нужно извлечь.        """        # Установите путь каталога, где будут храниться статьи ArXiv        dir: str = DIR        # Создайте экземпляр клиента arXiv        arxiv_client = arxiv.Client()        # Проверяем, предоставлен ли идентификатор arXiv; если нет, то вызываем ошибку        if arxiv_id is None:            raise ValueError("Пожалуйста, укажите идентификатор ArXiv статьи для извлечения.")        # Поиск статьи ArXiv с использованием предоставленного идентификатора ArXiv        search = arxiv.Search(id_list=[arxiv_id])        response = arxiv_client.results(search)        paper = next(response)  # Получить первый результат        title = paper.title  # Извлечь название статьи        # Проверяем, существует ли указанный каталог        if os.path.isdir(dir):            # Проверяем, существует ли уже файл PDF для статьи            if os.path.isfile(dir + "/" + title + ".pdf"):                return {"file_path": [dir + "/" + title + ".pdf"]}        else:            # Если каталог не существует, создаем его            os.mkdir(dir)        # Попытка загрузить PDF для статьи ArXiv        try:            paper.download_pdf(dirpath=dir, filename=title + ".pdf")            return {"file_path": [dir + "/" + title + ".pdf"]}        except:            # Если происходит ошибка во время загрузки, вызываем ошибку подключения            raise ConnectionError(message=f"Произошла ошибка при загрузке PDF для \                                            статьи ArXiv с идентификатором: {arxiv_id}")

Вышеуказанный компонент инициализирует клиент Arxiv, затем извлекает статью Arxiv, связанную с идентификатором и проверяет, была ли она уже загружена; он возвращает путь к PDF-файлу или загружает его в директорию.

Создание индексной конвейера

Теперь мы определим индексный конвейер для обработки и хранения документов в нашей векторной базе данных.

document_store = InMemoryDocumentStore()embedding_retriever = EmbeddingRetriever(    document_store=document_store,     embedding_model="sentence-transformers/All-MiniLM-L6-V2",     model_format="sentence_transformers",     top_k=10    )def indexing_pipeline(file_path: str = None):    pdf_converter = PDFToTextConverter()    preprocessor = PreProcessor(split_by="word", split_length=250, split_overlap=30)        indexing_pipeline = Pipeline()    indexing_pipeline.add_node(        component=pdf_converter,         name="PDFConverter",         inputs=["File"]        )    indexing_pipeline.add_node(        component=preprocessor,         name="PreProcessor",         inputs=["PDFConverter"]        )    indexing_pipeline.add_node(        component=embedding_retriever,        name="EmbeddingRetriever",         inputs=["PreProcessor"]        )    indexing_pipeline.add_node(        component=document_store,         name="InMemoryDocumentStore",         inputs=["EmbeddingRetriever"]        )    indexing_pipeline.run(file_paths=file_path)

Сначала мы определяем нашу встроенную в память хранилище документов, а затем извлекатель вложений. В извлекателе вложений мы указываем хранилище документов, модели вложений и количество документов для извлечения.

Мы также определили четыре узла, о которых мы ранее говорили. Конвертер pdf_converter преобразует PDF-файл в текст, предварительная обработка очищает и создает фрагменты текста, извлекатель вложений создает вложения документов, а InMemoryDocumentStore хранит векторные вложения. Метод run с указанием пути к файлу запускает конвейер, и каждый узел выполняется в порядке, в котором они были определены. Вы также можете заметить, как каждый узел использует выходные данные предыдущих узлов в качестве входных данных.

Создание конвейера запросов

Конвейер запросов также состоит из четырех узлов. Он отвечает за получение вложения из запрошенного текста, поиск похожих документов в векторном хранилище и, наконец, генерацию ответов с использованием LLM.

def query_pipeline(query: str = None):    if not query:        raise gr.Error("Пожалуйста, укажите запрос.")    prompt_text = """Синтезировать полный ответ из предоставленных абзацев статьи Arxiv и заданного вопроса.    Сосредоточьтесь на вопросе и избегайте ненужной информации в своем ответе.    Параграфы: {join(documents)}    Вопрос: {query}    Ответ:"""    prompt_node = PromptNode(                         "gpt-3.5-turbo",                          default_prompt_template=PromptTemplate(prompt_text),                          api_key="api-key",                          max_length=768,                          model_kwargs={"stream": False},                         )    query_pipeline = Pipeline()    query_pipeline.add_node(        component = embedding_retriever,         name = "Retriever",         inputs=["Query"]        )    query_pipeline.add_node(        component=TopPSampler(        top_p=0.90),         name="Sampler",         inputs=["Retriever"]        )    query_pipeline.add_node(        component=LostInTheMiddleRanker(1024),         name="LostInTheMiddleRanker",         inputs=["Sampler"]        )    query_pipeline.add_node(        component=prompt_node,         name="Prompt",         inputs=["LostInTheMiddleRanker"]        )    pipeline_obj = query_pipeline.run(query = query)        return pipeline_obj["results"]

Recall that embedding_retriever извлекает “k” похожих документов из векторного хранилища. Sampler отвечает за выбор образцов документов. LostInTheMiddleRanker ранжирует документы в начале или конце контекста на основе их релевантности. Наконец, prompt_node, где LLM – “gpt-3.5-turbo”. Мы также добавили шаблон подсказки, чтобы добавить больше контекста в беседу. Метод run возвращает объект конвейера – словарь.

Это было наше бэкэнд. Теперь дизайнируем интерфейс.

Интерфейс Gradio

Здесь есть класс Blocks для создания настраиваемого веб-интерфейса. Так что для этого проекта нам нужно текстовое поле, которое принимает Arxiv ID в качестве ввода пользователя, интерфейс чата и текстовое поле, которое принимает запросы пользователя. Вот как мы можем это сделать.

with gr.Blocks() as demo:    with gr.Row():        with gr.Column(scale=60):            text_box = gr.Textbox(placeholder="Введите Arxiv ID",                                   interactive=True).style(container=False)        with gr.Column(scale=40):            submit_id_btn = gr.Button(value="Отправить")    with gr.Row():        chatbot = gr.Chatbot(value=[]).style(height=600)        with gr.Row():        with gr.Column(scale=70):            query = gr.Textbox(placeholder = "Введите строку запроса",                                interactive=True).style(container=False)

Запустите команду gradio app.py в командной строке и перейдите по отображенному адресу localhost.

Теперь нам нужно определить события-триггеры.

submit_id_btn.click(        fn=embed_arxiv,         inputs=[text_box],        outputs=[text_box],        )query.submit(            fn=add_text,             inputs=[chatbot, query],             outputs=[chatbot, ],             queue=False            ).success(            fn=get_response,            inputs = [chatbot, query],            outputs = [chatbot,]            )demo.queue()demo.launch()

Чтобы сработали события, нам нужно определить функции, упомянутые в каждом событии. Нажмите на кнопку submit_iid_btn и отправьте входные данные из текстового поля в функцию embed_arxiv. Эта функция будет координировать получение и сохранение PDF-файла Arxiv в векторное хранилище.

arxiv_obj = ArxivComponent()def embed_arxiv(arxiv_id: str):    """        Args:            arxiv_id: Arxiv ID of the article to be retrieved.                   """    global FILE_PATH    dir: str = DIR       file_path: str = None    if not arxiv_id:        raise gr.Error("Provide an Arxiv ID")    file_path_dict = arxiv_obj.run(arxiv_id)    file_path = file_path_dict["file_path"]    FILE_PATH = file_path    indexing_pipeline(file_path=file_path)    return "Successfully embedded the file"

Мы определили объект ArxivComponent и функцию embed_arxiv. Он запускает метод “run” и использует возвращаемый путь к файлу в качестве параметра для Indexing Pipeline.

Теперь переходим к событию submit с функцией add_text в качестве параметра. Она отвечает за отображение чата в интерфейсе чата.

def add_text(history, text: str):    if not text:         raise gr.Error('enter text')    history = history + [(text,'')]     return history

Теперь мы определяем функцию get_response, которая получает и передает ответы LLM в интерфейс чата.

def get_response(history, query: str):    if not query:        gr.Error("Please provide a query.")        response = query_pipeline(query=query)    for text in response[0]:        history[-1][1] += text        yield history, ""

Эта функция берет строку запроса и передает ее в Query Pipeline для получения ответа. Наконец, мы перебираем строку ответа и возвращаем ее в чат-бота.

Собираем все вместе.

# Создаем экземпляр класса ArxivComponentarxiv_obj = ArxivComponent()def embed_arxiv(arxiv_id: str):    """    Получает и встраивает статью ArXiv по заданному идентификатору ArXiv.    Args:        arxiv_id (str): Идентификатор ArXiv статьи, которую нужно получить.    """    # Обращение к глобальной переменной FILE_PATH    global FILE_PATH        # Установка директории, в которой хранятся статьи ArXiv    dir: str = DIR        # Инициализация file_path None    file_path: str = None        # Проверка, предоставлен ли идентификатор ArXiv    if not arxiv_id:        raise gr.Error("Provide an Arxiv ID")        # Вызов метода run класса ArxivComponent для получения и сохранения статьи ArXiv    file_path_dict = arxiv_obj.run(arxiv_id)        # Извлечение пути к файлу из словаря    file_path = file_path_dict["file_path"]        # Обновление глобальной переменной FILE_PATH    FILE_PATH = file_path        # Вызов функции indexing_pipeline для обработки загруженной статьи    indexing_pipeline(file_path=file_path)    return "Successfully embedded the file"def get_response(history, query: str):    if not query:        gr.Error("Please provide a query.")        # Вызов функции query_pipeline для обработки запроса пользователя    response = query_pipeline(query=query)        # Добавление ответа в историю чата    for text in response[0]:        history[-1][1] += text        yield historydef add_text(history, text: str):    if not text:        raise gr.Error('Enter text')        # Добавление предоставленного пользователем текста в историю чата    history = history + [(text, '')]    return history# Создание интерфейса Gradio с блоками gr.Blocks() as demo:    with gr.Row():        with gr.Column(scale=60):            # Ввод текста для идентификатора Arxiv            text_box = gr.Textbox(placeholder="Введите идентификатор Arxiv",                                   interactive=True).style(container=False)        with gr.Column(scale=40):            # Кнопка для отправки идентификатора Arxiv            submit_id_btn = gr.Button(value="Submit")        with gr.Row():        # Интерфейс чат-бота        chatbot = gr.Chatbot(value=[]).style(height=600)        with gr.Row():        with gr.Column(scale=70):            # Ввод текста для запросов пользователя            query = gr.Textbox(placeholder="Введите строку запроса",                                interactive=True).style(container=False)        # Определение действий для щелчка по кнопке и отправки запроса    submit_id_btn.click(        fn=embed_arxiv,         inputs=[text_box],        outputs=[text_box],    )    query.submit(        fn=add_text,         inputs=[chatbot, query],         outputs=[chatbot, ],         queue=False    ).success(        fn=get_response,        inputs=[chatbot, query],        outputs=[chatbot,]    )# Выполнение очереди и запуск интерфейсадemo.queue()demo.launch()

Запустите приложение с помощью команды gradio app.py и посетите URL-адрес для взаимодействия с чат-ботом Arxic.

Вот как это будет выглядеть.

Вот репозиторий GitHub для приложения sunilkumardash9/chat-arxiv.

Возможные улучшения

Мы успешно создали простое приложение для общения с любой статьей Arxiv, но можно внести несколько улучшений.

  • Отдельное хранилище векторов: Вместо использования готового хранилища векторов вы можете использовать отдельные хранилища векторов, доступные с помощью Haystack, такие как Weaviate, Milvus и т.д. Это не только даст вам большую гибкость, но также значительно повысит производительность.
  • Цитирование: Мы можем добавить уверенность в ответы LLM, добавив правильные цитаты.
  • Дополнительные функции: Вместо просто чат-интерфейса мы можем добавить функции для отображения страниц PDF, используемых в качестве источников ответов LLM. Ознакомьтесь с этой статьей «Build a ChatGPT for PDFs with Langchain» и репозиторием GitHub для аналогичного приложения GitHub repository.
  • Фронтенд: Более хороший и интерактивный фронтенд был бы гораздо лучше.

Заключение

Таким образом, это было все о создании чат-приложения для статей Arxiv. Это приложение не ограничивается только Arxiv. Мы также можем расширить его до других сайтов, таких как PubMed. С некоторыми модификациями мы также можем использовать подобную архитектуру для общения с любым веб-сайтом. Так что в этой статье мы прошли от создания компонента Arxiv для загрузки статей Arxiv до встраивания их с использованием конвейеров haystack и наконец получения ответов от LLM.

Основные выдержки

  • Haystack – это решение с открытым исходным кодом для создания масштабируемых, готовых к производству приложений NLP.
  • Haystack предоставляет высокомодульный подход к созданию приложений реального мира. Он предоставляет узлы и конвейеры для оптимизации поиска информации, предобработки данных, вложения и генерации ответов.
  • Это библиотека с открытым исходным кодом от Huggingface для быстрого прототипирования любого приложения. Она обеспечивает простой способ обмена моделями ML с кем угодно.
  • Используйте аналогичный рабочий процесс для создания чат-приложений для других сайтов, таких как PubMed.

Часто задаваемые вопросы

Все изображения, показанные в этой статье, не принадлежат Analytics Vidhya и используются по усмотрению автора.