|
|
""" |
|
|
MVP: Comparador de Modelos de Análisis de Sentimiento |
|
|
|
|
|
Aplicación Gradio que compara dos modelos de análisis de sentimiento en español: |
|
|
- pysentimiento/robertuito-sentiment-analysis (entrenado en Twitter) |
|
|
- finiteautomata/beto-sentiment-analysis (BETO clásico) |
|
|
|
|
|
Características: |
|
|
- ✅ Comparación lado a lado de modelos |
|
|
- ✅ Análisis directo de texto |
|
|
- ✅ Análisis web desde URLs |
|
|
- ✅ Interfaz con pestañas |
|
|
- ✅ Ejemplos en español argentino |
|
|
|
|
|
Arquitectura: |
|
|
- Carga eficiente: modelos cargados una sola vez al inicio |
|
|
- Procesamiento paralelo: ambos modelos ejecutados simultáneamente |
|
|
- Extracción web: BeautifulSoup para contenido de páginas web |
|
|
|
|
|
Autor: Desarrollado para IFTS - Procesamiento de Lenguaje Natural |
|
|
Fecha: 2025 |
|
|
""" |
|
|
|
|
|
import gradio as gr |
|
|
from transformers import pipeline |
|
|
import time |
|
|
import requests |
|
|
from bs4 import BeautifulSoup |
|
|
from typing import Dict, Tuple, Optional |
|
|
from urllib.parse import urlparse |
|
|
|
|
|
|
|
|
MODELOS = { |
|
|
"RoBERTuito": "pysentimiento/robertuito-sentiment-analysis", |
|
|
"BETO": "finiteautomata/beto-sentiment-analysis", |
|
|
} |
|
|
|
|
|
|
|
|
ETIQUETAS_ES = {"POS": "Positivo", "NEG": "Negativo", "NEU": "Neutral"} |
|
|
|
|
|
|
|
|
class ComparadorSentimientos: |
|
|
"""Clase para manejar la comparación de modelos de sentimiento.""" |
|
|
|
|
|
def __init__(self): |
|
|
self.modelos = {} |
|
|
self._cargar_modelos() |
|
|
|
|
|
def _cargar_modelos(self): |
|
|
"""Carga ambos modelos una sola vez al inicio.""" |
|
|
print("Cargando modelos de analisis de sentimiento...") |
|
|
|
|
|
for nombre, modelo_path in MODELOS.items(): |
|
|
print(f" Cargando {nombre}: {modelo_path}") |
|
|
try: |
|
|
self.modelos[nombre] = pipeline( |
|
|
"sentiment-analysis", model=modelo_path, return_all_scores=False |
|
|
) |
|
|
print(f" {nombre} cargado exitosamente") |
|
|
except Exception as e: |
|
|
print(f" Error cargando {nombre}: {str(e)}") |
|
|
self.modelos[nombre] = None |
|
|
|
|
|
print("Modelos cargados!\n") |
|
|
|
|
|
def extraer_texto_web(self, url: str) -> str: |
|
|
""" |
|
|
Extrae texto de una página web. |
|
|
|
|
|
Args: |
|
|
url: URL de la página web |
|
|
|
|
|
Returns: |
|
|
Texto extraído o mensaje de error |
|
|
""" |
|
|
if not url.strip(): |
|
|
return "❌ Por favor ingresa una URL válida" |
|
|
|
|
|
|
|
|
try: |
|
|
parsed = urlparse(url) |
|
|
if not parsed.scheme or not parsed.netloc: |
|
|
return "❌ URL inválida. Asegúrate de incluir http:// o https://" |
|
|
except: |
|
|
return "❌ URL inválida" |
|
|
|
|
|
try: |
|
|
print(f"Extrayendo texto de: {url}") |
|
|
|
|
|
|
|
|
headers = { |
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" |
|
|
} |
|
|
|
|
|
|
|
|
response = requests.get(url, headers=headers, timeout=10) |
|
|
response.raise_for_status() |
|
|
|
|
|
|
|
|
soup = BeautifulSoup(response.content, "html.parser") |
|
|
|
|
|
|
|
|
for script in soup(["script", "style"]): |
|
|
script.decompose() |
|
|
|
|
|
|
|
|
texto = soup.get_text() |
|
|
|
|
|
|
|
|
lineas = (linea.strip() for linea in texto.splitlines()) |
|
|
chunks = (frase.strip() for frase in lineas if frase) |
|
|
texto_limpio = " ".join(chunks) |
|
|
|
|
|
|
|
|
if len(texto_limpio) > 5000: |
|
|
texto_limpio = texto_limpio[:5000] + "..." |
|
|
|
|
|
print(f"Texto extraido: {len(texto_limpio)} caracteres") |
|
|
return texto_limpio |
|
|
|
|
|
except requests.exceptions.Timeout: |
|
|
return "ERROR: Timeout - La pagina tardo demasiado en responder" |
|
|
except requests.exceptions.ConnectionError: |
|
|
return "ERROR: Error de conexion - Verifica la URL" |
|
|
except requests.exceptions.HTTPError as e: |
|
|
return f"ERROR: Error HTTP: {e}" |
|
|
except Exception as e: |
|
|
return f"ERROR: Error inesperado: {str(e)}" |
|
|
|
|
|
def _chunk_text(self, texto: str, max_tokens: int = 400) -> list: |
|
|
""" |
|
|
Divide texto largo en chunks más pequeños. |
|
|
|
|
|
Args: |
|
|
texto: Texto a dividir |
|
|
max_tokens: Máximo tokens por chunk (aproximado) |
|
|
|
|
|
Returns: |
|
|
Lista de chunks de texto |
|
|
""" |
|
|
|
|
|
max_chars = max_tokens * 4 |
|
|
|
|
|
if len(texto) <= max_chars: |
|
|
return [texto] |
|
|
|
|
|
|
|
|
oraciones = texto.split(".") |
|
|
chunks = [] |
|
|
chunk_actual = "" |
|
|
|
|
|
for oracion in oraciones: |
|
|
oracion = oracion.strip() + "." |
|
|
|
|
|
|
|
|
if len(chunk_actual) + len(oracion) > max_chars and chunk_actual: |
|
|
chunks.append(chunk_actual.strip()) |
|
|
chunk_actual = oracion |
|
|
else: |
|
|
chunk_actual += oracion |
|
|
|
|
|
|
|
|
if chunk_actual: |
|
|
chunks.append(chunk_actual.strip()) |
|
|
|
|
|
return chunks |
|
|
|
|
|
def _analizar_chunk(self, modelo, chunk: str) -> Optional[Dict]: |
|
|
""" |
|
|
Analiza un chunk individual de texto. |
|
|
|
|
|
Args: |
|
|
modelo: Pipeline de transformers |
|
|
chunk: Chunk de texto a analizar |
|
|
|
|
|
Returns: |
|
|
Resultado del análisis o None si hay error |
|
|
""" |
|
|
try: |
|
|
resultado = modelo(chunk)[0] |
|
|
return {"label": resultado["label"], "score": resultado["score"]} |
|
|
except Exception as e: |
|
|
print(f"Error analizando chunk: {str(e)}") |
|
|
return None |
|
|
|
|
|
def analizar_texto(self, texto: str) -> Dict[str, Dict[str, float]]: |
|
|
""" |
|
|
Analiza el sentimiento usando ambos modelos con chunking para textos largos. |
|
|
|
|
|
Args: |
|
|
texto: Texto a analizar |
|
|
|
|
|
Returns: |
|
|
Diccionario con resultados de ambos modelos |
|
|
""" |
|
|
if not texto.strip(): |
|
|
return { |
|
|
"error": "Por favor ingresa un texto válido", |
|
|
"RoBERTuito": {"Error": 1.0}, |
|
|
"BETO": {"Error": 1.0}, |
|
|
} |
|
|
|
|
|
resultados = {} |
|
|
|
|
|
|
|
|
limites_tokens = { |
|
|
"RoBERTuito": 400, |
|
|
"BETO": 100, |
|
|
} |
|
|
|
|
|
for nombre_modelo, modelo in self.modelos.items(): |
|
|
if modelo is None: |
|
|
resultados[nombre_modelo] = {"Error": 1.0} |
|
|
continue |
|
|
|
|
|
try: |
|
|
inicio = time.time() |
|
|
|
|
|
|
|
|
chunks = self._chunk_text(texto, limites_tokens[nombre_modelo]) |
|
|
|
|
|
print(f"Procesando {len(chunks)} chunks con {nombre_modelo}") |
|
|
|
|
|
|
|
|
resultados_chunks = [] |
|
|
for i, chunk in enumerate(chunks): |
|
|
resultado = self._analizar_chunk(modelo, chunk) |
|
|
if resultado: |
|
|
resultados_chunks.append(resultado) |
|
|
|
|
|
if not resultados_chunks: |
|
|
resultados[nombre_modelo] = { |
|
|
"Error": 1.0, |
|
|
"_error": "No se pudo procesar ningún chunk", |
|
|
} |
|
|
continue |
|
|
|
|
|
|
|
|
votos = {"POS": 0, "NEG": 0, "NEU": 0} |
|
|
suma_confianza = 0 |
|
|
|
|
|
for resultado in resultados_chunks: |
|
|
votos[resultado["label"]] += 1 |
|
|
suma_confianza += resultado["score"] |
|
|
|
|
|
|
|
|
etiqueta_ganadora = max(votos, key=votos.get) |
|
|
confianza_promedio = suma_confianza / len(resultados_chunks) |
|
|
|
|
|
tiempo = time.time() - inicio |
|
|
|
|
|
|
|
|
etiqueta_es = ETIQUETAS_ES.get(etiqueta_ganadora, etiqueta_ganadora) |
|
|
|
|
|
resultados[nombre_modelo] = { |
|
|
etiqueta_es: round(confianza_promedio, 4), |
|
|
"_tiempo": round(tiempo, 3), |
|
|
"_confianza": round(confianza_promedio, 4), |
|
|
"_chunks_procesados": len(resultados_chunks), |
|
|
"_total_chunks": len(chunks), |
|
|
} |
|
|
|
|
|
except Exception as e: |
|
|
resultados[nombre_modelo] = {"Error": 1.0, "_error": str(e)} |
|
|
|
|
|
return resultados |
|
|
|
|
|
|
|
|
|
|
|
comparador = ComparadorSentimientos() |
|
|
|
|
|
|
|
|
def analizar_sentimiento(texto: str) -> Tuple[str, str]: |
|
|
""" |
|
|
Función principal para la interfaz Gradio. |
|
|
|
|
|
Args: |
|
|
texto: Texto ingresado por el usuario |
|
|
|
|
|
Returns: |
|
|
Tupla con resultados formateados para ambos modelos |
|
|
""" |
|
|
resultados = comparador.analizar_texto(texto) |
|
|
|
|
|
|
|
|
robertuito_text = "" |
|
|
beto_text = "" |
|
|
|
|
|
if "error" in resultados: |
|
|
robertuito_text = f"ERROR: {resultados['error']}" |
|
|
beto_text = f"ERROR: {resultados['error']}" |
|
|
else: |
|
|
|
|
|
robertuito = resultados.get("RoBERTuito", {"Error": 1.0}) |
|
|
if "Error" in robertuito: |
|
|
robertuito_text = "ERROR en RoBERTuito" |
|
|
else: |
|
|
etiqueta = list(robertuito.keys())[0] |
|
|
confianza = robertuito[etiqueta] |
|
|
tiempo = robertuito.get("_tiempo", 0) |
|
|
chunks = robertuito.get("_chunks_procesados", 1) |
|
|
total_chunks = robertuito.get("_total_chunks", 1) |
|
|
robertuito_text = f"-> {etiqueta}: {confianza:.1%} ({tiempo:.2f}s, {chunks}/{total_chunks} chunks)" |
|
|
|
|
|
|
|
|
beto = resultados.get("BETO", {"Error": 1.0}) |
|
|
if "Error" in beto: |
|
|
beto_text = "ERROR en BETO" |
|
|
else: |
|
|
etiqueta = list(beto.keys())[0] |
|
|
confianza = beto[etiqueta] |
|
|
tiempo = beto.get("_tiempo", 0) |
|
|
chunks = beto.get("_chunks_procesados", 1) |
|
|
total_chunks = beto.get("_total_chunks", 1) |
|
|
beto_text = f"-> {etiqueta}: {confianza:.1%} ({tiempo:.2f}s, {chunks}/{total_chunks} chunks)" |
|
|
|
|
|
return robertuito_text, beto_text |
|
|
|
|
|
|
|
|
|
|
|
EJEMPLOS = [ |
|
|
["La verdad, este lugar está bárbaro. Muy recomendable."], |
|
|
["Qué buena onda la atención, volvería sin dudarlo."], |
|
|
["Me encantó la comida, aunque la música estaba muy fuerte."], |
|
|
["Una porquería de servicio, nunca más vuelvo."], |
|
|
["Qué garrón, tardaron una banda en traer el pedido."], |
|
|
["Re copado todo, la rompieron con el ambiente."], |
|
|
["Zafa, pero nada especial el lugar."], |
|
|
["Está piola el lugar, volvería."], |
|
|
] |
|
|
|
|
|
|
|
|
with gr.Blocks( |
|
|
title="Comparador de Modelos de Sentimiento", |
|
|
theme=gr.themes.Soft(), |
|
|
css=""" |
|
|
.gradio-container { |
|
|
max-width: 1200px; |
|
|
margin: auto; |
|
|
} |
|
|
.title { |
|
|
text-align: center; |
|
|
color: #2563eb; |
|
|
font-size: 2.5em; |
|
|
margin-bottom: 1em; |
|
|
} |
|
|
.subtitle { |
|
|
text-align: center; |
|
|
color: #64748b; |
|
|
font-size: 1.1em; |
|
|
margin-bottom: 2em; |
|
|
} |
|
|
.tab-content { |
|
|
padding: 1em; |
|
|
} |
|
|
""", |
|
|
) as demo: |
|
|
|
|
|
gr.HTML(""" |
|
|
<div class="title">🆚 Comparador de Modelos de Sentimiento</div> |
|
|
<div class="subtitle"> |
|
|
Compara RoBERTuito vs BETO en análisis de sentimiento para español<br> |
|
|
<strong>RoBERTuito:</strong> Especializado en lenguaje coloquial y redes sociales<br> |
|
|
<strong>BETO:</strong> Modelo clásico entrenado en Wikipedia |
|
|
</div> |
|
|
""") |
|
|
|
|
|
|
|
|
with gr.Tabs(): |
|
|
|
|
|
with gr.TabItem("📝 Análisis Directo", id="directo"): |
|
|
with gr.Row(): |
|
|
with gr.Column(scale=2): |
|
|
texto_input = gr.Textbox( |
|
|
label="📝 Texto para analizar", |
|
|
placeholder="Ingresa aquí el texto que quieres analizar...", |
|
|
lines=4, |
|
|
show_copy_button=True, |
|
|
) |
|
|
|
|
|
|
|
|
analizar_btn = gr.Button( |
|
|
"🔍 Analizar Sentimiento", variant="primary", size="lg" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(): |
|
|
gr.HTML( |
|
|
"<h3 style='text-align: center; color: #059669;'>🤖 RoBERTuito</h3>" |
|
|
) |
|
|
robertuito_output = gr.Textbox( |
|
|
label="Resultado RoBERTuito", interactive=False, lines=2 |
|
|
) |
|
|
|
|
|
with gr.Column(): |
|
|
gr.HTML( |
|
|
"<h3 style='text-align: center; color: #dc2626;'>📚 BETO</h3>" |
|
|
) |
|
|
beto_output = gr.Textbox( |
|
|
label="Resultado BETO", interactive=False, lines=2 |
|
|
) |
|
|
|
|
|
|
|
|
gr.Examples( |
|
|
examples=EJEMPLOS, |
|
|
inputs=texto_input, |
|
|
label="💡 Ejemplos en español argentino (clickea para probar)", |
|
|
examples_per_page=4, |
|
|
) |
|
|
|
|
|
|
|
|
analizar_btn.click( |
|
|
fn=analizar_sentimiento, |
|
|
inputs=[texto_input], |
|
|
outputs=[robertuito_output, beto_output], |
|
|
) |
|
|
|
|
|
texto_input.change( |
|
|
fn=analizar_sentimiento, |
|
|
inputs=[texto_input], |
|
|
outputs=[robertuito_output, beto_output], |
|
|
) |
|
|
|
|
|
|
|
|
with gr.TabItem("🌐 Análisis Web", id="web"): |
|
|
with gr.Row(): |
|
|
with gr.Column(scale=2): |
|
|
url_input = gr.Textbox( |
|
|
label="🔗 URL de la página web", |
|
|
placeholder="https://ejemplo.com/noticia...", |
|
|
lines=2, |
|
|
show_copy_button=True, |
|
|
) |
|
|
|
|
|
|
|
|
extraer_btn = gr.Button( |
|
|
"🌐 Extraer y Analizar", variant="primary", size="lg" |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
texto_web_output = gr.Textbox( |
|
|
label="📄 Texto extraído de la web", |
|
|
lines=6, |
|
|
interactive=False, |
|
|
placeholder="El texto extraído aparecerá aquí...", |
|
|
) |
|
|
|
|
|
|
|
|
with gr.Row(): |
|
|
with gr.Column(): |
|
|
gr.HTML( |
|
|
"<h3 style='text-align: center; color: #059669;'>🤖 RoBERTuito</h3>" |
|
|
) |
|
|
robertuito_web_output = gr.Textbox( |
|
|
label="Resultado RoBERTuito (Web)", interactive=False, lines=2 |
|
|
) |
|
|
|
|
|
with gr.Column(): |
|
|
gr.HTML( |
|
|
"<h3 style='text-align: center; color: #dc2626;'>📚 BETO</h3>" |
|
|
) |
|
|
beto_web_output = gr.Textbox( |
|
|
label="Resultado BETO (Web)", interactive=False, lines=2 |
|
|
) |
|
|
|
|
|
|
|
|
ejemplos_urls = [ |
|
|
["https://www.pagina12.com.ar/"], |
|
|
["https://www.clarin.com/"], |
|
|
["https://www.lanacion.com.ar/"], |
|
|
["https://www.infobae.com/"], |
|
|
] |
|
|
|
|
|
gr.Examples( |
|
|
examples=ejemplos_urls, |
|
|
inputs=url_input, |
|
|
label="📰 Ejemplos de sitios de noticias argentinas", |
|
|
examples_per_page=4, |
|
|
) |
|
|
|
|
|
|
|
|
def analizar_texto_web(url): |
|
|
"""Función que combina extracción web y análisis.""" |
|
|
texto_extraido = comparador.extraer_texto_web(url) |
|
|
|
|
|
if texto_extraido.startswith("❌"): |
|
|
return texto_extraido, "❌ Error", "❌ Error" |
|
|
|
|
|
|
|
|
robertuito_result, beto_result = analizar_sentimiento(texto_extraido) |
|
|
|
|
|
return texto_extraido, robertuito_result, beto_result |
|
|
|
|
|
extraer_btn.click( |
|
|
fn=analizar_texto_web, |
|
|
inputs=[url_input], |
|
|
outputs=[texto_web_output, robertuito_web_output, beto_web_output], |
|
|
) |
|
|
|
|
|
|
|
|
with gr.TabItem("ℹ️ Información", id="info"): |
|
|
gr.Markdown(""" |
|
|
## 🏗️ Arquitectura de la Aplicación |
|
|
|
|
|
Esta aplicación utiliza una arquitectura modular para comparar modelos de análisis de sentimiento: |
|
|
|
|
|
```mermaid |
|
|
graph TB |
|
|
A[Usuario] --> B{Interfaz Gradio} |
|
|
B --> C[Pestaña: Análisis Directo] |
|
|
B --> D[Pestaña: Análisis Web] |
|
|
C --> E[Texto Input] |
|
|
D --> F[URL Input] |
|
|
F --> G[Extracción Web] |
|
|
E --> H[Análisis Modelos] |
|
|
G --> H |
|
|
H --> I[RoBERTuito] |
|
|
H --> J[BETO] |
|
|
I --> K[Resultado] |
|
|
J --> K |
|
|
K --> L[Usuario] |
|
|
``` |
|
|
|
|
|
## 🤖 Modelos Utilizados |
|
|
|
|
|
### RoBERTuito (`pysentimiento/robertuito-sentiment-analysis`) |
|
|
- ✅ **Especializado** en lenguaje coloquial y redes sociales |
|
|
- ✅ **Entrenado** en tweets en español |
|
|
- ✅ **Ideal** para lenguaje informal y argentino |
|
|
|
|
|
### BETO (`finiteautomata/beto-sentiment-analysis`) |
|
|
- ✅ **Clásico** basado en BERT entrenado en Wikipedia |
|
|
- ✅ **Formal** y generalista |
|
|
- ✅ **Bueno** para textos periodísticos y formales |
|
|
|
|
|
## ✨ Características |
|
|
|
|
|
- **Carga eficiente**: Modelos cargados una sola vez al inicio |
|
|
- **Procesamiento paralelo**: Ambos modelos ejecutados simultáneamente |
|
|
- **Extracción web**: BeautifulSoup para contenido de páginas |
|
|
- **Manejo de errores**: Validación robusta de URLs y contenido |
|
|
- **Interfaz intuitiva**: Pestañas organizadas por funcionalidad |
|
|
|
|
|
## 🚀 Uso Recomendado |
|
|
|
|
|
### Para análisis directo: |
|
|
- Textos cortos y específicos |
|
|
- Mensajes de redes sociales |
|
|
- Comentarios y reseñas |
|
|
|
|
|
### Para análisis web: |
|
|
- Artículos de noticias completos |
|
|
- Páginas web con contenido extenso |
|
|
- Análisis de sentimiento periodístico |
|
|
|
|
|
## 📊 Métricas Incluidas |
|
|
|
|
|
- **Confianza**: Probabilidad asignada por cada modelo |
|
|
- **Tiempo de respuesta**: Latencia de cada análisis |
|
|
- **Longitud del texto**: Caracteres procesados |
|
|
""") |
|
|
|
|
|
|
|
|
with gr.Accordion("🔧 Configuración Técnica", open=False): |
|
|
gr.Markdown(f""" |
|
|
**Estado de modelos:** |
|
|
- RoBERTuito: {"✅ Cargado" if comparador.modelos.get("RoBERTuito") else "❌ Error"} |
|
|
- BETO: {"✅ Cargado" if comparador.modelos.get("BETO") else "❌ Error"} |
|
|
|
|
|
**Versión de librerías:** |
|
|
- Transformers: {comparador.modelos["RoBERTuito"].__class__.__module__.split(".")[0] if comparador.modelos.get("RoBERTuito") else "N/A"} |
|
|
- Gradio: {gr.__version__} |
|
|
|
|
|
**Características técnicas:** |
|
|
- Modelos pre-cargados para mejor rendimiento |
|
|
- Procesamiento paralelo activado |
|
|
- Extracción web con timeout de 10 segundos |
|
|
- Límite de texto: 5000 caracteres por análisis |
|
|
""") |
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
print("Iniciando aplicacion de comparacion de modelos...") |
|
|
demo.launch( |
|
|
server_name="0.0.0.0", |
|
|
server_port=7860, |
|
|
show_api=False, |
|
|
) |
|
|
|