Construyendo un Resumidor de Texto TFIDF de Plataforma Cruzada en Rust

Creando un Resumidor de Texto TFIDF de Plataforma Cruzada en Rust

NLP Multiplataforma en Rust

Optimización con Rayon con uso en C/C++, Android y Python

Foto de Patrick Tomasso en Unsplash

Las herramientas y utilidades de NLP han crecido considerablemente en el ecosistema de Python, permitiendo a los desarrolladores de todos los niveles construir aplicaciones de lenguaje de alta calidad a gran escala. Rust es una novedad en NLP, con organizaciones como HuggingFace que lo adoptan para construir paquetes de aprendizaje automático.

¡Hugging Face ha escrito un nuevo marco de aprendizaje automático en Rust, ahora de código abierto!

Recientemente, Hugging Face ha lanzado al código abierto un marco de aprendizaje automático de gran peso, Candle, que es una desviación de lo habitual en Python…

VoAGI.com

En este blog, exploraremos cómo podemos construir un resumidor de texto utilizando el concepto de TFIDF. Primero, tendremos una intuición de cómo funciona la técnica de resumir con TFIDF y por qué Rust podría ser un buen lenguaje para implementar tuberías de NLP y cómo podemos usar nuestro código Rust en otras plataformas como C/C++, Android y Python. Además, discutiremos cómo podemos optimizar la tarea de resumir con cómputo paralelo utilizando Rayon.

Este es el proyecto en GitHub:

GitHub – shubham0204/tfidf-summarizer.rs: Resumidor de texto simple, eficiente y multiplataforma basado en TFIDF…

Resumidor de texto simple, eficiente y multiplataforma basado en TFIDF en Rust – GitHub – shubham0204/tfidf-summarizer.rs…

github.com

¡Comencemos ➡️

Contenido

  1. Motivación
  2. Resumir textos de forma extractiva y abstractiva
  3. Entendiendo la resumi

    Entendiendo la Sumarización Automática de Texto-1: Métodos Extractivos

    ¿Cómo podemos resumir nuestros documentos automáticamente?

    towardsdatascience.com

    En la sumarización extractiva de textos, las frases o las oraciones se derivan directamente de la oración. Podemos clasificar las oraciones utilizando una función de puntuación y seleccionar las oraciones más adecuadas del texto considerando sus puntuaciones. En lugar de generar nuevo texto, como en la sumarización abstractiva, el resumen es una colección de oraciones seleccionadas del texto, evitando así los problemas que presentan los modelos generativos.

    • En la sumarización extractiva, se mantiene la precisión del texto, pero existe una alta probabilidad de que se pierda cierta información debido a que la granularidad de la selección de texto está limitada solo a las oraciones. Si una pieza de información se encuentra dispersa en múltiples oraciones, la función de puntuación debe tener en cuenta la relación que contiene esas oraciones.
    • La sumarización abtractiva de textos requiere un modelo de aprendizaje profundo más grande para capturar la semántica del lenguaje y construir un mapeo adecuado de documento a resumen. Entrenar dichos modelos requiere conjuntos de datos enormes y un tiempo de entrenamiento más largo, lo cual sobrecarga significativamente los recursos informáticos. Los modelos pre-entrenados pueden resolver el problema de los tiempos de entrenamiento más largos y las demandas de datos, pero siguen estando inherentemente sesgados hacia el dominio del texto en el que fueron entrenados.
    • Los métodos extractivos pueden tener funciones de puntuación libres de parámetros y no requieren ningún aprendizaje. Se encuentran dentro del régimen de aprendizaje no supervisado de Machine Learning, y son útiles ya que requieren menos cálculos y no están sesgados hacia el dominio del texto. La sumarización puede ser igualmente eficiente en artículos de noticias como en fragmentos de novelas.

    Con nuestra técnica basada en TF-IDF, no requerimos ningún conjunto de datos de entrenamiento ni modelos de aprendizaje profundo. Nuestra función de puntuación se basa en las frecuencias relativas de las palabras en diferentes oraciones.

    Entendiendo la Sumarización de Texto con TF-IDF

    Para clasificar cada oración, necesitamos calcular una puntuación que cuantifique la cantidad de información presente en la oración. TF-IDF se compone de dos términos: TF, que significa Frecuencia del Término, e IDF que significa Frecuencia Inversa del Documento.

    TF (Frecuencia del Término)-IDF (Frecuencia Inversa del Documento) desde cero en python.

    Creando un Modelo TF-IDF desde Cero

    towardsdatascience.com

    Consideramos que cada oración está compuesta por tokens (palabras),

    Expr 1: La oración S representada como una tupla de palabras

    La frecuencia del término de cada palabra, en la oración S, se define como,

    Expr 2: k representa el número total de palabras en la oración.

    La frecuencia inversa del documento de cada palabra, en la oración S, se define como,

    Expr 3: La frecuencia inversa del documento cuantifica la aparición de la palabra en otras oraciones.

    La puntuación de cada oración es la suma de las puntuaciones de TF-IDF de todas las palabras en esa oración,

    Expr 4: La puntuación de cada oración S que determina su inclusión en el resumen final.

    Importancia e intuición

    Como habrás observado, la frecuencia de términos sería menor para palabras más raras en la oración. Si la misma palabra tiene menos presencia en otras oraciones, entonces el puntaje IDF también es más alto. Por lo tanto, una oración que contiene palabras repetidas (mayor TF) que son más exclusivas solo para esa oración (mayor IDF) tendrá un puntaje TFIDF más alto.

    Implementación en Rust

    Comenzamos implementando nuestra técnica creando funciones que convierten un texto dado en un Vec de oraciones. Este problema se llama tokenización de oraciones, que identifica los límites de las oraciones dentro de un texto. Con paquetes de Python como nltk, el tokenizador de oraciones punkt está disponible para esta tarea, y también existe una versión de Rust de Punkt. rust-punkt ya no se mantiene, pero aun lo usamos aquí. También se ha escrito una función que divide la oración en palabras,

    use punkt::{SentenceTokenizer, TrainingData};use punkt::params::Standard;static STOPWORDS: [ &str ; 127 ] = [ "yo", "me", "mi", "mí misma", "nosotros", "nuestro", "nuestros", "nosotras", "tú",     "tuyo", "tuyos", "tú misma", "ustedes", "vosotros", "vosotras", "vuestro", "vuestros", "vosotras mismas",     "él", "él mismo", "ela", "de ella", "de ella misma", "ello", "suyo", "suyo mismo", "ellos", "ellos mismos", "ellas", "ellas mismas", "lo que", "cual", "quién", "quienes", "esto",     "eso", "estos", "esos", "que", "quién", "esta",     "ese", "esos", "soy", "eres", "es", "somos", "sois", "son", "ser", "sido", "siendo",      "hacer", "haces", "hace", "hacemos", "hacéis", "hacen", "hizo", "hacía", "hacías", "hacíamos", "hacíais", "hacían", "hago", "haces", "hace", "hacemos",      "hacéis", "hacen", "un", "una", "el", "la", "los", "las", "y", "pero", "si", "o", "porque", "como", "hasta", "mientras", "de",      "a", "por", "para", "con", "acerca de", "contra", "entre", "dentro de", "a través de", "durante", "antes", "después", "arriba",     "abajo", "en", "fuera", "en", "fuera", "en", "fuera", "arriba", "abajo", "de nuevo", "más", "entonces", "una vez",       "aquí", "allí", "cuando", "donde", "por qué", "cómo", "todo", "cualquier", "ambos", "cada", "pocos", "más", "la mayoría", "otros",        "algunos", "tal", "ninguno", "ni", "no", "sólo", "propio", "mismo", "así", "que", "demasiado", "muy", "s", "t", "puede",        "será", "justo", "no", "debería", "ahora" ] ;/// Transforma un `texto` en una lista de oraciones/// Utiliza el popular tokenizador de oraciones Punkt de un puerto en Rust: /// <`/`>https://github.com/ferristseng/rust-punkt<`/`>pub fn texto_a_oraciones( texto: &str ) -> Vec<String> {    let inglés = TrainingData::inglés();    let mut oraciones: Vec<String> = Vec::new() ;     for s in SentenceTokenizer::<Standard>::new(texto, &inglés) {        oraciones.push( s.to_owned() ) ;     }    oraciones}/// Transforma la oración en una lista de palabras (tokens)/// eliminando las palabras vacías mientras lo hacépub fn oración_a_tokens( oración: &str ) -> Vec<&str> {    let tokens: Vec<&str> = oración.split_ascii_whitespace().collect() ;     let tokens_filtrados: Vec<&str> = tokens                                .into_iter()                                .filter( |token| !STOPWORDS.contains( &token.to_lowercase().as_str() ) )                                .collect() ;    tokens_filtrados}

    En el fragmento anterior, eliminamos las palabras vacías, que son palabras que ocurren comúnmente en un idioma y no tienen una contribución significativa al contenido de información del texto.

    Preprocesamiento de texto: Eliminación de palabras vacías usando diferentes bibliotecas

    ¡Una guía práctica sobre la eliminación de stop words en inglés en Python!

    towardsdatascience.com

    A continuación, creamos una función que calcula la frecuencia de cada palabra presente en el corpus. Este método se utilizará para calcular la frecuencia de término de cada palabra presente en una oración. El par (palabra, frecuencia) se almacena en un Hashmap para una recuperación más rápida en etapas posteriores.

    use std::collections::HashMap;/// Dada una lista de palabras, construye un mapa de frecuencia/// donde las claves son las palabras y los valores son las frecuencias de esas palabras/// Este método se utilizará para calcular las frecuencias de término de cada palabra/// presente en una oraciónpub fn get_freq_map<'a>(words: &'a Vec<&'a str>) -> HashMap<&'a str, usize> {    let mut freq_map: HashMap<&str, usize> = HashMap::new();     for word in words {        if freq_map.contains_key(word) {            freq_map                .entry(word)                .and_modify(|e| {                     *e += 1;                 });         }        else {            freq_map.insert(*word, 1);         }    }    freq_map}

    A continuación, escribimos la función que calcula la frecuencia de término de las palabras presentes en una oración,

    // Calcula la frecuencia de término de los tokens presentes en la oración dada (tokenizada)// La frecuencia de término TF del token 'w' se expresa como,// TF(w) = (frecuencia de w en la oración) / (número total de tokens en la oración)fn compute_term_frequency<'a>(    tokenized_sentence: &'a Vec<&'a str>) -> HashMap<&'a str, f32> {    let words_frequencies = Tokenizer::get_freq_map(tokenized_sentence);    let mut term_frequency: HashMap<&str, f32> = HashMap::new();      let num_tokens = tokenized_sentence.len();     for (word, count) in words_frequencies {        term_frequency.insert(word, (count as f32) / (num_tokens as f32));     }    term_frequency}

    Otra función que calcula el IDF, frecuencia inversa de documento, para palabras en una oración tokenizada,

    // Calcula la frecuencia inversa de documento de los tokens presentes en la oración dada (tokenizada)// La frecuencia inversa de documento IDF del token 'w' se expresa como,// IDF(w) = log( N / (Número de documentos en los que aparece w) )fn compute_inverse_doc_frequency<'a>(    tokenized_sentence: &'a Vec<&'a str>,    tokens: &'a Vec<vec>) -> HashMap<&'a str, f32> {    let num_docs = tokens.len() as f32;     let mut idf: HashMap<&str, f32> = HashMap::new();     for word in tokenized_sentence {        let mut word_count_in_docs: usize = 0;         for doc in tokens {            word_count_in_docs += doc.iter().filter(|&token| token == word).count();        }        idf.insert(word, ((num_docs) / (word_count_in_docs as f32)).log10());    }    idf}</vec

    Ahora hemos añadido funciones para calcular las puntuaciones de TF e IDF de cada palabra presente en una oración. Para calcular una puntuación final para cada oración, que también determinará su rango, debemos calcular la suma de las puntuaciones TFIDF de todas las palabras presentes en una oración.

    pub fn compute(text: &str, reduction_factor: f32) -> String {    let sentences_owned: Vec = Tokenizer::text_to_sentences(text);     let mut sentences: Vec<&str> = sentences_owned                                            .iter()                                            .map(String::as_str)                                            .collect();     let mut tokens: Vec<vec> = Vec::new();     for sentence in &sentences {        tokens.push(Tokenizer::sentence_to_tokens(sentence));     }    let mut sentence_scores: HashMap<&str, f32> = HashMap::new();        for (i, tokenized_sentence) in tokens.iter().enumerate() {        let tf: HashMap<&str, f32> = Summarizer::compute_term_frequency(tokenized_sentence);         let idf: HashMap<&str, f32> = Summarizer::compute_inverse_doc_frequency(tokenized_sentence, &tokens);         let mut tfidf_sum: f32 = 0.0;         // Calcular la puntuación TFIDF para cada palabra        // y añadirla a tfidf_sum        for word in tokenized_sentence {            tfidf_sum += tf.get(word).unwrap() * idf.get(word).unwrap();         }        sentence_scores.insert(sentences[i], tfidf_sum);     }    // Ordenar las oraciones por sus puntuaciones    sentences.sort_by(|a, b|         sentence_scores.get(b).unwrap().total_cmp(sentence_scores.get(a).unwrap()));     // Calcular el número de oraciones que se incluirán en el resumen    // y devolver el resumen extraído    let num_summary_sents = (reduction_factor * (sentences.len() as f32)) as usize;    sentences[0..num_summary_sents].join(" ")}</vec

    Usando Rayon

    Para textos más largos, podemos realizar algunas operaciones en paralelo, es decir, en múltiples hilos de CPU utilizando una popular biblioteca de Rust llamada rayon-rs. En la función compute anterior, podemos realizar las siguientes tareas en paralelo:

    • Convertir cada oración en tokens y eliminar las palabras de parada
    • Calcular la suma de los puntajes de TFIDF para cada oración

    Estas tareas se pueden realizar de forma independiente en cada oración y no dependen de otras oraciones, por lo tanto, se pueden paralelizar. Para garantizar la exclusión mutua cuando diferentes hilos acceden a un contenedor compartido, utilizamos Arc (puntero contado de referencia atómica) y Mutex, que es una primitiva de sincronización básica para garantizar el acceso atómico.

    Arc asegura que el Mutex al que se hace referencia sea accesible para todos los hilos, y el Mutex mismo permite que solo un hilo acceda al objeto envuelto en él. Aquí hay otra función par_compute, que utiliza Rayon y realiza las tareas mencionadas anteriormente de forma paralela:

    pub fn par_compute(     text: &str ,     reduction_factor: f32 ) -> String {    let sentences_owned: Vec<String> = Tokenizer::text_to_sentences( text ) ;     let mut sentences: Vec<&str> = sentences_owned                                            .iter()                                            .map( String::as_str )                                            .collect() ;         // Tokenizar las oraciones en paralelo con Rayon    // Declarar un Vec<Vec<&str>> seguro para hilos para almacenar las oraciones tokenizadas    let tokens_ptr: Arc<Mutex<Vec<Vec<&str>>>> = Arc::new( Mutex::new( Vec::new() ) ) ;     sentences.par_iter()             .for_each( |sentence| {                 let sent_tokens: Vec<&str> = Tokenizer::sentence_to_tokens(sentence) ;                 tokens_ptr.lock().unwrap().push( sent_tokens ) ;              } ) ;     let tokens = tokens_ptr.lock().unwrap() ;     // Calcular puntajes para las oraciones en paralelo    // Declarar un HashMap<&str,f32> seguro para hilos para almacenar los puntajes de las oraciones    let sentence_scores_ptr: Arc<Mutex<HashMap<&str,f32>>> = Arc::new( Mutex::new( HashMap::new() ) ) ;     tokens.par_iter()          .zip( sentences.par_iter() )          .for_each( |(tokenized_sentence , sentence)| {        let tf: HashMap<&str,f32> = Summarizer::compute_term_frequency(tokenized_sentence) ;         let idf: HashMap<&str,f32> = Summarizer::compute_inverse_doc_frequency(tokenized_sentence, &tokens ) ;         let mut tfidf_sum: f32 = 0.0 ;                 for word in tokenized_sentence {            tfidf_sum += tf.get( word ).unwrap() * idf.get( word ).unwrap() ;         }        tfidf_sum /= tokenized_sentence.len() as f32 ;         sentence_scores_ptr.lock().unwrap().insert( sentence , tfidf_sum ) ;     } ) ;     let sentence_scores = sentence_scores_ptr.lock().unwrap() ;    // Ordenar las oraciones según sus puntajes    sentences.sort_by( | a , b |         sentence_scores.get(b).unwrap().total_cmp(sentence_scores.get(a).unwrap()) ) ;     // Calcular número de oraciones a incluir en el resumen    // y devolver el resumen extraído    let num_summary_sents = (reduction_factor * (sentences.len() as f32) ) as usize;    sentences[ 0..num_summary_sents ].join( ". " ) }

    Uso en varias plataformas

    C y C++

    Para utilizar estructuras y funciones de Rust en C, podemos utilizar cbindgen para generar encabezados en estilo C que contengan los prototipos de estructuras/funciones. Al generar los encabezados, podemos compilar el código de Rust en bibliotecas dinámicas o estáticas basadas en C que contengan la implementación de las funciones declaradas en los archivos de encabezado. Para generar una biblioteca estática en C, es necesario establecer el parámetro crate_type en Cargo.toml como staticlib,

    [lib]nombre = "summarizer"tipo_caja = [ "staticlib" ]

    A continuación, agregamos FFIs para exponer las funciones del resumidor en la ABI (interfaz binaria de la aplicación) en src/lib.rs,

    /// funciones que exponen los métodos de Rust como interfaces C/// Estos métodos son accesibles con la ABI (código de objeto compilado)mod c_binding {    use std::ffi::CString;    use crate::summarizer::Summarizer;    #[no_mangle]    pub extern "C" fn resumir( texto: *const u8 , longitud: usize , factor_reduccion: f32 ) -> *const u8 {        ...      }    #[no_mangle]    pub extern "C" fn par_resumir( texto: *const u8 , longitud: usize , factor_reduccion: f32 ) -> *const u8 {        ...    }}

    Podemos construir la biblioteca estática con cargo build y se generará libsummarizer.a en el directorio target.

    Android

    Con el Kit de Desarrollo Nativo de Android (NDK), podemos compilar el programa de Rust para los objetivos armeabi-v7a y arm64-v8a. Necesitamos escribir funciones de interfaz especiales con la Interfaz Nativa de Java (JNI), que se pueden encontrar en el módulo android en src/lib.rs.

    Kotlin JNI para Código Nativo

    Cómo llamar código nativo desde Kotlin.

    matt-moore.medium.com

    Python

    Con el módulo ctypes de Python, podemos cargar una biblioteca compartida (.so o .dll) y usar los tipos de datos compatibles con C para ejecutar las funciones definidas en la biblioteca. El código no está disponible en el proyecto de GitHub, pero estará pronto disponible.

    Enlaces Python: Llamar a C o C++ desde Python – Real Python

    ¿Qué son los enlaces de Python? ¿Deberías usar ctypes, CFFI u otra herramienta? En este tutorial paso a paso, obtendrás…

    realpython.com

    Alcance Futuro

    El proyecto puede ser ampliado y mejorado de muchas formas, que discutiremos a continuación:

    1. La implementación actual requiere la versión nightly de Rust, solo por una dependencia única punkt. punkt es un tokenizador de oraciones que se requiere para determinar los límites de las oraciones en el texto, después de lo cual se realizan otros cálculos. Si punkt puede ser construido con Rust estable, la implementación actual ya no requerirá Rust nightly.
    2. Agregar nuevas métricas para clasificar oraciones, especialmente aquellas que capturan dependencias entre oraciones. TFIDF no es la función de puntuación más precisa y tiene sus limitaciones. La construcción de grafos de oraciones y su uso para puntuar oraciones ha mejorado enormemente la calidad general del resumen extraído.
    3. El resumidor no ha sido probado con un conjunto de datos conocido. Los puntajes de Rouge (R1, R2 y RL) se usan con frecuencia para evaluar la calidad del resumen generado en conjuntos de datos estándar como el conjunto de datos del New York Times o el conjunto de datos de CNN Daily Mail. Medir el rendimiento frente a pruebas estándar proporcionará a los desarrolladores una mayor claridad y confiabilidad hacia la implementación.

    Conclusión

    El desarrollo de utilidades de procesamiento de lenguaje natural (NLP, por sus siglas en inglés) con Rust ofrece ventajas significativas, considerando la creciente popularidad de este lenguaje entre los desarrolladores debido a su rendimiento y promesas futuras. Espero que este artículo haya sido útil. Echa un vistazo al proyecto en GitHub:

    GitHub – shubham0204/tfidf-summarizer.rs: Resumidor de texto basado en TFIDF simple, eficiente y multiplataforma en Rust…

    Resumidor de texto basado en TFIDF simple, eficiente y multiplataforma en Rust – GitHub – shubham0204/tfidf-summarizer.rs…

    github.com

    ¡Si tienes alguna sugerencia para mejorar algo, considera abrir una incidencia o enviar una solicitud de fusión! Sigue aprendiendo y que tengas un buen día.

We will continue to update Zepes; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

Inteligencia Artificial

AlphaFold, Herramientas similares podrían ayudar en la preparación para la próxima pandemia

Los investigadores cada vez más están utilizando la inteligencia artificial para ayudar a prepararse para futuras pan...

Inteligencia Artificial

Destacar el texto mientras se está hablando utilizando Amazon Polly

Amazon Polly es un servicio que convierte texto en habla realista. Permite el desarrollo de una amplia gama de aplica...

Inteligencia Artificial

Este artículo de IA de Stanford y Google introduce agentes generativos agentes computacionales interactivos que simulan el comportamiento humano'.

Sin lugar a dudas, los bots de IA pueden generar un lenguaje natural de alta calidad y fluidez. Durante mucho tiempo,...

Inteligencia Artificial

Una forma más rápida de enseñar a un robot

Una nueva técnica ayuda a un usuario no técnico a entender por qué un robot falló, y luego ajustarlo con un esfuerzo ...