Как считать косинусную близость между вопросами
Что проверяют
Умеете ли вы пройти путь от сырого текста до числа и понимаете ли, что косинус только мера, а качество определяет представление текста. Следом спрашивают, как это масштабировать на сотни тысяч вопросов и как выбрать порог, выше которого вопросы считаются дублями.
Как отвечать
Формула: скалярное произведение векторов, делённое на произведение их норм. Косинус не зависит от длины векторов, поэтому короткий и длинный вопрос об одном и том же сравниваются честно. Для неотрицательных векторов, как у TF-IDF, значение лежит от 0 до 1.
Дальше представление текста и масштаб:
- TF-IDF:
TfidfVectorizerв scikit-learn по умолчанию нормирует строки по L2, поэтому косинус равен скалярному произведению иlinear_kernelдаёт тот же результат быстрее. Для русского с его формами слов хорошо работают символьные n-граммы,analyzer="char_wb"иngram_range=(3, 5), заодно они прощают опечатки. Альтернатива: лемматизация через pymorphy3; - эмбеддинги: модель из sentence-transformers с поддержкой русского (многоязычные MiniLM, E5, LaBSE). Ловит смысл: «Как вернуть деньги?» и «Оформить возврат» по словам почти не пересекаются, а по эмбеддингам близки. Векторы нормируете и снова берёте скалярное произведение;
- масштаб: матрица всех пар растёт квадратично. Вместо неё нужен поиск ближайших соседей: FAISS, HNSW-индекс, pgvector в Postgres.
Порог не угадывают: размечаете несколько сотен пар «дубль или нет», строите precision и recall по порогам и выбираете точку под задачу.
Где спотыкаются
Выкидывают стоп-слова по стандартному списку. В вопросах «как», «почему» и «не» несут смысл: «почему не работает оплата» и «как работает оплата» это разные вопросы.
Обучают векторизатор на двух-трёх текстах. IDF по такой выборке бессмыслен, словарь и веса строят на всём корпусе вопросов.
Считают полную матрицу cosine_similarity в памяти для большого корпуса. Плотная матрица 100 000 на 100 000 во float64 занимает 80 ГБ.