Índice
1-1 K-NN
- Implementación con bucles anidados para calcular distancias entre pares de muestras
- Función de predicción de etiquetas basada en matriz de distancias
- Optimización con bucle simple + vectorización parcial para cálculo de matriz de distancias
- Implementación completamente vectorizada para cálculo de matriz de distancias
- Determinación óptima del hiperparámetro k mediante validación cruzada
1-2 Softmax
Clasificador Softmax
- Verificación de la implementación básica de la función de pérdida Softmax
- Derivación de la fórmula del gradiente de la función de pérdida Softmax
- Optimización de la implementación básica para el cálculo de la pérdida
- Implemantación vectorizada del cálculo del gradiente
SGD
- Implementación del algoritmo SGD en la función de entrenamiento
- Implementación de la función de predicción para etiquetas
- Ajuste de hiperparámetros usando conjunto de validación
1-3 Red Neuronal de Dos Capas
- Implementación de la propagación hacia adelante en capas completamente conectadas
- Función de retropropagación para capas afines
- Implementación de la función de activación ReLU hacia adelante
- Implementación de la función de activación ReLU hacia atrás
- Función de pérdida softmax con cálculo de pérdida y gradiente
- Implementación de red neuronal de dos capas
- Entrenamiento de TwoLayerNet usando Solver
- Ajuste de hiperparámetros para mejorar precisión
1-4 Representaciones de Alto Nivel: Características de Imágenes
- Entrenamiento de clasificador Softmax en características
- Entrenamiento de red neuronal en características de imagen
1-5 Entrenamiento de Red Neuronal Completamente Conectada
Red neuronal multicapa
- Completar la clase FullyConnectedNet en cs231n/classifiers/fc_net.py
Verificación inicial de pérdida y gradiente
- Ajuste de learning rate y escala de inicialización de pesos para alcanzar 100% de precisión en 20 épocas
- Ajuste de learning rate y escala de inicialización de pesos para red de cinco capas
Reglas de actualización de parámetros
- 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:
- HOG (Histograma de Gradientes Orientados)
- 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: