Implementación de Algoritmos de Aprendizaje Profundo en CS231n

Índice

1-1 K-NN

  1. Implementación con bucles anidados para calcular distancias entre pares de muestras
  2. Función de predicción de etiquetas basada en matriz de distancias
  3. Optimización con bucle simple + vectorización parcial para cálculo de matriz de distancias
  4. Implementación completamente vectorizada para cálculo de matriz de distancias
  5. Determinación óptima del hiperparámetro k mediante validación cruzada

1-2 Softmax

Clasificador Softmax

  1. Verificación de la implementación básica de la función de pérdida Softmax
  2. Derivación de la fórmula del gradiente de la función de pérdida Softmax
  3. Optimización de la implementación básica para el cálculo de la pérdida
  4. Implemantación vectorizada del cálculo del gradiente

SGD

  1. Implementación del algoritmo SGD en la función de entrenamiento
  2. Implementación de la función de predicción para etiquetas
  3. Ajuste de hiperparámetros usando conjunto de validación

1-3 Red Neuronal de Dos Capas

  1. Implementación de la propagación hacia adelante en capas completamente conectadas
  2. Función de retropropagación para capas afines
  3. Implementación de la función de activación ReLU hacia adelante
  4. Implementación de la función de activación ReLU hacia atrás
  5. Función de pérdida softmax con cálculo de pérdida y gradiente
  6. Implementación de red neuronal de dos capas
  7. Entrenamiento de TwoLayerNet usando Solver
  8. Ajuste de hiperparámetros para mejorar precisión

1-4 Representaciones de Alto Nivel: Características de Imágenes

  1. Entrenamiento de clasificador Softmax en características
  2. Entrenamiento de red neuronal en características de imagen

1-5 Entrenamiento de Red Neuronal Completamente Conectada

Red neuronal multicapa

  1. Completar la clase FullyConnectedNet en cs231n/classifiers/fc_net.py

Verificación inicial de pérdida y gradiente

  1. Ajuste de learning rate y escala de inicialización de pesos para alcanzar 100% de precisión en 20 épocas
  2. Ajuste de learning rate y escala de inicialización de pesos para red de cinco capas

Reglas de actualización de parámetros

  1. Implementación de reglas de actualización SGD + Momentum & RMSProp & Adam

1-1 K-NN

En los archivos knn.ipynb y k_nearest_neighbor.py, implementar un clasificador k-NN.

1. Usando bucles anidados, calcular distancias entre todos los pares de muestras (prueba, entrenamiento)
# k_nearest_neighbor.py
def compute_distances_two_loops(self, X):
    """
    Calcular distancia entre cada punto de prueba y cada punto de entrenamiento usando dos bucles.
    Entrada:
    - X: Datos de prueba, (num_test, D)
    Retorna:
    - dists: (num_test, num_train), dists[i, j]: distancia euclidiana entre el punto de prueba i y el punto de entrenamiento j
    """
    num_test = X.shape[0]
    num_train = self.X_train.shape[0]
    dists = np.zeros((num_test, num_train))
    for i in range(num_test):
        for j in range(num_train):
            # Implementación:
            diferencia_cuadrada = np.square(X[i] - self.X_train[j])
            dists[i, j] = np.sqrt(np.sum(diferencia_cuadrada))
            
    return dists

Pregunta en línea 1:

2. Implementar función predict_labels para predecir etiquetas de prueba basadas en matriz de distancias
# k_nearest_neighbor.py
def predict_labels(self, dists, k=1):
    """
    Dada una matriz de distancias, predecir etiquetas para cada punto de prueba
    Entradas:
    - dists: (num_test, num_train), dists[i,j] es la distancia entre punto de prueba i y punto de entrenamiento j
    Retorna:
    - y: (num_test,) y[i] es la etiqueta predicha para x[i]
    """
    num_test = dists.shape[0]
    y_pred = np.zeros(num_test)
    for i in range(num_test):
        # Lista de longitud k con las etiquetas de los k vecinos más cercanos
        # al punto de prueba i.
        etiquetas_cercanas = []
        # Implementación:
        # Encontrar los k vecinos más cercanos del punto de prueba i
        indices_ordenados = np.argsort(dists[i])[:k]
        etiquetas_cercanas = self.y_train[indices_ordenados]
        # Contar y encontrar la etiqueta más frecuente
        conteo = np.bincount(etiquetas_cercanas)
        y_pred[i] = np.argmax(conteo)

    return y_pred

Pregunta en línea 2: Para un clasificador k-NN usando distancia L1, ¿qué pasos de preprocesamiento no cambiaría su rendimiento?

Respuesta 2:

3. Optimización con bucle simple + vectorización parcial para cálculo de matriz de distancias
# k_nearest_neighbor.py
def compute_distances_one_loop(self, X):
    """
    Calcular distancia entre cada punto de prueba y cada punto de entrenamiento usando un bucle.
    """
    num_test = X.shape[0]
    num_train = self.X_train.shape[0]
    dists = np.zeros((num_test, num_train))
    for i in range(num_test):
        # Implementación:                                                         
        # Calcular distancia L2 entre punto de prueba i y todos los puntos de entrenamiento
        diferencia = X[i] - self.X_train
        dists[i] = np.sqrt(np.sum(diferencia**2, axis=1))
    return dists

4. Implementación completamente vectorizada para cálculo de matriz de distancias
# k_nearest_neighbor.py
def compute_distances_no_loops(self, X):
    """
    Calcular distancia entre cada punto de prueba y cada punto de entrenamiento sin bucles explícitos.
    Usando la fórmula de diferencia de cuadrados completa
    """
    num_test = X.shape[0]
    num_train = self.X_train.shape[0]
    dists = np.zeros((num_test, num_train))
    # Implementación:
    termino1 = np.sum(X**2, axis=1)  # (500,)
    termino1 = termino1.reshape(-1, 1)  # Convertir a matriz 2D (500,1)
    termino2 = np.sum(self.X_train**2, axis=1)  # (5000,)
    termino2 = termino2.reshape(1, -1)  # (1,5000) para broadcasting
    producto = np.dot(X, self.X_train.T)  # (500,5000)
    dists = np.sqrt(termino1 + termino2 - 2*producto)
    return dists

5. Implementar validación cruzada para determinar el valor óptimo del hiperparámetro k
# knn.ipynb 
num_folds = 5
opciones_k = [1, 3, 5, 8, 10, 12, 15, 20, 50, 100]

# Almacenar num_folds listas, cada una con los datos para ese pliegue
pliegues_X = []
pliegues_y = []
# Implementación:
# Dividir datos de entrenamiento en num_folds partes
pliegues_X = np.array_split(X_train, num_folds)
pliegues_y = np.array_split(y_train, num_folds)

# Diccionario: clave = valores de k; valor = lista de longitud num_folds con precisiones
k_a_precisiones = {}
# Implementación:
# Ejecutar validación cruzada k-pliegues
for k in opciones_k:
    k_a_precisiones[k] = []  # Lista de longitud num_folds
    for i in range(num_folds):
        x_val = pliegues_X[i]
        y_val = pliegues_y[i]
        num_val = x_val.shape[0]

        x_entruzado = np.concatenate([pliegues_X[j] for j in range(num_folds) if j != i])
        y_entruzado = np.concatenate([pliegues_y[j] for j in range(num_folds) if j != i])

        clasificador.entrenar(x_entruzado, y_entruzado)
        y_pred = clasificador.predecir(x_val, k=k)
        precision = float(np.sum(y_pred == y_val) / num_val)

        k_a_precisiones[k].append(precision)

Pregunta en línea 3:

1-2 Softmax

Clasificador Softmax

El código para esta parte está en cs231n/classifiers/softmax.py

1. Probar la implementación básica de la función de pérdida Softmax
def softmax_loss_naive(W, X, y, reg):
    """
    Implementación básica de la función de pérdida Softmax (con bucles)
    Dimensiones: D características, C clases, N muestras
    Entrada:
    - W: pesos (D, C)
    - X: datos (N, D)
    - y: etiquetas (N,)
    - reg: fuerza de regularización

    Retorna: una tupla
    - pérdida
    - gradiente de W: (D, C)
    """
    # Inicializar pérdida y gradiente en cero
    loss = 0.0
    dW = np.zeros_like(W)
    # Calcular pérdida y gradiente
    num_clases = W.shape[1]
    num_muestras = X.shape[0]
    for i in range(num_muestras):
        puntuaciones = X[i].dot(W)  # Calcular puntuaciones
        # Calcular probabilidades de forma numéricamente estable
        puntuaciones -= np.max(puntuaciones)
        probs = np.exp(puntuaciones)
        probs /= probs.sum()  # Normalizar
        log_probs = np.log(probs)

        loss -= log_probs[y[i]]  # Pérdida de negativa log probabilidad
    # Pérdida de margen normalizada más término de regularización
    loss = loss / num_muestras + reg * np.sum(W * W)
    # TODO: calcular gradiente y guardar en dW
    return loss, dW

Pregunta en línea 1:

2. Derivar la fórmula del gradiente de la función de pérdida Softmax e implementarla

Derivación: Los detalles no están completamente claros

Implementación:

def softmax_loss_naive(W, X, y, reg):
    # Inicializar pérdida y gradiente en cero
    loss = 0.0
    dW = np.zeros_like(W)
    # Calcular pérdida y gradiente
    num_clases = W.shape[1]
    num_muestras = X.shape[0]
    for i in range(num_muestras):
        puntuaciones = X[i].dot(W)  # Calcular puntuaciones
        # Calcular probabilidades de forma numéricamente estable
        puntuaciones -= np.max(puntuaciones)
        probs = np.exp(puntuaciones)
        probs /= probs.sum()  # Normalizar
        log_probs = np.log(probs)
        loss -= log_probs[y[i]]  # Pérdida de negativa log probabilidad
        # Implementación:
        probs[y[i]] -= 1
        for j in range(num_clases):  # 0-9
            dW[:, j] += X[i] * probs[j]  # (1, 3026)

    # Pérdida de margen normalizada más término de regularización
    loss = loss / num_muestras + reg * np.sum(W * W)
    # Implementación: 
    dW = dW/num_muestras + 2*reg*W

    return loss, dW

3. Optimizar la implementación básica para el cálculo de la pérdida en softmax_loss_vectorized
def softmax_loss_vectorized(W, X, y, reg):
    """
    Función de pérdida Softmax, versión vectorizada.
    """
    # Inicializar pérdida y gradiente en cero
    loss = 0.0
    dW = np.zeros_like(W)

    # Implementación:
    # Cálculo vectorizado de la pérdida
    puntuaciones = X.dot(W)  # (500,10)
    puntuaciones -= np.max(puntuaciones, axis=1, keepdims=True)

    probs = np.exp(puntuaciones)
    probs /= probs.sum(axis=1, keepdims=True)  # (500,10)
    # (500,1) probabilidades de las clases correctas
    probs_clase_correcta = probs[np.arange(num_muestras), y] 
    loss = -np.sum(np.log(probs_clase_correcta))
    loss = loss/num_muestras + reg*np.sum(W**2)

    #############################################################################
    # TODO:                                                                     #
    # Implementar la versión vectorizada del gradiente de la pérdida Softmax,    #
    # guardando el resultado en dW.                                             #
    # Sugerencia: reutilizar algunos valores intermedios calculados para la     #
    # pérdida.                                                                  #
    #############################################################################
    return loss, dW

4. Implementar el cálculo vectorizado del gradiente
def softmax_loss_vectorized(W, X, y, reg):
    """
    Función de pérdida Softmax, versión vectorizada.
    """
    # Inicializar pérdida y gradiente en cero
    loss = 0.0
    dW = np.zeros_like(W)

    # Cálculo vectorizado de la pérdida
    puntuaciones = X.dot(W)  # (500,10)
    puntuaciones -= np.max(puntuaciones, axis=1, keepdims=True)

    probs = np.exp(puntuaciones)
    probs /= probs.sum(axis=1, keepdims=True)  # (500,10)
    # (500,1) probabilidades de las clases correctas
    probs_clase_correcta = probs[np.arange(num_muestras), y] 
    loss = -np.sum(np.log(probs_clase_correcta))
    loss = loss/num_muestras + reg*np.sum(W**2)

    # Implementación:
    probs[np.arange(num_muestras), y] -= 1
    dW = np.dot(X.T, probs)/num_muestras + 2*reg*W

    return loss, dW

SGD

El código para esta parte está en cs231n/classifiers/linear_classifier.py.

Implementaremos: usar SGD para minimizar la función de pérdida.

5. Implementar SGD en la función train
def entrenar(
        self,
        X,
        y,
        learning_rate=1e-3,
        reg=1e-5,
        num_iters=100,
        batch_size=200,
        verbose=False,
    ):
        """
        Entrenar un clasificador lineal usando descenso de gradiente estocástico (SGD).

        Entrada:
        - X: conjunto de entrenamiento
        - y: etiquetas
        - learning_rate: tasa de aprendizaje
        - reg: fuerza de regularización
        - num_iters: número de iteraciones
        - batch_size: número de muestras de entrenamiento por iteración

        Salida:
        lista: valores de pérdida en cada iteración de entrenamiento
        """
        num_muestras, dim = X.shape
        num_clases = (np.max(y) + 1)  
        if self.W is None:
            # Inicialización perezosa de W
            self.W = 0.001 * np.random.randn(dim, num_clases)
        # Optimizar W usando SGD
        historial_perdida = []
        for it in range(num_iters):
            # Almacenar datos muestreados aleatoriamente de tamaño batch_size
            X_lote = None  # (batch_size, dim)
            y_lote = None  # (batch_size,)
            # Implementación:
            # Muestreo aleatorio
            indices = np.random.choice(num_muestras, batch_size, replace=True)
            X_lote = X[indices]
            y_lote = y[indices]
            # Calcular pérdida y gradiente
            perdida, grad = self.perdida(X_lote, y_lote, reg)
            historial_perdida.append(perdida)
            # Actualizar pesos usando gradiente y tasa de aprendizaje
            self.W -= learning_rate * grad

            if verbose and it % 100 == 0:
                print("iteración %d / %d: pérdida %f" % (it, num_iters, perdida))

        return historial_perdida

6. Implementar la función predict para generar etiquetas predichas
def predecir(self, X):
    """
    Predecir etiquetas de puntos de datos usando pesos entrenados.
    Entrada:
    - X: (N, D)
    Retorna:
    - y_pred: etiquetas predichas (N,)
    """
    y_pred = np.zeros(X.shape[0])
    # Implementación:
    resultado = X.dot(self.W)  # (N,10)
    resultado = resultado - resultado.max(axis=1, keepdims=True)
    probs = np.exp(resultado)
    probs /= probs.sum(axis=1, keepdims=True)
    y_pred = probs.argmax(axis=1)

    return y_pred

7. Usar conjunto de validación para ajustar hiperparámetros (fuerza de regularización + tasa de aprendizaje)

Esta parte se completa en softmax.ipynb

# clave:(tasa_aprendizaje, fuerza_regularizacion) 
# valor:(precision_entrenamiento, precision_validacion)
resultados = {}  # almacenar todos los pares de precisiones
mejor_val = -1        # mayor precisión en validación
mejor_softmax = None  # objeto Softmax que alcanza mejor_val

tasas_aprendizaje = [1e-7, 1e-6]
fuerzas_regularizacion = [2.5e4, 1e4]
# Implementación:
# Entrenar un clasificador Softmax para cada par de hiperparámetros,
# calcular su precisión en entrenamiento y validación, y guardar la mejor precisión de validación
for lr in tasas_aprendizaje:
    for rs in fuerzas_regularizacion:
        softmax = Softmax()
        historial_perdida = softmax.entrenar(X_train, y_train, learning_rate=lr, reg=rs, 
                                          num_iters=1500, batch_size=200, verbose=True)
        y_pred_ent = softmax.predecir(X_train)
        train_acc = np.mean(y_pred_ent == y_train)

        y_pred_val = softmax.predecir(X_val)
        val_acc = np.mean(y_pred_val == y_val)

        resultados[(lr, rs)] = (train_acc, val_acc)

        if val_acc > mejor_val:
            mejor_val = val_acc
            mejor_softmax = softmax

1-3 Red Neuronal de Dos Capas

El código para esta parte está en cs231n/classifiers/two_layer_net.ipynb

1. Implementar propagación hacia adelante en capas completamente conectadas

Abrir el archivo cs231n/layers.py e implementar la función affine_forward.

def affine_forward(x, w, b):
    """
    Calcular propagación hacia adelante en capa completamente conectada
    D = d_1 * ... * d_k
    Entradas:
    - x: Arreglo numpy con datos de entrada, de forma (N, d_1, ..., d_k)
    - w: Arreglo numpy de pesos, de forma (D, M)
    - b: Arreglo numpy de sesgos, de forma (M,)

    Retorna una tupla de:
    - out: salida, de forma (N, M)
    - cache: (x, w, b)
    """
    out = None
    # Implementación: remodelar x a vector fila, calcular propagación hacia adelante,
    # almacenar resultado en out
    # No modificar x directamente, se necesita para el cache
    out = x.reshape(x.shape[0], -1) @ w + b  # (N,M)
    # Fin
    cache = (x, w, b)
    return out, cache

2. Implementar función de retropropagación affine_backward
# layers.py
def affine_backward(dout, cache):
    """
    Calcular retropropagación
    Entradas:
    - dout: gradiente ascendente (N, M)
    - cache: Tupla de:
      - x: Datos de entrada, de forma (N, d_1, ... d_k)
      - w: Pesos, de forma (D, M)
      - b: Sesgos, de forma (M,)

    Retorna una tupla de:
    - dx: Gradiente con respecto a x, de forma (N, d1, ..., d_k)
    - dw: Gradiente con respecto a w, de forma (D, M)
    - db: Gradiente con respecto a b, de forma (M,)
    """
    x, w, b = cache
    dx, dw, db = None, None, None
    # Implementación:
    dx = dout @ w.T  # (N,D)
    dx = dx.reshape(x.shape)

    x = x.reshape(x.shape[0], -1)
    dw = x.T @ dout  # (D,M)
    # Aplicar regla de la suma para descomponer dL en dL_1+dL_2+...+dL_{n-1}
    db = np.sum(dout, axis=0)  # (M,)
    # Fin                        
    return dx, dw, db

3. Implementar propagación hacia adelante de la función de activación ReLU
# layers.py
def relu_forward(x):
    """
    Calcular propagación hacia adelante de ReLUs
    Entrada:
    - x: Entradas, de cualquier forma

    Retorna una tupla de:
    - out: Salida, de la misma forma que x
    - cache: x
    """
    out = None
    # Implementación:
    out = np.maximum(x, 0)
    # Fin
    cache = x
    return out, cache

4. Implementar retropropagación de la función de activación ReLU
# layers.py
def relu_backward(dout, cache):
    """
    Retropropagación de ReLU
    Entrada:
    - dout: Derivadas ascendentes, de cualquier forma
    - cache: Entrada x, de misma forma que dout

    Retorna:
    - dx: Gradiente con respecto a x
    """
    dx, x = None, cache
    # Implementación:
    # y=max(0,x), solo cuando x>0, dy/dx=1
    dx = dout * (x > 0)
    # Fin
    return dx

Pregunta en línea 1:

5. Implementar función softmax_loss para calcular pérdida y gradiente
def softmax_loss(x, y):
    """
    Calcular pérdida y gradiente para clasificación softmax.
    Entradas:
    - x: Datos de entrada, de forma (N, C) donde x[i, j] es la puntuación
      para la clase j en la entrada i.
    - y: Vector de etiquetas, de forma (N,) donde y[i] es la etiqueta para x[i] y 0 <= y[i] < C

    Retorna una tupla de:
    - loss: Escalar con el valor de la pérdida
    - dx: Gradiente de la pérdida con respecto a x
    """
    loss, dx = None, None
    # Implementación:
    N = x.shape[0]
    x_desplazado = x - np.max(x, axis=1, keepdims=True)  # Evitar desbordamiento en exponencial
    # 1. Calcular probabilidades
    exponentes = np.exp(x_desplazado)
    suma_exp = np.sum(exponentes, axis=1, keepdims=True)  # denominador (N,)
    probs = exponentes / suma_exp  # probabilidades (N,C)
    # 2. Calcular pérdida
    probs_correctas = probs[np.arange(N), y]  # (N,) probabilidades de clases correctas
    loss = -np.sum(np.log(probs_correctas)) / N
    # 3. Calcular gradiente
    dx = probs.copy()
    dx[np.arange(N), y] -= 1
    dx /= N  # dx también debe ser normalizado, igual que la pérdida
    # Fin

    return loss, dx

6. Implementar red neuronal de dos capas

En el archivo cs231n/classifiers/fc_net.py, implementar la clase TwoLayerNet.

class TwoLayerNet(object):
    """
    Clase de red neuronal de dos capas completamente conectadas.
    Función de activación: ReLU; Función de pérdida: softmax
    Dimensiones de entrada D, dimensión de capa oculta H, clasificación en C clases
    Arquitectura: affine - relu - affine - softmax.
    Esta clase no implementa descenso de gradiente, interactuará con un objeto Solver
    separado que se encarga de la optimización.
    Los parámetros aprendidos se almacenan en self.params, un diccionario que
    asigna nombres de parámetros a arreglos NumPy.
    """

    def __init__(
        self,
        input_dim=3 * 32 * 32,
        hidden_dim=100,
        num_classes=10,
        weight_scale=1e-3,
        reg=0.0,
    ):
        """
        Inicializar una nueva red.
        Entradas:
        - input_dim: Entero dando el tamaño de la entrada
        - hidden_dim: Entero dando el tamaño de la capa oculta
        - num_classes: Entero dando el número de clases para clasificar
        - weight_scale: Escalar dando la desviación estándar para la
          inicialización aleatoria de los pesos.
        - reg: Escalar dando la fuerza de regularización L2.
        """
        self.params = {}
        self.reg = reg
        # Implementación:
        # Inicializar pesos: distribución gaussiana con media 0 y desviación estándar weight_scale
        # Inicializar sesgos en 0
        self.params['W1'] = weight_scale * np.random.randn(input_dim, hidden_dim)
        self.params['b1'] = np.zeros(hidden_dim)

        self.params['W2'] = weight_scale * np.random.randn(hidden_dim, num_classes)
        self.params['b2'] = np.zeros(num_classes)
        # Fin

    def loss(self, X, y=None):
        """
        Calcular pérdida y gradiente
        Entradas:
        - X: Arreglo de datos de entrada de forma (N, d_1, ..., d_k)
        - y: Arreglo de etiquetas, de forma (N,). y[i] da la etiqueta para X[i].
        Retorna:
        Si y es None, entonces ejecutar propagación hacia adelante en modo de prueba y retornar:
        - scores: Arreglo de forma (N, C) dando puntuaciones de clasificación, donde
          scores[i, c] es la puntuación de clasificación para X[i] y clase c.

        Si y no es None, entonces ejecutar propagación hacia adelante y hacia atrás
        en modo de entrenamiento y retornar una tupla de:
        - loss: Valor escalar de la pérdida
        - grads: Diccionario con las mismas claves que self.params, mapeando nombres de parámetros
          a gradientes de la pérdida con respecto a esos parámetros.
        """
        scores = None
        # Implementación:

        # Implementar propagación hacia adelante en red de dos capas, calcular puntuaciones, guardar en scores
        W1, b1, W2, b2 = self.params['W1'], self.params['b1'], self.params['W2'], self.params['b2']
        # cache = (fc_cache, relu_cache)
        salida1, cache1 = affine_relu_forward(X, W1, b1)
        scores, cache2 = affine_forward(salida1, W2, b2)  # última capa sin ReLU

        # Si y es None, estamos en modo de prueba, solo retornar puntuaciones
        if y is None:
            return scores

        loss, grads = 0, {}
        
        # Retropropagación, guardar pérdida loss, gradientes grads; usar softmax para calcular pérdida
        loss, dy = softmax_loss(scores, y)
        dx2, dw2, db2 = affine_backward(dy, cache2)
        dx1, dw1, db1 = affine_relu_backward(dx2, cache1)
        
        # Regularización L2 (requiere factor 0.5 para facilitar cálculo de gradiente)
        loss = loss + self.reg * 0.5 * (np.sum(W1**2) + np.sum(W2**2))
        
        # Agregar gradientes de términos de regularización L2
        dw2 += self.reg * W2
        dw1 += self.reg * W1

        grads['W1'] = dw1
        grads['b1'] = db1
        grads['W2'] = dw2
        grads['b2'] = db2

        #Fin
        return loss, grads

    def guardar(self, fname):
      """Guardar parámetros del modelo."""
      ...
    
    def cargar(self, fname):
      """Cargar parámetros del modelo."""
      ...

7. Entrenar un TwoLayerNet usando una instancia de Solver

El solver está implementado en cs231n/solver.py; en two_layer_net.ipynb se implementa su llamada

tamano_entrada = 32 * 32 * 3
tamano_oculto = 50
num_clases = 10
modelo = TwoLayerNet(tamano_entrada, tamano_oculto, num_clases)
solver = None

#Implementación:
solver = Solver(modelo, datos,
        regla_actualizacion='sgd',
        optim_config={
          'learning_rate': 1e-4,
        },
        decaimiento_lr=0.95,
        num_epocas=5, tamano_lote=200,
        imprimir_cada=100)
solver.entrenar()
#Fin

8. Ajustar hiperparámetros para mejorar precisión de predicción

Probar diferentes combinaciones de hiperparámetros, incluyendo:

  • Tamaño de capa oculta
  • Tasa de aprendizaje
  • Número de épocas de entrenamiento
  • Fuerza de regularización
mejor_modelo = None
# Ajustar parámetros para encontrar óptimos, guardar mejor modelo en mejor_modelo

# Buscar tasa de aprendizaje y fuerza de regularización
tasas_aprendizaje = [1e-4, 1e-3, 1e-2]
fuerzas_reg = [1e-3, 0.01, 0.1]
tamano_oculto = [100, 150, 200]
num_epocas = [5, 10, 20]

# Almacenar mejores resultados
mejor_val_acc = -1
mejor_modelo = None
mejor_solver = None
resultados = {}  # almacenar todos


# Búsqueda en cuadrícula
for lr in tasas_aprendizaje:
    for reg in fuerzas_reg:
      for hs in tamano_oculto:
        for ep in num_epocas:
          modelo = TwoLayerNet(tamano_entrada, hs, num_clases, reg=reg)
          solver = Solver(modelo, datos,
                    regla_actualizacion='sgd',
                    optim_config={'learning_rate': lr},
                    decaimiento_lr=0.95,
                    num_epocas=ep,
                    tamano_lote=200,
                    verbose=False)

          solver.entrenar()

          val_acc = solver.mejor_val_acc
          resultados[(lr, reg, hs, ep)] = val_acc

          if val_acc > mejor_val_acc:
              mejor_val_acc = val_acc
              mejor_modelo = modelo
              mejor_solver = solver

print("\n" + "="*60)
print(f"Mejor precisión en validación: {mejor_val_acc:.4f}")
print("Mejor combinación de hiperparámetros:")
print(f"  - Tasa de aprendizaje: {mejor_solver.optim_config['learning_rate']:.3e}")
print(f"  - Fuerza de regularización: {mejor_modelo.reg:.3e}")
print(f"  - Tamaño de capa oculta: {mejor_solver.modelo.params['W1'].shape[1]}")
print(f"  - Número de épocas: {mejor_solver.num_epocas}")

Mejor precisión en validación: 0.5420 Mejor combinación de hiperparámetros:

  • Tasa de aprendizaje: 1.000e-03
  • Fuerza de regularización: 1.000e-03
  • Tamaño de capa oculta: 150
  • Número de épocas: 20

Pregunta en línea 2:

1-4 Representaciones de Alto Nivel: Características de Imagen

Esta parte se completa en features.ipynb. Anteriormente trabajábamos directamente con píxeles originales; aquí extraemos dos tipos de características para cada imagen:

  1. HOG (Histograma de Gradientes Orientados)
  2. Histograma del canal Hue en espacio de color HSV

Concatanar ambos vectores de características para formar el vector de características final de la imagen, para entrenamiento posterior

1. Entrenar clasificador Softmax en características
# Usar el conjunto de validación para ajustar la tasa de aprendizaje y fuerza de regularización

from cs231n.classifiers.linear_classifier import Softmax

tasas_aprendizaje = [1e-9, 1e-8, 1e-7, 1e-6]
fuerzas_regularizacion = [5e4, 5e5, 5e6]

# clave:(tasa_aprendizaje, fuerza_regularizacion)
# valor:(precision_entrenamiento, precision_validacion)
resultados = {}
mejor_val = -1
mejor_softmax = None

# Usar validación para ajustar tasa de aprendizaje y fuerza de regularización;
# guardar mejor clasificador en mejor_softmax
# Implementación:
# Entrenar un clasificador Softmax para cada par de hiperparámetros,
# calcular su precisión en entrenamiento y validación, guardar mejor precisión de validación
for lr in tasas_aprendizaje:
  for rs in fuerzas_regularizacion:
    softmax = Softmax()
    historial_perdida = softmax.entrenar(X_train_feats, y_train, learning_rate=lr, reg=rs, 
                                      num_iters=1500, batch_size=200, verbose=False)
    y_pred_ent = softmax.predecir(X_train_feats)
    train_acc = np.mean(y_pred_ent == y_train)

    y_pred_val = softmax.predecir(X_val_feats)
    val_acc = np.mean(y_pred_val == y_val)

    resultados[(lr, rs)] = (train_acc, val_acc)

    if val_acc > mejor_val:
      mejor_val = val_acc
      mejor_softmax = softmax
# Fin

print('Mejor precisión: %f' % mejor_val)

2. Entrenar red neuronal en características de imagen
from cs231n.classifiers.fc_net import TwoLayerNet
from cs231n.solver import Solver

dim_entrada = X_train_feats.shape[1]
dim_oculta = 500
num_clases = 10

datos = {
    'X_train': X_train_feats,
    'y_train': y_train,
    'X_val': X_val_feats,
    'y_val': y_val,
    'X_test': X_test_feats,
    'y_test': y_test,
}

red = TwoLayerNet(dim_entrada, dim_oculta, num_clases)
mejor_red = None

# Entrenar red neuronal de dos capas en características, validar y ajustar parámetros,
# guardar mejor modelo en mejor_red
# Implementación
tasas_aprendizaje = np.linspace(1e-2, 2.75e-2, 4)
fuerzas_regularizacion = np.geomspace(1e-6, 1e-4, 3)

resultados = {}
mejor_val = -1

import itertools

for lr, reg in itertools.product(tasas_aprendizaje, fuerzas_regularizacion):
    modelo = TwoLayerNet(dim_entrada, dim_oculta, num_clases, reg=reg)
    solver = Solver(modelo, datos, optim_config={'learning_rate': lr}, num_epocas=15, verbose=False)
    solver.entrenar()

    resultados[(lr, reg)] = solver.mejor_val_acc

    if resultados[(lr, reg)] > mejor_val:
        mejor_val = resultados[(lr, reg)]
        mejor_red = modelo
    
print('mejor precisión de validación %f' % mejor_val)

# Fin

1-5 Entrenamiento de Red Neuronal Completamente Conectada

Red neuronal multicapa

Esta parte está en el archivo FullyConnectedNets.ipynb

1. Completar la clase FullyConnectedNet en cs231n/classifiers/fc_net.py

Reutilizar funciones ya implementadas como affine_forward, affine_backward, relu_forward, relu_backward y softmax_loss para implementar la inicialización, propagación hacia adelante y propagación hacia atrás de la red.

class FullyConnectedNet(object):
    """
    Clase de red neuronal multicapa completamente conectada.
    La red contiene cualquier número de capas ocultas, activaciones ReLU,
    función de pérdida softmax, y proporciona dropout y normalización
    batch/layer como opciones.
    Red L capas, arquitectura:

    {affine - [batch/layer norm] - relu - [dropout]} x (L - 1) - affine - softmax 
    [] son bloques opcionales {...} repetidos L - 1 veces.

    Los parámetros aprendidos se almacenan en el diccionario self.params,
    que se aprenderá a través de la clase Solver
    """

    def __init__(
        self,
        hidden_dims,
        input_dim=3 * 32 * 32,
        num_classes=10,
        dropout_keep_ratio=1,
        normalization=None,
        reg=0.0,
        weight_scale=1e-2,
        dtype=np.float32,
        seed=None,
    ):
        """Inicializar una nueva red FullyConnectedNet.

        Entradas:
        - hidden_dims: lista de enteros, tamaño de cada capa oculta
        - input_dim: tamaño de entrada
        - num_classes: número de clases
        - dropout_keep_ratio: intensidad de dropout, [0,1], si es 1 no se usa dropout
        - normalization: tipo de normalización: "batchnorm", "layernorm", None (por defecto, sin normalización) 
        - reg: fuerza de regularización L2
        - weight_scale: desviación estándar para inicialización aleatoria de pesos
        - dtype: objeto de tipo de dato numpy; todos los cálculos usarán este tipo
        - seed: si no es None, pasarlo a Dropout para hacerlo determinista
        """
        self.normalization = normalization
        self.use_dropout = dropout_keep_ratio != 1
        self.reg = reg
        self.num_layers = 1 + len(hidden_dims)
        self.dtype = dtype
        self.params = {}

        # Implementación:
        # Inicializar parámetros de la red
        dims = [input_dim] + hidden_dims + [num_classes]
        for i in range(self.num_layers):
            self.params['W' + str(i+1)] = weight_scale * np.random.randn(dims[i], dims[i+1])
            self.params['b' + str(i+1)] = np.zeros(dims[i+1])
            # Si se usa normalización batch y no es la última capa (no necesita parámetros de normalización)
            if self.normalization and i < self.num_layers - 1:
                # gamma inicializado en 1
                self.params['gamma' + str(i + 1)] = np.ones(dims[i + 1])
                # beta inicializado en 0 
                self.params['beta' + str(i + 1)] = np.zeros(dims[i + 1])
        # Fin

        # Al usar Dropout, necesitamos pasar un diccionario dropout_param a cada capa Dropout
        self.dropout_param = {}
        if self.use_dropout:
            self.dropout_param = {"mode": "train", "p": dropout_keep_ratio}
            if seed is not None:
                self.dropout_param["seed"] = seed

        # Al usar batch norm, rastrear medias/varianzas en ejecución; pasar un bn_params único para cada capa
        self.bn_params = []
        if self.normalization == "batchnorm":
            self.bn_params = [{"mode": "train"} for i in range(self.num_layers - 1)]
        if self.normalization == "layernorm":
            self.bn_params = [{} for i in range(self.num_layers - 1)]

        # Asegurar que todos los parámetros se conviertan al tipo de dato correcto
        for k, v in self.params.items():
            self.params[k] = v.astype(dtype)

    def loss(self, X, y=None):
        """Calcular pérdida y gradiente de la red completamente conectada
        Entradas:
        - X: (N, d_1, ..., d_k)
        - y: (N,). y[i] da la etiqueta para X[i].

        Retorna:
        y==None -> ejecutar propagación hacia adelante en modo de prueba y retornar:
        - scores: (N, C)

        y no es None -> ejecutar propagación hacia adelante y hacia atrás en modo de entrenamiento y retornar una tupla:
        - loss: valor escalar de la pérdida
        - grads: diccionario con las mismas claves que self.params, mapeando nombres de parámetros
          a gradientes de la pérdida con respecto a esos parámetros
        """
        X = X.astype(self.dtype)
        mode = "test" if y is None else "train"

        # Establecer modo de dropout y batch normalization (comportamiento diferente en cada modo)
        if self.use_dropout:
            self.dropout_param["mode"] = mode
        if self.normalization == "batchnorm":
            for bn_param in self.bn_params:
                bn_param["mode"] = mode
        scores = None

        #Implementación
        # Implementar propagación hacia adelante, calcular scores de x
        x_actual = X.reshape(X.shape[0], -1)
        caches = {}
        for i in range(self.num_layers-1):
           w_actual = self.params['W' + str(i+1)]
           b_actual = self.params['b' + str(i+1)]

           salida_afin, cache_afin = affine_forward(x_actual, w_actual, b_actual)
           salida_relu, cache_relu = relu_forward(salida_afin)

           caches['cache_afin' + str(i+1)] = cache_afin
           caches['cache_relu' + str(i+1)] = cache_relu

           x_actual = salida_relu
        
        w_actual = self.params['W' + str(self.num_layers)]
        b_actual = self.params['b' + str(self.num_layers)]
        scores, cache_afin = affine_forward(x_actual, w_actual, b_actual)

        caches['cache_afin' + str(self.num_layers)] = cache_afin
        # Fin
           
        # Al usar dropout, se necesita pasar dropout_param
        # Si modo de prueba, retornar temprano
        if mode == "test":
            return scores

        loss, grads = 0.0, {}
        # Implementar retropropagación de red completamente conectada.
        # Guardar pérdida en variable loss, gradientes en diccionario grads.

        # Implementación:
        loss, dy = softmax_loss(scores, y)

        # Agregar términos de regularización a la pérdida
        for i in range(1, self.num_layers+1):
           w = self.params['W' + str(i)]
           loss += 0.5 * self.reg * np.sum(w**2)

        # Retropropagación de la última capa, calcular gradientes, manejar por separado
        wi = self.params['W' + str(self.num_layers)]
        cache_actual = caches['cache_afin' + str(self.num_layers)]
        dx_actual, dw_actual, db_actual = affine_backward(dy, cache_actual)

        # Agregar términos de regularización
        grads['W' + str(self.num_layers)] = dw_actual + self.reg * wi
        grads['b' + str(self.num_layers)] = db_actual

        # Calcular gradientes
        for i in range(self.num_layers-1, 0, -1):
           wi = self.params['W' + str(i)]
           cache_afin = caches['cache_afin' + str(i)]
           cache_relu = caches['cache_relu' + str(i)]

           dx_actual = relu_backward(dx_actual, cache_relu)
           dx_actual, dw_actual, db_actual = affine_backward(dx_actual, cache_afin)

           grads['W' + str(i)] = dw_actual + self.reg * wi
           grads['b' + str(i)] = db_actual
        # Fin 
        return loss, grads

Verificación inicial de pérdida y gradiente

2. Ajustar learning rate y escala de inicialización de pesos para que la red de tres capas alcance 100% de precisión de entrenamiento en 20 épocas

Después de ajustar:

weight_scale = 3e-2; learning_rate = 1e-3

num_muestras = 50
datos_pequenos = {
  "X_train": datos["X_train"][:num_muestras],
  "y_train": datos["y_train"][:num_muestras],
  "X_val": datos["X_val"],
  "y_val": datos["y_val"],
}

escala_pesos = 3e-2   # Experimentar con esto!
tasa_aprendizaje = 1e-3  # Experimentar con esto!


modelo = FullyConnectedNet(
    [100, 100],
    weight_scale=escala_pesos,
    dtype=np.float64
)
solver = Solver(
    modelo,
    datos_pequenos,
    imprimir_cada=10,
    num_epocas=20,
    tamano_lote=25,
    regla_actualizacion="sgd",
    optim_config={"learning_rate": tasa_aprendizaje},
)
solver.entrenar()

plt.plot(solver.historial_perdida)
plt.title("Historial de pérdida de entrenamiento")
plt.xlabel("Iteración")
plt.ylabel("Pérdida de entrenamiento")
plt.grid(linestyle='--', linewidth=0.5)
plt.show()

3. Ajustar learning rate y escala de inicialización de pesos para que la red de cinco capas alcance 100% de precisión de entrenamiento en 20 épocas

learning_rate = 5e-4; weight_scale = 0.1

num_muestras = 50
datos_pequenos = {
  'X_train': datos['X_train'][:num_muestras],
  'y_train': datos['y_train'][:num_muestras],
  'X_val': datos['X_val'],
  'y_val': datos['y_val'],
}

tasa_aprendizaje = 5e-4  # Experimentar con esto!
escala_pesos = 0.1   # Experimentar con esto!


modelo = FullyConnectedNet(
    [100, 100, 100, 100],
    weight_scale=escala_pesos,
    dtype=np.float64
)
solver = Solver(
    modelo,
    datos_pequenos,
    imprimir_cada=10,
    num_epocas=20,
    tamano_lote=25,
    regla_actualizacion='sgd',
    optim_config={'learning_rate': tasa_aprendizaje},
)
solver.entrenar()

plt.plot(solver.historial_perdida)
plt.title('Historial de pérdida de entrenamiento')
plt.xlabel('Iteración')
plt.ylabel('Pérdida de entrenamiento')
plt.grid(linestyle='--', linewidth=0.5)
plt.show()

Pregunta en línea 1:

Reglas de actualización de parámetros

4. En el archivo cs231n/optim.py, implementar reglas de actualización SGD + Momentum & RMSProp & Adam (con corrección de sesgo)
def sgd_momentum(w, dw, config=None):
    """
    Ejecutar SGD con momento

    formato de config:
    - learning_rate: tasa de aprendizaje escalar
    - momentum: [0,1], si es 0 es SGD estándar
    - velocity: mismo formato que w/dw, almacena promedio móvil de gradientes
    """
    if config is None:
        config = {}
    config.setdefault("learning_rate", 1e-2)
    config.setdefault("momentum", 0.9)
    v = config.get("velocity", np.zeros_like(w))

    next_w = None
    # Implementar fórmula de actualización con momento, guardar valor actualizado en next_w, actualizar v
    # Implementación
    v = config["momentum"] * v - config["learning_rate"] * dw
    next_w = w + v
    # Fin
    config["velocity"] = v

    return next_w, config


def rmsprop(w, dw, config=None):
    """
    Usar regla de actualización RMSProp, que usa promedio móvil de cuadrados de gradiente
    para establecer tasa de aprendizaje adaptativa por parámetro.
    formato de config:
    - learning_rate: tasa de aprendizaje escalar
    - decay_rate:[0,1] da tasa de decaimiento para caché de cuadrados de gradiente
    - epsilon: escalar pequeño para suavizado para evitar división por cero
    - cache: promedio móvil de cuadrados de gradiente
    """
    if config is None:
        config = {}
    config.setdefault("learning_rate", 1e-2)
    config.setdefault("decay_rate", 0.99)
    config.setdefault("epsilon", 1e-8)
    config.setdefault("cache", np.zeros_like(w))

    next_w = None
    # Implementación
    tasa_decaimiento = config["decay_rate"]
    
    config["cache"] = tasa_decaimiento * config["cache"] + (1 - tasa_decaimiento) * dw**2
    
    next_w = w - config["learning_rate"] * dw / (np.sqrt(config["cache"]) + config["epsilon"])
    # Fin
    return next_w, config


def adam(w, dw, config=None):
    """
    Usar regla de actualización Adam, que incorpora promedios móviles tanto del gradiente
    como de su cuadrado y un término de corrección de sesgo.

    formato de config:
    - learning_rate: tasa de aprendizaje escalar.
    - beta1: Tasa de decaimiento para promedio móvil de primer momento del gradiente.
    - beta2: Tasa de decaimiento para promedio móvil de segundo momento del gradiente.
    - epsilon: Escalar pequeño para suavizado para evitar división por cero.
    - m: Promedio móvil del gradiente.
    - v: Promedio móvil del cuadrado del gradiente.
    - t: Número de iteración.
    """
    if config is None:
        config = {}
    config.setdefault("learning_rate", 1e-3)
    config.setdefault("beta1", 0.9)
    config.setdefault("beta2", 0.999)
    config.setdefault("epsilon", 1e-8)
    config.setdefault("m", np.zeros_like(w))
    config.setdefault("v", np.zeros_like(w))
    config.setdefault("t", 0)

    next_w = None
    # Implementación
    beta1 = config["beta1"]
    beta2 = config["beta2"]
    config["t"] += 1

    config["m"] = beta1 * config["m"] + (1 - beta1) * dw
    config["v"] = beta2 * config["v"] + (1 - beta2) * dw**2
    # Corrección de sesgo
    m_corregido = config["m"] / (1 - beta1**config["t"])
    v_corregido = config["v"] / (1 - beta2**config["t"])

    next_w = w - config["learning_rate"] * m_corregido / (np.sqrt(v_corregido) + config["epsilon"])
    # Fin

    return next_w, config

Pregunta en línea 2:

Etiquetas: Redes Neuronales aprendizaje profundo clasificación Softmax knn

Publicado el 6-22 19:57