11. Sistemas Difusos#
«El agua sigue muy fría, está helada, súbele mucho más al calentón» «El servicio y la comida de este restaurante fueron muy buenos, debemos dejar una buena propina», «Vas lento, acelera un poco».
Por ejemplo, ¿qué porcentaje de la cuenta podría considerarse una propina muy buena? Un 5% claramente no lo es; un 10% suele considerarse “normal” (sin considerar ciudades como Nueva York en la actualidad o países como Japón), mientras que en algunas ciudades un 15% o incluso un 18% podrían ya considerarse muy buenos.
Estas son algunas de las frases que podemos escuchar y entender perfectamente en algún contexto dado. La pregunta es: ¿podemos representar esto numéricamente?
No existe un límite exacto y universal: la interpretación depende del contexto, la cultura y la experiencia previa. De manera similar, términos como frío, muy frío, lento o rápido no tienen fronteras numéricas bien definidas. Cuando utilizamos el lenguaje natural, los números precisos y los operadores lógicos convencionales (como mayor que, menor que o igual a) resultan insuficientes para capturar este tipo de conocimiento impreciso.
Ante esta limitación, Lotfi A. Zadeh propuso, en la década de los sesenta, una extensión de la lógica clásica conocida como lógica difusa (fuzzy logic). En lugar de exigir pertenencia absoluta a un conjunto (verdadero o falso), la lógica difusa introduce el concepto de funciones de pertenencia, variables difusas y términos lingüísticos, permitiendo modelar grados de pertenencia intermedios.
Por ejemplo, retomando el caso de una propina muy buena, a continuación se muestra una posible asignación del grado de pertenencia (un valor entre cero y uno) para los porcentajes mencionados anteriormente:
Grado de pertenencia a muy buena:
5% → 0.0
10% → 0.4
15% → 0.8
18% → 1.0
Gracias a este enfoque, es posible representar computacionalmente conceptos vagos o subjetivos, como temperatura alta, velocidad moderada o servicio excelente, de una forma más cercana al razonamiento humano. Los sistemas difusos han sido ampliamente utilizados en áreas como el control automático, la inteligencia artificial, los sistemas expertos y la toma de decisiones, especialmente cuando los modelos matemáticos precisos son difíciles de obtener o simplemente no existen.
En las siguientes secciones estudiaremos los fundamentos de los sistemas difusos y su implementación utilizando Python, comenzando con las funciones de pertenencia y avanzando gradualmente hacia sistemas de inferencia difusa completos.
11.1. Funciones de Membresía#
Las funciones de membresía son un elemento fundamental de la lógica difusa. Continuando con el ejemplo de la propina, hasta el momento hemos asignado únicamente algunos valores discretos de porcentaje a su grado de pertenencia correspondiente.
Una función de membresía define un mapeo entre el dominio de valores que puede tomar una variable difusa y el rango de sus grados de pertenencia. En nuestro caso, el término lingüístico es muy buena, y su dominio puede definirse, por ejemplo, en el intervalo de 0% a 40%. Aunque matemáticamente sería posible no establecer un límite superior, en la práctica es importante definir un dominio adecuado de acuerdo con el problema que se desea modelar.
La función de membresía mapea entonces el dominio de los porcentajes de propina al intervalo de grados de pertenencia comprendido entre 0 y 1.
Veamos la definición formal de conjunto difuso propuesta por Zadeh [17]:
Conjunto Difuso#
Un conjunto difuso se define como un par \((U, m)\) donde:
\(U\) es un conjunto (usualmente no vacío), llamado universo de discurso, (en nuestro ejemplo es el conjunto de porcentajes de propina que podemos dar).
\(m\) es una función de membresía \(m : U \rightarrow [0,1]\) que asigna a cada elemento \(x \in U\), un grado de membresía.
Así decimos que la función \(m = \mu_A\) se denomina función de membresía del conjunto difuso \(A = (U, m)\).
Dado un elemento \(x \in U\), se dice que:
\(x\) no pertenece al conjunto difuso si \(m(x) = 0\),
\(x\) pertenece completamente al conjunto difuso si \(m(x) = 1\),
\(x\) pertenece parcialmente al conjunto difuso si \(0 < m(x) < 1\).
Resulta útil visualizar este concepto de manera gráfica así que entraremos en materia utilizando la librería de scikit fuzzy para definir el término difuso propina muy buena.
Visualización de funciones de membresía con scikit-fuzzy#
Una ventaja práctica de utilizar la librería scikit-fuzzy es que permite definir
funciones de membresía con formas estándar (triangulares, trapezoidales, gaussianas,
etc.) y visualizarlas de manera directa. Esto es útil para validar rápidamente si
la interpretación de un término lingüístico (por ejemplo, muy buena) coincide con
lo que esperamos en el problema.
En el ejemplo siguiente modelamos el término propina muy buena sobre el dominio de 0% a 40% utilizando una función trapezoidal.
import numpy as np
import skfuzzy as fuzz
import matplotlib.pyplot as plt
# Dominio (universo) de la variable: porcentaje de propina
x_propina = np.linspace(0, 40, 501)
# Función de membresía trapezoidal para el término lingüístico "muy buena"
# [a, b, c, d] define el inicio, subida, meseta y bajada (en porcentaje).
mx_muy_buena = fuzz.trapmf(x_propina, [10, 15, 20, 30])
# Gráfica
plt.figure(figsize=(8, 5)) # <- tamaño físico de la figura
plt.plot(x_propina, mx_muy_buena, linewidth=2)
plt.title("Función de membresía: propina 'muy buena'", fontsize=14)
plt.xlabel("Propina (%)", fontsize=12)
plt.ylabel("Grado de membresía μ", fontsize=12)
plt.xticks(fontsize=10)
plt.yticks(fontsize=10)
plt.ylim(-0.05, 1.05)
plt.grid(True)
plt.tight_layout()
plt.show()
Nota
Los parámetros [10, 15, 20, 30] son una elección de modelado. En
problemas reales estos valores se ajustan con conocimiento experto, datos
históricos o técnicas de optimización (esto se conecta con la siguiente
capítulo de cómputo evolutivo).
El código anterior nos genera la siguiente gráfica:
Función de membresía para el término lingüístico Muy buena (propina).#
La función define un incremento en el grado de membresía a partir del 10%, hasta alcanzar el valor máximo de 1.0 en el intervalo comprendido entre 15% y 20%. Modelamos Muy buena con una meseta y un descenso porque queremos reservar porcentajes altos para un término como excelente (aunque ambos pueden traslaparse). En este esquema, porcentajes mayores al 20% y más cercanos a 40% tendrían un grado de pertenencia más alto al término Excelente que al término Muy buena.
Variables lingüísticas#
En el contexto de nuestro ejemplo, la propina puede considerarse una variable lingüística definida sobre el universo de discurso de los porcentajes de propina. Esta variable puede tomar valores lingüísticos como poca, normal, buena, muy buena y excelente, los cuales representan conceptos cualitativos que usamos en el lenguaje natural.
Matemáticamente modelamos cada uno de los valores lingüísticos usando el término difuso correspondiente, es decir, un conjunto difuso definido sobre el universo de discurso y caracterizado por su función de membresía.
Por ejemplo, el valor lingüístico muy buena se representa mediante el término difuso asociado a la función de membresía \(\mu_{\text{muy\_buena}}(x)\), que asigna a cada porcentaje de propina un grado de pertenencia entre 0 y 1.
Reglas de inferencia difusas#
Una de las manera de representar el conocimiento computacionalmente es mediante reglas IF-THEN, las cuales especifican que acciones se realizarán cuando ciertas condiciones se cumplan. Las reglas IF-THEN (también llamadas reglas de producción) tienen una dos partes; el antecedente, conformado por un conjunto de condiciones y el consecuente constituido por un conjunto de conclusiones:
SI (condición) ENTONCES (conclusión).
Utilizando lógica difusa podemos definir reglas de inferencia que busquen capturar la forma en que un humano razona en situaciones donde los límites no son completamente nítidos. Por ejemplo, al decidir una propina, usamos razonamientos como:
“Si el servicio fue excelente, entonces la propina debe ser excelente”.
“Si el servicio fue bueno y la comida fue buena, entonces la propina es muy buena”.
“Si el servicio fue malo, entonces la propina es poca”.
En un sistema difuso, las condiciones (proposiciones difusas) se construyen combinando términos lingüísticos con conectores lógicos como AND y OR. Por ejemplo:
SI servicio es bueno AND comida es buena ENTONCES propina es muy buena.
SI servicio es malo OR comida es mala ENTONCES propina es poca.
Estas reglas no producen una decisión binaria. En su lugar, cada regla puede tener cierto grado de activación, dependiendo de qué tan bien se cumplan sus condiciones.
11.2. Sistemas de Inferencia Difusa#
Los sistemas de inferencia difusa (FISs) se basan en las reglas de inferencia difusas que vimos anteriormente. Los FIS definen relaciones entre variables de entrada y de salida. Las variables de entrada se incluyen en los antecedentes de la reglas y las variables de salida en los consecuentes. Dependiendo del tipo de consecuente, se pueden distinguir dos tipos de sistemas de inferencia difusa:
Modelo difuso lingüístico: donde ambos el antecedente y consecuentes son proposiciones difusas.
Modelo difuso Takagi-Sugeno el antecedente es una proposición difusa; el consecuente es una función nítida (crisp).
Los sistemas de inferencia difusa típicamente tienen estos cuatro componentes:
Base de Reglas. El conjunto de reglas difusas.
Máquina de Inferencia Difusa. Este modulo ejecuta las operaciones de inferencia difusa.
Fusificador. Este modulo transforma las entradas del sistema (valores numéricos) en valores lingüísticos.
Defusificador. Transforma los resultados difusas a valores numéricos.
Tipos de sistemas de inferencia difusa#
A continuación se describen los tres tipos más utilizados de FIS. La diferencia entre ellos es principalmente la forma en que producen la salida.
- Tsukamoto
En el método de inferencia Tsukamoto, la salida de cada regla es un valor nítido obtenido a partir del grado de activación de la regla. La salida global del sistema se calcula como un promedio ponderado de las salidas individuales de las reglas.
- Mamdani
En el método de Mamdani, cada regla produce una salida difusa.
Para obtener una salida nítida a partir del conjunto difuso resultante, se utilizan distintos métodos de defusificación (defuzzificación), entre los más comunes se encuentran: - El método del centroide. - La bisección del área. - El promedio de los máximos. - El criterio del máximo.
Este es uno de los métodos más utilizados debido a su interpretación intuitiva y a su cercanía con el razonamiento humano.
- Sugeno
En el método de Sugeno, el consecuente de cada regla no es un conjunto difuso, sino una función matemática de las variables de entrada, típicamente una combinación lineal de estas más un término constante. Este enfoque es especialmente adecuado para sistemas de control y optimización, ya que facilita el análisis matemático y la implementación computacional.
En este capítulo no entraremos en detalle sobre como se hace implementan internamente este tipo de sistemas. Lo que nos interesa es la implementación en Python. Implementemos un FIS que tome de entrada las variables difusas comida y servicio y nos de como salida la propina que vamos a dejar.
Implementación de un sistema de inferencia difusa en Python#
El primer paso es definir las variables lingüisticas y asignarlas a su posición ya sea en el antedecente o el consecuente. Lo importante para definir estas variables el dominio o universo de discurso. En el caso de las variables de entrada que miden la calidad de la comida y el servicio, estas irán de cero a diez. Como ya lo decidimos la propina va de 0 a 40 porciento.
import numpy as np
import skfuzzy as fuzz
from skfuzzy import control as ctrl
import matplotlib.pyplot as plt
# Variables de entrada (antecedentes)
comida = ctrl.Antecedent(np.arange(0, 11, 1), 'comida') # 0..10
servicio = ctrl.Antecedent(np.arange(0, 11, 1), 'servicio') # 0..10
# Variable de salida (consecuente)
propina = ctrl.Consequent(np.arange(0, 41, 1), 'propina') # 0..40 (%)
Una vez definidas las variables agregamos las funciones de membresía para los términos difusos de cada una. Utilizamos funciones triangulares y trapezoidales:
# Funciones de membresía: comida
comida['mala'] = fuzz.trapmf(comida.universe, [0, 0, 2, 4])
comida['regular'] = fuzz.trimf(comida.universe, [3, 5, 7])
comida['buena'] = fuzz.trapmf(comida.universe, [6, 8, 10, 10])
# Funciones de membresía: servicio
servicio['malo'] = fuzz.trapmf(servicio.universe, [0, 0, 2, 4])
servicio['regular'] = fuzz.trimf(servicio.universe, [3, 5, 7])
servicio['excelente'] = fuzz.trapmf(servicio.universe, [6, 8, 10, 10])
# Funciones de membresía: propina
propina['poca'] = fuzz.trapmf(propina.universe, [0, 0, 5, 10])
propina['normal'] = fuzz.trimf(propina.universe, [8, 12, 16])
propina['muy_buena'] = fuzz.trapmf(propina.universe, [14, 18, 22, 26])
propina['excelente'] = fuzz.trapmf(propina.universe, [22, 28, 40, 40])
Podemos ver las variables gráficamente utilizando el método view():
# (Opcional) Visualizar funciones
comida.view(); servicio.view(); propina.view()
plt.show()
Ejemplo:
Gráfica de las funciones de membresía para la variable lingüística propina.#
Base de reglas difusas#
Creamos las reglas utilizando los operadores lógicos, antecedente y consecuente según la variable lingüística y función de membresía:
regla1 = ctrl.Rule(servicio['malo'] | comida['mala'], propina['poca'])
regla2 = ctrl.Rule(servicio['regular'] & comida['regular'], propina['normal'])
regla3 = ctrl.Rule(servicio['excelente'] & comida['buena'], propina['excelente'])
# Reglas intermedias para dar suavidad al sistema
regla4 = ctrl.Rule(servicio['regular'] & comida['buena'], propina['muy_buena'])
regla5 = ctrl.Rule(servicio['excelente'] & comida['regular'], propina['muy_buena'])
Construcción y simulación del sistema#
Vamos a construir un FIS tipo Mamdani y probaremos el caso de una comida de 7.0 con un buen servicio 9.0. Las entradas son nítidas; la inferencia produce una salida difusa agregada y luego se obtiene una salida nítida mediante defusificación.
sistema = ctrl.ControlSystem([regla1, regla2, regla3, regla4, regla5])
simulacion = ctrl.ControlSystemSimulation(sistema)
# Entradas nítidas (crisp) del usuario
simulacion.input['comida'] = 7.0
simulacion.input['servicio'] = 9.0
# Ejecutar inferencia
simulacion.compute()
# Salida nítida
print("Propina sugerida (%):", simulacion.output['propina'])
# Visualizar el resultado sobre la membresía de salida
propina.view(sim=simulacion)
plt.tight_layout()
plt.show()
Podemos ver gráficamente el resultado de la inferencia con una defusificación por centroide:
Gráfica de las funciones de membresía para la variable lingüística propina.#
Hasta aquí vimos lógica difusa como herramienta de representación e inferencia. Ahora la aplicaremos en un caso clásico: control de un sistema dinámico.
11.3. Control Difuso#
No te preocupes si esta no es tu área de especialidad. El enfoque que veremos en este libro será principalmente computacional, con el objetivo de mostrar cómo la lógica difusa puede aplicarse al diseño de sistemas de control sin necesidad de un trasfondo profundo en teoría clásica de control.
Si estás estudiando ingeniería en electrónica, cibernética o algún área afín, es muy probable que hayas llevado algún curso de Teoría de Control o algo similar. En particular, en cibernética esta área es fundamental: incluso el nombre proviene del término griego que hace referencia a gobernar el timón de una embarcación. En este caso podrás profundizar análizando el código que utilizaremos para simulación.
¿Teoría de Control?#
La teoría de control tiene innumerables aplicaciones en la vida cotidiana, y disfrutamos de sus beneficios de manera constante. Por ejemplo, en un sistema de aire acondicionado, un controlador regula el encendido y la potencia para mantener una temperatura deseada. El cruise control de un automóvil es otro ejemplo, así como el piloto automático de un avión o un dron.
Incluso dispositivos tan comunes como una lavadora o una cafetera de espresso utilizan sistemas de control para regular variables como el flujo y la temperatura del agua. En todos estos casos, el objetivo es el mismo: ajustar el comportamiento del sistema para que siga una referencia deseada.
La teoría moderna de control se consolidó a finales de la década de 1960 con el desarrollo del control basado en modelos (model-based control, MBC) y del control óptimo. En este enfoque, el comportamiento de un sistema dinámico se describe mediante ecuaciones diferenciales o ecuaciones discretas en tiempo (ecuaciones en diferencias), generalmente no lineales, y el diseño del controlador se basa explícitamente en dicho modelo matemático.
Para aplicar control basado en modelos, el diseñador debe primero obtener un modelo suficientemente preciso del sistema que se desea controlar (conocido en teoría de control como la planta) y, posteriormente, diseñar un controlador que garantice estabilidad y desempeño. Si bien este enfoque ha sido extraordinariamente exitoso, en muchos sistemas reales la obtención de modelos precisos resulta difícil, costosa o incluso inviable.
Como alternativa a estos métodos, a principios de la década de 1970 surgió el control inteligente (intelligent control), el cual propone generar acciones de control a partir de conocimiento humano, experiencia operativa y evidencia experimental, en lugar de depender exclusivamente de un modelo matemático del sistema.
Entre las técnicas de control inteligente, el control difuso (fuzzy logic control) ha sido una de las más exitosas y ampliamente adoptadas. Desde los trabajos pioneros de Mamdani, hasta aplicaciones modernas en control industrial, robótica y sistemas complejos, el control difuso ha demostrado ser una herramienta robusta y flexible para enfrentar incertidumbre, no linealidades y conocimiento incompleto.
De manera paralela, en años recientes ha cobrado gran relevancia el control basado en datos (data-driven control), un conjunto amplio de técnicas en las que el modelo del sistema o el diseño del controlador se obtiene directamente a partir de datos. Este enfoque incluye métodos como aprendizaje por refuerzo, control por aprendizaje iterativo, herramientas de control robusto basadas en datos, así como técnicas apoyadas en redes neuronales y algoritmos evolutivos.
Una ventaja fundamental del control difuso frente a otros métodos de control inteligente es que se implementa mediante un sistema de inferencia difusa (fuzzy inference system, FIS), el cual representa explícitamente el conocimiento del sistema a través de reglas difusas. Estas reglas tienen una interpretación lingüística clara, lo que facilita su diseño, análisis y ajuste, ya sea de forma manual o automática.
En este capítulo introduciremos los fundamentos del control difuso desde una perspectiva práctica, comenzando con la estructura de un sistema de inferencia difusa y culminando con la implementación de controladores difusos en Python. En capítulos posteriores se abordará el ajuste sistemático de estos controladores mediante técnicas de optimización.
Control difuso de un sistema de seguimiento de ruta con rueda trasera#
Como ejercicio principal de este capítulo, diseñaremos un sistema difuso para controlar el volante de un robot tipo bicicleta que debe seguir una ruta previamente establecida. Este es un sistema dinámico, ya que en cada instante de tiempo se toman lecturas de la posición del robot y, con base en ellas, se ajusta el volante mediante una velocidad angular.
El esquema general de los elementos que intervienen en el sistema se muestra a continuación:
Modelo de rueda trasera para control de seguimiento.#
En esta simulación el mundo es bidimensional y la escala está expresada en metros. La unidad de tiempo es el segundo, y la simulación se ejecuta durante 50 segundos.
Definición de la ruta#
La ruta a seguir se define mediante una curva suave generada a partir de un spline. Este tipo de representación es similar a las curvas Bézier utilizadas en programas de ilustración. Para definir la ruta especificamos un conjunto de puntos \((x, y)\) en el plano 2D y una función spline que, dado un parámetro (real o entero), devuelve la posición correspondiente sobre la trayectoria.
Denotaremos la posición de la ruta como \(\mathbf{S}(t)\).
Modelo del robot#
El robot consiste en dos ruedas conectadas por un eje de longitud \(L\) y se desplaza únicamente en el plano 2D. El modelo es puramente cinemático: no se consideran efectos de fricción, derrape ni dinámicas verticales. La velocidad lineal se regula automáticamente mediante un controlador interno tipo PID, y se mantiene constante en \(3\,\text{m/s}\).
La posición del robot se toma a partir de la rueda trasera, la cual inicia en el punto \((0, 0)\).
Variables del controlador#
Las variables que intervienen en el controlador difuso, evaluadas en un instante de tiempo \(t_i\), son las siguientes:
eDistancia lateral de la rueda trasera al punto más cercano de la ruta. Este punto se calcula internamente en la simulación utilizando información del paso anterior \(t_{i-1}\) y la dirección de avance. El error se mide en metros y se define como negativo cuando el robot se encuentra a la izquierda de la ruta y positivo cuando se encuentra a la derecha. El objetivo del controlador es que este error tienda a cero.
e_\thetaError de orientación, definido como el ángulo entre el vector tangente a la ruta en el punto más cercano y el eje longitudinal del robot.
\omegaSalida del controlador difuso. Representa la velocidad angular utilizada para calcular el ángulo de dirección \(\delta\) del modelo tipo bicicleta.
Condiciones iniciales y objetivo#
La ruta y el robot inician en el punto \((0, 0)\) con velocidad inicial cero. El objetivo principal del control es minimizar el error de seguimiento, medido mediante el error cuadrático medio (root mean square error, RMSE) de la variable \(e\), y alcanzar la meta correspondiente al final de la ruta.
Código para implementar el controlador difuso#
El código para este ejemplo se encuentra en el directorio fuzzy_code y
consiste en los siguientes archivos:
fuzzy_code
├── angle.py
├── fuzzy_control.py
├── LICENSE
├── my_fis.py
├── path.py
└── rear_wheel_sim.py
En este capítulo solo editaremos los archivos my_fis.py y
fuzzy_control. Los otros scripts implementan la simulación del robot tipo
bicicleta descrito anteriormente. Los archivos incluyen:
my_fis.py
build_fisEn este método definimos el sistema de inferencia difuso (FIS) del controlador. Se definen las variables difusas, las funciones de membresía y las reglas. Al final se construye y regresa un objeto
ControlSystem.get_controllerEsta función regresa un callable que implementa el controlador difuso. La función interna (por ejemplo
controller) toma como entradaseye_thy regresa el valor defusificado deomega.plot_mfsGrafica las funciones de membresía de las variables del antecedente y del consecuente del FIS (en este ejemplo:
e,e_thyomega).
fuzzy_control.py
Este script une todo: define las rutas de prueba, ejecuta la simulación con el controlador seleccionado y muestra los resultados (gráficas y/o animación). En este capítulo lo usaremos como programa de prueba. Podemos editarlo y ver los resultados de la simulación.
El sistema de inferencia difuso#
El primer paso para construir el controlador difuso es definir el sistema de inferencia
en scikit-fuzzy. Esto lo hacemos en el archivo my_fis.py. En
particular, se debe definir la función build_fis que regresa el FIS
que será utilizado por el controlador.
Recordemos que el controlador recibe como entradas el error de orientación
e_th (rad) y el error lateral e (m), y devuelve como salida la
velocidad angular omega (rad/s). Ya con estas variables, el siguiente paso
es definir términos lingüísticos y sus funciones de membresía.
Funciones de membresía#
En esta primera propuesta utilizamos tres términos difusos por variable, con los mismos nombres en cada caso. Estos nombres son abreviaciones en inglés comunes cuando se trabaja con errores positivos y negativos:
NS: Negative Small (Negativo pequeño)Z: Zero (Cero)PS: Positive Small (Positivo pequeño)
Si se desea mayor granularidad, es común introducir términos adicionales:
NB: Negative Big (Negativo grande)PB: Positive Big (Positivo grande)
Incluso scikit-fuzzy permite generar términos automáticamente; sin embargo,
en este capítulo hacemos la definición explícita para que el proceso sea más
claro.
En este ejemplo, utilizaremos funciones trapezoidales en los extremos (NS y
PS) y una función triangular para la región cercana a cero (Z).
Nota
Un detalle importante es que los valores numéricos (rangos y puntos de quiebre) que se usan en las funciones de membresía son heurísticos: se eligen para obtener un comportamiento razonable en las rutas de prueba. En capítulos posteriores aprenderemos a ajustar estos parámetros de forma sistemática y automática.
Tabla de reglas difusas#
Podemos representar las reglas del controlador difuso de forma compacta en forma de
tabla. Cada fila representa una regla difusa,
indicando los términos lingüísticos de las variables de entrada e_th y e,
así como el término correspondiente de la variable de salida omega.
Regla |
|
|
|
|---|---|---|---|
R1 |
|
|
|
R2 |
|
|
|
R3 |
|
|
|
R4 |
|
|
|
R5 |
|
|
|
R6 |
|
|
|
R7 |
|
|
|
R8 |
|
|
|
R9 |
|
|
|
Cada regla se interpreta de la forma: si ``e_th`` es A y ``e`` es B, entonces ``omega`` es C, donde A, B y C son términos lingüísticos.
Un detalle importante que podemos notar es que el número de reglas difusas crece
rápidamente al incrementar la granularidad de las variables lingüísticas. Por
ejemplo, al pasar de tres términos difusos por variable (NS, Z, PS)
a cinco términos (NB, NS, Z, PS, PB), el número total de
reglas pasa de \(3 \\times 3 = 9\) a \(5 \\times 5 = 25\).
Este crecimiento combinatorial es una de las principales limitaciones de los sistemas difusos basados en reglas, ya que incrementa el esfuerzo de diseño y ajuste del controlador. Esta es una de las razones para considerar estrategias para manejar esta complejidad, incluyendo el ajuste automático de parámetros mediante algoritmos evolutivos.
Decisiones de diseño: rangos y parámetros#
En este controlador base elegimos universos de discurso y parámetros de las funciones de membresía de forma heurística:
Para
e_th(rad), se utiliza el intervalo \([-1.5, 1.5]\). Este rango cubre errores de orientación moderados (del orden de decenas de grados).Para
e(m), se utiliza el intervalo \([-3.0, 3.0]\). Este rango es suficiente para representar errores laterales típicos en las rutas de prueba.Para
omega(rad/s), se utiliza el intervalo \([-8.0, 8.0]\). Esta salida controla el giro del volante; valores grandes permiten maniobras más agresivas, pero también pueden provocar oscilaciones o pérdida de la ruta.
La región Z (cero) se define relativamente estrecha para que el controlador
responda con cambios en omega aun con errores pequeños; sin embargo, esta
elección no es única y puede ajustarse. Justamente, la posibilidad de ajustar
estos parámetros (manual o automáticamente) es parte central del siguiente
capítulo.
Aquí está el código que expresa las desiciones de diseño que tomamos:
my_fis.py# 1import numpy as np
2import skfuzzy as fuzz
3from skfuzzy import control as ctrl
4
5
6def build_fis(params=None):
7 """
8 Construye el sistema de inferencia difusa (FIS).
9
10 params : dict | None
11 Parámetros opcionales para modificar las funciones de membresía.
12 En esta versión base, se ignoran o se usan valores por defecto.
13 """
14
15 # Universos (sin normalizar todavía)
16 e_th = ctrl.Antecedent(np.linspace(-1.5, 1.5, 201), 'e_th') # rad aprox.
17 e = ctrl.Antecedent(np.linspace(-3.0, 3.0, 201), 'e') # m
18 omega = ctrl.Consequent(np.linspace(-8.0, 8.0, 201), 'omega') # rad/s
19
20 # e_th: NS, Z, PS
21 e_th['NS'] = fuzz.trapmf(e_th.universe, [-1.5, -1.5, -0.4, 0.0])
22 e_th['Z'] = fuzz.trimf(e_th.universe, [-0.15, 0.0, 0.15])
23 e_th['PS'] = fuzz.trapmf(e_th.universe, [0.0, 0.4, 1.5, 1.5])
24
25 # e: NS, Z, PS
26 e['NS'] = fuzz.trapmf(e.universe, [-3.0, -3.0, -0.8, 0.0])
27 e['Z'] = fuzz.trimf(e.universe, [-0.30, 0.0, 0.30])
28 e['PS'] = fuzz.trapmf(e.universe, [0.0, 0.8, 3.0, 3.0])
29
30 # omega: NS, Z, PS
31 omega['NS'] = fuzz.trapmf(omega.universe, [-8.0, -8.0, -2.5, 0.0])
32 omega['Z'] = fuzz.trimf(omega.universe, [-0.80, 0.0, 0.80])
33 omega['PS'] = fuzz.trapmf(omega.universe, [0.0, 2.5, 8.0, 8.0])
34
35 # Reglas explícitas (3x3)
36 rules = [
37 ctrl.Rule(e_th['NS'] & e['NS'], omega['PS']),
38 ctrl.Rule(e_th['NS'] & e['Z'], omega['PS']),
39 ctrl.Rule(e_th['NS'] & e['PS'], omega['Z']),
40
41 ctrl.Rule(e_th['Z'] & e['NS'], omega['PS']),
42 ctrl.Rule(e_th['Z'] & e['Z'], omega['Z']),
43 ctrl.Rule(e_th['Z'] & e['PS'], omega['NS']),
44
45 ctrl.Rule(e_th['PS'] & e['NS'], omega['Z']),
46 ctrl.Rule(e_th['PS'] & e['Z'], omega['NS']),
47 ctrl.Rule(e_th['PS'] & e['PS'], omega['NS']),
48 ]
49
50 fis = ctrl.ControlSystem(rules)
51 return fis
Visualización de funciones de membresía#
El archivo my_fis.py incluye la función plot_mfs() para visualizar las funciones de
membresía del controlador difuso. Esto permite inspeccionar gráficamente el
universo de cada variable y la forma de sus términos lingüísticos.
La visualización de funciones de membresía es especialmente útil durante la fase de diseño, ya que ayuda a verificar que:
los rangos (universos) de las variables sean consistentes con el problema,
exista traslape suficiente entre términos lingüísticos,
y la región cercana a cero (término
Z) tenga el ancho esperado.
Nota
En este proyecto la visualización se utiliza como herramienta de inspección. No modifica el comportamiento del controlador, solo ayuda a entenderlo.
Se pueden generar las gráficas ejecutando el script directamente:
Funciones de membresía de la variable e_th desde scikit fuzzy.#
python my_fis.py
Generación de controlador difuso#
En el mismo archivo my_fis.py se define una función que genera el
controlador como un callable. La simulación invoca este callable en cada
instante de tiempo, por lo que el simulador no necesita conocer cómo está
implementado internamente el FIS.
my_fis.py# 1def get_controller(params=None):
2 """
3 Devuelve un controlador callable: (e_th, e) -> omega.
4
5 Este patrón es un ejemplo de programación funcional en Python:
6 la función retorna otra función que encapsula el estado interno
7 del sistema difuso.
8 """
9
10 fis = build_fis(params)
11 sim = ctrl.ControlSystemSimulation(fis)
12
13 def controller(e_th, e):
14 # scikit-fuzzy acumula estado interno; para simulación en lazo cerrado
15 # suele ser más robusto reiniciar en cada evaluación.
16
17 sim.reset()
18 sim.input['e_th'] = float(e_th)
19 sim.input['e'] = float(e)
20 sim.compute()
21 return float(sim.output['omega'])
22
23 return controller
Nota
La función get_controller es un ejemplo de programación funcional en
Python: regresa otra función (un callable) que encapsula el comportamiento
del controlador. Esto permite desacoplar la simulación de la implementación
interna del FIS.
Script de prueba: fuzzy_control.py#
El script fuzzy_control.py permite probar rápidamente el controlador en un
conjunto pequeño de rutas. En esta sección utilizaremos una métrica simple (RMSE
del error lateral) únicamente como referencia numérica para comparar ejecuciones
entre distintas modificaciones del FIS.
fuzzy_control.py# 1import my_fis as fc
2import rear_wheel_sim as rw_sim
3import path
4import numpy as np
5
6
7def compute_rmse(traces):
8 errors = np.array([tr.error for tr in traces])
9 return np.sqrt(np.mean(errors**2))
10
11
12if __name__ == "__main__":
13 print("rear wheel feedback tracking start!!")
14
15 paths = [
16 ([0.0, 6.0, 12.5, 5.0, 7.5, 3.0, -1.0], [0.0, 0.0, 5.0, 6.5, 3.0, 5.0, -2.0]),
17 ([0.0, 1.0, 2.5, 5.0, 7.5, 3.0, -1.0], [0.0, -4.0, 6.0, 6.5, 3.0, 5.0, -2.0]),
18 # ([0.0, 2.0, 2.5, 5.0, 7.5, -3.0, -1.0], [0.0, 3.0, 6.0, 6.5, 5.0, 5.0, -2.0]),
19 ]
20
21 # Controlador de referencia (baseline): descomente si desea comparar
22 # controller = None
23
24 # Controlador difuso (edite my_fis.py para modificarlo)
25 controller = fc.get_controller()
26
27 rmses = []
28 for ax, ay in paths:
29 goal = [ax[-1], ay[-1]]
30 reference_path = path.CubicSplinePath(ax, ay)
31
32 result = rw_sim.simulacion(
33 reference_path,
34 goal,
35 controller=controller,
36 )
37
38 traces = result["traces"]
39 rmse = compute_rmse(traces)
40 rmses.append(rmse)
41
42 print(float(np.mean(rmses)))
43
44 # Visualización (opcional): se ejecuta después de la simulación
45 # rw_sim.animate(result, pause=0.001)
46 # rw_sim.plot(result)
Nota
En este capítulo utilizamos el RMSE únicamente como un indicador rápido para comparar cambios en el controlador. En el capítulo de algoritmos genéticos formalizaremos la evaluación del controlador como una función de aptitud (fitness) y discutiremos criterios de penalización y corte temprano.
Este script funciona como un programa de prueba que coordina los siguientes elementos:
Definición de rutas
Se define una lista de rutas de referencia. Es posible comentar algunas de ellas para analizar una ruta en particular o modificar los puntos que definen cada curva y observar los cambios en el comportamiento del robot. Como ejemplo, la primera ruta incluye una curva muy pronunciada al inicio.
Selección del controlador
Existen dos opciones de controlador:
Utilizar
None, lo que tiene como efecto emplear un controlador de referencia, el cual se describe más adelante.Utilizar un controlador difuso, obtenido a partir del archivo
my_fis.py.
Ejecución de la simulación
Para cada ruta se ejecuta una simulación, enviando al simulador la ruta de referencia, el punto meta y el controlador seleccionado.
Resultados de la simulación
La simulación regresa un diccionario con la información completa de la ejecución. Este resultado incluye una lista de lecturas tomadas en cada instante de tiempo. Cada lectura se almacena en una estructura del tipo
dataclass:@dataclass class SimulationTrace: """ Registro temporal de la simulación. """ t: int # tiempo (s) x: float # posición x (m) y: float # posición y (m) yaw: float # orientación (rad) v: float # velocidad lineal (m/s) error_theta: float # error de orientación (rad) error: float # error lateral (m) path_s: float # parámetro de progreso sobre la ruta
A partir de esta lista de lecturas es posible:
trazar el recorrido que siguió el robot,
calcular métricas de desempeño como el RMSE del error lateral,
analizar la evolución del ángulo de orientación (
yaw),o estudiar cómo se comporta el controlador a lo largo del trayecto.
Desde el punto de vista del diseño del software, fuzzy_control.py actúa como
un orquestador: no implementa directamente ni la dinámica del robot ni la
lógica del controlador, sino que conecta ambos componentes a través de una
interfaz común.
Un aspecto importante es que el simulador no conoce el tipo de controlador que se está utilizando. El controlador se pasa como un callable con la siguiente interfaz:
Esto permite intercambiar fácilmente distintos controladores (por ejemplo, uno difuso o uno basado en una ley de control clásica) sin modificar el código de la simulación.
Seguimiento de ruta exitoso.#
Seguimiento de ruta fallido.#
Controlador de referencia#
Una vez implementado y probado el controlador difuso, introducimos ahora un controlador de referencia basado en una ley de control clásica propuesta por Paden et al.. Este controlador se utiliza en la simulación cuando no se proporciona un controlador externo y sirve como punto de comparación (baseline).
Esta es su implementación:
rear_wheel_sim.py# 1def paden_control(error, error_theta, v, k):
2 """
3 Controlador de referencia basado en Paden et al.
4 """
5
6 # Constantes del controlador (baseline)
7 KTH = 1.0
8 KE = 0.5
9
10 omega = (
11 v * k * math.cos(error_theta) / (1.0 - k * error)
12 - KTH * abs(v) * error_theta
13 - KE * v * math.sin(error_theta) / error_theta * error
14 )
15
16 return omega
Comparación entre control difuso y control clásico#
La Tabla siguiente resume las principales diferencias conceptuales entre el controlador difuso y el controlador de referencia utilizado en la simulación.
Característica |
Controlador difuso |
Controlador de referencia |
|---|---|---|
Tipo de enfoque |
Heurístico / basado en reglas |
Analítico / basado en modelo |
Uso de modelo |
No requiere modelo explícito |
Requiere modelo cinemático |
Variables de entrada |
Errores |
Errores, velocidad |
Dependencia de la ruta |
Implícita (a través del error) |
Explícita (curvatura de la ruta) |
Uso de velocidad |
No |
Sí |
Interpretabilidad |
Alta (reglas lingüísticas) |
Media (expresión matemática) |
Facilidad de ajuste |
Intuitiva (funciones y reglas) |
Requiere conocimiento analítico |
Robustez ante incertidumbre |
Alta |
Dependiente del modelo |
Propósito en este capítulo |
Ejemplo de control inteligente |
Referencia (baseline) |
Limitaciones del controlador difuso base#
El controlador difuso presentado en este capítulo tiene algunas limitaciones. Como se observa en la figura anterior, incluso en ciertas rutas el robot pierde el seguimiento. Esto es una característica común de los sistemas de control heurísticos: su desempeño depende fuertemente de las decisiones de diseño que se toman durante su construcción.
En particular, el comportamiento del controlador difuso está altamente influenciado por los siguientes elementos:
La granularidad de las variables difusas, ya que puede ser necesario introducir más términos lingüísticos para capturar comportamientos más finos.
Los rangos definidos para las variables difusas.
El tipo de funciones de membresía utilizadas.
Los parámetros específicos de dichas funciones de membresía.
El conjunto de reglas difusas que conforman la base de conocimiento.
Esto implica que el controlador debe ser optimizado, es decir, debe existir un proceso de afinamiento (tuning) en el que se realicen ajustes sistemáticos a la parametrización del sistema. Dado que se trata de un enfoque heurístico, estos ajustes suelen validarse mediante pruebas repetidas en simulación.
Como hemos visto, cada simulación requiere tiempo y recursos computacionales, especialmente cuando se evalúa el desempeño del controlador sobre múltiples rutas. Esta observación motiva naturalmente el uso de métodos automáticos de optimización, los cuales abordaremos en el siguiente capítulo mediante técnicas de cómputo evolutivo.
Resumen del capítulo#
En este capítulo se introdujo la lógica difusa como una extensión de la lógica clásica para representar y procesar conocimiento impreciso expresado mediante términos lingüísticos como frío, lento o muy bueno. A diferencia de los conjuntos clásicos, los conjuntos difusos permiten grados de pertenencia continuos entre 0 y 1, definidos mediante funciones de membresía, lo que hace posible modelar computacionalmente conceptos dependientes del contexto.
Se estudiaron las variables lingüísticas y sus valores difusos, así como las funciones de membresía más comunes (triangulares y trapezoidales). A partir de ellas, se mostró cómo capturar el razonamiento humano mediante reglas difusas IF–THEN, las cuales constituyen la base de conocimiento de un Sistema de Inferencia Difusa (FIS). También se revisaron los componentes principales de un FIS y se compararon los enfoques más utilizados: Mamdani, Sugeno y Tsukamoto.
El capítulo enfatizó la implementación práctica de sistemas difusos en Python
utilizando la librería scikit-fuzzy. Como ejemplo introductorio, se construyó
un sistema difuso para sugerir una propina a partir de la calidad del servicio y
la comida, ilustrando el proceso completo desde la definición de variables
lingüísticas hasta la defusificación de la salida.
Finalmente, los conceptos se aplicaron a un problema de control difuso más complejo: el seguimiento de trayectoria de un robot tipo bicicleta con rueda trasera. Se diseñó un controlador difuso basado en errores de orientación y posición lateral, se integró en una simulación dinámica y se evaluó su desempeño mediante métricas simples como el RMSE.
Se concluye mostrando las limitaciones del controlador base y se considera el uso de técnicas de optimización automática, en particular métodos de cómputo evolutivo, como el siguiente paso para el ajuste de sistemas difusos.