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
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.
- Modelos de Lenguaje Grandes y Bases de Datos Vectoriales para Recomendaciones de Noticias
- IA en la industria de la música ¿Cómo dará forma al metaverso musical y a los sonidos del futuro?
- La influencia oculta de la contaminación de datos en los grandes modelos de lenguaje
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
- Motivación
- Resumir textos de forma extractiva y abstractiva
- 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),
La frecuencia del término de cada palabra, en la oración S, se define como,
La frecuencia inversa del documento de cada palabra, en la oración S, se define como,
La puntuación de cada oración es la suma de las puntuaciones de TF-IDF de todas las palabras en esa oración,
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 comonltk
, el tokenizador de oracionespunkt
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 unHashmap
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óncompute
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) yMutex
, que es una primitiva de sincronización básica para garantizar el acceso atómico.Arc
asegura que elMutex
al que se hace referencia sea accesible para todos los hilos, y elMutex
mismo permite que solo un hilo acceda al objeto envuelto en él. Aquí hay otra funciónpar_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ámetrocrate_type
enCargo.toml
comostaticlib
,[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 directoriotarget
.Android
Con el Kit de Desarrollo Nativo de Android (NDK), podemos compilar el programa de Rust para los objetivos
armeabi-v7a
yarm64-v8a
. Necesitamos escribir funciones de interfaz especiales con la Interfaz Nativa de Java (JNI), que se pueden encontrar en el móduloandroid
ensrc/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:
- La implementación actual requiere la versión
nightly
de Rust, solo por una dependencia únicapunkt
.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. Sipunkt
puede ser construido con Rust estable, la implementación actual ya no requerirá Rustnightly
. - 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.
- 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!
Was this article helpful?
93 out of 132 found this helpful
Related articles
- Este documento de IA presenta BioCLIP aprovechando el conjunto de datos TreeOfLife-10M para transformar la visión por computadora en biología y conservación
- SalesForce AI Research BannerGen Una biblioteca de código abierto para la generación de banners de múltiples modalidades.
- ¡El Estudio de IA de Google Tu puerta de entrada al universo creativo de Gemini!
- Lo mejor de TDS en 2023 sobre ChatGPT y LLMs
- Los documentos LLM más importantes de la semana del 04/12 al 10/12
- Aprende IA juntos – Boletín de la comunidad Towards AI #5
- IA en roles íntimos novias y terapeutas