- Descripción General del Modelo BGE-M3
BGE-M3 es un modelo de incrustación de texto diseñado específicamente para mejorar los sistemas de recuperación de información. Su capacidad principal es transformar cadenas de texto (oraciones, párrafos o documentos completos) en vectores numéricos de alta dimensionalidad que capturan el significado semántico. Comparando la proximidad de estos vectores, un sistema puede identificar los documentos más relevantes para una consulta determinada.
Este modelo se distingue por integrar de manera unifciada tres enfoques de búsqueda avanzados:
- Recuperación densa: Genera vectores compactos que representan el significado profundo del texto. Es ideal para búsquedas semánticas donde las palabras clave exactas no coinciden.
- Recuperación dispersa: Extrae y pondera los términos léxicos (palabras clave). Es eficaz para la coincidencia precisa de terminología específica.
- Recuperación multi-vector (ColBERT): Representa el texto como una secuencia de vectores por token, permitiendo una correspondencia granular a nivel de fragmento para documentos largos.
Algunas características técnicas relevantes incluyen una dimensión de vector de 1024, una longitud máxima de entrada de 8192 tokens y soporte para más de 100 idiomas. El modelo no genera texto ni realiza clasificación directa; su función primordial es la transformación de texto a vector.
- Configuración del Entorno e Implementación Básica
Para implementar el servicio, se requiere un entorno Linux con Python 3.8+ y al menos 8GB de RAM. El uso de una GPU NVIDIA con CUDA acelera significativamente la inferencia. A continuación, se presenta una secuencia de comandos para verificar el entorno y preparar la implementación.
# Verificar versión de Python y memoria disponible
python3 --version
free -h
# Verificar GPU si está disponible
nvidia-smi
Se puede crear un script de inicio automatizado (lanzar_servicio.sh) que gestione las dependencias y arranque la aplicación.
#!/bin/bash
export TRANSFORMERS_NO_TF=1
export HF_HOME=~/.cache/huggingface
cd /ruta/al/proyecto/bge_m3
if [ ! -f ".dependencias_instaladas" ]; then
pip install FlagEmbedding gradio sentence-transformers torch --upgrade
touch .dependencias_instaladas
fi
python3 interfaz_gradio.py
El script principal (interfaz_gradio.py) carga el modelo y expone una interfaz web sencilla para probar los diferentes modos de codificación.
from FlagEmbedding import BGEM3FlagModel
import gradio as gr
import torch
# Instancia global del modelo
_modelo_instancia = None
def obtener_modelo():
global _modelo_instancia
if _modelo_instancia is None:
_modelo_instancia = BGEM3FlagModel(
'BAAI/bge-m3',
use_fp16=True,
device='cuda' if torch.cuda.is_available() else 'cpu'
)
return _modelo_instancia
def codificar_textos(lista_textos, tipo_codificacion="dense"):
modelo = obtener_modelo()
if tipo_codificacion == "dense":
resultados = modelo.encode(lista_textos, return_dense=True, return_sparse=False, return_colbert_vecs=False)
return resultados['dense_vecs']
elif tipo_codificacion == "sparse":
resultados = modelo.encode(lista_textos, return_dense=False, return_sparse=True, return_colbert_vecs=False)
return resultados['lexical_weights']
elif tipo_codificacion == "colbert":
resultados = modelo.encode(lista_textos, return_dense=False, return_sparse=False, return_colbert_vecs=True)
return resultados['colbert_vecs']
else: # modo híbrido
resultados = modelo.encode(lista_textos, return_dense=True, return_sparse=True, return_colbert_vecs=True)
return {
'vectores_densos': resultados['dense_vecs'],
'pesos_lexicos': resultados['lexical_weights'],
'vectores_colbert': resultados['colbert_vecs']
}
def crear_interfaz():
def procesar_consulta(texto_entrada, modo_seleccionado):
if not texto_entrada.strip():
return "El campo de texto no puede estar vacío."
try:
salida = codificar_textos([texto_entrada], modo_seleccionado)
if modo_seleccionado == "dense":
vector = salida[0]
resumen = f"Vector denso de {len(vector)} dimensiones. Muestra: {vector[:5]}..."
elif modo_seleccionado == "sparse":
pesos = salida[0]
terminos_ordenados = sorted(pesos.items(), key=lambda x: x[1], reverse=True)[:5]
resumen = f"Términos clave y pesos: {terminos_ordenados}"
else: # colbert
vectores = salida[0]
resumen = f"Generados {len(vectores)} vectores (uno por token) de dimensión {len(vectores[0])}."
return resumen
except Exception as e:
return f"Error en la codificación: {str(e)}"
with gr.Blocks(title="Servicio de Incrustación BGE-M3") as demo:
gr.Markdown("# Servicio de Incrustación de Texto BGE-M3")
texto_input = gr.Textbox(label="Texto a codificar", placeholder="Ingrese texto aquí...")
modo_radio = gr.Radio(["dense", "sparse", "colbert"], value="dense", label="Modo de Codificación")
boton_procesar = gr.Button("Generar Incrustación")
area_resultado = gr.Textbox(label="Resultado", interactive=False)
boton_procesar.click(fn=procesar_consulta, inputs=[texto_input, modo_radio], outputs=area_resultado)
return demo
if __name__ == "__main__":
obtener_modelo() # Precarga del modelo
app = crear_interfaz()
app.launch(server_name="0.0.0.0", server_port=7860)
Se puede iniciar el servicio en segundo plano usando nohup y verificar su estado revisando el puerto 7860 con netstat o ss.
- Desarrollo de una API de Alta Concurrencia
Para pasar de una interfaz de demostración a un servicio de producción, se construye una API RESTful optimizada usando FastAPI. Esto permite un mejor control sobre la concurrencia, el manejo de errores y el rendimiento.
import asyncio
from concurrent.futures import ThreadPoolExecutor
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List, Optional
from FlagEmbedding import BGEM3FlagModel
import torch
import time
app = FastAPI(title="API de Incrustación BGE-M3")
EJECUTOR = ThreadPoolExecutor(max_workers=4)
# Modelo de datos de solicitud
class SolicitudIncrustacion(BaseModel):
textos: List[str]
modo: str = "dense"
tamano_lote: Optional[int] = 32
# Modelo de datos de respuesta
class RespuestaIncrustacion(BaseModel):
incrustaciones: List
tiempo_procesamiento: float
modelo_utilizado: str
# Singleton para el modelo
class GestorModelo:
_instancia = None
_modelo = None
@classmethod
def obtener(cls):
if cls._instancia is None:
cls._instancia = cls()
cls._instancia._cargar_modelo()
return cls._instancia
def _cargar_modelo(self):
if self._modelo is None:
self._modelo = BGEM3FlagModel('BAAI/bge-m3', use_fp16=True, device='cuda' if torch.cuda.is_available() else 'cpu')
# Precalentamiento
self._modelo.encode(["precalentamiento"], batch_size=1)
async def codificar_async(self, lista_textos, modo, tamano_lote):
bucle = asyncio.get_event_loop()
return await bucle.run_in_executor(EJECUTOR, self._codificar_sync, lista_textos, modo, tamano_lote)
def _codificar_sync(self, lista_textos, modo, tamano_lote):
inicio = time.time()
params = {'return_dense': modo == 'dense' or modo == 'hybrid',
'return_sparse': modo == 'sparse' or modo == 'hybrid',
'return_colbert_vecs': modo == 'colbert' or modo == 'hybrid'}
resultados = self._modelo.encode(lista_textos, batch_size=tamano_lote, **params)
if modo == "dense":
salida = resultados['dense_vecs'].tolist()
elif modo == "sparse":
salida = [dict(zip(k, v)) for k, v in zip(resultados['lexical_weights']['input_ids'], resultados['lexical_weights']['weights'])]
elif modo == "colbert":
salida = [vec.tolist() for vec in resultados['colbert_vecs']]
else:
salida = {
'vectores_densos': resultados['dense_vecs'].tolist(),
'pesos_lexicos': [dict(zip(k, v)) for k, v in zip(resultados['lexical_weights']['input_ids'], resultados['lexical_weights']['weights'])],
'vectores_colbert': [vec.tolist() for vec in resultados['colbert_vecs']]
}
duracion = time.time() - inicio
return salida, duracion
@app.post("/incrustar", response_model=RespuestaIncrustacion)
async def endpoint_incrustar(solicitud: SolicitudIncrustacion):
gestor = GestorModelo.obtener()
if not solicitud.textos:
raise HTTPException(status_code=400, detail="La lista de textos no puede estar vacía.")
if len(solicitud.textos) > 128: # Límite arbitrario de ejemplo
raise HTTPException(status_code=413, detail="Lote de textos demasiado grande.")
incrustaciones, duracion = await gestor.codificar_async(
solicitud.textos,
solicitud.modo,
solicitud.tamano_lote
)
return RespuestaIncrustacion(
incrustaciones=incrustaciones,
tiempo_procesamiento=duracion,
modelo_utilizado="BAAI/bge-m3"
)
@app.get("/salud")
async def salud():
gestor = GestorModelo.obtener()
# Realiza una inferencia mínima para comprobar el estado
await gestor.codificar_async(["test"], "dense", 1)
return {"estado": "saludable"}
La API se inicia con uvicorn y se puede desplegar con múltiples workers. La arquitectura propuesta incluye un balanceador de carga (como Nginx) frente a varias instancias de la API, que a su vez se comunican con un caché (como Redis) y opcionalmente con una base de datos vectorial para la búsqueda a gran escala.
- Escenarios de Aplicación Práctica
La API implementada puede integrarse en diversos sistemas.
Búsqueda Semántica en Catálogo de Productos
Para un sistema de e-commerce, se puede pre-calcular y almacenar los vectores densos de las descripciones de productos. La consulta del usuario se codifica en el mismo espacio vectorial y se buscan los productos cuyos vectores tengan mayor similitud coseno.
import numpy as np
class MotorBusquedaSemantica:
def __init__(self, cliente_api):
self.cliente = cliente_api
self.indice_productos = {} # ID -> vector denso
def indexar_productos(self, lista_productos):
textos = [prod['descripcion'] for prod in lista_productos]
respuesta = self.cliente.solicitar_incrustacion(textos, modo="dense")
for i, producto in enumerate(lista_productos):
self.indice_productos[producto['id']] = np.array(respuesta['incrustaciones'][i])
def buscar(self, consulta_usuario, top_k=5):
vector_consulta = np.array(self.cliente.solicitar_incrustacion([consulta_usuario], modo="dense")['incrustaciones'][0])
puntuaciones = []
for id_producto, vector_prod in self.indice_productos.items():
similitud = np.dot(vector_consulta, vector_prod) / (np.linalg.norm(vector_consulta) * np.linalg.norm(vector_prod))
puntuaciones.append((id_producto, similitud))
puntuaciones.sort(key=lambda x: x[1], reverse=True)
return puntuaciones[:top_k]
Sistema de Preguntas Frecuentes con Búsqueda Híbrida
Para mejorar la precisión de un chatbot de atención al cliente, se pueden combinar las puntuaciones de los modos denso y disperso. Una consulta como "cómo cambio mi contraseña" coincidirá semánticamente con "olvidé mi clave" y, de forma léxica, con la palabra "contraseña".
class SistemaFAQHibrido:
def __init__(self, cliente_api, pesos={'dense': 0.7, 'sparse': 0.3}):
self.cliente = cliente_api
self.pesos = pesos
self.faq_datos = [] # Lista de dicts con 'pregunta', 'respuesta', 'vectores'
def cargar_preguntas(self, faqs):
preguntas = [faq['pregunta'] for faq in faqs]
resp_dense = self.cliente.solicitar_incrustacion(preguntas, modo="dense")
resp_sparse = self.cliente.solicitar_incrustacion(preguntas, modo="sparse")
for i, faq in enumerate(faqs):
self.faq_datos.append({
**faq,
'vector_dense': np.array(resp_dense['incrustaciones'][i]),
'vector_sparse': resp_sparse['incrustaciones'][i] # Asumiendo un formato utilizable
})
def buscar_mejor_respuesta(self, consulta):
# Codificar la consulta en ambos modos
v_consulta_dense = np.array(self.cliente.solicitar_incrustacion([consulta], modo="dense")['incrustaciones'][0])
v_consulta_sparse = self.cliente.solicitar_incrustacion([consulta], modo="sparse")['incrustaciones'][0]
puntuaciones_finales = []
for faq in self.faq_datos:
# Calcular similitud densa (coseno)
sim_dense = np.dot(v_consulta_dense, faq['vector_dense']) / (np.linalg.norm(v_consulta_dense) * np.linalg.norm(faq['vector_dense']))
# Calcular similitud dispersa (ej: intersección de términos ponderados)
sim_sparse = self._calcular_similitud_lexica(v_consulta_sparse, faq['vector_sparse'])
puntuacion_combinada = (self.pesos['dense'] * sim_dense) + (self.pesos['sparse'] * sim_sparse)
puntuaciones_finales.append((faq, puntuacion_combinada))
puntuaciones_finales.sort(key=lambda x: x[1], reverse=True)
return puntuaciones_finales[0] if puntuaciones_finales else None
def _calcular_similitud_lexica(self, pesos1, pesos2):
# Implementación simplificada de similitud entre vectores dispersos
# Esto depende de cómo se estructure la salida del modo 'sparse'
terminos_comunes = set(pesos1.keys()) & set(pesos2.keys())
if not terminos_comunes:
return 0.0
return sum(min(pesos1[t], pesos2[t]) for t in terminos_comunes)