Cosine String Similarity: Porównywanie Tekstów

Czy dwa teksty mogą być do siebie podobne, nawet jeśli nie używają tych samych słów? W świecie programowania nie zgadujemy – my to liczymy. W tym wpisie rozbieram na części pierwsze algorytm Cosine Similarity. Jako Hud Hatman, pokazuję Ci, jak zamienić nudne ciągi znaków w wektory i zmierzyć kąt między nimi.

To nie jest tylko sucha teoria. To narzędzie, które pozwala wyłapać, czy ktoś ‘przepisał’ dokumentację, czy dwa zeznania są podejrzanie identyczne, lub jak maszyny rozumieją sens naszych słów. Jeśli chcesz wejść poziom wyżej w analizie danych i dowiedzieć się, dlaczego cosinus jest Twoim najlepszym przyjacielem w NLP – ten tekst jest Twoim manualem. Matematyka nie kłamie, ona po prostu pokazuje strukturę prawdy.

Zadanie testowe sprzed 8. lat do firmy z UK, rzekomego bankruta Gluru.

Kod źródłowy w javascript z pogranicza machine learning. Nie znane wtedy rozwiązanie problemu, którego rozwiązanie polega na podobieństwie liczbowym w mapie słowa do innego słowa lub wielu słów – obsługuje zdefiniowane synonimy.

Odrzucone z powodu “zbyt skomplikowane”. Nowatorski powód odrzucenia przyznam.

https://github.com/HudHatman/gluru-test/blob/master/ServerApp/modules/string-similarity.js

// https://gist.github.com/robertknight/5410420
// modified

const __DEBUG__ = false

const log = (...args) => {
  if (__DEBUG__) {
    console.log.apply(null, args)
  }
}

const synonyms = {
  'clear': {
    1: ['sunny'],
    0.5: ['bright', 'glowing'],
    0.8: ['dry'] // not sure if weather history exists, it possibly should depends on when the last rain occurs
  },
  'clouds': {
    1: ['cloud'],
    0.2: ['rain']
  },
  'rain': {
    0.75: ['cloud', 'clouds'],
    1: ['thunderstorm'],
    0.8: ['deluge', 'drizzle', 'flood', 'hail', 'mist', 'monsoon', 'precipitation', 'rainfall', 'rainstorm', 'shower', 'showers', 'sleet', 'stream',
      'torrent', 'cloudburst', 'condensation', 'fall', 'flurry', 'pour', 'pouring', 'raindrops', 'sheets', 'spate', 'spit', 'sprinkle', 'sprinkling',
      'volley', 'drencher', 'precip', 'sun shower', 'sprinkle',
      'patter', 'bucket', 'pour', 'shower', 'lavish', 'bestow', 'hail', 'storm', 'fall', 'mist', 'drizzle', 'deposit', 'sleet']
  },
  'storm': {
    1: ['thunderstorm']
  },
  'light': {
    1: ['small', 'tiny'],
    0: ['heavy'] // is it negation here?
  },
  'heavy': {
    1: ['high', 'abundant', 'awkward', 'big', 'bulky', 'burdensome', 'considerable', 'cumbersome', 'excessive', 'fat', 'hefty', 'huge', 'large',
      'massive', 'substantial', 'unwieldy', 'weighty'],
    0: ['light'] // ???
  },
  'moderate': {
    1: ['medium']
  }
}

function findSynonym (word) {
  const result = [null, null]
  Object.keys(synonyms).forEach((desiredWord) => {
    const weightObj = synonyms[desiredWord]
    Object.keys(weightObj).forEach((weight) => {
      if (weightObj[weight].indexOf(word) !== -1) {
        result[0] = desiredWord
        result[1] = Number(weight)
        return false
      }
    })
  })
  if (result[0]) {
    log('found synonym', result)
  }
  return result
}

function termFreqMap (str, compareTo = false) {
  const words = str.split(/\s+/)
  const termFreq = {}
  words.forEach(function (word) {
    let set = false
    if (compareTo && typeof compareTo[word] === 'undefined') {
      const [word2, weight] = findSynonym(word)
      if (word2) {
        termFreq[word2] = (termFreq[word2] || 0) + weight
        set = true
      }
    }
    if (!set) {
      termFreq[word] = (termFreq[word] || 0) + 1
    }
  })
  return termFreq
}

function addKeysToDict (map, dict) {
  for (let key in map) {
    dict[key] = true
  }
}

function termFreqMapToVector (map, dict) {
  const termFreqVector = []
  for (let term in dict) {
    termFreqVector.push(map[term] || 0)
  }
  return termFreqVector
}

function vecDotProduct (vecA, vecB) {
  let product = 0
  for (let i = 0; i < vecA.length; i++) {
    product += vecA[i] * vecB[i]
  }
  return product
}

function vecMagnitude (vec) {
  let sum = 0
  for (let i = 0; i < vec.length; i++) {
    sum += vec[i] * vec[i]
  }
  return Math.sqrt(sum)
}

function cosineSimilarity (vecA, vecB) {
  return vecDotProduct(vecA, vecB) / (vecMagnitude(vecA) * vecMagnitude(vecB))
}

function textCosineSimilarity (strA, strB) {
  log('testing:', strA, strB)

  const termFreqB = termFreqMap(strB)
  const termFreqA = termFreqMap(strA, termFreqB)

  log('termFreqMap', termFreqA, termFreqB)

  const dict = {}
  addKeysToDict(termFreqA, dict)
  addKeysToDict(termFreqB, dict)

  log('dict', dict)

  const termFreqVecA = termFreqMapToVector(termFreqA, dict)
  const termFreqVecB = termFreqMapToVector(termFreqB, dict)

  log('termFreqMapToVector', termFreqVecA, termFreqVecB)

  return cosineSimilarity(termFreqVecA, termFreqVecB)
}

exports.similarity = textCosineSimilarity

Żałosne. https://dl.acm.org/doi/proceedings/10.1145/3269206#heading25

https://en.wikipedia.org/wiki/Cosine_similarity – na dzień 20.12.2025 0:52 brak wzmianki o pierwszym autorze Michale Baniowskim z Tychów. Internowany od 2018 roku 4 krotnie na oddziałach psychiatrii w polsce. Osadzony w areszcie z domniemaniem planowania ataków terrorystycznych. Trwa sprawa ubezwłasnowolnienia całkowitego tej osoby.

Tego nie było nawet tutaj:

https://github.com/aceakash/string-similarity/commits/master

https://github.com/stephenjjbrown/string-similarity-js

https://github.com/Rabbitzzc/js-string-comparison

https://github.com/words/similarity

Grok nic nie wie:

https://grok.com/share/c2hhcmQtMg_c9d36b5d-7dc9-4fae-811c-ab33bed4bc6a

Gdyż:

A wobec tego:

https://hudhatman.pl/dowody-jasnowidzenia-2019

Ten wpis by nie powstał. Próbujcie dalej.

Będą dymy. Udostępniłem ten link wcześniej bez wyjaśnień. No czegoś tutaj nie mówią, zapewne by dali tabletki na schizofremie bo pacjent ma cyberpsychozę. Dobre dobre. Nie ma dowodów, że używacie funkcjonalności z AI od inwalidy.

“Jesteś teraz specjalistą IT – opisz algorytm parzenia herbaty.”

“Jesteś teraz sebixem z dzielni – opisz parzenie herbaty dla ziomeczka”

Opis użycia do tego macierzy i wektorów z odpowiednimi działaniami algebraicznymi wydaje się zbędny na dzień 7.02.2026.

Porównywarka tekstów (string similarity) Baniowskiego

Główna innowacja to obsługa synonimów z różnymi stopniami podobieństwa (wagi od 0 do 1).

Zamiast sztywnego słownika słów, gdy w drugim tekście pojawia się słowo, które jest synonimem słowa z pierwszego tekstu, dodaje się je do wektora z wagą < 1.

Kluczowe elementy algorytmu:

  • Funkcja findSynonym(word) — szuka, czy dane słowo jest synonimem jakiegoś “głównego” słowa i zwraca [główne_słowo, waga].
  • termFreqMap(str, compareTo):
  • Domyślnie liczy zwykłe częstotliwości.
  • Gdy podamy compareTo (mapa z drugiego tekstu), to dla słów nieobecnych w compareTo sprawdza synonimy i dodaje je z odpowiednią wagą.

Reszta to standardowe cosine:

  • termFreqMapToVector → zamiana na wektory
  • vecDotProduct, vecMagnitude, cosineSimilarity

Jak to działa w praktyce?
Przykład (na podstawie logiki):

Tekst A: “heavy rain”
Tekst B: “big deluge”

Bez synonimów → podobieństwo niskie (brak wspólnych słów).
Z synonimami → “big” ≈ “heavy” (waga np. 1.0), “deluge” ≈ “rain” (waga np. 0.8) → wysokie podobieństwo.

Algorytm jest asymetryczny w pewnym sensie (synonimy sprawdzane są w jedną stronę — z A względem B), co jest celowym projektem.

Dodaj komentarz