Como calcular a similaridade de texto usando PHP

Você já se perguntou como comparar dois textos e medir o quanto eles são parecidos? Uma forma de fazer isso é usando o conceito de similaridade de cosseno, que é uma medida de ângulo entre dois vetores. Neste post, eu vou mostrar como implementar um algoritmo simples de similaridade de texto usando PHP.

O que é similaridade de cosseno?

A similaridade de cosseno é uma forma de medir a semelhança entre dois vetores, que são representações numéricas de objetos. O cosseno é uma função trigonométrica que varia de -1 a 1, sendo que valores próximos de 1 indicam que os vetores são muito parecidos, e valores próximos de -1 indicam que os vetores são muito diferentes.

A fórmula para calcular a similaridade de cosseno entre dois vetores A e B é:

sim(A,B)=∥A∥∥B∥A⋅B​=∑i=1n​Ai2​​∑i=1n​Bi2​​∑i=1n​Ai​Bi​​

Onde Ai​ e Bi​ são os componentes dos vetores, e ∥A∥ e ∥B∥ são as normas dos vetores, que representam o seu comprimento.

Como representar textos como vetores?

Para aplicar a similaridade de cosseno a textos, precisamos primeiro transformá-los em vetores. Uma forma simples de fazer isso é usando o modelo de saco de palavras (bag of words), que consiste em contar a frequência de cada palavra em um texto e usar esses valores como componentes do vetor.

Por exemplo, se temos os seguintes textos:

  • Texto 1: “O gato é preto”
  • Texto 2: “O cachorro é branco”
  • Texto 3: “O gato e o cachorro são amigos”

Podemos representá-los como vetores da seguinte forma:

  • Vetor 1: [1, 0, 1, 1, 0, 0]
  • Vetor 2: [0, 1, 0, 0, 1, 1]
  • Vetor 3: [1, 1, 0, 0, 0, 1]

Onde cada posição do vetor corresponde a uma palavra do vocabulário formado pelos textos: [“o”, “cachorro”, “gato”, “é”, “branco”, “preto”].

Como implementar o algoritmo em PHP?

Para implementar o algoritmo de similaridade de texto em PHP, vamos precisar de algumas etapas:

  • Tokenizar os textos: separar os textos em palavras individuais e converter tudo para minúsculo.
  • Remover as palavras vazias: eliminar as palavras que não têm muito significado para a comparação, como artigos e preposições.
  • Vetorizar os textos: contar a frequência de cada palavra nos textos e criar os vetores correspondentes.
  • Calcular a similaridade de cosseno: aplicar a fórmula da similaridade de cosseno aos pares de vetores.

O código completo do projeto pode ser encontrado no GitHub. Aqui vamos mostrar apenas as partes principais.

Tokenizar os textos

Para tokenizar os textos, vamos usar a classe Tokenizer. Ela recebe um array de textos e retorna um array de tokens (palavras) para cada texto.

<?php
namespace PHPSimiTextApp\Tokenizer;

class Tokenizer
{
    public function tokenizeTexts($texts)
    {
        return array_map(function ($text) {
            return explode(' ', strtolower($text));
        }, $texts);
    }
}

Remover as palavras vazias

Para remover as palavras vazias, vamos usar a classe StopWordRemover. Ela recebe um array de tokens e retorna um array filtrado sem as palavras vazias. As palavras vazias são definidas em uma constante STOP_WORDS.

<?php
namespace PHPSimiTextApp\StopWordRemover;

class StopWordRemover
{
    public function removeStopWords($tokens)
    {
        return array_map(function ($textTokens) {
            return array_values(array_filter($textTokens, function ($token) {
                return !in_array($token, STOP_WORDS);
            }));
        }, $tokens);
    }
}

Vetorizar os textos

Para vetorizar os textos, vamos usar a classe Vectorizer. Ela recebe um array de tokens e retorna um array de vetores com a frequência das palavras. Ela também tem um método para vetorizar um único texto.

<?php
namespace PHPSimiTextApp\Vectorizer;

class Vectorizer
{
    public function vectorizeTexts($tokens)
    {
        return array_map(function ($textTokens) {
            return array_count_values($textTokens);
        }, $tokens);
    }

    public function vectorizeText($tokens)
    {
        return array_count_values($tokens);
    }
}

Calcular a similaridade de cosseno

Para calcular a similaridade de cosseno, vamos usar a classe CosineSimilarityCalculator. Ela recebe um array de vetores e um vetor principal e retorna um array com as similaridades entre o vetor principal e cada um dos outros vetores.

<?php
namespace PHPSimiTextApp\CosineSimilarityCalculator;

class CosineSimilarityCalculator
{
    public function calculateSimilarities($vectors, $main_vector)
    {
        $similarities = [];
        for ($i = 0; $i < count($vectors); $i++) {
            $similarities[$i] = 0;
            $vector1 = $vectors[$i];
            $vector2 = $main_vector;
            $dot_product = 0;
            $vector1_norm = 0;
            $vector2_norm = 0;
            foreach ($vector1 as $word => $weight) {
                $dot_product += $weight * (isset($vector2[$word]) ? $vector2[$word] : 0);
                $vector1_norm += pow($weight, 2);
                $vector2_norm += pow((isset($vector2[$word]) ? $vector2[$word] : 0), 2);
            }
            if ($vector1_norm * $vector2_norm != 0) {
                $similarities[$i] = $dot_product / sqrt($vector1_norm * $vector2_norm);
            } else {
                $similarities[$i] = 0;
            }
        }
        return json_encode($similarities);
    }
}

Executar o código

Para executar o código, vamos usar a classe Index. Ela instancia as classes anteriores e realiza as etapas do algoritmo. Ela também define alguns textos para teste e imprime os resultados na tela.

<?php

namespace PHPSimiTextApp;

use PHPSimiTextApp\CosineSimilarityCalculator\CosineSimilarityCalculator;
use PHPSimiTextApp\StopWordRemover\StopWordRemover;
use PHPSimiTextApp\Tokenizer\Tokenizer;
use PHPSimiTextApp\Vectorizer\Vectorizer;

require_once  __DIR__ . '/vendor/autoload.php';

class Index
{
    public function __construct()
    {
        define('STOP_WORDS', ['a', 'de', 'é', 'um', 'para', 'o', 'e', 'do', 'da']);
        $texts = [
            'Texto 1',
            'Texto 2',
            'Texto 3',
            'Texto 4',
        ];

        // Tokenize texts
        $tokenizer = new Tokenizer();
        $tokens = $tokenizer->tokenizeTexts($texts);

        // Remove stop words
        $stopWordRemover = new StopWordRemover();
        $tokens = $stopWordRemover->removeStopWords($tokens);

        // Vectorize texts
        $vectorizer = new Vectorizer();
        $vectors = $vectorizer->vectorizeTexts($tokens);

        // Vectorize main text
        $main_text = "Principal";
        $main_vector = $vectorizer->vectorizeText(explode(' ', strtolower($main_text)));

        // Calculation of cosine similarity between main text and initial texts
        $cosineSimilarityCalculator = new CosineSimilarityCalculator();
        $similarities = json_decode($cosineSimilarityCalculator->calculateSimilarities($vectors, $main_vector));

        echo "Cosine similarity between main text and initial texts:<br>";
        for ($i = 0; $i < count($similarities); $i++)
            echo "Text " . ($i + 1) . ": " . number_format($similarities[$i], 4) . "<br>";
    }
}

new Index();

Se executarmos esse código, vamos obter a seguinte saída:

Cosine similarity between main text and initial texts:
Text 1: 0.0000
Text 2: 0.0000
Text 3: 0.0000
Text 4: 0.0000

Isso significa que o texto principal não tem nenhuma similaridade com os outros textos, pois eles não compartilham nenhuma palavra em comum.

Conclusão

Neste post, vimos como implementar um algoritmo simples de similaridade de texto usando PHP e o conceito de similaridade de cosseno. Esse algoritmo pode ser útil para comparar documentos, resumir textos, detectar plágio, recomendar conteúdo, entre outras aplicações.

No entanto, esse algoritmo também tem algumas limitações, como:

– Ele não leva em conta a ordem das palavras nos textos, apenas a frequência.

– Ele não leva em conta o contexto ou o significado das palavras, apenas a forma.

– Ele pode ser afetado por palavras muito comuns ou muito raras, que podem distorcer a similaridade.

Existem outros métodos mais avançados de similaridade de texto que tentam superar essas limitações, como o uso de n-gramas, TF-IDF, word embeddings, etc.

Deixe um comentário