2.6 - El descenso del gradiente: por lotes, por mini-lotes y estocástico¶
El cuaderno Regresión lineal desde cero entrena su modelo con el descenso del gradiente por lotes: cada actualización usa todos los ejemplos. Las notas de Luis en el capítulo 2 describen las alternativas y las decisiones que las acompañan: el tamaño del lote (todos los ejemplos, uno solo o un grupo pequeño), las épocas, la tasa de aprendizaje, la inicialización, los criterios de parada y vigilar el coste en un conjunto de validación, además de en el de entrenamiento. En este cuaderno implementamos cada una en el mismo problema, predecir las mascarillas a partir del número de pacientes:
- Importar las bibliotecas.
- Los datos.
- Un solo bucle de entrenamiento para todas las variantes.
- Descenso por lotes, por mini-lotes y estocástico.
- El camino sobre la superficie de coste.
- Por qué se barajan los datos.
- La tasa de aprendizaje.
- Cuándo parar.
- Por dónde empezar.
- Reducir la tasa de aprendizaje cuando el coste de validación deja de mejorar.
Paso 1: Importar las bibliotecas¶
import numpy as np
import matplotlib.pyplot as plt
np.set_printoptions(precision=3, suppress=True)
Paso 2: Los datos¶
El ejemplo de Luis usa un conjunto de entrenamiento de 1.000 ejemplos, así que simulamos 1.000 días con la misma regla que el cuaderno desde cero (entre 50 y 150 pacientes, mascarillas $= 25 + 0.8 \times$ pacientes más ruido) y 250 días más para validación, que nunca se usan para actualizar los parámetros. Como en aquel cuaderno, el número de pacientes se estandariza con la media y la desviación típica de los días de entrenamiento. La solución de mínimos cuadrados da el coste más bajo que puede alcanzar cualquier recta en esos 1.000 días: es el suelo al que intentan llegar todas las variantes.
rng = np.random.default_rng(42)
def simulate_days(n):
patients = 50 + 100 * rng.random(n)
masks = 25 + 0.8 * patients + rng.normal(0, 10, n)
return patients, masks
x_train, y_train = simulate_days(1000)
x_val, y_val = simulate_days(250)
mean, std = x_train.mean(), x_train.std()
z_train, z_val = (x_train - mean) / std, (x_val - mean) / std # pacientes estandarizados
def cost(w, b, z, y):
return np.mean((w * z + b - y) ** 2) # ECM
best_w, best_b = np.polyfit(z_train, y_train, 1)
floor = cost(best_w, best_b, z_train, y_train)
print(f"Días de entrenamiento: {len(y_train)}, días de validación: {len(y_val)}")
print(f"Mínimos cuadrados con los datos escalados: w = {best_w:.3f}, b = {best_b:.3f}, coste más bajo posible = {floor:.3f}")
Días de entrenamiento: 1000, días de validación: 250 Mínimos cuadrados con los datos escalados: w = 23.905, b = 103.995, coste más bajo posible = 102.465
Con los pacientes estandarizados, $b$ es simplemente la media de mascarillas, 103,995, y $w$ = 23,905 es el cambio en mascarillas por cada desviación típica de pacientes. Ninguna recta puede bajar de un coste de entrenamiento de 102,465 en estos días: lo que queda es el ruido que añadimos, cuya varianza es 100.
Paso 3: Un solo bucle de entrenamiento para todas las variantes¶
Una época es una pasada por los datos de entrenamiento. En cada época, el bucle baraja los ejemplos, los divide en lotes y, para cada lote, calcula el gradiente solo con los ejemplos de ese lote y actualiza los parámetros. La actualización es la de las ecuaciones de Luis, donde ahora $m$ es el tamaño del lote:
$$w := w - \alpha \, \frac{2}{m} \sum_{i \in \text{lote}} (\hat{y}_i - y_i)\, z_i \qquad b := b - \alpha \, \frac{2}{m} \sum_{i \in \text{lote}} (\hat{y}_i - y_i)$$
Con batch_size=1000 es el descenso por lotes, con batch_size=1 el estocástico, y cualquier valor intermedio es el descenso por mini-lotes. Al final de cada época, el bucle anota el coste en el conjunto de entrenamiento y, si le pasamos uno, en el de validación. Los dos últimos argumentos son para el paso 10.
def gradient(w, b, z, y):
error = w * z + b - y
return 2 * np.mean(error * z), 2 * np.mean(error) # dC/dw, dC/db
def train(z, y, batch_size, learning_rate, epochs, shuffle=True, seed=0, start=(0.0, 0.0),
z_val=None, y_val=None, reduce_factor=None):
order_rng = np.random.default_rng(seed)
w, b = start
m = len(y)
history = {'cost': [], 'val_cost': [], 'learning_rate': [], 'path': [(w, b)], 'updates': 0}
for epoch in range(epochs):
order = order_rng.permutation(m) if shuffle else np.arange(m) # 1. barajar los datos
for first in range(0, m, batch_size): # 2. para cada lote
batch = order[first:first + batch_size]
dw, db = gradient(w, b, z[batch], y[batch]) # el gradiente solo de este lote
w, b = w - learning_rate * dw, b - learning_rate * db
history['path'].append((w, b))
history['updates'] += 1
history['cost'].append(cost(w, b, z, y))
history['learning_rate'].append(learning_rate)
if z_val is not None: # 3. coste en el conjunto de validación
history['val_cost'].append(cost(w, b, z_val, y_val))
stalled = len(history['val_cost']) > 1 and history['val_cost'][-1] > history['val_cost'][-2] - 0.01
if reduce_factor is not None and stalled: # 4. paso 10: reducir la tasa de aprendizaje
learning_rate = learning_rate * reduce_factor
return w, b, history
Paso 4: Descenso por lotes, por mini-lotes y estocástico¶
Entrenamos cuatro veces durante 20 épocas con la misma tasa de aprendizaje, 0,05, cambiando solo el tamaño del lote. En el ejemplo de Luis, con 1.000 ejemplos y lotes de 100, una época son 10 actualizaciones.
runs = {}
for batch_size in [1000, 100, 10, 1]:
w, b, history = train(z_train, y_train, batch_size, learning_rate=0.05, epochs=20)
runs[batch_size] = history
print(f"lote de {batch_size:>4}: {history['updates'] // 20:>4} actualizaciones por época, "
f"coste tras 1 época {history['cost'][0]:8.2f}, tras 20 épocas {history['cost'][-1]:7.2f}")
plt.figure(figsize=(9, 4))
for batch_size, history in runs.items():
plt.plot(range(1, 21), history['cost'], marker='o', ms=3, label=f'lote de {batch_size}')
plt.axhline(floor, color='gray', ls='--', label='mínimos cuadrados')
plt.yscale('log'); plt.xticks([1, 5, 10, 15, 20]); plt.xlabel('época'); plt.ylabel('coste de entrenamiento (ECM)')
plt.legend(); plt.grid(True); plt.show()
lote de 1000: 1 actualizaciones por época, coste tras 1 época 9325.54, tras 20 épocas 270.77 lote de 100: 10 actualizaciones por época, coste tras 1 época 1489.90, tras 20 épocas 102.47 lote de 10: 100 actualizaciones por época, coste tras 1 época 105.41, tras 20 épocas 102.85 lote de 1: 1000 actualizaciones por época, coste tras 1 época 138.28, tras 20 épocas 130.81
Las cuatro ejecuciones hacen el mismo trabajo por época, porque cada una procesa los 1.000 días una vez; lo que cambia es cuántas veces actualizan los parámetros. El descenso por lotes actualiza una vez por época, así que tras 20 épocas solo ha dado 20 pasos y su coste, 270,77, sigue lejos del suelo. Los mini-lotes de 100 lo alcanzan, 102,47, y los lotes de 10 ya están en 105,41 tras la primera época. El descenso estocástico también es rápido al principio, 138,28 tras una época, pero nunca se asienta: cada actualización sigue a un solo día, con su ruido, así que con una tasa de aprendizaje constante su coste sube y baja de una época a otra y acaba en 130,81. Es lo que dicen las notas de Luis: los extremos, todos los ejemplos o uno solo, pueden ser más lentos o más inestables, y los mini-lotes dan actualizaciones más estables sin renunciar a la velocidad.
Paso 5: El camino sobre la superficie de coste¶
El coste es una función de los dos parámetros, $C(w, b)$, y con los datos escalados sus curvas de nivel son círculos alrededor del mínimo. Encima dibujamos las primeras actualizaciones de tres variantes: las 20 que hace el descenso por lotes en 20 épocas y las 30 primeras de las otras dos.
W, B = np.meshgrid(np.linspace(-45, 90, 200), np.linspace(-10, 125, 200))
C = np.mean((W[..., None] * z_train + B[..., None] - y_train) ** 2, axis=-1)
fig, axes = plt.subplots(1, 3, figsize=(16, 5.5), sharey=True)
for ax, batch_size in zip(axes, [1000, 10, 1]):
path = np.array(runs[batch_size]['path'][:31])
ax.contour(W, B, C, levels=[150, 500, 1500, 3500, 6500, 10000], colors='gray', linewidths=0.7)
ax.plot(path[:, 0], path[:, 1], 'o-', ms=3)
ax.plot(best_w, best_b, 'r*', ms=14)
ax.set_aspect('equal')
ax.set_title(f'lote de {batch_size}: {len(path) - 1} primeras actualizaciones')
ax.set_xlabel('w')
axes[0].set_ylabel('b')
plt.tight_layout(); plt.show()
El descenso por lotes camina en línea recta hacia el mínimo: sobre curvas de nivel circulares, la dirección de máximo descenso apunta directamente al centro. Sus pasos se acortan a medida que el gradiente se reduce, y tras 20 actualizaciones todavía no ha llegado. Los mini-lotes de 10 siguen casi la misma dirección pero se tambalean, porque cada lote de 10 días da un gradiente ligeramente distinto; en 30 actualizaciones, menos de un tercio de época, ya están junto a la estrella. El descenso estocástico va en zigzag, porque cada paso sigue a un solo día. También se acerca rápido, pero sigue moviéndose alrededor del mínimo en lugar de detenerse en él.
Paso 6: Por qué se barajan los datos¶
El descenso por mini-lotes y el estocástico actualizan los parámetros antes de haber visto todos los datos, así que importa el orden en que llegan los ejemplos, y los registros reales suelen venir en algún orden, por fecha o por tamaño. Por eso cada época de train empieza barajándolos. Supongamos que los días llegan ordenados por el número de pacientes, del más tranquilo al más concurrido, y ejecutamos el descenso estocástico durante una época con y sin barajar.
by_patients = np.argsort(x_train)
z_sorted, y_sorted = z_train[by_patients], y_train[by_patients]
for shuffle in [False, True]:
w, b, history = train(z_sorted, y_sorted, batch_size=1, learning_rate=0.05, epochs=1, shuffle=shuffle)
print(f"barajar={shuffle!s:<5} w = {w:6.3f}, b = {b:7.3f}, coste tras una época = {history['cost'][-1]:7.2f}")
print(f"mínimos cuadrados: w = {best_w:6.3f}, b = {best_b:7.3f}, coste {floor:.2f}")
barajar=False w = -24.034, b = 189.562, coste tras una época = 9722.36 barajar=True w = 25.701, b = 101.374, coste tras una época = 112.56 mínimos cuadrados: w = 23.905, b = 103.995, coste 102.46
Sin barajar, una época de descenso estocástico termina en w = −24,034 y b = 189,562, una recta con pendiente negativa, con un coste de 9722,36. Cada actualización acerca la recta al día que acaba de ver, así que al final de la época la recta solo recuerda los días más concurridos, los últimos que vio, y hasta el signo de su pendiente es erróneo. Con los mismos días barajados, el mismo algoritmo termina en w = 25,701 y b = 101,374, con un coste de 112,56, cerca de la recta de mínimos cuadrados. El descenso por lotes no necesita barajar, porque cada una de sus actualizaciones usa todos los días de todos modos.
Paso 7: La tasa de aprendizaje¶
Luis enseña al equipo cómo evoluciona el entrenamiento con distintas tasas de aprendizaje. Su figura muestra el tamaño del gradiente, que debe bajar de una tolerancia para que el algoritmo se detenga: con una tasa demasiado grande oscila e incluso aumenta, porque los pasos "saltan" por encima del mínimo; con una demasiado pequeña, la línea es casi plana. Repetimos su experimento con el descenso por lotes, 30 actualizaciones con cada tasa, y seguimos también al sesgo $b$ en su camino hacia el mínimo.
tolerance = 0.01
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 4))
for learning_rate, style in [(0.001, 'o-'), (0.1, 'o-'), (0.9, 'o--'), (1.02, 'o-')]:
w, b, history = train(z_train, y_train, batch_size=1000, learning_rate=learning_rate, epochs=30)
sizes = [np.hypot(*gradient(w, b, z_train, y_train)) for w, b in history['path']]
ax1.plot(sizes, style, ms=3, label=f'tasa {learning_rate}')
ax2.plot([b for w, b in history['path']], style, ms=3)
print(f"tasa {learning_rate:<5}: tamaño del gradiente {sizes[0]:.1f} al principio, {sizes[-1]:10.4f} tras 30 actualizaciones; "
f"b tras 1, 2, 3 actualizaciones: {history['path'][1][1]:7.1f} {history['path'][2][1]:7.1f} {history['path'][3][1]:7.1f}")
ax1.axhline(tolerance, color='gray', ls='--', label='tolerancia')
ax1.set_yscale('log'); ax1.set_xlabel('actualización'); ax1.set_ylabel('tamaño del gradiente |∇C|'); ax1.set_title('El tamaño del gradiente')
ax2.axhline(best_b, color='gray', ls=':', label='mínimo de b'); ax2.set_ylim(-150, 250)
ax2.set_xlabel('actualización'); ax2.set_ylabel('b'); ax2.set_title('El sesgo en su camino hacia el mínimo')
fig.legend(*[sum(h, []) for h in zip(ax1.get_legend_handles_labels(), ax2.get_legend_handles_labels())], loc='lower center', ncol=6)
plt.tight_layout(rect=(0, 0.08, 1, 1)); plt.show()
tasa 0.001: tamaño del gradiente 213.4 al principio, 200.9748 tras 30 actualizaciones; b tras 1, 2, 3 actualizaciones: 0.2 0.4 0.6 tasa 0.1 : tamaño del gradiente 213.4 al principio, 0.2642 tras 30 actualizaciones; b tras 1, 2, 3 actualizaciones: 20.8 37.4 50.7 tasa 0.9 : tamaño del gradiente 213.4 al principio, 0.2642 tras 30 actualizaciones; b tras 1, 2, 3 actualizaciones: 187.2 37.4 157.2 tasa 1.02 : tamaño del gradiente 213.4 al principio, 692.1902 tras 30 actualizaciones; b tras 1, 2, 3 actualizaciones: 212.2 -8.5 221.0
Aparecen los tres comportamientos de la figura de Luis. Con 0,001 la línea es casi plana: el gradiente pasa de 213,4 a 200,9748 en 30 actualizaciones y $b$ apenas ha empezado a subir, así que necesitaría miles de actualizaciones. Con 0,1 el gradiente se reduce en el mismo factor en cada actualización, hasta 0,2642, y $b$ sube sin sobresaltos hasta el mínimo. Con 1,02 cada paso cae más lejos que el anterior, $b$ pasa por 212,2, −8,5 y 221,0, y el gradiente crece hasta 692,1902: el algoritmo diverge.
La tasa de 0,9 muestra el salto por encima del mínimo: $b$ pasa por 187,2, 37,4 y 157,2, de un lado de su mínimo, 104,0, al otro. Aun así converge, porque cada salto es más corto que el anterior. Su gradiente tiene exactamente el mismo tamaño que con 0,1 en cada actualización, y la línea verde discontinua tapa la naranja. El motivo es que, con este coste y los datos escalados, cada actualización multiplica la distancia al mínimo por $1 - 2\alpha$: 0,8 con una tasa de 0,1 y −0,8 con 0,9, el mismo tamaño pero al otro lado. Por la misma razón, aquí diverge cualquier tasa mayor que 1. El tamaño del gradiente no distingue 0,1 de 0,9; los parámetros, sí.
Paso 8: Cuándo parar¶
Luis enumera tres criterios de parada: el coste deja de mejorar de forma significativa, la magnitud del gradiente es muy pequeña o se alcanza un número máximo de épocas. Con el descenso por lotes y una tasa de aprendizaje de 0,1, contamos cuántas actualizaciones necesita cada criterio y a qué distancia del suelo está el coste cuando se activa.
def batch_descent_until(stop, learning_rate=0.1, max_updates=1000):
w, b, previous = 0.0, 0.0, None
for update in range(1, max_updates + 1):
dw, db = gradient(w, b, z_train, y_train)
w, b = w - learning_rate * dw, b - learning_rate * db
current = cost(w, b, z_train, y_train)
if stop(previous, current, np.hypot(dw, db)):
return update, current
previous = current
return max_updates, current
criteria = {
'cambio del coste menor que 0,001': lambda prev, cur, grad: prev is not None and abs(prev - cur) < 0.001,
'gradiente menor que 0,01': lambda prev, cur, grad: grad < tolerance,
'máximo de 50 actualizaciones': lambda prev, cur, grad: False,
}
for name, stop in criteria.items():
updates, final = batch_descent_until(stop, max_updates=50 if 'máximo' in name else 1000)
print(f"{name:<33} para tras {updates:>3} actualizaciones, coste {final:.4f} ({final - floor:+.4f} sobre el suelo)")
cambio del coste menor que 0,001 para tras 36 actualizaciones, coste 102.4659 (+0.0012 sobre el suelo) gradiente menor que 0,01 para tras 46 actualizaciones, coste 102.4647 (+0.0000 sobre el suelo) máximo de 50 actualizaciones para tras 50 actualizaciones, coste 102.4647 (+0.0000 sobre el suelo)
Parar cuando el coste cambia menos de 0,001, la regla del cuaderno desde cero, termina tras 36 actualizaciones, a 0,0012 del suelo. Parar cuando el gradiente es menor que la tolerancia del paso 7, 0,01, necesita 46 actualizaciones y cae en el suelo con cuatro decimales. Un máximo de 50 actualizaciones también llega, pero solo porque lo hemos elegido conociendo la respuesta: con una tasa de aprendizaje de 0,001, 50 actualizaciones pararían lejos del mínimo. Por eso el máximo se usa como red de seguridad, junto con uno de los otros dos criterios.
Paso 9: Por dónde empezar¶
Luis recomienda inicializaciones aleatorias o heurísticas, para evitar una convergencia lenta o los mínimos locales. Comparamos tres puntos de partida para el descenso por mini-lotes (lotes de 100): ceros, un punto aleatorio y uno heurístico, $b$ igual a la media de mascarillas, que es la mejor predicción constante.
starts = {'ceros': (0.0, 0.0),
'aleatorio': tuple(np.random.default_rng(3).normal(0, 50, 2)),
'heurístico (b = media)': (0.0, y_train.mean())}
for name, start in starts.items():
w, b, history = train(z_train, y_train, batch_size=100, learning_rate=0.05, epochs=20, start=start)
first_close = next(e for e, c in enumerate(history['cost'], 1) if c < floor + 0.5)
print(f"{name:<23} inicio (w, b) = ({start[0]:6.2f}, {start[1]:7.2f}) -> w = {w:.3f}, b = {b:.3f}, "
f"a menos de 0,5 del suelo tras {first_close} época(s)")
ceros inicio (w, b) = ( 0.00, 0.00) -> w = 23.849, b = 103.998, a menos de 0,5 del suelo tras 5 época(s) aleatorio inicio (w, b) = (102.05, -127.78) -> w = 23.849, b = 103.998, a menos de 0,5 del suelo tras 6 época(s) heurístico (b = media) inicio (w, b) = ( 0.00, 104.00) -> w = 23.849, b = 103.998, a menos de 0,5 del suelo tras 4 época(s)
Los tres inicios terminan en la misma recta, w = 23,849 y b = 103,998, porque este coste tiene un solo mínimo: su superficie es un cuenco, los círculos del paso 5, y todos los caminos llevan al fondo. Incluso acaban con los mismos decimales, porque usan los mismos lotes barajados y tras 20 épocas el punto de partida ya se ha olvidado. El inicio solo cambia la longitud del camino: desde el inicio aleatorio, (102,05, −127,78), se necesitan 6 épocas para quedar a menos de 0,5 del suelo, desde ceros 5 y desde el inicio heurístico 4, porque empieza con el sesgo ya acertado. La advertencia de Luis sobre los mínimos locales es para costes con varios valles, como los de las redes neuronales; la regresión lineal con el ECM solo tiene uno.
Paso 10: Reducir la tasa de aprendizaje cuando el coste de validación deja de mejorar¶
Las notas de Luis insisten en vigilar el coste en el conjunto de validación, además de en el de entrenamiento, y mencionan métodos adaptativos como Momentum, RMSProp o Adam, que ajustan la tasa de aprendizaje durante el entrenamiento. La versión más sencilla de esa idea usa el coste de validación: después de cada época, si no ha mejorado al menos 0,01, la tasa de aprendizaje se multiplica por un factor de reducción, aquí 0,5. Lo comparamos con el mismo descenso estocástico con una tasa constante. Los 250 días de validación nunca se usan para actualizar los parámetros, solo para decidir cuándo reducir la tasa.
_, _, constant = train(z_train, y_train, batch_size=1, learning_rate=0.05, epochs=20, z_val=z_val, y_val=y_val)
_, _, reduced = train(z_train, y_train, batch_size=1, learning_rate=0.05, epochs=20, z_val=z_val, y_val=y_val, reduce_factor=0.5)
print(f"tasa constante: coste de entrenamiento {constant['cost'][-1]:.3f}, coste de validación {constant['val_cost'][-1]:.3f}")
print(f"tasa reducida: coste de entrenamiento {reduced['cost'][-1]:.3f}, coste de validación {reduced['val_cost'][-1]:.3f}")
print("tasa de aprendizaje en cada época:", ", ".join(f"{rate:g}" for rate in reduced['learning_rate']))
best_val = cost(best_w, best_b, z_val, y_val)
print(f"coste de validación de la recta de mínimos cuadrados: {best_val:.3f}")
fig, ax = plt.subplots(figsize=(9, 4))
ax.plot(range(1, 21), constant['val_cost'], marker='o', ms=3, label='tasa de aprendizaje constante')
ax.plot(range(1, 21), reduced['val_cost'], marker='o', ms=3, label='a la mitad cuando la validación deja de mejorar')
ax.axhline(best_val, color='gray', ls='--', label='recta de mínimos cuadrados')
ax.set_xticks([1, 5, 10, 15, 20]); ax.set_xlabel('época'); ax.set_ylabel('coste de validación'); ax.legend(); ax.grid(True); plt.show()
tasa constante: coste de entrenamiento 130.810, coste de validación 136.984 tasa reducida: coste de entrenamiento 102.468, coste de validación 122.273 tasa de aprendizaje en cada época: 0.05, 0.05, 0.05, 0.025, 0.0125, 0.0125, 0.00625, 0.003125, 0.003125, 0.003125, 0.003125, 0.0015625, 0.0015625, 0.0015625, 0.00078125, 0.000390625, 0.000195313, 0.000195313, 9.76563e-05, 9.76563e-05 coste de validación de la recta de mínimos cuadrados: 122.092
Con una tasa constante, el descenso estocástico termina las 20 épocas con un coste de entrenamiento de 130,810 y un coste de validación de 136,984, y su coste de validación sube y baja en la figura: cada actualización sigue a un solo día y, con 0,05, la recta nunca se asienta. Reducir la tasa a la mitad cada vez que el coste de validación se estanca hace los pasos cada vez más pequeños, de 0,05 a 9,76563e-05, y la recta se asienta: un coste de entrenamiento de 102,468, prácticamente el suelo, y un coste de validación de 122,273, junto al 122,092 de la recta de mínimos cuadrados. Los días de validación tienen su propio ruido, así que incluso la mejor recta obtiene en ellos un coste más alto que en los días de entrenamiento.
Conclusión¶
- La misma regla de actualización da tres algoritmos según cuántos ejemplos use cada paso. Los mini-lotes son el compromiso habitual: muchas actualizaciones por época, cada una barata y no demasiado ruidosa.
- Una época es siempre una pasada por los datos, pero el número de actualizaciones que contiene es $m$ dividido por el tamaño del lote: 1, 10, 100 o 1.000 en este cuaderno.
- Barajar importa en cualquier variante que actualiza antes de ver todos los datos.
- La tasa de aprendizaje, el criterio de parada y el punto de partida son decisiones, y cada una se puede comprobar con unas pocas líneas de código.
- Vigilar el coste en un conjunto de validación, y reducir la tasa de aprendizaje cuando deja de mejorar, es lo que permite a los métodos estocásticos asentarse cerca del mínimo en lugar de rebotar a su alrededor.