Косинусная близость: считаем похожесть руками
Главная формула векторного поиска — и почему берут именно угол, а не расстояние.
Косинусная близость — косинус угла между двумя векторами. От 1 (одно направление, максимально похожи) через 0 (перпендикулярны) до -1 (противоположны).
Формула и интуиция
Косинус близости считается так: скалярное произведение векторов делим на произведение их длин.
cos(a, b) = dot(a, b) / (|a| * |b|)
dot(a, b) = a1*b1 + a2*b2 + ... + an*bn
|a| = sqrt(a1^2 + a2^2 + ... + an^2)Ключевая идея: косинус смотрит на направление векторов, а не на их длину. Для текста это удобно: длинный документ и его короткий пересказ «смотрят в одну сторону», и косинус это поймает, даже если длины векторов разные.
Считаем близость текстов руками
Возьмём игрушечные эмбеддинги по осям [животные, техника, еда] и отранжируем слова по близости к «кошке». Никаких библиотек — только математика.
import math
def cosine(a, b):
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(x * x for x in b))
return dot / (na * nb)
vectors = {
"кошка": [0.9, 0.1, 0.2],
"собака": [0.8, 0.1, 0.1],
"ноутбук": [0.1, 0.9, 0.0],
"пицца": [0.1, 0.0, 0.9],
}
query = "кошка"
q = vectors[query]
print("Близость к слову:", query)
ranked = sorted(vectors.items(), key=lambda kv: cosine(q, kv[1]), reverse=True)
for word, vec in ranked:
print(f" {word:8} cos = {cosine(q, vec):.3f}")Вывод:
Близость к слову: кошка кошка cos = 1.000 собака cos = 0.995 пицца cos = 0.322 ноутбук cos = 0.214
«Собака» — почти как «кошка» (обе про животных), а «пицца» и «ноутбук» далеко. Это и есть ядро векторного поиска: чтобы найти релевантное, считаем косинус запроса со всем и берём самые высокие значения.
Косинус и нормализация
Если заранее привести все векторы к длине 1 (нормализовать), косинус превращается в обычное скалярное произведение — считать быстрее. Покажем, что это одно и то же.
import math
def dot(x, y):
return sum(p * q for p, q in zip(x, y))
def normalize(v):
n = math.sqrt(dot(v, v)) or 1e-9
return [x / n for x in v]
def cosine(x, y):
return dot(x, y) / (math.sqrt(dot(x, x)) * math.sqrt(dot(y, y)))
a, b = [3.0, 4.0], [4.0, 3.0]
an, bn = normalize(a), normalize(b)
print("Косинус исходных: ", round(cosine(a, b), 4))
print("dot нормализованных: ", round(dot(an, bn), 4))Вывод:
Косинус исходных: 0.96 dot нормализованных: 0.96
Поэтому многие векторные базы хранят нормализованные векторы и используют dot — результат тот же, а вычислений меньше.
Итог
- Косинус близости = dot / (|a|*|b|), смотрит на направление, не на длину.
- 1 — максимально похожи, 0 — не связаны, -1 — противоположны.
- На нормализованных векторах косинус = скалярное произведение.