Немного патетики
Для семантического анализа обычно используется косинусное сходство — довольно привычный и всем понятный инструмент. В современном мире NLP часто отдается предпочтение более эффективным эмбеддингам на основе LLM в отличие от более классических CountVectorizer или, скажем, TF-IDF. И казалось бы, да, у нас есть многомерный вектор, мы вычислили сходство, оно работает, ведь в самом эмбеддинге зашито множество семантических связей, и все круто и классно. Но иногда хочется посмотреть на это все с другой стороны, со стороны нелинейных и потенциально более мощных алгоритмов, ведь есть вариант улучшить либо само качество эмбеддингов, либо то, как мы их сравниваем. И так как в статье мы будем рассматривать второй вариант, то сразу скажу, что можно использовать взаимную информацию или коэффициент D Хеффдинга. Но здесь речь пойдет о временных рядах и немного о частичке теории динамических систем и теории хаоса, что довольно удивительно. Надеюсь, я заинтриговал, и поэтому сейчас расскажу о мотивации использования этих подходов, алгоритме и, вообще, в чем вся суть.
Код алгоритма доступен на GitHub.
Для создания всех ембеддингов будет использоваться модель UAE-Large-V1.
Мотивация
Возможно, некоторые из читателей задались вопросом, как мы можем применить способы анализа временных рядов к нашему эмбеддингу.
Так вот:
Эмбеддинги являются, в основном, многомерными векторами. Возьмем, ради примера, длину вектора в 1024 единицы. Нам никто не мешает его разложить на временной ряд, просто представив, что каждый индекс — это определенный момент времени. Собственно, почему бы и нет, главное — чтобы был результат.
Вот как выглядит наш эмбеддинг, если его подать как временной ряд, немного похож на сигнал, не так ли?
Красные точки — это наши значения вектора, фактически их можно считать как локальные экстремумы нашего временного ряда, то есть они несут самую ценную информацию о нем.
Предлагаю также посмотреть на эти «сигналы», которые похожи по семантическим связям, а которые нет. Вот, кстати, попробуйте понять, кто схож, а кто нет.
Если вы смогли понять, что 1 и 3 являются более похожими, то это круто, но не всегда это бывает возможным. Поэтому я предлагаю посмотреть на алгоритм, который как раз таки на это способен.
Первый вектор был - "A man is playing music."
Второй вектор был - "A panda is climbing."
Третий вектор был - "A man plays a guitar."
Алгоритм
Я провёл некоторые исследования, и главными критериями была точность и скорость выполнения. Так вот, лучше всего себя показали вейвлет-преобразования в связке с коэффициентом фазовой синхронизации.
Сам алгоритм выглядит следующим образом:
Нормализация векторов
Первым шагом алгоритма является нормализация векторов. Этот процесс важен для обеспечения того, чтобы все векторы имели единую длину.
Преобразование в комплексные векторы
Этот шаг включает в себя преобразование обычных векторов в комплексные, что позволяет нам работать не только с амплитудой значений, но и с их фазой. Для этого мы разделяем каждый нормализованный вектор на две части: первая половина становится действительной частью комплексного числа, а вторая — мнимой. Этот процесс обогащает наши данные, добавляя дополнительный уровень информации, который будет использован в дальнейшем анализе.
Вот пример работы:
Возьмём вектор V = [ 1, 2, 3, 4].
Шаг 1: Разделение вектора
Первое, что нам нужно сделать, это разделить вектор на две части. Поскольку в нашем векторе четыре измерения, мы разделяем его пополам:
Первая половина: [ 1, 2]
Вторая половина: [ 3, 4]
Шаг 2: Создание комплексного вектора
Теперь мы преобразуем эти две части в комплексный вектор, где первая половина будет действительной частью, а вторая половина — мнимой. Таким образом, каждый элемент первой половины соединяется с соответствующим элементом второй половины для формирования комплексных чисел:
Комплексный элемент 1: 1 + 3i
Комплексный элемент 2: 2 + 4i
Результат
Итак, наш исходный вектор V = [ 1, 2, 3, 4] преобразуется в комплексный вектор:
Vкомплекс = [ 1 + 3i , 2 + 4i ].
Вычисление вейвлет-преобразования
Далее, применяя вейвлет-преобразование к полученным комплексным векторам, мы разлагаем каждый вектор на компоненты, которые лучше описывают локальные особенности данных. Вейвлет-преобразование позволяет анализировать эмбеддинги на разных масштабах, выделяя как высокочастотные, так и низкочастотные особенности.
Вычисление фазовых характеристик и средней фазовой синхронности
После вычисления вейвлет-коэффициентов мы переходим к анализу фазовых характеристик. Фазовая информация позволяет нам оценить, насколько синхронно изменяются различные части эмбеддингов. Вычисление средней фазовой синхронности между парой эмбеддингов дает представление о степени их семантической связанности.
Вычисляется по формуле:
Где ϕ1(tn) и ϕ2(tn) — это фазовые углы двух сигналов в момент времени tn.
При точном совпадении фаз коэффициент равен единице, при отсутствии синхронизации — нулю, что очень удобно для нас.
Вот еще для наглядности при применении к временным рядам
После вычисления коэфициента фазовой синхронизации, мы переходим к вычислению улучшенной мере фазовой синхронности (P).
Улучшенный коэффициент фазовой синхронности P предоставляет более точную оценку синхронизации сигналов, компенсируя колебания синхронности за счёт включения меры вариативности V.
Рачет Окончательного Результата
После того как мы получили нормализованные и преобразованные в комплексные числа вектора и вычислили их вейвлет-преобразования, а также фазовые характеристики, мы переходим к ключевому этапу алгоритма — расчету окончательного результата сравнения векторов.
Окончательный результат для каждого вектора из списка получается комбинированием фазовой синхронности и косинусного сходства. Это делается путём умножения фазовой синхронности на модифицированное косинусное сходство: ps * (0.5 * (cs + 1))
, где ps
- улучшенный коэффициент фазовой синхронности(P), cs
- косинусное сходство. Модификация косинусного сходства (0.5 * (cs + 1))
переводит его диапазон из [-1, 1]
в [0, 1]
, чтобы обеспечить положительное влияние на итоговую метрику.
По поводу гиперпараметров в вейвлет-преобразованиях
В целом Вейвлеты Добеши (Daubechies) лучше всего подходят. Вейвлеты Добеши обозначаются как dbN, где N указывает на порядок вейвлета. Порядок вейвлета влияет на его способность захватывать информацию о сигнале и шуме в данных.
Стандартные гиперпараметры — db4, а уровень декомпозиции — 4.
Для более тонких семантических взаимосвязей:
Вейвлеты нижнего порядка, такие как Daubechies (db2, db3), могут быть более подходящими, поскольку они обеспечивают лучшую локализацию во времени и позволяют выделить более точные и локализованные семантические взаимосвязи в данных.
Для более глобальных паттернов:
Вейвлеты высшего порядка, такие как Daubechies (db4, db6 и выше), предпочтительнее для анализа глобальных паттернов. Их гладкие и длинные фильтры помогают захватывать более широкие и гладкие семантические структуры в данных, игнорируя мелкие детали.
Уровень декомпозиции
Формула для определения максимально возможного уровня декомпозиции N при выполнении вейвлет-преобразования сигнала. Эта формула помогает гарантировать, что декомпозиция сигнала будет выполнена без потери информации и без превышения границ, заданных длиной сигнала L и длиной используемого вейвлет-фильтра Lfilter.
где:
N — рекомендуемый уровень декомпозиции.
L — длина сигнала.
Lfilter — длина (или количество коэффициентов) вейвлет-фильтра, используемого для анализа. Это значение зависит от выбранного вейвлета и обычно находится в диапазоне от 2 до 20. Для конкретных вейвлетов, таких как Daubechies, Lfilter увеличивается с увеличением порядка вейвлета.
Тесты Алгоритма
Тестирование алгоритма будет происходить в двух видах: просто с синтетическими векторами, а также эмбеддингами.
Устанавливаем пакетpip install PyWaveSync
Далее код для тестирования на синтетическом наборе данных.
from wavesync.wavesync import WaveSync
import numpy as np
ws = WaveSync()
np.random.seed(42)
vec = np.random.rand(1024)
similar_vecs = [vec + np.random.normal(0, 0.01, len(vec)) for _ in range(5)]
dissimilar_vecs = [np.random.rand(len(vec)) for _ in range(5)]
vec_a = np.random.rand(1024)
vec_b = -vec_a # Coordinate-wise opposite of vec_a
# Test with similar, dissimilar, and opposite vectors
wavesync_similar_scores = ws.compare(vec, similar_vecs)
wavesync_dissimilar_scores = ws.compare(vec, dissimilar_vecs)
wavesync_opposite_scores = ws.compare(vec_a, [vec_b])
print(wavesync_similar_scores, wavesync_dissimilar_scores, wavesync_opposite_scores)
Результат:
Похожие
[0.9946460671728045, 0.9875065629124861, 0.989072345923832, 0.991220540585618, 0.9954265121946134]
Разные
[0.005015474802584571, 0.004847775250650383, 0.0012281423357652212, 0.006391884708080264, 0.0002667694383566343]
Противоположный
[0.0]
Вот для сравнения при использовании косинусного сходства:
Похожие
[0.9998493328107453, 0.9998563262925324, 0.9998411950733593, 0.999847887188542, 0.9998466519512551]
Разные
[0.7358156012914848, 0.7530882884696813, 0.7373955262095263, 0.7466333075853196, 0.7318980690691882]
Противоположный
[-1.0]
По результату видно, что алгоритм идеально справляется с пониманием сходства векторов, также интеграция в него косинусного сходства позволяет учитывать направление векторов, что очень важно, и поэтому для противоположных мы получаем - 0.0.
Тепер на реальных предложениях:
python -m pip install -U angle-emb
from angle_emb import AnglE
from wavesync.wavesync import WaveSync
sentences = [
"An animal is biting a person's finger.",
"A woman is reading.",
"A man is lifting weights in a garage.",
"A man plays the violin.",
"A man is eating food.",
"A man plays the piano.",
"A panda is climbing.",
"A man plays a guitar.",
"A woman is slicing meat.",
"A men is playing music on piano on the street for cat."]
angle = AnglE.from_pretrained('WhereIsAI/UAE-Large-V1', pooling_strategy='cls').cuda()
vecs = angle.encode(sentences, to_numpy=True)
vec = angle.encode(["A man is playing music."], to_numpy=True)
ws = WaveSync()
wavesync_scores = ws.compare(vec, vecs)
print(wavesync_scores)
Запрос: "A man is playing music."
"An animal is biting a person's finger." - 0.04
"A woman is reading." - 0.02
"A man is lifting weights in a garage." - 0.06
"A man plays the violin." - 0.29
"A man is eating food." - 0.09
"A man plays the piano." - 0.35
"A panda is climbing." - 0.01
"A man plays a guitar." - 0.51
"A woman is slicing meat." - 0.01
"A men is playing music on piano on the street for cat." - 0.31
Как видим, алгоритм прекрасно понимает разницу между эмбеддингами, и благодаря модернизации коэффициента фазовой синхронизации, алгоритм более тонко реагирует на различия между ними.
Скорость
На одной системе при 100 000 сравнениях векторов размером 1024 при стандартных гиперпараметрах, средняя скорость одного сравнения составляла - 2e-4 секунды, когда только косинусное сходство имело - 8e-6.
Даже несмотря на большую разницу в скорости, учитывая подход WaveSync, скорость по-прежнему довольно высока, и в большинстве задач, где не требуется обработка сверхогромного количества встраиваний, он хорошо себя показывает, но в любом случае есть вариант сначала отсеять самые непохожие, используя косинусное сходство, а потом применить WaveSync.
Вывод
Временные ряды, вейвлеты и прочее — это замечательно, но в чём же заключается главное преимущество алгоритма?
Главное преимущество алгоритма по сравнению с использованием косинусного сходства состоит в его способности более эффективно и точно различать похожие и разные вектора. Это обеспечивает более ясную интерпретацию результатов для аналитических целей.
Моя цель была демонстрация того, как с помощью нестандартного подхода к анализу временных рядов можно достичь замечательных результатов, которые полностью конкурентоспособны с косинусным сходством по скорости и точности за счёт анализа нелинейных зависимостей. Это позволяет взглянуть под новым углом на работу с эмбеддингами в NLP и, в общем, на сравнение многомерных векторов.
Если у вас есть идеи, вопросы или предложения, буду рад их видеть в комментариях.