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

query = f"""Use the below article to answer questions about benefits in our company, if you don't know the answer write "Sorry I don't know the answer please contact HR"

Article:
\"\"\"
{benefits}
\"\"\"

Question: {question}?"""

Используя пример из предыдущего сообщения в блоге, предположим, что ваша компания начинает предоставлять больше преимуществ, а ваша переменная преимущества теперь содержит набор текста. Если вы попытаетесь выполнить вызов API OpenAI, вы получите такая ошибка:

InvalidRequestError: максимальная длина контекста этой модели составляет 4097 токенов, однако вы запросили 5125 токенов (4125 в приглашении; 1000 для завершения). Пожалуйста, уменьшите подсказку; или длина завершения.

Как видите, существует ограничение на количество токенов, которые вы можете отправить в одном запросе.

Чтобы решить эту проблему, вы должны проанализировать вопрос и отправить только те вложения, которые могут помочь ИИ ответить на этот вопрос.

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

Но что такое векторы?

Когда мы говорим о векторах в контексте машинного обучения, мы имеем в виду следующее: Векторы — это группы чисел, представляющие что-то.
Это может быть изображение, слово или почти что угодно.

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

Как работает векторный поиск?

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

Чтобы упростить понимание, давайте рассмотрим группы только с двумя числами. Например, сравним группу [1,2] с двумя другими группами: [2,2] и [2,500]. В этом случае группа [1,2] будет ближе к [2,2], чем к [2,500].

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

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

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

Для нашего примера мы будем использовать векторную базу данных с открытым исходным кодом под названием Milvus (https://milvus.io/).
Вы можете следовать этому руководству, чтобы настроить его локально: https://milvus.io/docs/install_standalone-docker.md

Мы все объяснили, у нас настроена наша база данных векторов, давайте погрузимся в код.

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

Как только у нас будут готовы данные, мы можем подключиться к базе данных и создать схему:

from pymilvus import (
    connections,
    FieldSchema,
    CollectionSchema,
    DataType,
    Collection,
)

connections.connect("default", host="localhost", port="19530")

fields = [
    FieldSchema(name="pk", dtype=DataType.INT64, is_primary=True, auto_id=False),
    FieldSchema(name="title", dtype=DataType.VARCHAR, max_length=300),
    FieldSchema(name="file_name", dtype=DataType.VARCHAR, max_length=300),
    FieldSchema(name="embeddings", dtype=DataType.FLOAT_VECTOR, dim=1536)
]
schema = CollectionSchema(fields, "articles with titles")
article_collection = Collection("articles2", schema)

Мы создаем простую схему, которая содержит первичный ключ, заголовок (первая строка в файле), имя файла и вложения.
Последнее поле — это вектор, который будет сгенерирован с помощью OpenAI text-embedding-ada-002 модель.
Последнее, что нужно сделать при настройке базы данных, — это настроить индексацию:

index_params = {
    "metric_type":"L2",
    "index_type":"IVF_FLAT",
    "params":{"nlist":32}
}

article_collection.create_index(
    field_name="embeddings",
    index_params=index_params
)

Приведенный выше код создает индекс для поля встраивания с использованием метрики L2, которая представляет евклидово расстояние.

Когда база данных готова, мы можем ее заполнить. Что нам делать:
- Пройтись по всем файлам
- Вызвать OpenAI API для генерации вектора из текста из файла
- Сохранить все векторы для всех файлов в базе данных

def get_embedding(text, model="text-embedding-ada-002"):
    text = text.replace("\n", " ")
    return openai.Embedding.create(input = [text], model=model)['data'][0]['embedding']
i = 0
primary_keys = []
titles = []
embeddings = []
file_names = []
for file in data:
    with open(file, 'r') as data_file:
        lines = data_file.readlines()
        title = lines[0]
        text = ' '.join([str(elem) for elem in lines])
        vector_embedding = get_embedding(text)
        primary_keys.append(i)
        titles.append(title)
        file_names.append(data_file.name)
        embeddings.append(vector_embedding)
        i = i+1

article_collection.insert([primary_keys, titles, file_names, embeddings])

Теперь у нас все готово для ответов на вопросы.

Ответ на вопрос будет состоять из нескольких шагов.

Шаг 1. Нам нужно найти, какая статья в базе данных лучше всего соответствует заданному запросу

Вопрос, указанный в качестве параметра метода, является вектором, подробнее об этом позже

def find_articles(question):
    query_embeddings = get_embedding(question)
    search_params = {"metric_type": "L2", "params": {"nprobe": 10}, "offset": 0}

    results = article_collection.search(
        data=[query_embeddings],
        anns_field="embeddings",
        param=search_params,
        limit=1,
        output_fields=['title', 'file_name'],
        expr=None,
        consistency_level="Strong"
    )

    for result in results[0]:
        return result.entity.get('file_name')

Мы используем встроенную функцию поиска от Milvus и поиск L2 (евклидово расстояние).
Мы также говорим базе данных, чтобы она возвращала только 1 результат, в производственной среде вы должны возвращать, возможно, первые 3 или 5 результатов, и мы говорим это вернуть заголовок и имя файла.

Шаг 2. Прочитайте необработанный текст для возвращенного файла, чтобы его можно было использовать для встраивания

def get_raw_text(file):
    with open(file, 'r') as data_file:
        lines = data_file.readlines()
        return ' '.join([str(elem) for elem in lines])

Простой метод, просто прочитайте файл с заданным именем файла и верните все его строки в одну строку.

Шаг 3. Позвоните в OpenAI, чтобы получить ответ на вопрос

def get_final_answer(question, summaries):
    prompt = f"""Use the below article to answer questions about benefits in our company, if you don't know the answer write "Sorry I don't know the answer please contact HR:
     Article:
    \"\"\"
    {summaries}
    \"\"\"
    Question: {question}?
"""
    response = openai.Completion.create(model="text-davinci-003", prompt=prompt, temperature=0, max_tokens=1000)
    return response['choices'][0]['text']

Тот же метод, что и в предыдущем посте, ничего дополнительно объяснять, просто задайте вопрос и встраивание (резюме) и получите ответ.

Шаг 4. Объедините все вышеперечисленное, чтобы получить ответ

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

def answer(question):
    article_collection.load()
    article_candidates = find_articles(question)
    context = get_raw_text(article_candidates)
    final_answer = get_final_answer(question, context)
    return final_answer

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

После этого мы читаем необработанный текст и вызываем функцию, которая вызывает OpenAI, вот и все.

Сделав это, теперь у нас есть система, которую мы можем расширить настолько, насколько захотим, мы можем предоставить дополнительные знания, просто вставив данные в базу данных векторов, и / или чат-бот сможет ответить на вопрос об этих новых данных.

Небольшое примечание:

article_collection.load()

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

Блокнот с кодом из этого поста доступен по адресу:
https://github.com/dsl94/gpt-vector-database/blob/main/gpt-vector-database.ipynb