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

Сценарий

Но прежде чем начинать по нашему рецепту, мы должны подумать о нашем сюжете. Под сюжетом я подразумеваю то, что мы собираемся построить в конце дня. Что ж, вся эта серия блогов посвящена созданию простого приложения под названием DocChat. Концепция очень проста: приложение на базе LLM, в которое мы можем загружать и общаться с нашим документом.

Простой не так ли? Ну нет. Я имею в виду, да, это просто, когда вы просто используете несколько строк кода и позволяете различным сервисам, таким как HuggingFace, обслуживать основной сервер. Но когда вы работаете над созданием возможностей компании, вы никогда не сможете использовать эти 10 строк кода. Затем в этот раз вам придется построить большую часть вещей с самого начала и, возможно, использовать какую-то часть кода. Отсюда и моя серия блогов. Я отношусь к этому приложению LLM как к другому комплексному приложению ML с такими вещами, как

  • обслуживание через API
  • подключение к другим внутренним рабочим процессам
  • сделать вещи бессерверными и превратить их в облачные приложения на базе LLM и т. д.

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

О чем пока идет речь в сериале

Поэтому в этом блоге мы рассматриваем тему «Подключение LLM к базам знаний». Только для новичков здесь, которые сразу запрыгнули в этот блог, мы пока рассмотрели эти вещи.

  • Мы создали собственный LLM на основе gpt4 с пользовательскими функциями, обернутыми вокруг ленгчейна.
  • Затем мы обсудили, как структурировать проект и поддерживать текущие стандарты, используя беспорядок в файлах cookie, и как практиковать управление конфигурацией с помощью Hydra для управления сложными конфигурациями приложений.

Сегодня в дополнение к этим двум мы добавим несколько строк кода для поддержки функций добавления документов и внедрения этих документов в нашу векторную базу данных (здесь мы выбираем Chroma) и подключения ее к нашему LLM. Мы используем gpt4all embeddings, чтобы встроить текст для поиска по запросу.

Давайте начнем

Этот блог будет очень простым. Для начала нам предстоит выбрать папку, куда мы будем сбрасывать все документы. В моем случае у меня есть папка с именем source_documents/, куда я буду сбрасывать все свои PDF-файлы.

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

# specify the directory where Chroma will build the vector db
CHROMA_DB_DIRECTORY='db'

# specify where the source documents are
DOCUMENT_SOURCE_DIRECTORY='/path/to/source/documents'

# settings include things like which database backend chroma will use
# here we will be using duck db
# and document will be stored under db
# with no telemetry (i.e. nothing will be tracked)

CHROMA_SETTINGS = Settings(
    chroma_db_impl='duckdb+parquet',
    persist_directory=CHROMA_DB_DIRECTORY,
    anonymized_telemetry=False
)

# target number of relevant chunks to return 
TARGET_SOURCE_CHUNKS=4

# the number of characters that will make up a chunk
CHUNK_SIZE=500

# the number of overlapping characters to maintain the chunk
# continuity
CHUNK_OVERLAP=50

# whether to show or hide specific documents while LLM giving response
# i.e. whether we need to show the document sources that our LLM
# referred while giving the answer 

HIDE_SOURCE_DOCUMENTS=False

Эти параметры очень важны и могут быть изменены в зависимости от выбранного нами LLM (некоторые LLM лучше поддерживают огромную длину контекста, и в этом случае мы можем увеличить размер фрагмента до 1000 или более, а первые n фрагментов установить на 4, мы можно добавить больше, но тогда поиск может занять больше времени)

Теперь давайте импортируем необходимые импорты

import os
from typing import Optional
from chromadb.config import Settings
from langchain.vectorstores import Chroma
from langchain.document_loaders import DirectoryLoader
from langchain.embeddings import GPT4AllEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter

Надеюсь, нам не нужно много объяснять, что импортируется. Это довольно интуитивно понятно. Мы загружаем наш клиент Chroma vector db python, обернутый вокруг langchain, загрузчик каталогов Langchain, который загружает все документы, когда мы передаем каталог, GPT4AllEmebeddings от GPT4All Nomic AI и RecursiveCharacterTextSplitter, которые будут нести ответственность за создание фрагментов наших документов, прежде чем передать их нашему вложения.

ОБРАТИТЕ ВНИМАНИЕ: необходимо что-то установить (если в Linux) перед использованием загрузчика каталогов. Потому что DirectoryLoader в Langchain загружает любые документы, такие как .pdf/.txt/.ppt и т. д., и, следовательно, для его установки требуются дополнительные зависимости.

Установить пакеты Linux

# Update package lists
sudo apt update

# Install tesseract-ocr and libtesseract-dev
sudo apt install tesseract-ocr libtesseract-dev

# Install more dependencies
sudo apt-get install \
    libleptonica-dev \
    tesseract-ocr-dev \
    python3-pil \
    tesseract-ocr-eng

# Install the python depedencies

pip install pytesseract

Теперь давайте создадим простой вызов MyKnowledgeBase, куда мы добавим эти методы.

class PDFKnowledgeBase:
    def __init__(self, pdf_source_folder_path: str) -> None:
        """
        Loads pdf and creates a Knowledge base using the Chroma
        vector DB.
        Args:
            pdf_source_folder_path (str): The source folder containing 
            all the pdf documents
        """
        self.pdf_source_folder_path = pdf_source_folder_path

    def load_pdfs(self):
        # method to load all the pdf's inside the directory
        # using DirectoryLoader
        pass

    def split_documents(self, loaded_docs, chunk_size=1000):
        # split the documents into chunks and return the 
        # chunked documents
        pass

    def convert_document_to_embeddings(
        self, chunked_docs, embedder
    ):
        # convert the chunked docs to embeddings and add that 
        # to our vector db
        pass

    def return_retriever_from_persistant_vector_db(
        self, embedding_function
    ):  
        # return a retriever object which will retrieve the 
        # relevant chunks 
        pass

Теперь давайте начнем писать каждую функцию одну за другой. Начиная с наших load_pdf функций. Что ж, все, что нам нужно сделать, это создать экземпляр класса DirectoryLoader и указать папки с исходными документами внутри конструктора.

def load_pdfs(self):
    # instantiate the DirectoryLoader class 
    # load the pdfs using loader.load() function

    loader = DirectoryLoader(
        self.pdf_source_folder_path
    )
    loaded_pdfs = loader.load()
    return loaded_pdfs

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

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

def split_documents(
    self,
    loaded_docs,
    chunk_size: Optional[int] = 500,
    chunk_overlap: Optional[int] = 20,
):
    # instantiate the RecursiveCharacterTextSplitter class 
    # by providing the chunk_size and chunk_overlap

    splitter = RecursiveCharacterTextSplitter(
        chunk_size=chunk_size,
        chunk_overlap=chunk_overlap,
    )
    
    # Now split the documents into chunks and return
    chunked_docs = splitter.split_documents(loaded_docs)
    return chunked_docs

А вот и самое интересное. Теперь пришло время создать вложения для наших фрагментированных документов и зарегистрировать эти вложения в нашей базе знаний, которая будет нашей векторной базой данных (здесь Chroma)

def convert_document_to_embeddings(
    self, chunked_docs, embedder
):
    # instantiate the Chroma db python client 
    # embedder will be our embedding function that will map our chunked
    # documents to embeddings

    vector_db = Chroma(
        persist_directory=CHROMA_DB_DIRECTORY,
        embedding_function=embedder,
        client_settings=CHROMA_SETTINGS,
    )
    
    # now once instantiated we tell our db to inject the chunks
    # and save all inside the db directory
    vector_db.add_documents(chunked_docs)
    vector_db.persist()

    # finally return the vector db client object
    return vector_db

Теперь осталась наша последняя функция, которая, по сути, состоит в том, чтобы предоставить объект-извлекатель, который имеет только один объект, учитывая мои query, я могу просто получить соответствующие фрагменты документа из поиска сходства векторов в нашей предоставленной базе данных векторов.

def return_retriever_from_persistant_vector_db(
    self, embedder
):
    # first check whether the database is created or not
    # if not then throw error
    # because if the database is not instantiated then 
    # we can not get the retriever

    if not os.path.isdir(CHROMA_DB_DIRECTORY):
        raise NotADirectoryError(
            "Please load your vector database first."
        )
    
    vector_db = Chroma(
        persist_directory=CHROMA_DB_DIRECTORY,
        embedding_function=embedder,
        client_settings=CHROMA_SETTINGS,
    )

    # used the returned embedding function to provide the retriver object
    # with number of relevant chunks to return will be = 4 
    # based on the one we set inside our settings

    return vector_db.as_retriever(
        search_kwargs={"k": TARGET_SOURCE_CHUNKS}
    )

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

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

Вы проделали потрясающую работу 💪, теперь пришло время собрать все вместе, чтобы посмотреть, как это получилось.

import os
from typing import Optional

from chromadb.config import Settings
from langchain.vectorstores import Chroma
from langchain.document_loaders import DirectoryLoader
from langchain.embeddings import GPT4AllEmbeddings
from langchain.text_splitter import RecursiveCharacterTextSplitter


CHROMA_DB_DIRECTORY='db'
DOCUMENT_SOURCE_DIRECTORY='/path/to/source/documents'
CHROMA_SETTINGS = Settings(
    chroma_db_impl='duckdb+parquet',
    persist_directory=CHROMA_DB_DIRECTORY,
    anonymized_telemetry=False
)
TARGET_SOURCE_CHUNKS=4
CHUNK_SIZE=500
CHUNK_OVERLAP=50
HIDE_SOURCE_DOCUMENTS=False

class MyKnowledgeBase:
    def __init__(self, pdf_source_folder_path: str) -> None:
        """
        Loads pdf and creates a Knowledge base using the Chroma
        vector DB.
        Args:
            pdf_source_folder_path (str): The source folder containing 
            all the pdf documents
        """
        self.pdf_source_folder_path = pdf_source_folder_path

    def load_pdfs(self):
        loader = DirectoryLoader(
            self.pdf_source_folder_path
        )
        loaded_pdfs = loader.load()
        return loaded_pdfs

    def split_documents(
        self,
        loaded_docs,
    ):
        splitter = RecursiveCharacterTextSplitter(
            chunk_size=CHUNK_SIZE,
            chunk_overlap=CHUNK_OVERLAP,
        )
        chunked_docs = splitter.split_documents(loaded_docs)
        return chunked_docs

    def convert_document_to_embeddings(
        self, chunked_docs, embedder
    ):
        vector_db = Chroma(
            persist_directory=CHROMA_DB_DIRECTORY,
            embedding_function=embedder,
            client_settings=CHROMA_SETTINGS,
        )

        vector_db.add_documents(chunked_docs)
        vector_db.persist()
        return vector_db

    def return_retriever_from_persistant_vector_db(
        self, embedder
    ):
        if not os.path.isdir(CHROMA_DB_DIRECTORY):
            raise NotADirectoryError(
                "Please load your vector database first."
            )
        
        vector_db = Chroma(
            persist_directory=CHROMA_DB_DIRECTORY,
            embedding_function=embedder,
            client_settings=CHROMA_SETTINGS,
        )

        return vector_db.as_retriever(
            search_kwargs={"k": TARGET_SOURCE_CHUNKS}
        )

    def initiate_document_injetion_pipeline(self):
        loaded_pdfs = self.load_pdfs()
        chunked_documents = self.split_documents(loaded_docs=loaded_pdfs)
        
        print("=> PDF loading and chunking done.")

        embeddings = GPT4AllEmbeddings()
        vector_db = self.convert_document_to_embeddings(
            chunked_docs=chunked_documents, embedder=embeddings
        )

        print("=> vector db initialised and created.")
        print("All done")

Отличная работа 🔥. Круто, теперь давайте предположим, что у нас есть файл с именем knowledgebase.py, в котором записаны все эти коды. Внутри этого файла давайте сначала воспользуемся этой функцией для загрузки и создания нашей базы данных. Потому что в нашем основном файле мы просто загрузим БД и прикрепим ее с нашим LLM. Мы не хотим создавать документы каждый раз, когда запускаем наш основной файл. Следовательно, лучше всего будет либо запустить файл knowledgebase.py тут и там внутри if __name__ == '__main__', либо импортировать модуль MyKnowledgeBase в любой другой файл, чтобы вызвать его отдельно (возможно, когда вы часто добавляете файлы).

Теперь у нас осталось еще две вещи.

  1. Сначала используем наш замечательный класс, чтобы сделать нашу базу знаний
  2. Внутри нашей основной папки, где находится наш чат LLM, мы прикрепим наш ретривер из нашей векторной базы данных, чтобы создать цепочку поиска с использованием Langchain для завершения нашего полного конвейера.

Получение документов с помощью нашего модуля базы знаний

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

from knowledgebase import MyKnowledgeBase
from knowledgebase import (
    DOCUMENT_SOURCE_DIRECTORY
)

# kb is here knowledge base
kb = MyKnowledgeBase(
        pdf_source_folder_path=DOCUMENT_SOURCE_DIRECTORY
)

kb.initiate_document_injetion_pipeline()

И как только это будет сделано, наш каталог будет показывать что-то вроде этого

├── db
│   ├── chroma-collections.parquet
│   ├── chroma-embeddings.parquet
│   └── index
│       ├── id_to_uuid_bb6c59a0-db4d-4bcf-bc0d-e8c4fc78ee34.pkl
│       ├── index_bb6c59a0-db4d-4bcf-bc0d-e8c4fc78ee34.bin
│       ├── index_metadata_bb6c59a0-db4d-4bcf-bc0d-e8c4fc78ee34.pkl
│       └── uuid_to_id_bb6c59a0-db4d-4bcf-bc0d-e8c4fc78ee34.pkl
└── source_documents
    ├── diff_lm.pdf
    └── llama2.pdf

Внутри source_documents я хранил два PDF-файла исследовательской работы, и после вызова нашего конвейера загрузки создается новая папка db get, которая становится нашей векторной базой данных.

Использование Langchain для подключения нашей векторной базы данных и LLM

Теперь самое интересное. Здесь мы будем подключать нашу векторную базу данных к нашему LLM, здесь мы используем наш собственный класс LLM, обернутый вокруг Langchain, и мы используем gpt4all для нашего поставщика LLM. Вы можете проверить эту ссылку, чтобы увидеть, как мы это сделали.

Начните с импорта наших библиотек и модуля

# import our MyGPT4ALL class from mode module
# import MyKnowledgeBase class from our knowledgebase module

from model import MyGPT4ALL
from knowledgebase import MyKnowledgeBase
from knowledgebase import (
    DOCUMENT_SOURCE_DIRECTORY
)

# import all the langchain modules
from langchain.chains import RetrievalQA
from langchain.embeddings import GPT4AllEmbeddings

Обратите внимание:прежде чем идти дальше, я хочу сообщить вам, ребята, что некоторые части входящего кода сильно зависят от моих предыдущих блогов. Так что либо вы можете проверить этот блог, чтобы узнать, как обстоят дела, либо вы можете сделать другой, просто посмотреть, как обстоят дела, и вместо gpt4all вы можете заменить своего поставщика LLM по выбору, например Open AI или HuggingFace и т. д.

Начнем с настройки некоторых переменных

GPT4ALL_MODEL_NAME='ggml-gpt4all-j-v1.3-groovy.bin'
GPT4ALL_MODEL_FOLDER_PATH='/home/anindya/.local/share/nomic.ai/GPT4All/'
GPT4ALL_BACKEND='llama'
GPT4ALL_ALLOW_STREAMING=True
GPT4ALL_ALLOW_DOWNLOAD=False

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

llm = MyGPT4ALL(
    model_folder_path=GPT4ALL_MODEL_FOLDER_PATH,
    model_name=GPT4ALL_MODEL_NAME,
    allow_streaming=True,
    allow_download=False
)

Вместо MyGPT4ALL просто замените поставщика LLM по вашему выбору. Теперь давайте определим нашу базу знаний. Здесь мы делаем сильное предположение, что мы вызываем нашу базу знаний вместе с нашим ретривером после того, как мы выполнили процесс приема, иначе он выдаст ошибку.

embeddings = GPT4AllEmbeddings()

kb = MyKnowledgeBase(
    pdf_source_folder_path=DOCUMENT_SOURCE_DIRECTORY
)

# get the retriver object from the vector db 

retriever = kb.return_retriever_from_persistant_vector_db()

Итак, теперь у нас есть ретривер, у нас есть LLM, как мы можем соединить их? А вот и наш RetrievalQA от Langchain, который соединяет наш LLM с ретривером (или, можно сказать, соединяет их в цепочку), и, следовательно, мы можем делать все, что угодно, просто вызывая функцию.

Вот пример примера

qa_chain = RetrievalQA.from_chain_type(
    llm = llm,
    chain_type='stuff',
    retriever=retriever,
    return_source_documents=True, verbose=True
)

Здесь chain_type='stuff' означает, что все, что будет извлечено из цепочки, мы будем заполнять их (или, скажем, складывать друг на друга), чтобы создать окончательную подсказку для предоставления LLM. Вот потрясающая диаграмма, показывающая, как работает цепочка.

Итак, у нас все настроено, пора запускать двигатель. Мы создадим простой бесконечный цикл, в котором мы будем принимать ввод от пользователя, если ввод exit, то мы выйдем из цикла или начнем наш qa_chain, чтобы получить результаты.

Результаты представляют собой кортеж из двух вещей (ответ, релевантные_документы). Поскольку мы установили return_source_documents=True, следовательно, длина релевантных_документов не будет равна 0, иначе 0. И поскольку она не равна 0, мы просматриваем каждый документ и показываем после ответов. Это говорит нам о соответствующих документах, которые были извлечены, которые предоставили контекст для нашего LLM.

Вот пример кода для этого

# main.py file

while True:
    query = input("What's on your mind: ")
    if query == 'exit':
        break
    result = qa_chain(query)
    answer, docs = result['result'], result['source_documents']

    print(answer)

    print("#"* 30, "Sources", "#"* 30)
    for document in docs:
        print("\n> SOURCE: " + document.metadata["source"] + ":")
        print(document.page_content)
    print("#"* 30, "Sources", "#"* 30)

Итак, теперь, когда я делаю python3 main.py

Это дает этот потрясающий результат

What's on your mind: What is difference between Llama2 and Llama1


> Entering new RetrievalQA chain...
The main differences between LLAma1 (Lila) and LLaMa2 are in their architecture, training data, and performance on various benchmarks. Here's a brief overview of the key points to consider when comparing these two models:

* Architecture: The primary difference is that LLaMa2 has an additional layer for temporal organization of knowledge compared to its predecessor (Lila). This means it can handle more complex tasks involving time-related information, such as scheduling meetings or appointments. LLaMa1 was designed primarily for natural language processing and text generation applications without any explicit focus on temporal reasoning.
* Training data: LLaMa2 has access to a larger training dataset compared to its predecessor (Lila), which includes more diverse examples of human-generated content, such as emails, social media posts, or even chat transcripts from online platforms like Reddit or Twitter. This increased diversity in the training set may lead to better performance on tasks that require understanding temporal patterns and relationships between events over time.
* Performance: LLaMa2 has shown significant improvements compared to Lila when it comes to handling complex natural language processing (NLP) tasks, such as text classification, question-answering, or named entity recognition. This is likely due in
> Finished chain.
The main differences between LLAma1 (Lila) and LLaMa2 are in their architecture, training data, and performance on various benchmarks. Here's a brief overview of the key points to consider when comparing these two models:

* Architecture: The primary difference is that LLaMa2 has an additional layer for temporal organization of knowledge compared to its predecessor (Lila). This means it can handle more complex tasks involving time-related information, such as scheduling meetings or appointments. LLaMa1 was designed primarily for natural language processing and text generation applications without any explicit focus on temporal reasoning.
* Training data: LLaMa2 has access to a larger training dataset compared to its predecessor (Lila), which includes more diverse examples of human-generated content, such as emails, social media posts, or even chat transcripts from online platforms like Reddit or Twitter. This increased diversity in the training set may lead to better performance on tasks that require understanding temporal patterns and relationships between events over time.
* Performance: LLaMa2 has shown significant improvements compared to Lila when it comes to handling complex natural language processing (NLP) tasks, such as text classification, question-answering, or named entity recognition. This is likely due in

############################## Sources ##############################

> SOURCE: /home/anindya/workspace/repos/end-to-end-llm/source_documents/llama2.pdf:
Liama 2 is a new technology that carries risks with use. Testing conducted to date has been in English, and has not covered, nor could it cover all scenarios. For these reasons, as with all LLMs, Lama 2’s potential outputs cannot be predicted in advance, and the model may in some instances produce inaccurate or objectionable responses to user prompts. Therefore, before deploying any applications of LLama 2, developers should perform safety testing and tuning tailored to their specific applications of the model. Please see the Responsible Use Guide available available at https://ai.meta.com/1lama/responsible-user- guide

Table 52: Model card for LLAMA 2.

Это круто 😎😎. Ууууу, поздравляю, что зашли так далеко. Эта часть сообщения в блоге намеренно не охватывает сторону управления конфигурацией кода и, следовательно, упрощена. Чтобы увидеть, как мы делаем то же самое, но с лучшими практиками, загляните в репозиторий GitHub.

Итак, в этом блоге вы узнали, как мы можем использовать Langchain, Chroma и GPT4All для создания собственных возможностей LLM без использования Open AI-подобного API.

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

Рекомендации