Deep dive into embeddings and semantic search
Языковые модели позволяют компьютерам выходить за рамки стандартного поиска по ключевым словам и находить нужные фрагменты по смыслу текста. Это называется семантическим поиском, и сегодня мы реализуем его на практике.
Применение семантического поиска выходит за рамки построения веб-поиска. С ним вы можете создать собственную поисковую систему по внуренним документам компании или помочь пользователям лучше ориентироваться в вашем FAQ. Еще один пример практического внедрения семантического поиска - рекомендации релевантных статей в блоге после прочтения одной из них.
Вот как это выглядит в формате схемы:
Итак, в рамках текущего гайда мы пройдем следующие шаги:
- Получим csv файл 1000 вопросов на русском
- Превратим текст в числовые embeddings
- Используем поиск по индексу и ближайшим соседям
- Визуализируем файл вопросов на основе embeddings
Весь представленный ниже код можно получить в виде .ipynb ноутбука здесь.
# Установите Compressa для создания embeddings, Umap – для уменьшения их размерности до 2 измерений;
# Altair – для визуализации, Annoy – для приблизительного поиска ближайших соседей;
!pip install langchain-compressa
!pip install umap-learn
!pip install altair
!pip install annoy
# Возможно, у вас также не установлены какие-то из популярных пакетов
!pip install pandas
!pip install numpy
!pip install tqdm
!pip install scikit-learn
!pip install gdown
1. Настраиваем окружение
# Импортируем библиотеки
from langchain_compressa import CompressaEmbeddings
import os
import gdown
import numpy as np
import re
import pandas as pd
from tqdm import tqdm
import altair as alt
from sklearn.metrics.pairwise import cosine_similarity
from annoy import AnnoyIndex
import umap.umap_ as umap
import warnings
warnings.filterwarnings('ignore')
pd.set_option('display.max_colwidth', None)
Для ячейки ниже вам понадобится API ключ Compressa. Вы можете получить его после регистрации.
os.environ["COMPRESSA_API_KEY"] = "ваш_ключ"
2. Загрузите датасет с вопросами
Мы специально подготовили 1000 вопросов на русском языке для работы с ними в рамках этого гайда.
#Скачиваем датасет c google диска Сompressa
file_id = '1wRC8bKBY5W8lrXU9cTKCgAdoL0g72ANI'
url = f'https://drive.google.com/uc?id={file_id}'
gdown.download(url, '1000_ru_questions.csv', quiet=False)
# Импортируем в pandas dataframe
df = pd.read_csv(file_path)
# Проверяем, что все загрузилось корректно
df.head(10)
2. Превращаем датасет вопросов в embeddings
Следующий шаг - превратить наши текстовые вопросы в числовые embeddings.
Схематично это выгл ядит так:
# Получаем наши embeddings
embeddings = CompressaEmbeddings()
texts = list(df['question'])
embeds = embeddings.embed_documents(texts)
# Проверяем размерность наших embeddings (напомним, что у нас 1000 вопросов)
embeds = np.array(embeds)
print(embeds.shape)
3. Используем поиск по индексу и ближайшему соседу
Еще одна схема для наглядности :)
Давайте теперь используем Annoy для построения индекса, который хранит embeddings в специальном виде, оптимизированном для быстрого поиска. Э тот подход хорошо масштабируется на большое количество текстов. Есть и другие решения - Faiss, ScaNN, PyNNDescent).
После создания индекса, можно использовать его для получения ближайших соседей для какого-то из существующих вопросов.
# Создаем поисковый индекс, передаем размер наших embeddings
search_index = AnnoyIndex(embeds.shape[1], 'angular')
# Добавляем все вектора в поисковый индекс и контрольно тестируем
for i in range(len(embeds)):
search_index.add_item(i, embeds[i])
search_index.build(10) # 10 trees
search_index.save('test.ann')
3.1. Ищем похожие по смыслу вопросы для одного примера из датасета
Если нас интересует только близость вопросов в датасете (без внешних запросов), тогда проще всего посчитать расстояние между всеми парами embeddings, которые мы получили ранее.
# Выбираем один из вопросов, чтобы найти другие, похожие на него
example_id = 109
# Достаем ближайших соседей
similar_item_ids = search_index.get_nns_by_item(example_id,10,
include_distances=True)
# Форматируем и выводим ближайшие вопросы и расстояние до них
results = pd.DataFrame(data={'вопросы': df.iloc[similar_item_ids[0]]['question'],
'расстояние': similar_item_ids[1]}).drop(example_id)
print(f"Вопрос:'{df.iloc[example_id]['question']}'\nПохожие вопросы:")
results