Implementación de检索助手 con GME y Qwen2-VL-2B: Construcción paso a paso

En esta guía, te mostraré cómo construir un asistente de búsqueda inteligente que comprende tanto texto como imágenes, utilizando el modelo GME (Generating Multimodal Embeddings) basado en Qwen2-VL-2B.

1. Fundamentos del Sistema

1.1 ¿Qué es GME?

El modelo GME es un sistema de embedding multimodal que procesa tres tipos de入口ada:

  • Texto plano: artículos, descripciones técnicas, resúmenes
  • Imágenes aisladas: gráficos, capturas de pantalla, fotografías
  • Pares imagen-texto: figuras conlegendaindividual

La magia reside en que todos estos elementos se convierten en vectores numéricos en un espacio compartido. Cuando buscas algo, el sistema calcula la similitud entre vectores para encontrar coincidencias.

1.2 Ventajas de Qwen2-VL-2B

Este modelo visual ofrece características destacables:

Resolución dinámica: Procesa imágenes de cualquier dimensionesin perder calidad interpretativa.

Comprensión de documentos: Especialmente efectivo con gráficos complejos, ecuaciones y diagramas técnicos.

Espacio unificado: Textos e imágenes cohabitan en el mismo espacio vectorial, habilitando búsquedas cruzadas: texto→imagen, imagen→texto, imagen→imagen.

2. Preparación del Entorno

2.1 Requisitos del Sistema

Antes de comenzar, asegurate de tener:

  • Python 3.8+
  • 8GB mínimo de RAM
  • 3GB de espacio en disco para modelos
  • Conexión a internet para descarga inicial

2.2 Instalación de Dependencias

Ejecuta estos comandos en tu terminal:

mkdir asistente_busqueda
cd asistente_busqueda

python -m venv env
# Windows
env\Scripts\activate
# Linux/Mac
source env/bin/activate

pip install sentence-transformers gradio torch torchvision pillow pymupdf

2.3 Verificación del Modelo

# verificar_modelo.py
from sentence_transformers import SentenceTransformer

print("Cargando modelo GME...")

modelo = SentenceTransformer('Alibaba-NLP/gte-multimodel-qwen2-vl-2b')

# Prueba de codificación
texto_prueba = "Redes neuronales convolucionales para análisis de imágenes"
vector = modelo.encode(texto_prueba)

print(f"Dimensión del vector: {vector.shape}")
print("Modelo operativo correctamente")

3. Arquitectura del Sistema

3.1 Módulo de Procesamiento de Documentos

# procesador_docs.py
import os
import fitz
import json
from PIL import Image
import io
from typing import List, Dict

class ProcesadorDocumentos:
    def __init__(self, modelo):
        self.modelo = modelo
        
    def extraer_pdf(self, ruta_pdf: str) -> List[Dict]:
        """Extrae contenido textual y visual de archivos PDF"""
        elementos = []
        
        try:
            documento = fitz.open(ruta_pdf)
            for num_pagina in range(len(documento)):
                pagina = documento[num_pagina]
                
                # Extraer texto
                texto = pagina.get_text()
                if texto.strip():
                    elementos.append({
                        'tipo': 'texto',
                        'contenido': texto,
                        'pagina': num_pagina + 1,
                        'origen': os.path.basename(ruta_pdf)
                    })
                
                # Extraer imágenes
                for indice_img,img in enumerate(pagina.get_images()):
                    xref = img[0]
                    pix = fitz.Pixmap(documento, xref)
                    if pix.n - pix.alpha < 4:
                        datos_img = pix.tobytes("png")
                        elementos.append({
                            'tipo': 'imagen',
                            'contenido': datos_img,
                            'pagina': num_pagina + 1,
                            'origen': os.path.basename(ruta_pdf),
                            'indice_imagen': indice_img
                        })
                    pix = None
            
            documento.close()
            
        except Exception as e:
            print(f"Error procesando PDF: {e}")
            
        return elementos
    
    def procesar_carpeta(self, ruta_carpeta: str) -> List[Dict]:
        """Procesa todos los documentos en un directorio"""
        todos_elementos = []
        extensiones_validas = ['.pdf', '.txt', '.md', '.jpg', '.jpeg', '.png']
        
        for raiz, _, archivos in os.walk(ruta_carpeta):
            for archivo in archivos:
                ruta_completa = os.path.join(raiz, archivo)
                extension = os.path.splitext(archivo)[1].lower()
                
                if extension == '.pdf':
                    elementos = self.extraer_pdf(ruta_completa)
                    todos_elementos.extend(elementos)
                    
                elif extension in ['.txt', '.md']:
                    with open(ruta_completa, 'r', encoding='utf-8') as f:
                        contenido = f.read()
                    todos_elementos.append({
                        'tipo': 'texto',
                        'contenido': contenido,
                        'origen': archivo
                    })
                    
                elif extension in ['.jpg', '.jpeg', '.png']:
                    with open(ruta_completa, 'rb') as f:
                        datos = f.read()
                    todos_elementos.append({
                        'tipo': 'imagen',
                        'contenido': datos,
                        'origen': archivo
                    })
        
        return todos_elementos
    
    def generar_embeddings(self, documentos: List[Dict]) -> List[Dict]:
        """Convierte documentos a vectores numéricos"""
        documentos_vectorizados = []
        
        for i, doc in enumerate(documentos):
            try:
                if doc['tipo'] == 'texto':
                    embedding = self.modelo.encode(doc['contenido'])
                else:
                    imagen = Image.open(io.BytesIO(doc['contenido']))
                    embedding = self.modelo.encode(imagen)
                
                doc['embedding'] = embedding.tolist()
                documentos_vectorizados.append(doc)
                
                if (i + 1) % 10 == 0:
                    print(f"Procesados {i + 1}/{len(documentos)}")
                    
            except Exception as e:
                print(f"Error en elemento {i}: {e}")
                continue
        
        return documentos_vectorizados
    
    def guardar_indice(self, documentos: List[Dict], ruta_salida: str):
        """Almacena el índice en formato JSON"""
        with open(ruta_salida, 'w', encoding='utf-8') as f:
            json.dump(documentos, f, ensure_ascii=False, indent=2)

3.2 Motor de Búsqueda

# motor_busqueda.py
import numpy as np
from PIL import Image
import io
import json
from typing import List, Dict, Union

class MotorBusqueda:
    def __init__(self, modelo, ruta_documentos: str = None):
        self.modelo = modelo
        self.documentos = []
        self.embeddings = np.array([])
        
        if ruta_documentos:
            self.cargar_desde_archivo(ruta_documentos)
    
    def cargar_desde_archivo(self, ruta: str):
        """Carga documentos previamente indexados"""
        with open(ruta, 'r', encoding='utf-8') as f:
            datos = json.load(f)
        
        self.documentos = datos
        self.embeddings = np.array([doc['embedding'] for doc in datos])
    
    def agregar_documentos(self, documentos: List[Dict]):
        """Añade nuevos documentos al índice"""
        for doc in documentos:
            if 'embedding' not in doc:
                if doc['tipo'] == 'texto':
                    emb = self.modelo.encode(doc['contenido'])
                else:
                    img = Image.open(io.BytesIO(doc['contenido']))
                    emb = self.modelo.encode(img)
                doc['embedding'] = emb.tolist()
            
            self.documentos.append(doc)
            self.embeddings = np.array([d['embedding'] for d in self.documentos])
    
    def buscar(self, consulta: Union[str, Image.Image], k: int = 5) -> List[Dict]:
        """Encuentra los documentos más similares"""
        if isinstance(consulta, str):
            vector_consulta = self.modelo.encode(consulta)
        else:
            vector_consulta = self.modelo.encode(consulta)
        
        if len(self.embeddings) == 0:
            return []
        
        # Normalización para similitud coseno
        consulta_norm = vector_consulta / np.linalg.norm(vector_consulta)
        docs_norm = self.embeddings / np.linalg.norm(self.embeddings, axis=1, keepdims=True)
        
        # Calcular similitudes
        similitudes = np.dot(docs_norm, consulta_norm)
        
        # Ordenar y obtener top-k
        indices_top = np.argsort(similitudes)[::-1][:k]
        
        resultados = []
        for idx in indices_top:
            resultado = self.documentos[idx].copy()
            resultado['similitud'] = float(similitudes[idx])
            resultados.append(resultado)
        
        return resultados
    
    def busqueda_hibrida(self, consulta_texto: str = None, 
                         consulta_imagen: Image.Image = None, 
                         k: int = 5) -> List[Dict]:
        """Combina búsquedas de texto e imagen"""
        if consulta_texto and consulta_imagen:
            v_texto = self.modelo.encode(consulta_texto)
            v_imagen = self.modelo.encode(consulta_imagen)
            vector_combinado = (v_texto + v_imagen) / 2
        elif consulta_texto:
            vector_combinado = self.modelo.encode(consulta_texto)
        elif consulta_imagen:
            vector_combinado = self.modelo.encode(consulta_imagen)
        else:
            return []
        
        return self._buscar_con_vector(vector_combinado, k)
    
    def _buscar_con_vector(self, vector: np.ndarray, k: int) -> List[Dict]:
        """Búsqueda usando vector precalculado"""
        if len(self.embeddings) == 0:
            return []
        
        vect_norm = vector / np.linalg.norm(vector)
        docs_norm = self.embeddings / np.linalg.norm(self.embeddings, axis=1, keepdims=True)
        
        similitudes = np.dot(docs_norm, vect_norm)
        indices = np.argsort(similitudes)[::-1][:k]
        
        return [
            {**self.documentos[idx], 'similitud': float(similitudes[idx])}
            for idx in indices
        ]

3.3 Interfaz Grádica

# aplicacion.py
import gradio as gr
from sentence_transformers import SentenceTransformer
from procesador_docs import ProcesadorDocumentos
from motor_busqueda import MotorBusqueda
import os
import json

# Inicialización
print("Inicializando sistema...")
modelo = SentenceTransformer('Alibaba-NLP/gte-multimodel-qwen2-vl-2b')
buscador = MotorBusqueda(modelo)
documentos-globales = []

def indexar_archivos(archivos):
    """Procesa y indexa documentos subidos"""
    if not archivos:
        return "Selecciona archivos primero"
    
    procesador = ProcesadorDocumentos(modelo)
    elementos_indexar = []
    
    for archivo in archivos:
        if archivo.endswith('.pdf'):
            elem = procesador.extraer_pdf(archivo)
        elif archivo.endswith(('.txt', '.md')):
            with open(archivo, 'r', encoding='utf-8') as f:
                contenido = f.read()
            elem = [{'tipo': 'texto', 'contenido': contenido, 
                    'origen': os.path.basename(archivo)}]
        elif archivo.endswith(('.jpg', '.jpeg', '.png')):
            with open(archivo, 'rb') as f:
                datos = f.read()
            elem = [{'tipo': 'imagen', 'contenido': datos,
                    'origen': os.path.basename(archivo)}]
        else:
            continue
        
        elementos_indexar.extend(elem)
    
    # Vectorización
    vectorizados = procesador.generar_embeddings(elementos_indexar)
    buscador.agregar_documentos(vectorizados)
    
    global documentos-globales
    documentos-globales.extend(vectorizados)
    
    if vectorizados:
        procesador.guardar_indice(vectorizados, 'indice_documentos.json')
    
    return f"Indexados {len(vectorizados)} elementos"

def realizar_busqueda(texto, imagen, top_k):
    """Ejecuta búsqueda multimodal"""
    if not buscador.documentos:
        return "Indexa documentos primero", []
    
    resultados = buscador.busqueda_hibrida(
        consulta_texto=texto,
        consulta_imagen=imagen,
        k=top_k
    )
    
    if not resultados:
        return "Sin resultados", []
    
    presentacion = "Resultados:\n\n"
    vista_detallada = []
    
    for i, res in enumerate(resultados):
        tipo = res['tipo']
        origen = res.get('origen', 'Desconocido')
        pagina = res.get('pagina', '')
        sim = res['similitud']
        
        presentacion += f"{i+1}. [{tipo}] {origen}"
        if pagina:
            presentacion += f" - Página {pagina}"
        presentacion += f" (Similitud: {sim:.3f})\n"
        
        if tipo == 'texto':
            fragmento = res['contenido'][:200] + "..." if len(res['contenido']) > 200 else res['contenido']
            presentacion += f"   {fragmento}\n"
        
        presentacion += "\n"
        
        vista_detallada.append({
            'tipo': tipo,
            'origen': origen,
            'similitud': sim,
            'contenido': res['contenido'][:500] + "..." if len(res.get('contenido', '')) > 500 else res.get('contenido', '')
        })
    
    return presentacion, vista_detallada

# Construcción de interfaz
with gr.Blocks(title="Asistente de Búsqueda Documental") as interfaz:
    gr.Markdown("# Asistente de Búsqueda Multimodal")
    gr.Markdown("Sistema de recuperación basado en GME con Qwen2-VL-2B")
    
    with gr.Tab("Gestión de Documentos"):
        with gr.Row():
            with gr.Column():
                entrada_archivos = gr.File(
                    label="Subir documentos",
                    file_count="multiple",
                    file_types=[".pdf", ".txt", ".md", ".jpg", ".jpeg", ".png"]
                )
                boton_procesar = gr.Button("Indexar", variant="primary")
            
            with gr.Column():
                salida_estado = gr.Textbox(label="Estado", interactive=False)
        
        boton_procesar.click(
            indexar_archivos,
            inputs=[entrada_archivos],
            outputs=[salida_estado]
        )
    
    with gr.Tab("Búsqueda"):
        with gr.Row():
            with gr.Column():
                consulta_texto = gr.Textbox(
                    label="Consulta textual",
                    placeholder="Describe lo que buscas...",
                    lines=3
                )
                consulta_imagen = gr.Image(
                    label="Imagen de referencia (opcional)",
                    type="pil"
                )
                control_top = gr.Slider(1, 20, value=5, step=1, label="Resultados")
                boton_buscar = gr.Button("Buscar", variant="primary")
            
            with gr.Column():
                salida_resultados = gr.Textbox(lines=15, label="Resultados")
                salida_detalle = gr.JSON(label="Detalles")
        
        boton_buscar.click(
            realizar_busqueda,
            inputs=[consulta_texto, consulta_imagen, control_top],
            outputs=[salida_resultados, salida_detalle]
        )
    
    with gr.Tab("Acerca de"):
        gr.Markdown("## Especificaciones")
        gr.Markdown(f"- Modelo: GME-Qwen2-VL-2B")
        gr.Markdown(f"- Documentos indexados: {len(documentos-globales)}")
        gr.Markdown("### Capacidades")
        gr.Markdown("""
        - Búsqueda texto→texto
        - Búsqueda imagen→imagen
        - Búsqueda cruzada texto↔imagen
        -检索 híbrida multimodal
        """)

if __name__ == "__main__":
    if os.path.exists("indice_documentos.json"):
        print("Cargando índice existente...")
        buscador.cargar_desde_archivo("indice_documentos.json")
        with open("indice_documentos.json", 'r') as f:
            documentos-globales = json.load(f)
    
    interfaz.launch(server_name="0.0.0.0", server_port=7860)

4. Casos de Uso Prácticos

4.1 Investigación Académica

Un investigador tiene 50 artigos sobre visión por computadora. Con este sistema puede:

  • Buscar "detección de objetos en tiempo real" y encontrar artículos relacionados aunque usen terminología diferente
  • Subir un diagrama de arquitectura de red y localizar artículos con enfoques similares
  • Combinar descripción textual con imágenes de referencia

4.2 Documentación Técnica

Equipos de ingeniería de software manejan cientos de documentos con capturas de pantalla, diagramas de flujo y especificaciones. El sistema permite:

  • Localizar documentación relacionada por similitud semántica
  • Encontrar diagrams similares en diferentes proyectos
  • Mantener consistencia en documentación existente

5. Optimizaciones de Rendimiento

5.1 Procesamiento por Lotes

def procesar_lotes(elementos, tamano_lote=32):
    """Procesa documentos en grupos para mayor velocidad"""
    resultados = []
    
    for i in range(0, len(elementos), tamano_lote):
        lote = elementos[i:i+tamano_lote]
        
        for elem in lote:
            try:
                if elem['tipo'] == 'texto':
                    emb = modelo.encode(elem['contenido'])
                else:
                    img = Image.open(io.BytesIO(elem['contenido']))
                    emb = modelo.encode(img)
                
                elem['embedding'] = emb.tolist()
                resultados.append(elem)
            except Exception as e:
                continue
        
        print(f"Progreso: {min(i+tamano_lote, len(elementos))}/{len(elementos)}")
    
    return resultados

5.2 Caché de Resultados

import hashlib
import pickle
from functools import lru_cache

class MotorBusquedaCache(MotorBusqueda):
    def __init__(self, modelo, directorio_cache="cache"):
        super().__init__(modelo)
        self.directorio_cache = directorio_cache
        os.makedirs(directorio_cache, exist_ok=True)
    
    def hash_consulta(self, consulta):
        """Genera identificador único para la consulta"""
        if isinstance(consulta, str):
            datos = consulta.encode()
        else:
            datos = consulta.tobytes()
        return hashlib.md5(datos).hexdigest()
    
    @lru_cache(maxsize=100)
    def buscar_cache(self, hash_consulta, k):
        """Almacena resultados de consultas repetidas"""
        ruta_cache = os.path.join(self.directorio_cache, f"{hash_consulta}_{k}.pkl")
        
        if os.path.exists(ruta_cache):
            with open(ruta_cache, 'rb') as f:
                return pickle.load(f)
        
        # Ejecutar búsqueda real requiere modificar lógica padre
        # Esta es una implementación conceptual
        return None

6. Consideraciones Finales

Este sistema de búsqueda multimodal ofrece mejoras significativas sobre métodos tradicionales:

Comprensión profunda: No busca palabras clave, sino conceptos y relaciones semánticas.

Flexibilidad total: Cualquier tipo de consulta (texto, imagen o ambas) produce resultados relevantes.

Escalabilidad: La arquitectura modular permite扩展 con bases de datos vectoriales para manejar millones de documentos.

Para implementaciones en producción, considera usar FAISS o Milvus para indexing eficiente, y arquitecturas de microservicios para escalar horizontalmente.

Etiquetas: Python multimodal deep-learning nlp transformers

Publicado el 7-10 00:03