Косинусная близость: насколько похожи два текста

Когда тексты стали векторами, «похожесть» текстов превращается в геометрию: чем меньше угол между векторами, тем тексты ближе по содержанию.

Косинусная близость — мера схожести двух векторов, равная косинусу угла между ними: 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, и для эмбеддингов.
Проверьте себя
1. Почему для сравнения текстов косинусная близость удобнее обычного расстояния?
AОна быстрее вычисляется на любых данных
BОна смотрит на направление вектора, а не длину, и не штрафует за объём текста
CОна всегда даёт целые числа
DОна не требует превращать текст в векторы
2. Чему равна косинусная близость двух текстов без единого общего слова (в bag-of-words)?
A1
B0
C−1
DЗависит от длины текстов
3. Что стоит в числителе формулы косинусной близости?
AСкалярное произведение векторов A · B
BСумма длин векторов
CРазность векторов
DПроизведение длин |A| · |B|
Поддержать проект