Косинусная близость: насколько похожи два текста
Когда тексты стали векторами, «похожесть» текстов превращается в геометрию: чем меньше угол между векторами, тем тексты ближе по содержанию.
Косинусная близость — мера схожести двух векторов, равная косинусу угла между ними: 1 — направления совпадают (тексты похожи), 0 — перпендикулярны (нет общих слов).
Почему не просто «расстояние»
Казалось бы, можно мерить обычное расстояние между векторами. Но длинный документ и короткий на ту же тему дадут векторы очень разной длины — расстояние будет большим, хотя темы совпадают. Косинус смотрит только на направление вектора, игнорируя его длину. Поэтому он идеально подходит для текстов: важна тема (направление), а не объём (длина).
Формула
cos(A, B) = (A · B) / (|A| · |B|)
A · B — скалярное произведение: сумма произведений соответствующих компонент
|A| — длина вектора: корень из суммы квадратов компонент
Результат лежит от 0 до 1 для векторов частот (там нет отрицательных значений): 1 — тексты максимально похожи, 0 — ни одного общего слова.
Считаем близость руками
Возьмём три документа: два про котов и один про погоду. Превратим в bag-of-words векторы и посчитаем косинус между ними.
import math
from collections import Counter
docs = [
"кот любит молоко и кот спит",
"кот пьёт молоко",
"сегодня идёт дождь",
]
tokenized = [d.split() for d in docs]
vocab = sorted({w for doc in tokenized for w in doc})
def vec(tokens):
c = Counter(tokens)
return [c.get(w, 0) for w in vocab]
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(y * y for y in b))
if na == 0 or nb == 0:
return 0.0
return dot / (na * nb)
v = [vec(t) for t in tokenized]
print("cos(док1, док2) =", round(cosine(v[0], v[1]), 3), "(оба про котов)")
print("cos(док1, док3) =", round(cosine(v[0], v[2]), 3), "(кот vs дождь)")
print("cos(док2, док3) =", round(cosine(v[1], v[2]), 3), "(кот vs дождь)")
Вывод:
cos(док1, док2) = 0.612 (оба про котов) cos(док1, док3) = 0.0 (кот vs дождь) cos(док2, док3) = 0.0 (кот vs дождь)
Результат честно отражает смысл: два «кошачьих» документа похожи (0.612), а с документом про дождь у них нет общих слов — близость ровно 0. Мы измерили смысловое сходство текстов, не понимая ни слова «по-человечески», — просто геометрией векторов.
Шаг за шагом для первой пары
Полезно увидеть «руками», откуда взялось 0.612. У док1 и док2 общие слова — «кот» и «молоко». Скалярное произведение учитывает только совпадающие по позиции ненулевые компоненты, а длины векторов нормируют результат. Чем больше общих и значимых слов, тем выше косинус.
Где это применяют
- Поиск похожих документов: найти статьи на ту же тему.
- Семантический поиск: ранжировать документы по близости к запросу (особенно с TF-IDF-векторами).
- Дедупликация: находить почти одинаковые тексты.
- Рекомендации: «похожие на это».
А в разделе про эмбеддинги мы применим ровно ту же косинусную формулу — но уже к плотным векторам слов, и тогда «кот» и «кошка» окажутся близки, даже не совпадая по строке.
Итог
- Похожесть текстов = косинус угла между их векторами.
- Косинус смотрит на направление, а не длину, поэтому не штрафует за объём текста.
- 1 — максимально похожи, 0 — нет общих признаков.
- Та же формула работает и для bag-of-words, и для TF-IDF, и для эмбеддингов.