WordPress y Machine Learning para mostrar artículos relacionados

- 12 August 2015 - 2 mins read

Hace poco he empezado a interesarme por el Machine Learning y he querido hacer un primer ejercicio práctico con algo que conozco bien: mi blog en WordPress. La idea es sencilla: mostrar al final de cada artículo una lista de artículos relacionados, no basándome en etiquetas ni categorías, sino en el contenido real de los posts.

¿Por qué es un ejercicio de Machine Learning?

Aunque el código es muy básico y no usa librerías ni modelos avanzados, aplica conceptos fundamentales de Machine Learning y procesamiento de lenguaje natural (NLP):

  • Representación de texto: convertimos el contenido de cada post en un vector numérico basado en la frecuencia de palabras (BoW: bag-of-words).
  • Medición de similitud: calculamos la similitud entre textos usando la métrica de similitud del coseno, que es una forma común de comparar documentos.
  • Clasificación implícita: los posts que tienen mayor similitud se “clasifican” como más relacionados.

Cómo funciona el sistema

  1. Se carga el contenido de todos los posts publicados.
  2. Se limpia el texto, eliminando palabras vacías (stopwords) y signos de puntuación.
  3. Se convierte el texto en un vector de frecuencias de palabras.
  4. Se calcula la similitud de coseno entre el post actual y todos los demás.
  5. Se seleccionan los 4 posts con mayor similitud y se muestran como relacionados.

El código completo

/**
 * Clase para encontrar artículos relacionados basándose en el contenido usando Machine Learning
 *
 * @package RelatedPosts
 * @author Alberto Roura
 * @version 1.0
 */
class RelatedPostsByContent {

    /**
     * @param int $post_id ID del post para el cual buscar relacionados
     * @param int $limit Número máximo de posts relacionados a devolver (por defecto 4)
     * @return array Array de posts relacionados con título, excerpt y permalink
     */
    public function get_related($post_id, $limit = 4) {
        $query = new WP_Query(array(
            'post_type' => 'post',
            'post_status' => 'publish',
            'posts_per_page' => -1,
            'fields' => 'all',
            'no_found_rows' => true,
            'update_post_meta_cache' => false,
            'update_post_term_cache' => false
        ));

        $posts = $query->posts;

        $current = null;
        foreach ($posts as $p) {
            if ($p->ID == $post_id) {
                $current = $p;
                break;
            }
        }
        if (!$current) return [];

        $lang = $this->detect_language($current->post_content);
        $vector_current = $this->text_to_vector($current->post_content, $lang);

        $scores = array();
        foreach ($posts as $post) {
            if ($post->ID == $post_id) continue;
            if ($this->detect_language($post->post_content) !== $lang) continue;

            $vector = $this->text_to_vector($post->post_content, $lang);
            $similarity = $this->cosine_similarity($vector_current, $vector);
            $scores[$post->ID] = $similarity;
        }

        arsort($scores);
        $top_ids = array_slice(array_keys($scores), 0, $limit);

        $results = array();
        foreach ($top_ids as $id) {
            $results[] = array(
                'title' => get_the_title($id),
                'excerpt' => get_the_excerpt($id),
                'permalink' => get_permalink($id),
            );
        }

        return $results;
    }

    /**
     * @param string $text Texto del cual detectar el idioma
     * @return string 'es' para español o 'en' para inglés
     */
    protected function detect_language($text) {
        $text = strtolower(strip_tags($text));
        $count_es = preg_match_all('/\b(el|la|de|que|en|una|como)\b/u', $text);
        $count_en = preg_match_all('/\b(the|and|of|to|with|this)\b/u', $text);
        return $count_es >= $count_en ? 'es' : 'en';
    }

    /**
     * @param string $text Texto a convertir en vector
     * @param string $lang Idioma del texto ('es' o 'en') para aplicar stopwords correctas
     * @return array Vector asociativo donde las claves son palabras y los valores son frecuencias
     */
    protected function text_to_vector($text, $lang) {
        $stopwords = array(
            'es' => array('el','la','los','las','de','y','a','en','por','con','para','es','un','una','que','se','no','al','como','más'),
            'en' => array('the','and','of','to','in','is','that','with','for','on','as','it','at','this','by','an','be')
        );

        $text = strtolower(strip_tags($text));
        $text = preg_replace('/[^a-záéíóúüñ\s]/u', '', $text);
        $words = explode(' ', $text);

        $filtered = array();
        foreach ($words as $word) {
            if ($word !== '' && !in_array($word, $stopwords[$lang])) {
                $filtered[] = $word;
            }
        }

        return array_count_values($filtered);
    }

    /**
     * @param array $vec1 Primer vector de frecuencias de palabras
     * @param array $vec2 Segundo vector de frecuencias de palabras
     * @return float Valor de similitud entre 0 y 1
     */
    protected function cosine_similarity($vec1, $vec2) {
        $intersection = array_intersect_key($vec1, $vec2);
        $dot_product = 0;
        foreach ($intersection as $word => $_) {
            $dot_product += $vec1[$word] * $vec2[$word];
        }

        $sum1 = 0;
        foreach ($vec1 as $val) {
            $sum1 += $val * $val;
        }

        $sum2 = 0;
        foreach ($vec2 as $val) {
            $sum2 += $val * $val;
        }

        $denominator = sqrt($sum1) * sqrt($sum2);
        return $denominator ? $dot_product / $denominator : 0;
    }
}

Cómo mostrar los posts relacionados en tu tema

Solo tienes que llamar a esta función y procesar el resultado para mostrar los enlaces:

// Crear una instancia de la clase
$finder = new RelatedPostsByContent();

// Obtener posts relacionados para el post actual
$related_posts = $finder->get_related(get_the_ID());

// Mostrar los posts relacionados si existen
if (!empty($related_posts)) {
    echo '<h3>Artículos relacionados</h3><ul>';
    foreach ($related_posts as $post) {
        echo '<li><a href="'.$post['permalink'].'">'.$post['title'].'</a></li>';
    }
    echo '</ul>';
}


Share: Link copied to clipboard

Tags:

Previous: RSCSS
Next: Sobre Ashley Madison y la Superioridad Moral

Where: Home > Technical > WordPress y Machine Learning para mostrar artículos relacionados