Mapas autoorganizados: teoría e implementación en Python con NumPy

En esta guía, cubriremos los mapas autoorganizados en detalle, implementaremos un SOM en Python con Numpy y experimentaremos con los hiperparámetros para saber cómo afectan al modelo.

Introducción

En esta guía, veremos un modelo de aprendizaje no supervisado, conocido como Mapa autoorganizado (SOM), así como su implementación en Python. Usaremos un ejemplo de Color RGB para entrenar el SOM y demostrar su rendimiento y uso típico.

Mapas autoorganizados: una introducción general

Un Mapa autoorganizado fue introducido por primera vez por Teuvo Kohonen en 1982 y también se conoce a veces como Mapa de Kohonen. Es un tipo especial de red neuronal artificial, que construye un mapa de los datos de entrenamiento. El mapa es generalmente una cuadrícula rectangular 2D de pesos, pero se puede extender a un modelo dimensional 3D o superior. También son posibles otras estructuras de rejilla como rejillas hexagonales.

Un SOM se usa principalmente para la visualización de datos y proporciona un resumen visual rápido de las instancias de entrenamiento. En una cuadrícula rectangular 2D, cada celda está representada por un vector de peso. Para un SOM entrenado, cada peso de celda representa un resumen de algunos ejemplos de entrenamiento. Las celdas cercanas entre sí tienen pesos similares, y los ejemplos similares se pueden asignar a celdas en una pequeña vecindad entre sí.

La siguiente figura es una ilustración aproximada de la estructura del SOM:

self-organizing map illustration

Un SOM se entrena usando aprendizaje competitivo.

Aprendizaje competitivo es una forma de aprendizaje no supervisado, donde los elementos constitutivos compiten para producir un resultado satisfactorio, y solo uno gana la competencia.

Cuando se ingresa un ejemplo de entrenamiento en la cuadrícula, se determina la Mejor Unidad de Coincidencia (BMU) (ganador de la competencia). La BMU es la celda cuyos pesos están más cerca del ejemplo de entrenamiento.

A continuación, los pesos de la BMU y los pesos de las celdas vecinas a la BMU se adaptan para acercarse a la instancia de entrenamiento de entrada. Si bien existen otras variantes válidas para entrenar un SOM, en esta guía presentamos la implementación más popular y ampliamente utilizada del SOM.

Como usaremos algunas rutinas de Python para demostrar las funciones utilizadas para entrenar un SOM, importemos algunas de las bibliotecas que usaremos:

1
2
import numpy as np
import matplotlib.pyplot as plt

El algoritmo detrás de los mapas autoorganizados de entrenamiento {#el algoritmo detrás de los mapas autoorganizados de entrenamiento}

El algoritmo básico para entrenar un SOM se proporciona a continuación:

  • Inicializar todos los pesos de cuadrícula del SOM
  • Repetir hasta alcanzar convergencia o épocas máximas
    • Shuffle the training examples
    • For each training instance \(x\)
      • Find the best matching unit BMU
      • Update the weight vector of BMU and its neighboring cells

Los tres pasos para inicializar, encontrar la BMU y actualizar los pesos se explican en las siguientes secciones. ¡Vamos a empezar!

Inicializando el SOM GRID

Todos los pesos de la cuadrícula SOM se pueden inicializar aleatoriamente. Los pesos de la cuadrícula SOM también se pueden inicializar mediante ejemplos elegidos al azar del conjunto de datos de entrenamiento.

¿Cuál debería elegir?

Los SOM son sensibles al peso inicial del mapa, por lo que esta elección afecta el modelo general. Según Un caso de estudio realizado por Ayodeji y Evgeny de la Universidad de Leicester y la Universidad Federal de Siberia:

Al comparar la proporción del mapa SOM final de RI (inicialización aleatoria) que superó a PCI (inicialización de componentes principales) en las mismas condiciones, se observó que RI funcionó bastante bien para conjuntos de datos no lineales .

Sin embargo, para conjuntos de datos casi lineales, el resultado no es concluyente. En general, podemos concluir que la hipótesis sobre las ventajas del PCI es definitivamente incorrecta para conjuntos de datos esencialmente no lineales.

La inicialización aleatoria supera a la inicialización no aleatoria para conjuntos de datos no lineales. Para conjuntos de datos cuasi-lineales, no está muy claro qué enfoque gana consistentemente. Dados estos resultados, nos ceñiremos a la inicialización aleatoria.

Búsqueda de la mejor unidad coincidente (BMU)

Como se mencionó anteriormente, la mejor unidad de coincidencia es la celda de la cuadrícula SOM que está más cerca del ejemplo de entrenamiento \(x\). Un método para encontrar esta unidad es calcular la distancia euclidiana de \(x\) a partir del peso de cada celda de la cuadrícula.

La celda con la distancia mínima se puede elegir como BMU.

Un punto importante a tener en cuenta es que la distancia euclidiana no es el único método posible para seleccionar la BMU. También se puede usar una medida de distancia alternativa o una métrica de similitud para determinar la BMU, y elegir esto depende principalmente de los datos y el modelo que está construyendo específicamente.

Actualización del vector de peso de BMU y celdas vecinas {#actualización del vector de peso de bmu y celdas vecinas}

Un ejemplo de entrenamiento \(x\) afecta varias celdas de la cuadrícula SOM tirando de los pesos de estas celdas hacia ella. El cambio máximo ocurre en la BMU y la influencia de \(x\) disminuye a medida que nos alejamos de la BMU en la cuadrícula SOM. Para una celda con coordenadas \((i,j)\), su peso \(w_{ij}\) se actualiza en la época \(t+1\) como:

$$
w_{ij}^{(t+1)} \leftarrow w_{ij}^{(t)} + \Delta w_{ij}^{(t)}
$$

Donde \(\Delta w_{ij}^{(t)}\) es el cambio que se agregará a \(w_{ij}^{(t)}\). Se puede calcular como:

$$
\Delta w_{ij}^{(t)} = \eta^{(t)} f_{i,j}(g,h,\sigma_t) (x-w_{ij }^{(t)})
$$

Para esta expresión:

  • \(t\) es el número de época
  • \((g,h)\) son las coordenadas de BMU
  • \(\eta\) es la tasa de aprendizaje
  • \(\sigma_t\) es el radio
  • \(f_{ij}(g,h,\sigma_t)\) es la función de distancia de vecindad

En las siguientes secciones, presentaremos los detalles de esta expresión de entrenamiento con pesas.

La tasa de aprendizaje

La tasa de aprendizaje \(\eta\) es una constante en el rango [0,1] y determina el tamaño de paso del vector de peso hacia el ejemplo de entrenamiento de entrada. Para \(\eta=0\), no hay cambio en el peso, y cuando \(\eta=1\) el vector de peso \(w_{ij}\) toma el valor de \(x\).

\(\eta\) se mantiene alto al principio y decae a medida que avanzan las épocas. Una estrategia para reducir la tasa de aprendizaje durante la fase de entrenamiento es usar el decaimiento exponencial:

$$
\eta^{(t)} = \eta ^0 e^{-t*\lambda}
$$

Donde \(\lambda<0\) es la tasa de decaimiento.

Para comprender cómo cambia la tasa de aprendizaje con la tasa de disminución, representemos gráficamente la tasa de aprendizaje en varias épocas cuando la tasa de aprendizaje inicial se establece en uno:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
epochs = np.arange(0, 50)
lr_decay = [0.001, 0.1, 0.5, 0.99]
fig,ax = plt.subplots(nrows=1, ncols=4, figsize=(15,4))
plt_ind = np.arange(4) + 141
for decay, ind in zip(lr_decay, plt_ind):
    plt.subplot(ind)
    learn_rate = np.exp(-epochs * decay)
    plt.plot(epochs, learn_rate, c='cyan')
    plt.title('decay rate: ' + str(decay))
    plt.xlabel('epochs $t$')
    plt.ylabel('$\eta^(t)$')
fig.subplots_adjust(hspace=0.5, wspace=0.3)
plt.show()

learning rates for self organizing maps

La función de distancia del vecindario

La función de distancia de vecindad está dada por:

$$
f_{ij}(g,h,\sigma_t) = e^\frac{-d((i,j),(g,h))^2}{2\sigma_t^2 }
$$

donde \(d((i,j),(g,h))\) es la distancia de las coordenadas \((i,j)\) de una celda a las coordenadas de la UMB \( (g,h)\), y \(\sigma_t\) es el radio en la época \(t\). Normalmente, la distancia euclidiana se usa para calcular la distancia, sin embargo, se puede usar cualquier otra métrica de distancia o similitud.

Como la distancia de la BMU consigo misma es cero, el cambio de peso de la BMU se reduce a:

$$
\Delta w_{gh} = \eta (x-w_{gh})
$$

Para una unidad \((i,j)\) que tiene una gran distancia desde la UMB, la función de distancia de vecindad se reduce a un valor cercano a cero, lo que lleva a una magnitud muy pequeña de \(\Delta w_ {ij}\). Por lo tanto, dichas unidades no se ven afectadas por el ejemplo de entrenamiento \(x\). Un ejemplo de entrenamiento, por lo tanto, solo afecta la BMU y las celdas en las inmediaciones de la BMU. A medida que nos alejamos de la BMU, el cambio en los pesos se vuelve cada vez menor hasta que es insignificante.

El radio determina la región de influencia de un ejemplo de entrenamiento \(x\). Un valor de radio alto afecta a una mayor cantidad de celdas y un radio más pequeño afecta solo a la BMU. Una estrategia común es comenzar con un radio grande y reducirlo a medida que avanzan las épocas, es decir:

$$
\sigma_t = \sigma_0 e^{-t*\beta}
$$

Aquí \(\beta<0\) es la tasa de descomposición. La tasa de caída correspondiente al radio tiene el mismo efecto sobre el radio que la tasa de caída correspondiente a la tasa de aprendizaje. Para obtener una visión más profunda del comportamiento de la función de vecindad, representémosla frente a la distancia para diferentes valores del radio. Un punto a tener en cuenta en estos gráficos es que la función de distancia se aproxima a un valor cercano a cero cuando la distancia excede 10 para \(\sigma^2 \leq 10\).

Usaremos este hecho más adelante para hacer que el entrenamiento sea más eficiente en la parte de implementación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
distance = np.arange(0, 30)
sigma_sq = [0.1, 1, 10, 100]
fig,ax = plt.subplots(nrows=1, ncols=4, figsize=(15,4))
plt_ind = np.arange(4) + 141
for s, ind in zip(sigma_sq, plt_ind):
    plt.subplot(ind)
    f = np.exp(-distance ** 2 / 2 / s)
    plt.plot(distance, f, c='cyan')
    plt.title('$\sigma^2$ = ' + str(s))
    plt.xlabel('Distance')
    plt.ylabel('Neighborhood function $f$')
fig.subplots_adjust(hspace=0.5, wspace=0.3)
plt.show()

decay rate for self organizing maps

Implementación de un mapa autoorganizado en Python usando NumPy

Como no hay una rutina integrada para un SOM en la biblioteca de aprendizaje automático estándar de facto, Scikit-Learn, haremos una implementación rápida manualmente usando NumPy. El modelo de aprendizaje automático no supervisado es bastante sencillo y fácil de implementar.

Implementaremos el SOM como una cuadrícula mxn 2D, por lo que se requiere una matriz NumPy 3D. La tercera dimensión es necesaria para almacenar los pesos en cada celda:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# Return the (g,h) index of the BMU in the grid
def find_BMU(SOM,x):
    distSq = (np.square(SOM - x)).sum(axis=2)
    return np.unravel_index(np.argmin(distSq, axis=None), distSq.shape)
    
# Update the weights of the SOM cells when given a single training example
# and the model parameters along with BMU coordinates as a tuple
def update_weights(SOM, train_ex, learn_rate, radius_sq, 
                   BMU_coord, step=3):
    g, h = BMU_coord
    #if radius is close to zero then only BMU is changed
    if radius_sq < 1e-3:
        SOM[g,h,:] += learn_rate * (train_ex - SOM[g,h,:])
        return SOM
    # Change all cells in a small neighborhood of BMU
    for i in range(max(0, g-step), min(SOM.shape[0], g+step)):
        for j in range(max(0, h-step), min(SOM.shape[1], h+step)):
            dist_sq = np.square(i - g) + np.square(j - h)
            dist_func = np.exp(-dist_sq / 2 / radius_sq)
            SOM[i,j,:] += learn_rate * dist_func * (train_ex - SOM[i,j,:])   
    return SOM    

# Main routine for training an SOM. It requires an initialized SOM grid
# or a partially trained grid as parameter
def train_SOM(SOM, train_data, learn_rate = .1, radius_sq = 1, 
             lr_decay = .1, radius_decay = .1, epochs = 10):    
    learn_rate_0 = learn_rate
    radius_0 = radius_sq
    for epoch in np.arange(0, epochs):
        rand.shuffle(train_data)      
        for train_ex in train_data:
            g, h = find_BMU(SOM, train_ex)
            SOM = update_weights(SOM, train_ex, 
                                 learn_rate, radius_sq, (g,h))
        # Update learning rate and radius
        learn_rate = learn_rate_0 * np.exp(-epoch * lr_decay)
        radius_sq = radius_0 * np.exp(-epoch * radius_decay)            
    return SOM

Analicemos las funciones clave utilizadas para implementar un mapa autoorganizado:

find_BMU() devuelve las coordenadas de la celda de la cuadrícula de la mejor unidad coincidente cuando se le proporciona la cuadrícula SOM y un ejemplo de entrenamiento x. Calcula el cuadrado de la distancia euclidiana entre el peso de cada celda y x, y devuelve (g,h), es decir, la celda se coordina con la distancia mínima.

La función update_weights() requiere una cuadrícula SOM, un ejemplo de entrenamiento x, los parámetros learn_rate y radius_sq, las coordenadas de la mejor unidad coincidente y un parámetro step. Teóricamente, todas las celdas del SOM se actualizan en el siguiente ejemplo de entrenamiento. Sin embargo, mostramos anteriormente que el cambio es insignificante para las celdas que están lejos de la UMB. Por lo tanto, podemos hacer que el código sea más eficiente cambiando solo las celdas en una pequeña vecindad de la BMU. El parámetro paso especifica el número máximo de celdas a la izquierda, derecha, arriba y abajo para cambiar al actualizar los pesos.

Finalmente, la función train_SOM() implementa el principal procedimiento de entrenamiento de un SOM. Requiere una grilla SOM inicializada o parcialmente entrenada y train_data como parámetros. La ventaja es poder entrenar el SOM desde una etapa de entrenamiento anterior. Además, se requieren los parámetros learn_rate y radius_sq junto con sus correspondientes tasas de decaimiento lr_decay y radius_decay. El parámetro epochs se establece en 10 de forma predeterminada, pero se puede cambiar si es necesario.

Ejecutar el mapa autoorganizado en un ejemplo práctico {#ejecutar el mapa autoorganizado en un ejemplo práctico}

Uno de los ejemplos comúnmente citados para entrenar un SOM es el de colores aleatorios. Podemos entrenar una cuadrícula SOM y visualizar fácilmente cómo se organizan varios colores similares en las celdas vecinas.

Las celdas alejadas unas de otras tienen colores diferentes.

Ejecutemos la función train_SOM() en una matriz de datos de entrenamiento llena de colores RGB aleatorios.

El siguiente código inicializa una matriz de datos de entrenamiento y una cuadrícula SOM con colores RGB aleatorios. También muestra los datos de entrenamiento y la cuadrícula SOM inicializada aleatoriamente. Tenga en cuenta que la matriz de entrenamiento es una matriz de 3000x3; sin embargo, la hemos remodelado a una matriz de 50x60x3 para su visualización:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Dimensions of the SOM grid
m = 10
n = 10
# Number of training examples
n_x = 3000
rand = np.random.RandomState(0)
# Initialize the training data
train_data = rand.randint(0, 255, (n_x, 3))
# Initialize the SOM randomly
SOM = rand.randint(0, 255, (m, n, 3)).astype(float)
# Display both the training matrix and the SOM grid
fig, ax = plt.subplots(
    nrows=1, ncols=2, figsize=(12, 3.5), 
    subplot_kw=dict(xticks=[], yticks=[]))
ax[0].imshow(train_data.reshape(50, 60, 3))
ax[0].title.set_text('Training Data')
ax[1].imshow(SOM.astype(int))
ax[1].title.set_text('Randomly Initialized SOM Grid')

mapa de autoorganización inicializado aleatoriamente

Entrenemos ahora el SOM y revisémoslo cada 5 épocas como una descripción general rápida de su progreso:

1
2
3
4
5
6
7
8
9
fig, ax = plt.subplots(
    nrows=1, ncols=4, figsize=(15, 3.5), 
    subplot_kw=dict(xticks=[], yticks=[]))
total_epochs = 0
for epochs, i in zip([1, 4, 5, 10], range(0,4)):
    total_epochs += epochs
    SOM = train_SOM(SOM, train_data, epochs=epochs)
    ax[i].imshow(SOM.astype(int))
    ax[i].title.set_text('Epochs = ' + str(total_epochs))

self organizing map training and results

El ejemplo anterior es muy interesante ya que muestra cómo la cuadrícula organiza automáticamente los colores RGB para que varios tonos del mismo color estén juntos en la cuadrícula SOM. El arreglo se lleva a cabo ya en la primera época, pero no es ideal. Podemos ver que el SOM converge en alrededor de 10 épocas y hay menos cambios en las épocas posteriores.

Efecto de la tasa de aprendizaje y el radio

Para ver cómo varía la tasa de aprendizaje para diferentes tasas y radios de aprendizaje, podemos ejecutar el SOM durante 10 épocas al comenzar desde la misma cuadrícula inicial. El siguiente código entrena el SOM para tres valores diferentes de la tasa de aprendizaje y tres radios diferentes.

El SOM se representa después de 5 épocas para cada simulación:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
fig, ax = plt.subplots(
    nrows=3, ncols=3, figsize=(15, 15), 
    subplot_kw=dict(xticks=[], yticks=[]))

# Initialize the SOM randomly to the same state

for learn_rate, i in zip([0.001, 0.5, 0.99], [0, 1, 2]):
    for radius_sq, j in zip([0.01, 1, 10], [0, 1, 2]):
        rand = np.random.RandomState(0)
        SOM = rand.randint(0, 255, (m, n, 3)).astype(float)        
        SOM = train_SOM(SOM, train_data, epochs = 5,
                        learn_rate = learn_rate, 
                        radius_sq = radius_sq)
        ax[i][j].imshow(SOM.astype(int))
        ax[i][j].title.set_text('$\eta$ = ' + str(learn_rate) + 
                                ', $\sigma^2$ = ' + str(radius_sq))
                                

effects and tuning self organizing map hyperparameters

El ejemplo anterior muestra que para valores de radio cercanos a cero (primera columna), el SOM solo cambia las celdas individuales pero no las celdas vecinas. Por lo tanto, no se crea un mapa adecuado independientemente de la tasa de aprendizaje. También se encuentra un caso similar para tasas de aprendizaje más pequeñas (primera fila, segunda columna). Al igual que con cualquier otro algoritmo de aprendizaje automático, se requiere un buen equilibrio de parámetros para un entrenamiento ideal.

Yendo más lejos: proyecto de extremo a extremo portátil

¿Tu naturaleza inquisitiva te hace querer ir más allá? Recomendamos consultar nuestro Proyecto guiado: ["Predicción práctica del precio de la vivienda: aprendizaje automático en Python"](https://wikihtp.com/courses/hands-on-house- precio-predicción-aprendizaje-máquina-en-python/#cta){target="_blank"}.

[](https://wikihtp.com/ cursos/predicción-de-precio-de-la-casa-práctica-aprendizaje-de-máquina-en-python/#cta)

En este proyecto guiado, aprenderá a crear potentes modelos tradicionales de aprendizaje automático, así como modelos de aprendizaje profundo, utilizar Ensemble Learning y capacitar a los meta-aprendices para predecir los precios de la vivienda a partir de una bolsa de modelos Scikit-Learn y Keras.

Con Keras, la API de aprendizaje profundo creada sobre Tensorflow, experimentaremos con arquitecturas, crearemos un conjunto de modelos apilados y entrenaremos una red neuronal meta-aprendizaje (modelo de nivel 1) para averiguar el precio de un casa.

El aprendizaje profundo es sorprendente, pero antes de recurrir a él, se recomienda intentar resolver el problema con técnicas más simples, como los algoritmos de aprendizaje superficial. Nuestro rendimiento de referencia se basará en un algoritmo de Regresión de bosque aleatorio. Además, exploraremos la creación de conjuntos de modelos a través de Scikit-Learn a través de técnicas como embalaje y votación.

Este es un proyecto integral y, como todos los proyectos de aprendizaje automático, comenzaremos con Análisis exploratorio de datos, seguido de Preprocesamiento de datos y, finalmente, Creación de modelos de aprendizaje superficial y Profundo para ajustarse a los datos que hemos explorado y limpiado previamente.

Conclusiones

En esta guía, discutimos el modelo teórico de un SOM y su implementación detallada. Demostramos el SOM en colores RGB y mostramos cómo diferentes tonos del mismo color se organizan en una cuadrícula 2D.

Si bien los SOM ya no son muy populares en la comunidad de aprendizaje automático, siguen siendo un buen modelo para el resumen y la visualización de datos.