Векторные представления (эмбеддинги, векторы) — это по‑настоящему приятный инструмент, но в любом рассказе о векторных представлениях эта техника скрыта за ворохом каких‑то страшных словес.
Если вам удастся продраться через эти словеса, то вы откроете для себя мощные и интересные приёмы, применимые для решения всевозможных интересных задач.
Я выступал с лекцией о векторных представлениях на конференции PyBay 2023. Эта статья — улучшенная версия той самой лекции, и она должна быть интересна сама по себе, даже если не смотреть видео.
Если вы пока не знакомы с эмбеддингами, то, полагаю, в этой статье вы найдёте всю необходимую информацию, которая позволит вам приступить к их использованию при решении реалистичных задач.
38-минутная видеоверсия
Вот видеоверсия той лекции, которую я прочитал на PyBay.
Что такое векторные представления?
Векторные представления — это технология, смежная с гораздо более широкой темой больших языковых моделей (LLM). На основе больших языковых моделей построены такие инструменты как ChatGPT, Bard и Claude.
![](https://habrastorage.org/getpro/habr/upload_files/334/4d2/9c0/3344d29c03ad02430717a48731eacdfd.jpeg)
В основе векторных представлений лежит один приём, следующий: берём некий образец контента — например, запись в блоге — и преобразуем эту информацию в массив чисел с плавающей точкой.
Ключевая черта этого массива заключается в том, что длина его всегда будет одинакова, независимо от длины контента. Длина зависит от того, какой моделью эмбеддинга вы пользуетесь. Соответственно, массив может иметь длину 300, 1000 или 1536 чисел.
Чтобы было проще рассуждать о данном массиве чисел, лучше всего представить, что он образует координатную сетку, наложенную на очень причудливое многомерное пространство.
Сложно визуализировать 1 536-мерное пространство, поэтому здесь я проиллюстрирую ту же идею в трёх измерениях:
![](https://habrastorage.org/getpro/habr/upload_files/e3f/37c/d10/e3f37cd10002ba46230e62f6073f7f9b.jpeg)
Зачем же помещать контент в такое пространство? Оказывается, можно узнать интересные детали о данной информации, ориентируясь на её местоположение — в частности, какие информационные фрагменты находятся поблизости от исследуемого.
Положение в данном пространстве соответствует семантике, заложенной в данный фрагмент контента — соответствует страннейшей, в основном непостижимой картине мира, которая выстроена этой моделью. Она может схватывать в контенте цвета, формы, концепции или всевозможные иные характеристики, которые были вплетены в вектор.
Никто в полной мере не представляет, что именно означает каждое из этих отдельных чисел, но известно, что по расположению чисел можно узнавать ценную информацию о контенте.
Находим похожий контент при помощи векторных представлений
Одна из первых задач, которую мне довелось решить при помощи векторных представлений — реализовать фичу «похожий контент» в моём блоге по TIL (TIL = «Things I Learnt» («Что я узнал»)). Мне хотелось выводить список «похожие статьи» в подвале каждой страницы.
Я это сделал именно при помощи векторных представлений — в данном случае, воспользовался моделью OpenAI text‑embedding‑ada-002, предоставляемой через соответствующий API.
Сейчас у меня на сайте 472 статьи. Для каждой из этих статей я вычислил 1 536-мерное векторное представление (массив из чисел с плавающей точкой) и сохранил эти векторы в базе данных SQLite у меня на сайте.
Теперь, если я хочу найти статьи, похожие на заданную, я могу вычислить косинусное сходство между векторным представлением для данной статьи и для любой другой статьи, учтённой в базе данных, а затем найти 10 совпадений, которые расположены «ближе всего» к исходной статье.
Вот пример списка, расположенного внизу этой страницы. Топ-5 статей, соотносящихся с постом Geospatial SQL queries in SQLite using TG, sqlite‑tg and datasette‑sqlite‑tg — это:
Geopoly in SQLite—2023–01–04
Viewing GeoPackage data with SpatiaLite and Datasette—2022–12–11
Using SQL with GDAL—2023–03–09
KNN queries with SpatiaLite—2021–05–16
GUnion to combine geometries in SpatiaLite—2022-04-12
Список получился очень хорошим!
Вот функция Python, при помощи которой вычисляются расстояния по данному косинусному сходству:
def cosine_similarity(a, b):
dot_product = sum(x * y for x, y in zip(a, b))
magnitude_a = sum(x * x for x in a) ** 0.5
magnitude_b = sum(x * x for x in b) ** 0.5
return dot_product / (magnitude_a * magnitude_b)
Мой TIL-сайт работает на Python-фреймворке Datasette, поддерживающем построение сайтов поверх базы данных SQLite. Подробнее о том, как работает этот механизм, я описывал в статье, посвящённой архитектурному паттерну Baked Data.
Можно выполнять поиск по таблице SQLite, в которой содержатся вычисленные векторы. Они находятся в tils/embeddings.
![](https://habrastorage.org/getpro/habr/upload_files/c44/abf/64e/c44abf64e818aed1e953895d2901da5a.png)
Это двоичные значения. Можно выполнить этот SQL-запрос, чтобы просмотреть их в шестнадцатеричном формате:
select id, hex(embedding) from embeddings
![](https://habrastorage.org/getpro/habr/upload_files/ee9/beb/3d6/ee9beb3d65fe51ae15dbc934154dde4d.png)
В таком виде их всё ещё не слишком удобно читать. Можно воспользоваться специальной SQL-функцией llm_embed_decode()
, чтобы превратить их в массив в формате JSON:
select id, llm_embed_decode(embedding) from embeddings limit 10
Можете попробовать здесь. Как видите, каждая статья сопровождается таким же массивом, состоящим из 1 536 чисел с плавающей точкой.
![](https://habrastorage.org/getpro/habr/upload_files/fd0/58e/b5e/fd058eb5e4d294f624c04580e56c554e.png)
Можно воспользоваться и другой специальной SQL-функцией, llm_embed_cosine(vector1, vector2)
, которая также рассчитывает такие косинусные расстояния и находит наиболее похожий контент.
Эта SQL-функция определяется здесь в моём плагине datasette-llm-embed.
Вот запрос, возвращающий пять статей, наиболее похожих на мою статью о SQLite из TG:
select
id,
llm_embed_cosine(
embedding,
(
select
embedding
from
embeddings
where
id = 'sqlite_sqlite-tg.md'
)
) as score
from
embeddings
order by
score desc
limit 5
В ответ на выполнение этого запроса получаем следующие результаты:
id | score |
sqlite_sqlite-tg.md | 1.0 |
sqlite_geopoly.md | 0.8817322855676049 |
spatialite_viewing-geopackage-data-with-spatialite-and-datasette.md | 0.8813094978399854 |
gis_gdal-sql.md | 0.8799581261326747 |
spatialite_knn.md | 0.8692992294266506 |
Как и ожидалось, похожесть статьи самой на себя равна 1.0. Все остальные статьи соотносятся в SQLite с геопространственными SQL‑запросами.
На выполнение этого запроса затрачивается примерно 400 мс. Чтобы получалось быстрее, я предвычисляю топ-10 значений сходства для каждой статьи и сохраняю их в отдельной таблице под названием tils/similarities.
![](https://habrastorage.org/getpro/habr/upload_files/8b6/2d0/2ba/8b62d02baa9dd6c17db756baf8ed9b8e.png)
Я написал на Python функцию для поиска похожих документов в этой таблице и вызывал её из шаблона, при помощи которого отображалась вся страница со статьёй.
В моей статье «Хранение и выдача документов при помощи openai‑to‑sqlite и векторных представлений» в TIL детально объяснено, как всё это работает, в том числе, как при помощи GitHub Actions выбирать новые векторные представления, записав эти действия в рамки сборочного скрипта, который развёртывает сайт.
Чтобы реализовать этот проект, я воспользовался API для работы с векторами от OpenAI. Он работает с минимальными издержками: для моего TIL‑сайта я обошёлся векторами всего примерно для 402 500 токенов, а при цене $0,0001 за 1 000 токенов я потратил на это всего 4 цента!
Работать с этим API проще простого: методом POST отправляете ему некоторый текст вместе с ключом API, а он возвращает вам ответ массив в формате JSON, состоящий из чисел с плавающей точкой.
![](https://habrastorage.org/getpro/habr/upload_files/613/81e/3bd/61381e3bd532aae7630c39bef857a98c.jpeg)
Но... это проприетарная модель. Несколько месяцев назад OpenAI упразднила некоторые из своих старых моделей векторных представлений. В результате у вас могут возникнуть проблемы, если вы успели сохранить много векторов из этих моделей. Ведь если вы соберётесь работать с новой нормально поддерживаемой моделью, в которую будете внедрять новые векторы, то вам придётся и пересчитывать все старые векторы в контексте этой новой модели.
![](https://habrastorage.org/getpro/habr/upload_files/97f/5a3/492/97f5a3492ed2c5ec51bca72d6f181163.jpeg)
К чести OpenAI, компания обещала «покрыть финансовые издержки пользователей, повторно внедряющих старый контент при помощи новых моделей», но это всё равно повод соблюдать осторожность и не впадать в зависимость от проприетарных моделей.
Есть и хорошие новости — так, уже появились исключительно мощные модели с открытой лицензией, которые вы можете эксплуатировать на вашем железе и не бояться, что вашу модель отключат. Сейчас поговорим о них подробнее.
Разбираемся на примере Word2Vec, как устроены и как работают эти новинки
Около 10 лет назад Google Research опубликовала влиятельную статью, в которой описала созданную в компании модель векторных представлений — одну из самых ранних. Она называется Word2Vec.
Эта статья называется Efficient Estimation of Word Representations in Vector Space (Эффективная оценка представлений слов в векторном пространстве), датирована 16-м января 2013 года. Именно с этой статьи начался рост интереса к векторным представлениям.
Модель Word2Vec принимает отдельные слова и превращает каждое из них в список из 300 чисел. В этом списке чисел улавливается некоторая информация, связанная со смыслом зашифрованного слова.
Работу такой модели удобнее всего проиллюстрировать на примере.
По адресу turbomaze.github.io/word2vecjson находится интерактивный инструмент, собранный Энтони Лю, а также 10 000-е подмножество слов из корпуса Word2Vec. Можете посмотреть этот JavaScript-файл, в котором приведён JSON для этих 10 000 слов и связанные с каждым из них 300-компонентные массивы чисел.
![](https://habrastorage.org/getpro/habr/upload_files/517/551/60a/51755160a93db5ce0f36431344eeb7ae.jpg)
Ищем слово, чтобы далее найти похожие слова, опираясь на косинусное расстояние до их представлений в модели Word2Vec. Например, для слова «france» возвращаются следующие связанные результаты:
Слово | Сходство |
france | 1 |
french | 0,7000748343471224 |
belgium | 0,6933180492111168 |
paris | 0,6334910653433325 |
germany | 0,627075617939471 |
italy | 0,6135215284228007 |
spain | 0,6064218103692152 |
Получилась смесь реалий, связанных с Францией, и понятий из географии Европы.
В данном случае было бы очень интересно попробовать арифметические операции над этими векторами.
Берём вектор для «germany» (Германия), прибавляем к нему «paris» (Париж) и вычитаем «france» (Франция). Полученный вектор оказывается наиболее близок к «berlin» (Берлину)!
В этой модели схвачена некоторая информация о государствах и географии, причём её хватает, чтобы при помощи обычной арифметики можно было на основе этой модели извлекать новые данные о мире.
Модель Word2Vec была обучена на корпусе из 1,6 миллиарда слов. Современные модели векторных представлений обучены на гораздо более широких множествах данных и позволяют гораздо полнее судить о взаимосвязях, лежащих в их основе.
Рассчитываем векторное представление на основе моей большой языковой модели LLM
Я написал утилиту командной строки и библиотеку на Python, которая называется LLM.
Подробнее о LLM (больших языковых моделях) можно прочитать здесь:
llm, ttok and strip-tags—CLI tools for working with ChatGPT and other LLMs
The LLM CLI tool now supports self-hosted language models via plugins
Build an image search engine with llm-clip, chat with models with llm chat
Моя библиотека LLM — это инструмент для работы с большими языковыми моделями. Чтобы установить его, сделайте так:
pip install llm
Или при помощи Homebrew:
brew install llm
Можете пользоваться моим инструментом как утилитой для командной строки, позволяющим взаимодействовать с LLM, либо как библиотеку Python.
Из коробки она может работать с OpenAI API. Устанавливаем ключ API, а затем можем выполнять такие команды:
llm 'ten fun names for a pet pelican'
Но самое интересное начинается, когда мы доходим до установки плагинов. При помощи этих плагинов можно добавить совершенно новые языковые модели, в том числе такие, что работают непосредственно на вашей машине.
Несколько месяцев назад я расширил LLM так, что теперь в ней поддерживаются и модели векторных представлений.
Вот как при помощи LLM использовать модель с запоминающимся названием all-MiniLM-L6-v2:
![](https://habrastorage.org/getpro/habr/upload_files/268/e08/a6e/268e08a6e72bd0dca9c56b3746747de3.jpeg)
Сначала устанавливаем llm, а затем уже с её помощью устанавливаем плагин llm-sentence-transformers, служащий обёрткой для библиотеки SentenceTransformers.
pip install llm
llm install llm-sentence-transformers
Далее нужно зарегистрировать модель all-MiniLM-L6-v2
. Следующая команда скачивает модель с Hugging Face на ваш компьютер:
llm sentence-transformers register all-MiniLM-L6-v2
Можно протестировать эту модель, сделав векторное представление одной (следующей) фразы:
llm embed -m sentence-transformers/all-MiniLM-L6-v2 \
-c 'Hello world'
На вывод получаем массив JSON, который начинается так:
[-0.03447725251317024, 0.031023245304822922, 0.006734962109476328, 0.026108916848897934, -0.03936201333999634, ...
Сами по себе подобные векторные представления не очень интересны — чтобы извлекать полезные результаты, мы должны сохранить такие представления и начинать их сравнивать.
LLM может сохранять векторные представления в виде «коллекции» — это таблица SQLite. При помощи команды embed‑multi можно одновременно переводить в векторные представления несколько единиц контента одновременно и сохранять их в виде коллекции.
Вот как работает следующая команда:
llm embed-multi readmes \
--model sentence-transformers/all-MiniLM-L6-v2 \
--files ~/ '**/README.md' --store
Далее мы заполняем коллекцию «readmes».
Опция --files
принимает два аргумента: каталог, в котором нужно искать информацию, а также маску, с которой сопоставляются имена файлов. В данном случае я выполняю рекурсивный поиск в моём домашнем каталоге — ищу любой файл с именем README.md
.
Опция --store
приводит к тому, что LLM сохраняет в таблице SQLite не только векторное представление, но и необработанный текст.
У меня на компьютере для выполнения этой команды потребовалось около 30 минут, но она сработала! Теперь у меня есть коллекция readmes
, в которой 16 796 строк — по одной на каждый файл README.md
, находящийся у меня в домашнем каталоге.
Поиск на основе тональности
Итак, у нас есть коллекция векторных представлений, и мы можем искать в ней информацию при помощи команды llm similar:
![](https://habrastorage.org/getpro/habr/upload_files/301/20d/fb7/30120dfb7411a77a72ddd1e999e55c7d.jpeg)
llm similar readmes -c 'sqlite backup tools' | jq .id
Мы запрашиваем из коллекции readmes такие элементы, которые схожи с векторным представлением фразы «sqlite backup tools».
По умолчанию эта команда выводит информацию в формате JSON, и этот вывод содержит полный текст файлов README, поскольку ранее мы сохранили их при помощи --store
.
Если мы по конвейеру передаём результаты через jq .id
, то команда выводит только ID подходящих строк.
В верхней части списка — следующие результаты:
"sqlite-diffable/README.md"
"sqlite-dump/README.md"
"ftstri/salite/ext/repair/README.md"
"simonw/README.md"
"sqlite-generate/README.md"
"sqlite-history/README.md"
"dbf-to-sqlite/README.md"
"ftstri/sqlite/ext/README.md"
"sqlite-utils/README.md"
"ftstri/sqlite/README.md'
Это хорошая подборка! В каждом из этих README описывается либо инструмент для работы с резервными копиями SQLite, либо проект, который тем или иным образом связан с резервным копированием.
В данном случае интересно, что в тексте этих README совсем не обязательно будет буквально фигурировать слово «backup» (резервная копия). Контент семантически связан с этим термином, но полных текстовых совпадений в нём может и не быть.
Такую работу можно назвать «семантическим поиском». Мне нравится представлять её как поиск на основе тональности.
По тональности текста эти README явно связаны с искомым термином, и всё на основе той причудливой логики, согласно которой представления слов выстраиваются в многомерном пространстве.
Эта технология полезна до абсурда. Если вы когда‑нибудь пытались написать поисковый движок для сайта, то знаете, что при помощи одних лишь точных совпадений человек не всегда может найти то, что ищет.
Подобный семантический поиск поможет нам создавать более качественные поисковики для работы с самыми разными видами контента.
Инструмент Symbex для создания векторных представлений кода
Я занимаюсь разработкой ещё одного инструмента, он называется Symbex. При помощи этого инструмента можно исследовать символы, встречающиеся в базе кода на Python.
Исходно я написал его, чтобы было удобнее находить функции и классы Python, а затем по конвейеру передавать их в LLM. Она бы объясняла и помогала переписывать их.
Затем я догадался, что при помощи этого инструмента можно вычислить векторные представления для всех функций в базе кода, а затем на основе этих представлений написать поисковик специально для работы с кодом.
Добавил возможность, позволяющую выводить в формате JSON или CSV информацию о найденных символах в том же формате, который llm embed‑multi может использовать в качестве ввода.
Вот как я собрал коллекцию всех функций в моём проекте Datasette, воспользовавшись свежей моделью под названием gte‑tiny — всего 60 МБ размером!
llm sentence-transformers register TaylorAI/gte-tiny
cd datasette/datasette
symbex '*' '*:*' --nl | \
llm embed-multi functions - \
--model sentence-transformers/TaylorAI/gte-tiny \
--format nl \
--store
symbex '*' '*:*' --nl
находит в текущем каталоге все функции (*) и методы класса (паттерн *:*) и выводит их в формате JSON, где разделителем служит переход на новую строку.
В качестве ввода команда llm embed-multi ... --format nl
ожидает JSON с построчной разбивкой, поэтому прямо в неё мы можем по конвейеру передавать вывод symbex
.
В результате векторные представления по умолчанию хранятся в базе данных LLM SQLite, стандартной для таких случаев. Чтобы указать альтернативное местоположение, можете добавить --database /tmp/data.db
.
А теперь... я могу выполнять в моей базе кода семантический поиск с учётом тональности!
Для этого можно было бы воспользоваться командой llm similar, но такие операции поиска также выполнимы при помощи Datasette как таковой.
Вот SQL-запрос для этой цели, чтобы его выполнить, мы воспользуемся рассмотренным выше плагином datasette-llm-embed:
with input as (
select
llm_embed(
'sentence-transformers/TaylorAI/gte-tiny',
:input
) as e
)
select
id,
content
from
embeddings,
input
where
collection_id = (
select id from collections where name = 'functions'
)
order by
llm_embed_cosine(embedding, input.e) desc
limit 5
Datasette автоматически преобразует параметр :input в поле формы.
Выполняя этот код, в ответ я получаю функции, концептуально относящиеся к перечислению плагинов:
![](https://habrastorage.org/getpro/habr/upload_files/29f/c1c/f88/29fc1cf88190b38248a16a7a569a397e.jpg)
Ключевая идея в данном случае — использовать SQLite в качестве точки интеграции и того субстрата, на котором можно скомбинировать сразу множество инструментов.
Я могу задействовать отдельные инструменты, извлекающие функции из базы кода, прогоняющие их через модель векторного представления, записывающие эти векторы в SQLite, а уже затем выполнять запросы применительно к результатам.
Теперь реально представить в векторной форме любую информацию, которую можно подавать в инструмент конвейерным способом, чтобы затем она поступала на обработку в другие компоненты данной экосистемы.
Сочетание векторных представлений текста и картинок при помощи CLIP
В настоящее время моя излюбленная модель векторных представлений — это CLIP.
CLIP — это просто потрясающая модель, выпущенная компанией OpenAI ещё в январе 2021 года, когда большая часть их разработок ещё оставалась открытой. Эта модель позволяет включать в векторные представления одновременно текст и картинки.
В данном случае особенно важно, что она представляет оба этих вида информации в одном и том же векторном пространстве.
Если вы хотите сделать векторное представление строки «dog» («собака»), то получите локацию в 512-мерном пространстве (зависящую от того, какая конфигурация CLIP у вас задана).
Если сделаете векторное представление фотоснимка собаки, то получите локацию в том же пространстве… причём, в пересчёте на расстояние эта единица будет располагаться поблизости от векторного представления строки «dog»!
Таким образом, можно искать «родственные» изображения, опираясь на текст, и наоборот, искать «близкий» текст по картинкам.
Я соорудил интерактивную демку, чтобы было понятнее, как всё это работает. Демка представляет собой ноутбук Observable, выполняющий модель CLIP непосредственно в браузере.
Это довольно тяжёлая веб‑страница; для работы с ней требуется загрузить 158 МБ ресурсов (64,6 МБ для текстовой модели CLIP и 87,6 МБ для модели, работающей с картинками). Но, как только она загружена, с её помощью можно преобразовать в вектор картинку, затем преобразовать в вектор текстовую строку и вычислить расстояние между двумя этими векторами.
Вот, например, фото, которое я сделал на пляже:
![](https://habrastorage.org/getpro/habr/upload_files/e0d/2b3/ab7/e0d2b3ab7e3c29deef4fb82c09e0ee15.jpg)
Далее будем вводить различные строки и вычислять балл сходства между ними и картинкой, выражаемый в данном случае как процентное значение:
![](https://habrastorage.org/getpro/habr/upload_files/cea/b3c/1b7/ceab3c1b799c3340ffb82da1f9b65073.gif)
Текст | Балл |
Beach | 26, 946% |
city | 19,839% |
sunshine | 24,146% |
sunshine beach | 26,741% |
california | 25,686% |
california beach | 27,427% |
Дух захватывает, что всё это делается на чистом JavaScript, который выполняется в браузере!
Конечно, тут сразу просматривается загвоздка: такая модель нам не поможет, если мы дадим ей произвольное фото и спросим: «насколько эта картинка похожа на термин „city“ (город)?».
Эта проблема решается, если выстроить поверх данной модели дополнительные интерфейсы. Опять же, можно с её помощью разрабатывать поисковики, работающие с учётом анализа тональности.
Вот отличный пример такого рода.
Искатель вентилей: поиск вентилей для кранов при помощи CLIP
Дрю Брёниг воспользовался LLM и моим плагином llm‑clip, создав с их помощью сантехнический поисковик — он позволял находить вентили для обычных кранов.
Он как раз делал ремонт у себя в ванной, и ему понадобилось купить новые вентили для кранов. Так, он проанализировал методом скрапинга 20 000 фотографий вентилей для кранов с сайта одной сантехнической компании и исследовал все их при помощи CLIP.
На основе полученных результатов он разработал Faucet Finder— собственный инструмент (развёртываемый на основе Datasette) для поиска таких вентилей, которые выглядят схоже с другими вентилями.
![](https://habrastorage.org/getpro/habr/upload_files/516/50c/71a/51650c71a5701668b3e449a8eb13d2c6.jpg)
В числе прочего такой инструмент позволяет вам найти дорогой вентиль, который вам нравится, а затем подыскать более дешёвые варианты, которые визуально очень похожи на первый!
Подробнее Дрю написал о своём проекте в статье Finding Bathroom Faucets with Embeddings.
В демо‑программе Дрю используются предвычисленные векторные представления, позволяющие выводить похожие результаты без необходимости выполнять модель CLIP на сервере.
Вдохновившись этим примером, я потратил некоторое время, постаравшись разобраться, как развернуть серверную модель CLIP, которая хостилась бы прямо на моём аккаунте Fly.io.
На инстансе Datasette от Дрю присутствует эта таблица с векторными представлениями, предоставляемыми через Datasette API.
Я развернул мой собственный инстанс с этим API, чтобы создавать векторные представления текстовых строк, затем собрал демо‑ноутбук Observable, работающий с обоими этими API и комбинирующий результаты.
observablehq.com/@simonw/search-for-faucets-with-clip-api
Теперь я могу искать такие комбинации как «золотисто-фиолетовый» и получать в ответ варианты вентилей с учётом тональности:
![](https://habrastorage.org/getpro/habr/upload_files/3f8/846/020/3f8846020b395b505f4fc54546d93d38.jpg)
Возможность всего за несколько часов поднять такой ультраспецифичный поисковик — как раз такая штука, благодаря которой я стремлюсь иметь в арсенале векторные представления и уметь пользоваться этим инструментом.
Кластеризация векторных представлений
Поиск родственного контента и семантический поиск/поиск на основе тональности — два наиболее распространённых варианта применения векторов, но с их помощью можно проделывать ещё множество других приятных вещей.
Одна из таких вещей — кластеризация.
Для этой цели я написал плагин под названием llm‑cluster, реализующий этот функционал при помощи модуля sklearn.cluster из библиотеки scikit‑learn.
Чтобы это было проще продемонстрировать, я воспользовался моим инструментом paginate‑json и API GitHub для создания тем обсуждения, после чего собрал все темы у меня в репозитории simonw/llm, составив из них коллекцию llm‑issues:
paginate-json 'https://api.github.com/repos/simonw/llm/issues?state=all&filter=all' \
| jq '[.[] | {id: .id, title: .title}]' \
| llm embed-multi llm-issues - \
--store
Теперь могу создать 10 кластеров тем вот таким образом:
llm install llm-cluster
llm cluster llm-issues 10
Кластеры выводятся в виде массива JSON, имеющего примерно такой вид (сокращено):
[
{
"id": "2",
"items": [
{
"id": "1650662628",
"content": "Initial design"
},
{
"id": "1650682379",
"content": "Log prompts and responses to SQLite"
}
]
},
{
"id": "4",
"items": [
{
"id": "1650760699",
"content": "llm web command - launches a web server"
},
{
"id": "1759659476",
"content": "`llm models` command"
},
{
"id": "1784156919",
"content": "`llm.get_model(alias)` helper"
}
]
},
{
"id": "7",
"items": [
{
"id": "1650765575",
"content": "--code mode for outputting code"
},
{
"id": "1659086298",
"content": "Accept PROMPT from --stdin"
},
{
"id": "1714651657",
"content": "Accept input from standard in"
}
]
}
]
По-видимому, между ними есть связь, но можно сделать и лучше. У команды llm
cluster предусмотрена опция --summary
, при применении которой результирующий текст кластера прогоняется через LLM, а затем на его основе генерируется информативное название для каждого кластера:
llm cluster llm-issues 10 --summary
В результате получаем такие названия как «Log Management and Interactive Prompt Tracking» (Управление логами и интерактивное отслеживание промптов) и «Continuing Conversation Mechanism and Management» (Механизм поддержания разговора и управление им). Подробнее см. в README.
Визуализация в 2D при помощи анализа главных компонент
При работе с сильно многомерным пространством возникает явная проблема: его в самом деле очень сложно визуализировать.
Можно воспользоваться техникой под названием «Анализ главных компонент» (PCA), чтобы снизить размерность данных и привести их в более удобоваримый вид. Оказывается, важная семантическая составляющая контента продолжает сохраняться и в более низких измерениях.
Мэтт Уэбб воспользовался моделью векторных представлений OpenAI, чтобы сгенерировать векторы для описаний каждого из эпизодов подкаста «Our Time» от BBC. С их помощью он нашёл содержательно связанные эпизоды, но вдобавок применил к результату анализ главных компонент, и у него получилась интерактивная 2D-визуализация.
![](https://habrastorage.org/getpro/habr/upload_files/21a/6b4/ac2/21a6b4ac21d15033730b71b0c6baf45b.gif)
Если сократить 1 536 измерений всего до двух, то, оказывается, возникает очень удобный способ исследовать данные! Так, поблизости друг от друга оказываются эпизоды об истории войн, а в другом кластере собираются эпизоды о современных научных открытиях.
Мэтт описал эту работу в своей статье Browse the BBC In Our Time archive by Dewey decimal code.
Оценивание фраз с учётом их средних расположений
Ещё векторными представлениями удобно пользоваться как средством для классификации.
Сначала вычисляем среднее местоположение группы векторов, которые мы предварительно успели классифицировать, затем сравниваем, где относительно этих локаций находятся векторные представления нового контента — и на основе этой информации категоризируем контент.
Амелия Уоттенбергер продемонстрировала красивый пример такой классификации в статье Getting creative with embeddings.
Она хотела помочь людям улучшить письменный стиль, предлагая читателям грамотно смешивать конкретные и абстрактные суждения. Но как определить, является ли данное предложение в тексте конкретным или абстрактным?
Она придумала, что можно сгенерировать образцы предложений, относящихся к двум этим типам, вычислить их средние местоположения, а затем присваивать балл новым предложениям в зависимости от того, насколько близко они расположены к той или иной крайности этого новоиспечённого спектра.
![](https://habrastorage.org/getpro/habr/upload_files/75c/913/fb9/75c913fb971914eb970e1eec0171649f.jpg)
Эти баллы даже можно передать цветом, чтобы зритель сразу мог составить впечатление, насколько конкретным или абстрактным является заданное высказывание!
Это действительно очень красивый пример, демонстрирующий, какие креативные задачи можно решать при помощи интерфейсов, надстраиваемых на основе этой технологии.
Ответы на вопросы при помощи поисковой расширенной генерации (RAG)
В завершение этого поста расскажу ещё об одной идее, благодаря которой я впервые заинтересовался векторными представлениями.
Всякий, кто попробует свои силы в работе с ChatGPT, задаёт один и тот же вопрос: как можно было бы использовать версию этой программы, чтобы она могла отвечать на вопросы, основываясь на моих личных заметках, либо на внутренних документах, которыми владеет компания?
Сначала кажется, что для достижения такого результата нужно обучить собственную модель именно на таком контенте, возможно, чего бы это ни стоило.
Оказывается, что без этого можно обойтись. Берёшь и пользуешься готовой большой языковой моделью (развёрнутой на хостинге или работающей на локальной машине), а при работе применяешь метод под названием «поисковая расширенная генерация» или RAG.
Идея заключается в следующем: пользователь задаёт вопрос. Вы ищете в вашей закрытой документации контент, который представляется значимым в контексте вопроса, затем скармливаете LLM выдержки из этой информации (учитывая, какой объём текста она принимает на вход, обычно в пределах от 3000 до 6000 слов), а также исходный вопрос.
Затем LLM может сформулировать ответ на вопрос, опираясь на дополнительный контент, который вы предоставляете.
Такой «детский трюк» оказывается поразительно эффективен. Запустить такой механизм в простейшем виде не составляет труда; сложности начинаются, когда нужно добиться от него максимально качественной работы, учитывая, что пользователи могут задать практически бесконечное множество вопросов.
Ключевая проблема при работе с RAG — найти такие отрывки, которые лучше всего подойдут в качестве промпта для LLM.
Семантический поиск «на основе тональности», организуемый при помощи векторных представлений — как раз та технология, которая нужна для сбора потенциально релевантного контента, который поможет отвечать на вопросы пользователей.
Я попробовал сделать версию такого движка на материале текстов из моего блога и описал её в статье Embedding paragraphs from my blog with E5-large‑v2.
Для этого воспользовался моделью E5-large‑v2. Эта модель, обученная в расчёте именно на такой вариант применения.
Самое сложное при подыскивании релевантного контента для вопросно‑ответных задач — в том, что вопрос пользователя, например, «что такое „shot‑scraper“?» — необязательно покажется машине похожим на информацию, содержащуюся в ответе на этот вопрос. Вопросы и ответы имеют разную грамматическую структуру.
Модель E5-large‑v2 справляется с этим, поскольку поддерживает два типа содержимого. В ней можно векторизовать утверждения (констатации фактов) и запросы (вопросы) в одном и том же пространстве, подобно тому, как CLIP одновременно поддерживает изображения и текст.
Я векторизовал 19 000 абзацев текста из моего блога в виде утверждений, а далее могу векторизовать вопрос и с его помощью найти те абзацы, в которых с максимальной вероятностью содержится ответ на этот вопрос.
В результате получается RAG, реализованный в виде однострочного сценария Bash:
llm similar blog-paragraphs -c "query: $1" \
| jq '.content | sub("passage: "; "")' -r \
| llm -m mlc-chat-Llama-2-7b-chat-hf-q4f16_1 \
"$1" -s 'You answer questions as a single paragraph'
В этом примере используется модель Llama 2 Chat 7B, работающая у меня на ноутбуке (с плагином llm-mlc), поэтому я могу отвечать на вопросы, даже не подключаясь к Интернету и используя исключительно тот код, что работает у меня на ноутбуке.
Выполнив ввод:
./blog-answer.sh 'What is shot-scraper?'
Получаю вывод:
Shot‑scraper — это утилита на Python, в которую обёрнута библиотека Playwright. В Shot‑scraper предоставляется как интерфейс командной строки, так и поток управления конфигурацией на основе YAML‑разметки, позволяющий автоматизировать как снятие скриншотов веб‑страниц, так и скрапинг содержащихся на них данных, реализуемый на языке JavaScript. Инструмент позволяет снимать единичные скриншоты либо много скриншотов в виде серии с одновременном описании их в YAML‑файле. Кроме того, инструмент позволяет выполнять содержащийся на странице JavaScript и возвращать результирующее значение.
В самом деле, этот текст очень хорошо описывает мой инструмент shot‑scraper. Я проверил и убедился, что в этом тексте нет никаких прямых пересечений с описаниями, которые я ранее публиковал у себя в блоге.
Вопросы и ответы
В завершение моего выступления было отведено немного времени на вопросы и ответы. Здесь я кратко остановлюсь и на них.
Как всё описанное сочетается с LangChain?
LangChain — это популярный фреймворк для реализации фич поверх LLM. Он покрывает большое поле проблем. Единственное, что у меня не сложилось с LangChain — это насколько долго пришлось разбираться, как он устроен, и что с его помощью можно сделать. Поисковая расширенная генерация — это одна из ключевых возможностей LangChain, так что многие из вещей, описанных в этом посте, на самом деле можно сделать и при помощи LangChain, если достаточно постараться.
Мой подход ко всем этим вещам отличается от принятого в LangChain, так как я стремлюсь собирать комплект небольших инструментов, приспособленнных к совместной работе, а не делать цельный фреймворк, который решал бы все задачи за один прогон.
Вы пробовали какие-нибудь функции определения расстояния кроме как косинусное сходство?
Нет. Кажется, косинусное сходство — выбор номер один, им пользуются все и каждый, и я пока не нашёл времени исследовать другие варианты.
Кстати, я научил ChatGPT прописывать для меня все разнообразные версии косинусного сходства, как на Python, так и на JavaScript!
При работе с RAG особенно захватывает, какие широкие возможности настройки предусмотрены в этой штуке. Можно попробовать различные функции определения расстояния, разные модели векторных представлений, стратегии формирования промптов и разные LLM. Здесь есть огромное поле для экспериментов.
Какие вещи приходится подкорректировать, если нужно обработать 1 миллиард объектов?
Все демо‑примеры, которые я привёл сегодня, были невелики; не более чем по 20 000 векторных представлений в каждом. Это достаточно немного, чтобы всё что угодно «в лоб» обрабатывать функциями косинусного сходства и за приемлемое время получать результат.
Если вам приходится работать с более крупными объёмами данных, то вам в помощь сейчас предлагается всё больше вариантов.
Сейчас есть множество стартапов, запускающих новые «векторные базы данных». В сущности, эти базы данных спроектированы именно так, чтобы отвечать на вопросы методом поиска ближайших соседей, сравнивая задачу с вектором, причём, максимально быстро.
Не уверен, что вам для этого понадобилась бы совершенно новая база данных; мне кажется, разумнее добавлять собственные индексы в уже имеющиеся базы данных. Например, в SQLite для этого предусмотрен sqlite‑vss, а в PostgreSQL — pgvector.
Кроме того, у меня был успешный опыт работы с библиотекой FAISS. В частности, с её помощью я написал для Datasette плагин, использующий эту библиотеку — он называется datasette‑faiss.
Какие улучшения в области моделей векторных представлений вы были бы рады увидеть?
Меня по‑настоящему захватывают мультимодальные модели. Отличный пример такого рода — CLIP, но я также экспериментировал с ImageBind, которая «изучает объединённое представление, включающее шесть разных модальностей: изображения, текст, аудио, глубину, температуру и данные гиростабилизатора». Представляется, данные бывают гораздо более разнообразными, чем чисто текстовые и визуальные.
Кроме того, мне нравится, что эти модели становятся всё компактнее. Выше в этой статье я продемонстрировал новую модель gtr‑tiny, размер которой — всего 60 МБ. Когда появляется возможность использовать эти технологии на маломощных устройствах или в браузере, такие перспективы в самом деле вдохновляют.
Что ещё почитать
Если вы хотите подробнее познакомиться с низкоуровневым устройством векторных представлений, рекомендую следующие источники:
What are embeddings? от Викки Бойкис
Text Embeddings Visually Explained от Меора Амера
The Tensorflow Embedding Projector — интерактивный инструмент для исследования пространств векторных представлений
Learn to Love Working with Vector Embeddings — подборка обучающих материалов от компании Pinecone, разрабатывающей векторные базы данных.