Inteligencia artificial
Patrones de diseño en Python para ingenieros de IA y LLM: Una guía práctica

Como ingenieros de IA, crear código limpio, eficiente y mantenible es crucial, especialmente al construir sistemas complejos.
Los patrones de diseño son soluciones reutilizables para problemas comunes en el diseño de software. Para ingenieros de IA y LLM, los patrones de diseño ayudan a construir sistemas robustos, escalables y mantenibles que manejan flujos de trabajo complejos de manera eficiente. Este artículo explora los patrones de diseño en Python, centrándose en su relevancia en sistemas de IA y LLM. Explicaré cada patrón con casos de uso prácticos y ejemplos de código en Python.
Vamos a explorar algunos patrones de diseño clave que son particularmente útiles en contextos de IA y aprendizaje automático, junto con ejemplos en Python.
Por qué los patrones de diseño importan para los ingenieros de IA
Los sistemas de IA a menudo involucran:
- Creación de objetos complejos (por ejemplo, carga de modelos, pipelines de preprocesamiento de datos).
- Administración de interacciones entre componentes (por ejemplo, inferencia de modelos, actualizaciones en tiempo real).
- Manejo de escalabilidad, mantenibilidad y flexibilidad para requisitos cambiantes.
Los patrones de diseño abordan estos desafíos, proporcionando una estructura clara y reduciendo las soluciones ad hoc. Se dividen en tres categorías principales:
- Patrones de creación: Se centran en la creación de objetos. (Singleton, Factory, Builder)
- Patrones estructurales: Organizan las relaciones entre objetos. (Adapter, Decorator)
- Patrones de comportamiento: Manejan la comunicación entre objetos. (Strategy, Observer)
1. Patrón Singleton
El patrón Singleton garantiza que una clase tenga solo una instancia y proporciona un punto de acceso global a esa instancia. Esto es especialmente valioso en flujos de trabajo de IA donde los recursos compartidos, como configuraciones, sistemas de registro o instancias de modelos, deben ser gestionados consistentemente sin redundancia.
Cuándo usar
- Administración de configuraciones globales (por ejemplo, hiperparámetros del modelo).
- Compartir recursos a través de múltiples hilos o procesos (por ejemplo, memoria de GPU).
- Garantizar acceso consistente a un solo motor de inferencia o conexión a la base de datos.
Implementación
Aquí está cómo implementar un patrón Singleton en Python para gestionar configuraciones para un modelo de IA:
class ModelConfig:
"""Clase Singleton para gestionar configuraciones globales del modelo."""
_instance = None # Variable de clase para almacenar la instancia singleton
def __new__(cls, *args, **kwargs):
if not cls._instance:
# Crear una nueva instancia si no existe
cls._instance = super().__new__(cls)
cls._instance.settings = {} # Inicializar diccionario de configuración
return cls._instance
def set(self, key, value):
"""Establecer un par clave-valor de configuración."""
self.settings[key] = value
def get(self, key):
"""Obtener un valor de configuración por clave."""
return self.settings.get(key)
# Ejemplo de uso
config1 = ModelConfig()
config1.set("model_name", "GPT-4")
config1.set("batch_size", 32)
# Acceder a la misma instancia
config2 = ModelConfig()
print(config2.get("model_name")) # Salida: GPT-4
print(config2.get("batch_size")) # Salida: 32
print(config1 is config2) # Salida: True (ambas son la misma instancia)
Explicación
- Método
__new__: Este método garantiza que solo se cree una instancia de la clase. Si una instancia ya existe, devuelve la existente. - Estado compartido: Tanto
config1comoconfig2apuntan a la misma instancia, lo que hace que todas las configuraciones sean accesibles globalmente y consistentes. - Caso de uso en IA: Utilice este patrón para gestionar configuraciones globales como rutas a conjuntos de datos, configuraciones de registro o variables de entorno.
2. Patrón Factory
El patrón Factory proporciona una forma de delegar la creación de objetos a subclases o métodos de fábrica dedicados. En sistemas de IA, este patrón es ideal para crear diferentes tipos de modelos, cargadores de datos o pipelines dinámicamente según el contexto.
Cuándo usar
- Crear modelos dinámicamente según la entrada del usuario o los requisitos de la tarea.
- Administrar la lógica de creación de objetos complejos (por ejemplo, pipelines de preprocesamiento de datos de múltiples pasos).
- Desacoplar la instanciación de objetos del resto del sistema para mejorar la flexibilidad.
Implementación
Aquí está cómo construir una fábrica para crear modelos para diferentes tareas de IA, como clasificación de texto, resumen y traducción:
class BaseModel:
"""Clase base abstracta para modelos de IA."""
def predict(self, data):
raise NotImplementedError("Las subclases deben implementar el método `predict`")
class TextClassificationModel(BaseModel):
def predict(self, data):
return f"Clasificando texto: {data}"
class SummarizationModel(BaseModel):
def predict(self, data):
return f"Resumiendo texto: {data}"
class TranslationModel(BaseModel):
def predict(self, data):
return f"Traduciendo texto: {data}"
class ModelFactory:
"""Clase de fábrica para crear modelos de IA dinámicamente."""
@staticmethod
def create_model(task_type):
"""Método de fábrica para crear modelos según el tipo de tarea."""
task_mapping = {
"classification": TextClassificationModel,
"summarization": SummarizationModel,
"translation": TranslationModel,
}
model_class = task_mapping.get(task_type)
if not model_class:
raise ValueError(f"Tipo de tarea desconocido: {task_type}")
return model_class()
# Ejemplo de uso
task = "classification"
model = ModelFactory.create_model(task)
print(model.predict("IA transformará el mundo!"))
# Salida: Clasificando texto: IA transformará el mundo!
Explicación
- Clase base abstracta: La clase
BaseModeldefine la interfaz (predict) que todas las subclases deben implementar, garantizando la consistencia. - Lógica de fábrica: La clase
ModelFactoryselecciona dinámicamente la clase adecuada según el tipo de tarea y crea una instancia. - Extensibilidad: Agregar un nuevo tipo de modelo es sencillo; solo hay que implementar una nueva subclase y actualizar el
task_mappingde la fábrica.
Caso de uso en IA
Imagínese que está diseñando un sistema que selecciona un LLM diferente (por ejemplo, BERT, GPT o T5) según la tarea. El patrón Factory facilita la extensión del sistema a medida que se agregan nuevos modelos sin modificar el código existente.
3. Patrón Builder
El patrón Builder separa la construcción de un objeto complejo de su representación. Es útil cuando un objeto implica múltiples pasos para inicializar o configurar.
Cuándo usar
- Construir pipelines de múltiples pasos (por ejemplo, preprocesamiento de datos).
- Administrar configuraciones para experimentos o entrenamiento de modelos.
- Crear objetos que requieren muchos parámetros, asegurando legibilidad y mantenibilidad.
Implementación
Aquí está cómo utilizar el patrón Builder para crear una pipeline de preprocesamiento de datos:
class DataPipeline:
"""Clase Builder para construir una pipeline de preprocesamiento de datos."""
def __init__(self):
self.steps = []
def add_step(self, step_function):
"""Agregar un paso de preprocesamiento a la pipeline."""
self.steps.append(step_function)
return self # Devolver self para permitir encadenamiento de métodos
def run(self, data):
"""Ejecutar todos los pasos en la pipeline."""
for step in self.steps:
data = step(data)
return data
# Ejemplo de uso
pipeline = DataPipeline()
pipeline.add_step(lambda x: x.strip()) # Paso 1: Eliminar espacios en blanco
pipeline.add_step(lambda x: x.lower()) # Paso 2: Convertir a minúsculas
pipeline.add_step(lambda x: x.replace(".", "")) # Paso 3: Eliminar puntos
processed_data = pipeline.run(" ¡Hola Mundo! ")
print(processed_data) # Salida: hola mundo
Explicación
- Métodos encadenados: El método
add_steppermite el encadenamiento para una sintaxis compacta y intuitiva al definir pipelines. - Ejecución paso a paso: La pipeline procesa los datos ejecutando cada paso en secuencia.
- Caso de uso en IA: Utilice el patrón Builder para crear pipelines de preprocesamiento de datos complejos o configuraciones de entrenamiento de modelos.
4. Patrón Strategy
El patrón Strategy define una familia de algoritmos intercambiables, encapsulando cada uno y permitiendo que el comportamiento cambie dinámicamente en tiempo de ejecución. Esto es especialmente útil en sistemas de IA donde el mismo proceso (por ejemplo, inferencia o procesamiento de datos) puede requerir diferentes enfoques según el contexto.
Cuándo usar
- Cambiar entre diferentes estrategias de inferencia (por ejemplo, procesamiento por lotes versus transmisión en tiempo real).
- Aplicar diferentes técnicas de procesamiento de datos dinámicamente.
- Seleccionar estrategias de gestión de recursos según la infraestructura disponible.
Implementación
Aquí está cómo utilizar el patrón Strategy para implementar dos estrategias de inferencia diferentes para un modelo de IA:
class InferenceStrategy:
"""Clase base abstracta para estrategias de inferencia."""
def infer(self, model, data):
raise NotImplementedError("Las subclases deben implementar el método `infer`")
class BatchInference(InferenceStrategy):
"""Estrategia para inferencia por lotes."""
def infer(self, model, data):
print("Realizando inferencia por lotes...")
return [model.predict(item) for item in data]
class StreamInference(InferenceStrategy):
"""Estrategia para inferencia en transmisión."""
def infer(self, model, data):
print("Realizando inferencia en transmisión...")
results = []
for item in data:
results.append(model.predict(item))
return results
class InferenceContext:
"""Clase de contexto para cambiar entre estrategias de inferencia dinámicamente."""
def __init__(self, strategy: InferenceStrategy):
self.strategy = strategy
def set_strategy(self, strategy: InferenceStrategy):
"""Cambiar la estrategia de inferencia dinámicamente."""
self.strategy = strategy
def infer(self, model, data):
"""Delegar la inferencia a la estrategia seleccionada."""
return self.strategy.infer(model, data)
# Clase de modelo simulado
class MockModel:
def predict(self, input_data):
return f"Predicho: {input_data}"
# Ejemplo de uso
model = MockModel()
data = ["sample1", "sample2", "sample3"]
context = InferenceContext(BatchInference())
print(context.infer(model, data))
# Salida:
# Realizando inferencia por lotes...
# ['Predicho: sample1', 'Predicho: sample2', 'Predicho: sample3']
# Cambiar a inferencia en transmisión
context.set_strategy(StreamInference())
print(context.infer(model, data))
# Salida:
# Realizando inferencia en transmisión...
# ['Predicho: sample1', 'Predicho: sample2', 'Predicho: sample3']
Explicación
- Clase de estrategia abstracta: La clase
InferenceStrategydefine la interfaz que todas las estrategias deben seguir. - Estrategias concretas: Cada estrategia (por ejemplo,
BatchInference,StreamInference) implementa la lógica específica para ese enfoque. - Cambio dinámico: La clase
InferenceContextpermite cambiar la estrategia de inferencia en tiempo de ejecución, ofreciendo flexibilidad para diferentes casos de uso.
Cuándo usar
- Cambiar entre inferencia por lotes para procesamiento fuera de línea y inferencia en transmisión para aplicaciones en tiempo real.
- Ajustar dinámicamente técnicas de procesamiento de datos o algoritmos según la tarea o formato de entrada.
5. Patrón Observer
El patrón Observer establece una relación de uno a muchos entre objetos. Cuando un objeto (el sujeto) cambia de estado, todos sus dependientes (observadores) son notificados automáticamente. Esto es particularmente útil en sistemas de IA para monitoreo en tiempo real, manejo de eventos o sincronización de datos.
Cuándo usar
- Monitorear métricas como precisión o pérdida durante el entrenamiento del modelo.
- Actualizaciones en tiempo real para paneles de control o registros.
- Administrar dependencias entre componentes en flujos de trabajo complejos.
Implementación
Aquí está cómo utilizar el patrón Observer para monitorear el rendimiento de un modelo de IA en tiempo real:
class Subject:
"""Clase base para sujetos que son observados."""
def __init__(self):
self._observers = []
def attach(self, observer):
"""Adjuntar un observador al sujeto."""
self._observers.append(observer)
def detach(self, observer):
"""Desadjuntar un observador del sujeto."""
self._observers.remove(observer)
def notify(self, data):
"""Notificar a todos los observadores de un cambio en el estado."""
for observer in self._observers:
observer.update(data)
class ModelMonitor(Subject):
"""Sujeto que monitorea las métricas de rendimiento del modelo."""
def update_metrics(self, metric_name, value):
"""Simular la actualización de una métrica de rendimiento y notificar a los observadores."""
print(f"Actualizada {metric_name}: {value}")
self.notify({metric_name: value})
class Observer:
"""Clase base para observadores."""
def update(self, data):
raise NotImplementedError("Las subclases deben implementar el método `update`")
class LoggerObserver(Observer):
"""Observador que registra las métricas."""
def update(self, data):
print(f"Registrando métrica: {data}")
class AlertObserver(Observer):
"""Observador que genera alertas si se superan umbrales."""
def __init__(self, threshold):
self.threshold = threshold
def update(self, data):
for metric, value in data.items():
if value > self.threshold:
print(f"ALERTA: {metric} superó el umbral con valor {value}")
# Ejemplo de uso
monitor = ModelMonitor()
logger = LoggerObserver()
alert = AlertObserver(threshold=90)
monitor.attach(logger)
monitor.attach(alert)
# Simular actualizaciones de métricas
monitor.update_metrics("precisión", 85) # Registra la métrica
monitor.update_metrics("precisión", 95) # Registra y genera alerta
- Sujeto: Administra una lista de observadores y notifica a todos cuando su estado cambia. En este ejemplo, la clase
ModelMonitorrastrea las métricas. - Observadores: Realizan acciones específicas cuando son notificados. Por ejemplo, la clase
LoggerObserverregistra las métricas, mientras que la claseAlertObservergenera alertas si se supera un umbral. - Diseño desacoplado: Los observadores y los sujetos están sueltos, lo que hace que el sistema sea modular y extensible.
Cómo difieren los patrones de diseño para ingenieros de IA versus ingenieros tradicionales
Los patrones de diseño, aunque universalmente aplicables, toman características únicas cuando se implementan en ingeniería de IA en comparación con la ingeniería de software tradicional. La diferencia radica en los desafíos, objetivos y flujos de trabajo inherentes a los sistemas de IA, que a menudo requieren que los patrones se adapten o se extiendan más allá de sus usos convencionales.
1. Creación de objetos: Necesidades estáticas vs. dinámicas
- Ingeniería tradicional: Los patrones de creación de objetos como Factory o Singleton se utilizan a menudo para administrar configuraciones, conexiones a bases de datos o estados de sesión de usuario. Estos son generalmente estáticos y bien definidos durante el diseño del sistema.
- Ingeniería de IA: La creación de objetos a menudo implica flujos de trabajo dinámicos, como:
- Crear modelos sobre la marcha según la entrada del usuario o los requisitos del sistema.
- Cargar diferentes configuraciones de modelos para tareas como traducción, resumen o clasificación.
- Instanciar múltiples pipelines de procesamiento de datos que varían según las características del conjunto de datos (por ejemplo, tabular versus texto no estructurado).
Ejemplo: En IA, un patrón Factory podría generar dinámicamente un modelo de aprendizaje profundo según el tipo de tarea y las limitaciones de hardware, mientras que en sistemas tradicionales, podría generar simplemente un componente de interfaz de usuario.
2. Restricciones de rendimiento
- Ingeniería tradicional: Los patrones de diseño suelen optimizarse para la latencia y el rendimiento en aplicaciones como servidores web, consultas a bases de datos o renderizado de interfaz de usuario.
- Ingeniería de IA: Los requisitos de rendimiento en IA se extienden a latencia de inferencia, utilización de GPU/TPU y optimización de memoria. Los patrones deben acomodar:
- Almacenamiento en caché de resultados intermedios para reducir cálculos redundantes (patrones Decorator o Proxy).
- Cambiar algoritmos dinámicamente (patrón Strategy) para equilibrar la latencia y la precisión según la carga del sistema o restricciones en tiempo real.
3. Naturaleza centrada en los datos
- Ingeniería tradicional: Los patrones a menudo operan en estructuras de entrada y salida fijas (por ejemplo, formularios, respuestas de API REST).
- Ingeniería de IA: Los patrones deben manejar variabilidad en los datos tanto en estructura como en escala, incluyendo:
- Procesamiento de datos en flujo para sistemas en tiempo real.
- Datos multimodales (por ejemplo, texto, imágenes, videos) que requieren pipelines con pasos de procesamiento flexibles.
- Conjuntos de datos de gran escala que necesitan pipelines de preprocesamiento y aumento de datos eficientes, a menudo utilizando patrones como Builder o Pipeline.
4. Experimentación versus estabilidad
- Ingeniería tradicional: El énfasis está en construir sistemas estables y predecibles donde los patrones garantizan un rendimiento y una confiabilidad consistentes.
- Ingeniería de IA: Los flujos de trabajo de IA a menudo son experimentales y involucran:
- Iterar sobre diferentes arquitecturas de modelos o técnicas de preprocesamiento de datos.
- Actualizar dinámicamente componentes del sistema (por ejemplo, volver a entrenar modelos, cambiar algoritmos).
- Extender flujos de trabajo existentes sin romper pipelines de producción, a menudo utilizando patrones extensibles como Decorator o Factory.
Ejemplo: Un patrón Factory en IA podría no solo instanciar un modelo, sino también adjuntar pesos preentrenados, configurar optimizadores y vincular callbacks de entrenamiento, todo de manera dinámica.












