О чем эта статья

На основе содержимого сайта компании по продаже автозапчастей Sterling Parts создадим приложение вопросов и ответов, для интеллектуальности которых используем GPT-3, для кода  —  Ruby.

Вопросы ИИ задают пользователи, ответы берутся со страниц FAQ, About Us и Terms and Conditions.

Необходимые условия

  1. Средний уровень знаний Ruby.
  2. Понимание, как интегрировать Ruby с Openai.

Github

Код доступен на Github.

Описание процесса

Чтобы подготовить базу знаний для задаваемых по ней в GPT-3 вопросов, создадим два скрипта на Ruby. В отличие от ChatGPT, обучим модель ответам по конкретному содержимому сайта.

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

Вот вектор  —  числовое представление значения, которое содержится в тексте:

Пример векторного вложения

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

Слово «семантический» относится к смысловому значению языка. Если при поиске по ключевым словам в базе данных выявляются точные или частичные совпадения, то при семантическом поиске  —  совпадения по смысловому значению или сути вопроса.

Пример:

Вопрос: «Как резать яблоки?»

Статья базы знаний 1: «Чтобы очистить апельсин, сначала нужно взять машинку для очистки апельсинов».

Статья базы знаний 2: «Чтобы нарезать яблоко, сначала нужно взять острый нож».

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

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

В GPT-3 имеется конечная точка вложения, ею текст преобразуется в вектор из 1500 значений, каждое из которых  —  какой-то признак текста.

Вот признаки, которые могут быть в векторном вложении:

  1. Семантическое значение.
  2. Части речи.
  3. Частота использования.
  4. Ассоциации с другими словами.
  5. Грамматическая структура.
  6. Тональность.
  7. Длина текста.

В GPT-3 имеются ограничения по токенам, поэтому базу знаний разбиваем на фрагменты всего не более 3000 токенов или 2000 слов. Каждый фрагмент преобразуется в вектор и сохраняется в БД доступным для поиска.

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

Этот фрагмент и вопрос отправляются в GPT-3, откуда выдается содержательный ответ.

Описание процесса

В базе знаний вопросов и ответов будет два скрипта на Ruby: embeddings.rb для подготовки векторных вложений и скрипт ИИ questions.rb.

Вот этапы создания ИИ вопросов и ответов:

  1. Разбиваем данные базы знаний на фрагменты по 2000 слов каждом и сохраняем в текстовом файле.
  2. Преобразуем каждый текстовый файл в векторное вложение с помощью конечной точки вложений OpenAI. [embeddings.rb]
  3. Сохраняем в БД для последующих запросов вложения и исходный текст базы знаний. Базой данных будет CSV-файл. [embeddings.rb]
  4. Получаем пользовательский вопрос и преобразуем в его векторное вложение. [questions.rb]
  5. Сравниваем вектор вопроса по базе данных и находим текст базы знаний с ближайшим к вопросу семантическим значением. [questions.rb]
  6. Специальной подсказкой передаем пользовательский вопрос и текст базы знаний в конечную точку GPT-3 completions. [questions.rb]
  7. Получаем от GPT-3 ответ и показываем его пользователю. [questions.rb]

Этап 1. Подготовка данных

В GPT-3 подсказка и ответ вместе ограничены 4096 токенами, поэтому важно преобразовать данные базы знаний на фрагменты по 2000 слов. Нужно достаточно места для токенов вопроса и ответа.

По возможности создаем фрагменты с близким значением. На страницах FAQ, About Us и Terms and Conditions сайта Sterling Parts менее 1000-2000 слов, поэтому семантически имеет смысл поместить каждую из них в отдельный текстовый файл.

В приложении на Ruby создаем папку training-data, в ней будет текст базы знаний для приложения:

Visual Studio с папкой training-data

Эти страницы сайта Sterling Parts сохранятся в стандартных файлах .txt:

Страница Terms and Conditions  —  самая большая, 2008 слов. Скопируйте и вставьте свой контент в текстовые файлы папки training-data. Названия неважны, главное  —  чтобы файлы были .txt:

Данные базы знаний в текстовых файлах

Этап 2. Преобразование данных в векторные вложения

Для работы с API OpenAI потребуется ruby-openai, устанавливаем нужные библиотеки:

gem install ruby-openai dotenv

Сохраняем API-ключ OpenAI в файле .env.

Импортируем библиотеку openai и API-ключом создаем новый экземпляр:

# embeddings.rb

require 'dotenv'
require 'ruby/openai'

Dotenv.load()

openai = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])

Далее  —  процесс извлечения всех данных из текстовых файлов. В Ruby проходится папка training-data, данные каждого текстового файла считываются и сохраняются в массиве:

# embeddings.rb

# Данные каждого файла сохраняются в массиве
text_array = []

# В папке /training-data перебираются все файлы .txt
Dir.glob("training-data/*.txt") do |file|
# Данные каждого файла считываются и добавляются в массив
# В методе dump пробелы преобразуются в символы новой строки \n
text = File.read(file).dump()
text_array << text
end

Все данные для обучения сохранены в text_array, преобразуем каждое тело текста в векторное вложение конечной точкой OpenAI embeddings, которой принимается два параметра: model в виде text-embedding-ada-002 и input в виде текста каждого файла:

# embeddings.rb

# Вложения сохраняются в этом массиве
embedding_array = []

# Перебирается каждый элемент массива
text_array.each do |text|
# Текст передается в API вложений, откуда возвращается вектор и
# сохраняется в переменной response.
response = openai.embeddings(
parameters: {
model: "text-embedding-ada-002",
input: text
}
)

# Из объекта response извлекается вложение
embedding = response['data'][0]['embedding']

# Создается хеш Ruby, в котором содержатся вектор и исходный текст
embedding_hash = {embedding: embedding, text: text}
# Хеш сохраняется в массиве.
embedding_array << embedding_hash
end

Выводим переменную embedding и видим вектор из 1500 значений. Это векторное вложение.

В embedding_array сохраняются значения векторных вложений и исходного текста, впоследствии сохраняемые в БД для целей семантического поиска.

Этап 3. Сохранение вложений в CSV, т. е. в БД

Для целей этой статьи база данных заменяется CSV-файлом. У специализированных векторных БД очень эффективные алгоритмы семантического поиска. Для Ruby оптимальный инструмент  —  Redis с его векторным поиском.

На этом этапе создается CSV-файл с двумя столбцами  —  embedding и text  —  для сохранения из каждого файла исходного текста с его векторным вложением.

Импортируем библиотеку csv:

# embeddings.rb

require 'dotenv'
require 'ruby/openai'
require 'csv'

Вот окончательный код для скрипта embeddings.rb, в нем создается CSV-файл с заголовками embedding и text, перебирается embedding_array, а соответствующие векторные вложения и текст сохраняются в CSV:

embeddings.rb

CSV.open("embeddings.csv", "w") do |csv|
# Так задаются заголовки
csv << [:embedding, :text]
embedding_array.each do |obj|
# Чтобы избежать ошибок с разделением запятыми между значениями в CSV,
# вектор вложения сохраняется в виде строки
csv << [obj[:embedding], obj[:text]]
end
end

Дальше скрипт embeddings.rb запускается, CSV-файл заполняется данными:

ruby embeddings.rb

Вот файловая структура после запуска:

Файловая структура Visual Studio Code.

Этап 4. Получение вопроса пользователя

Переходим к Ruby-скрипту questions.rb. Это основной файл для запроса в CSV векторов вопроса пользователя. При выполнении возвращается исходный текст с наибольшим семантическим сходством и интеллектуальный ответ от GPT-3.

Он запускается в приложении Rails, и вопрос пользователя получается из текстового поля представления. Для простоты выполним все в CLI.

Создаем файл questions.rb и импортируем нужную библиотеку:

# questions.rb

require 'dotenv'
require 'ruby/openai'
require 'csv'

Dotenv.load()

openai = OpenAI::Client.new(access_token: ENV['OPENAI_API_KEY'])

Вопрос пользователя будет в запросе к базе знаний, поэтому:

# questions.rb

puts "Welcome to the Sterling Parts AI Knowledge Base. How can I help you?"
question = gets

С помощью конечной точки OpenAI embeddings преобразуем вопрос пользователя в векторное вложение, чтобы потом математической формулой найти в файле embeddings.csv ближайший по сути и значению текст:

# questions.rb

# Вопрос преобразуется в векторное вложение
response = openai.embeddings(
parameters: {
model: "text-embedding-ada-002",
input: question
}
)

# Значение вложения извлекается
question_embedding = response['data'][0]['embedding']

Выводя вложение, увидим массив из 1500 значений. Это векторное вложение.

Этап 5. Поиск в CSV текста с ближайшим к вопросу семантическим значением

Здесь применяется косинусное сходство, им в машинном обучении определяется близость двух векторов. Это легко изобразить в двух измерениях на графике, поскольку у вектора два значения:

Источник

Косинусным сходством определяется отношение двух векторов. Его значение варьируется между 0 и 1, где 1  —  идентичные векторы.

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

С библиотекой Ruby cosine-similarity нет необходимости понимать внутренние механизмы выполняемых вычислений:

gem install cosine-similarity

Импортируем библиотеку вверху файла questions.rb:

# questions.rb

require 'dotenv'
require 'ruby/openai'
require 'csv'
require 'cosine-similarity'

Дальше перебираем все строки CSV-файла и сравниваем вектор вопроса с векторами исходного текста. В методе cosine_similarity вопрос сравнивается с каждым исходным текстом, возвращается число от 0 до 1.

Интересует сходство с наибольшим значением. Это текст, ближайший к вопросу по сути и значению:

# questions.rb

# По мере прохождения CSV в коде, показатели сходства сохраняются
similarity_array = []

# Проходится CSV, вычисляется косинусное сходство между
# вектором вопроса и вложением каждого текста
CSV.foreach("embeddings.csv", headers: true) do |row|
# Вложение извлекается из столбца, выполняется его парсинг в массив
text_embedding = JSON.parse(row['embedding'])

# В массив добавляется показатель сходства
similarity_array << cosine_similarity(question_embedding, text_embedding)
end

# Возвращается индекс наивысшего показателя сходства
index_of_max = similarity_array.index(similarity_array.max)

В переменной index_of_max теперь содержится индекс наивысшего показателя сходства. Им из CSV извлекается текст, который с вопросом пользователя отправляется в GPT-3:

# questions.rb

# Так сохраняется исходный текст
original_text = ""

# Проходится CSV, находится текст с наивысшим
# показателем сходства
CSV.foreach("embeddings.csv", headers: true).with_index do |row, rowno|
if rowno == index_of_max
original_text = row['text']
end
end

Этап 6. Передача пользовательского вопроса и текста базы знаний в конечную точку GPT-3 completions

Остаемся с файлом questions.rb. В скрипте теперь хранятся вопрос пользователя question и исходный текст original_text, ближайшие друг к другу по значению. Чтобы получить на вопрос пользователя интеллектуальный ответ, передадим эту информацию в конечную точку GPT-3 completions.

Специальной подсказкой GPT-3 подводится к тому, чтобы отвечать должным образом, соответственно вопросу пользователя и назначению базы знаний. Это делается в рамках структуры подсказки:

prompt = 
"You are an AI assistant. You work for Sterling Parts which is a car parts
online store located in Australia. You will be asked questions from a
customer and will answer in a helpful and friendly manner.

You will be provided company information from Sterline Parts under the
[Article] section. The customer question will be provided unders the
[Question] section. You will answer the customers questions based on the
article.

If the users question is not answered by the article you will respond with
'I'm sorry I don't know.'

[Article]
#{original_text}

[Question]
#{question}"

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

Если от GPT лучшего ответа не поступает, стоит поменять подсказку и поэкспериментировать с ней в интерактивной среде OpenAI.

Дальше, чтобы получить интеллектуальный ответ, подсказка передается в конечную точку GPT completions:

response = openai.completions(
parameters: {
model: "text-davinci-003",
prompt: prompt,
temperature: 0.2,
max_tokens: 500,
}
)

Температура в коде низкая, а значит, от GPT вернется наиболее вероятный ответ. Хотите от GPT более творческого подхода к ответам? Увеличьте ее до 0,9.

Ответ в этой конечной точке сгенерируется за секунды  —  в зависимости от нагрузки сервера.

Этап 7. Получение ответа от GPT-3 и отображение его для пользователя

В конце выводим ответ GPT для пользователя:

puts "\nAI response:\n"
puts response['choices'][0]['text'].lstrip

Скрипт завершен.

Отладка ответа

Почему от GPT возвращается неудовлетворительный ответ. Три фактора:

  1. Подготовка данных.
  2. Структура подсказки.
  3. Параметр температуры.

Подготовка данных

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

Чтобы доработать поиск сходства, разбейте их на мелкие, сгруппированные по значению тексты.

Структура подсказки

Здесь нужна практика, метод проб и ошибок вне конкуренции.

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

Параметр температуры

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

Доработка модели

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

Имеется ряд подходов, которые сводятся к встроенной подводке и тонкой настройке.

Встроенная подводка заключается в приведении в подсказке примеров вопросов и ответов, например, для получения от GPT ответов в конкретном формате или определенным способом. Я часто применяю этот подход, когда нужен ответ в JSON.

Тонкая настройка  —  совершенно иной подход, применяемый для обучения собственной GPT-модели задачам, которым она ранее не обучалась. Например, задавать ей вопросы, нацеленные на извлечение конкретной информации.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Kane Hooper: Creating an Intelligent Knowledge Base Q&A App with GPT-3 and Ruby

Предыдущая статьяПочему гейм-дизайнеру нужно программирование
Следующая статьяКак создать веб-приложение для преобразования речи в текст с Node.js