You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

57 line
2.2 KiB

  1. from collections import Counter
  2. import logging
  3. from math import sqrt
  4. from collections.abc import Sequence
  5. logger = logging.getLogger(__name__)
  6. def cosine_similarity(a: Sequence[float], b: Sequence[float]) -> float:
  7. logger.debug("Computing cosine similarity between numerical vectors of length %d and %d", len(a), len(b))
  8. if len(a) != len(b):
  9. logger.error("Vector dimension mismatch: vector 'a' length (%d) != vector 'b' length (%d)", len(a), len(b))
  10. raise ValueError("Vectors must have the same dimension")
  11. dot = 0.0
  12. norm_a_sq = 0.0
  13. norm_b_sq = 0.0
  14. for x, y in zip(a, b):
  15. dot += x * y
  16. norm_a_sq += x * x
  17. norm_b_sq += y * y
  18. denominator = sqrt(norm_a_sq * norm_b_sq)
  19. if denominator == 0.0:
  20. logger.debug("Zero denominator encountered in cosine_similarity (norm_a_sq=%f, norm_b_sq=%f). Returning 0.0", norm_a_sq, norm_b_sq)
  21. return 0.0
  22. similarity = dot / denominator
  23. logger.debug("Calculated vector cosine similarity: dot=%f, denominator=%f, similarity=%f", dot, denominator, similarity)
  24. return similarity
  25. def cosine_lists(a: list[str], b: list[str], *, casefold: bool = True) -> float:
  26. logger.debug("Computing token cosine similarity for list_a=%s and list_b=%s (casefold=%s)", a, b, casefold)
  27. def tokens(xs: list[str]) -> Counter[str]:
  28. return Counter(x.casefold() if casefold else x for x in xs)
  29. ca, cb = tokens(a), tokens(b)
  30. if not ca or not cb:
  31. logger.debug("Empty token set detected (count_a=%d, count_b=%d). Cosine similarity is 0.0", len(ca), len(cb))
  32. return 0.0
  33. common_tokens = ca.keys() & cb.keys()
  34. dot = sum(ca[t] * cb[t] for t in common_tokens)
  35. norm_a = sqrt(sum(v * v for v in ca.values()))
  36. norm_b = sqrt(sum(v * v for v in cb.values()))
  37. if norm_a == 0.0 or norm_b == 0.0:
  38. logger.debug("Zero norm detected (norm_a=%f, norm_b=%f). Cosine similarity is 0.0", norm_a, norm_b)
  39. return 0.0
  40. similarity = dot / (norm_a * norm_b)
  41. logger.debug("Token similarity calculation: common_tokens=%s, dot=%f, norm_a=%f, norm_b=%f -> similarity=%.4f",
  42. list(common_tokens), dot, norm_a, norm_b, similarity)
  43. return similarity