paint-brush
El aprendizaje profundo se basa en matemáticas de punto flotante. ¿Qué pasa si eso es un error?por@abhiyanampally_kob9nse8
573 lecturas
573 lecturas

El aprendizaje profundo se basa en matemáticas de punto flotante. ¿Qué pasa si eso es un error?

por 40m2025/02/11
Read on Terminal Reader

Demasiado Largo; Para Leer

El sistema de numeración logarítmica (LNS) es una representación numérica alternativa a la aritmética de punto flotante (FP). LNS representa números en una escala logarítmica, convirtiendo las multiplicaciones en sumas, lo que puede resultar computacionalmente más económico en ciertas arquitecturas de hardware. Sin embargo, la suma y la resta en LNS requieren aproximaciones, lo que reduce la precisión. Usamos LNS para entrenar un perceptrón multicapa simple y completamente conectado en MNIST.
featured image - El aprendizaje profundo se basa en matemáticas de punto flotante. ¿Qué pasa si eso es un error?
undefined HackerNoon profile picture
0-item
1-item

Cuando me encontré por primera vez con la idea de usar el sistema de numeración logarítmica (LNS) en el aprendizaje profundo, me intrigó, pero también me mostré escéptico. Como la mayoría de nosotros, siempre había trabajado con aritmética de punto flotante (FP), el estándar para el cálculo numérico en el aprendizaje profundo. El FP proporciona un buen equilibrio entre precisión y alcance, pero tiene desventajas: mayor uso de memoria, mayor complejidad computacional y mayor consumo de energía. Entonces, decidí experimentar y ver por mí mismo: ¿cómo se compara el LNS con el FP al entrenar un perceptrón multicapa (MLP) simple y completamente conectado en MNIST?

¿Por qué considerar LNS?

LNS representa números en una escala logarítmica, convirtiendo multiplicaciones en sumas, lo que puede resultar computacionalmente más económico en ciertas arquitecturas de hardware. Esta eficiencia se produce a costa de la precisión, especialmente en operaciones de suma y resta, que son más complejas en LNS. Sin embargo, los posibles beneficios (menor consumo de memoria, cálculos más rápidos y menor consumo de energía) despertaron mi curiosidad y me animaron a probarlo.

Antecedentes: sistema de numeración de punto flotante frente a sistema de numeración logarítmico

Representación de punto flotante (FP)

La aritmética de punto flotante es la representación numérica estándar en la mayoría de los marcos de aprendizaje profundo, como PyTorch y TensorFlow. Los números de punto flotante tienen:


  • Un bit de signo (que determina un valor positivo o negativo)
  • Un exponente (factor de escala)
  • Una mantisa (significando) (precisión del número)


El FP32 (precisión simple) se utiliza habitualmente en el aprendizaje profundo y ofrece un equilibrio entre precisión numérica y eficiencia computacional. Los formatos más eficientes, como FP16 y BF16, están ganando popularidad para acelerar el entrenamiento.

Sistema de numeración logarítmica (LNS)

LNS es una representación numérica alternativa en la que los números se almacenan como logaritmos: [ x = \log_b (y) ] donde ( b ) es la base del logaritmo. LNS tiene varias ventajas:


  • La multiplicación se simplifica a suma : ( x_1 * x_2 = b^{(\log_b x_1 + \log_b x_2)} )
  • La división se simplifica a resta : ( x_1 / x_2 = b^{(\log_b x_1 - \log_b x_2)} )
  • Las funciones de crecimiento exponencial se vuelven lineales


Sin embargo, la suma y la resta en LNS requieren aproximaciones, lo que reduce la precisión.

Operaciones aritméticas LNS

Para explorar más a fondo LNS, implementé operaciones aritméticas básicas como suma, resta, multiplicación y división utilizando representaciones internas de LNS.


 import torch import numpy as np import xlns as xl # Assuming xlns module is installed and provides xlnsnp # Function to convert floating-point numbers to xlns internal representation def float_to_internal(arr): xlns_data = xl.xlnsnp(arr) return xlns_data.nd # Function to convert xlns internal representation back to floating-point numbers def internal_to_float(internal_data): original_numbers = [] for value in internal_data: x = value // 2 s = value % 2 # Use x and s to create xlns object xlns_value = xl.xlns(0) xlns_value.x = x xlns_value.s = s original_numbers.append(float(xlns_value)) return original_numbers # Function to perform LNS addition using internal representation def lns_add_internal(x, y): max_part = torch.maximum(x, y) diff = torch.abs(x - y) adjust_term = torch.log1p(torch.exp(-diff)) return max_part + adjust_term # Function to perform LNS subtraction using internal representation def lns_sub_internal(x, y): return lns_add_internal(x, -y) # Function to perform LNS multiplication using internal representation def lns_mul_internal(x, y): return x + y # Function to perform LNS division using internal representation def lns_div_internal(x, y): return x - y # Input floating-point arrays x_float = [2.0, 3.0] y_float = [-1.0, 0.0] # Convert floating-point arrays to xlns internal representation x_internal = float_to_internal(x_float) y_internal = float_to_internal(y_float) # Create tensors from the internal representation tensor_x_nd = torch.tensor(x_internal, dtype=torch.int64) tensor_y_nd = torch.tensor(y_internal, dtype=torch.int64) # Perform the toy LNS addition on the internal representation result_add_internal = lns_add_internal(tensor_x_nd, tensor_y_nd) # Perform the toy LNS subtraction on the internal representation result_sub_internal = lns_sub_internal(tensor_x_nd, tensor_y_nd) # Perform the toy LNS multiplication on the internal representation result_mul_internal = lns_mul_internal(tensor_x_nd, tensor_y_nd) # Perform the toy LNS division on the internal representation result_div_internal = lns_div_internal(tensor_x_nd, tensor_y_nd) # Convert the internal results back to original floating-point values result_add_float = internal_to_float(result_add_internal.numpy()) result_sub_float = internal_to_float(result_sub_internal.numpy()) result_mul_float = internal_to_float(result_mul_internal.numpy()) result_div_float = internal_to_float(result_div_internal.numpy()) # Convert the results back to PyTorch tensors result_add_tensor = torch.tensor(result_add_float, dtype=torch.float32) result_sub_tensor = torch.tensor(result_sub_float, dtype=torch.float32) result_mul_tensor = torch.tensor(result_mul_float, dtype=torch.float32) result_div_tensor = torch.tensor(result_div_float, dtype=torch.float32) # Print results print("Input x:", x_float) print("Input y:", y_float) print("Addition Result:", result_add_float) print("Addition Result Tensor:", result_add_tensor) print("Subtraction Result:", result_sub_float) print("Subtraction Result Tensor:", result_sub_tensor) print("Multiplication Result:", result_mul_float) print("Multiplication Result Tensor:", result_mul_tensor) print("Division Result:", result_div_float) print("Division Result Tensor:", result_div_tensor)


A continuación se muestra un desglose de mi implementación experimental del Sistema de Numeración Logarítmica (LNS).

1. Concepto básico de LNS y desafíos en PyTorch

En LNS, los números se representan como logaritmos, lo que transforma la multiplicación y la división en suma y resta. Sin embargo, implementar esto con PyTorch presenta desafíos específicos, ya que los tensores de PyTorch utilizan representaciones de punto flotante internamente. Esto genera varios requisitos:


  • Mantener la representación logarítmica durante todos los cálculos.
  • Garantizar la estabilidad numérica.
  • Maneje las conversiones con cuidado.
  • Gestionar la representación interna utilizando dos componentes:
    • x : el valor logarítmico.
    • s : un bit de signo (0 o 1).

2. Representación interna y conversión

El primer paso es convertir números de punto flotante a su representación interna LNS.

 import torch import numpy as np import xl # Hypothetical external LNS library def float_to_internal(arr): xlns_data = xl.xlnsnp(arr) return xlns_data.nd # Convert floating-point arrays to xlns internal representation x_float = np.array([2.0, 3.0]) y_float = np.array([-1.0, 0.0]) x_internal = float_to_internal(x_float) y_internal = float_to_internal(y_float) # Create tensors from the internal representation tensor_x_nd = torch.tensor(x_internal, dtype=torch.int64) tensor_y_nd = torch.tensor(y_internal, dtype=torch.int64)


El uso de dtype=torch.int64 es crucial porque:

  • Conserva la representación interna exacta de LNS sin errores de redondeo de punto flotante.
  • Empaca el valor logarítmico y el bit de signo en un solo entero.
  • Evita que operaciones de punto flotante no deseadas dañen la representación LNS.

3. Operaciones aritméticas básicas

a) Multiplicación

 def lns_mul_internal(x, y): return x + y

La multiplicación en LNS se convierte en suma:

  • Si a = log(x) y b = log(y) , entonces log(x×y) = log(x) + log(y) .

b) División

 def lns_div_internal(x, y): return x - y

La división se convierte en resta:

  • log(x/y) = log(x) - log(y) .

c) Adición

 def lns_add_internal(x, y): max_part = torch.maximum(x, y) diff = torch.abs(x - y) adjust_term = torch.log1p(torch.exp(-diff)) return max_part + adjust_term


La suma es más compleja y numéricamente sensible porque:

  • Implica operaciones exponenciales y logarítmicas.
  • La implementación directa de punto flotante podría provocar desbordamiento o subdesbordamiento.
  • Utiliza la ecuación: log(x + y) = log(max(x,y)) + log(1 + exp(log(min(x,y)) - log(max(x,y)))) .
  • Utiliza log1p en lugar de log(1 + x) directo para una mejor estabilidad numérica.

4. Tipo de seguridad y gestión de conversiones

 def internal_to_float(internal_data): for value in internal_data: x = value // 2 # Integer division s = value % 2 # Integer modulo


El proceso de conversión mantiene una clara separación:

  1. Convertir de flotante a representación interna LNS (enteros).
  2. Realizar operaciones LNS utilizando aritmética de números enteros.
  3. Vuelva a convertir a flotante solo cuando sea necesario.
 # Convert results back to float and tensor result_add_float = internal_to_float(result_add_internal.numpy()) result_add_tensor = torch.tensor(result_add_float, dtype=torch.float32)

5. Principales ventajas y limitaciones

Ventajas

  • La multiplicación y la división se simplifican a suma y resta.
  • Amplio rango dinámico con aritmética de punto fijo.
  • Potencialmente más eficiente para ciertas aplicaciones.

Limitaciones

  • La suma y la resta son operaciones más complejas .
  • Sobrecarga de conversión entre punto flotante y LNS.
  • Requiere un manejo especial para el cero y los números negativos.
  • La compatibilidad de tensores de PyTorch requiere una gestión de tipos cuidadosa.

6. Posibilidades de optimización

Para mejorar el rendimiento, se podría:

  1. Implemente una función de autograduación de PyTorch personalizada para operaciones LNS.
  2. Cree un tipo de tensor personalizado que admita LNS de forma nativa.
  3. Utilice kernels CUDA para operaciones LNS eficientes en la GPU.


La implementación actual implica concesiones prácticas:

  • Prioriza la claridad y la facilidad de mantenimiento sobre el máximo rendimiento.
  • Utiliza la infraestructura tensorial existente de PyTorch mientras preserva la precisión de LNS.
  • Mantiene la estabilidad numérica mediante una cuidadosa gestión de tipos.
  • Minimiza las conversiones entre representaciones .

7. Ejemplo de flujo de datos

Los siguientes pasos demuestran la canalización completa utilizando valores de ejemplo [2.0, 3.0] y [-1.0, 0.0] :

  1. Convierte flotantes de entrada en representación interna de LNS.
  2. Cree tensores enteros para almacenar valores LNS.
  3. Realizar operaciones aritméticas en el dominio LNS.
  4. Convierte los resultados nuevamente a punto flotante.
  5. Cree tensores PyTorch finales para su posterior procesamiento.


Esta implementación cierra con éxito la brecha entre el sistema tensor de punto flotante de PyTorch y la aritmética LNS, manteniendo al mismo tiempo la estabilidad y precisión numérica.


Entrenamiento de un MLP completamente conectado en el conjunto de datos MNIST Digit con FP y LNS

Configuración del experimento

Entrené un MLP completamente conectado en el conjunto de datos MNIST utilizando representaciones FP y LNS. La arquitectura del modelo fue simple:

  • Capa de entrada: 784 neuronas (imágenes aplanadas de 28 x 28)
  • Capas ocultas: Dos capas con 256 y 128 neuronas, activaciones ReLU
  • Capa de salida: 10 neuronas (una para cada dígito, utilizando softmax)
  • Función de pérdida: entropía cruzada
  • Optimizador: Adam


Para la implementación de LNS, tuve que salirme de mi flujo de trabajo habitual de PyTorch. A diferencia de FP, que PyTorch admite de forma nativa, PyTorch no proporciona operaciones LNS integradas. Encontré un proyecto de GitHub llamado xlns , que implementa representaciones de números logarítmicos y aritmética, lo que hace posible el uso de LNS en redes neuronales.

MLP de punto flotante en PyTorch

Comenzamos implementando un MLP totalmente conectado basado en FP estándar usando PyTorch:

 import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms import matplotlib.pyplot as plt import numpy as np import time # For calculating elapsed time # Define the multi-layer perceptron (MLP) model with one hidden layer class MLP(nn.Module): def __init__(self): super(MLP, self).__init__() # Input: 28*28 features; Hidden layer: 100 neurons; Output layer: 10 neurons self.fc1 = nn.Linear(28 * 28, 100) self.relu = nn.ReLU() self.fc2 = nn.Linear(100, 10) self.logsoftmax = nn.LogSoftmax(dim=1) # For stable outputs with NLLLoss def forward(self, x): # Flatten image: (batch_size, 1, 28, 28) -> (batch_size, 784) x = x.view(x.size(0), -1) x = self.fc1(x) x = self.relu(x) x = self.fc2(x) return self.logsoftmax(x) def train_and_validate(num_epochs=5, batch_size=64, learning_rate=0.01, split_ratio=0.8): # Set the device to GPU if available device = torch.device("cuda" if torch.cuda.is_available() else "cpu") print(f"Training on device: {device}") # Transformation for MNIST: convert to tensor and normalize transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) # Load the MNIST training dataset train_dataset_full = torchvision.datasets.MNIST( root='./data', train=True, transform=transform, download=True ) # Split the dataset into training and validation sets n_total = len(train_dataset_full) n_train = int(split_ratio * n_total) n_val = n_total - n_train train_dataset, val_dataset = torch.utils.data.random_split(train_dataset_full, [n_train, n_val]) # Create DataLoaders for training and validation datasets train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=batch_size, shuffle=True) val_loader = torch.utils.data.DataLoader(val_dataset, batch_size=batch_size, shuffle=False) # Initialize the model, loss function, and optimizer; move model to device model = MLP().to(device) criterion = nn.NLLLoss() optimizer = optim.SGD(model.parameters(), lr=learning_rate) # Lists to store training and validation accuracies for each epoch train_accuracies = [] val_accuracies = [] # Record the start time for measuring elapsed time start_time = time.time() # Training loop for epoch in range(num_epochs): model.train() running_loss = 0.0 correct_train = 0 total_train = 0 for inputs, labels in train_loader: # Move inputs and labels to device inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() # Compute running loss and training accuracy running_loss += loss.item() * inputs.size(0) _, predicted = torch.max(outputs.data, 1) total_train += labels.size(0) correct_train += (predicted == labels).sum().item() train_accuracy = 100.0 * correct_train / total_train train_accuracies.append(train_accuracy) # Evaluate on validation set model.eval() correct_val = 0 total_val = 0 with torch.no_grad(): for inputs, labels in val_loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) _, predicted = torch.max(outputs.data, 1) total_val += labels.size(0) correct_val += (predicted == labels).sum().item() val_accuracy = 100.0 * correct_val / total_val val_accuracies.append(val_accuracy) print(f"Epoch [{epoch+1}/{num_epochs}], Loss: {running_loss/total_train:.4f}, " f"Train Acc: {train_accuracy:.2f}%, Val Acc: {val_accuracy:.2f}%") # Calculate elapsed time elapsed_time = time.time() - start_time print(f"Training completed in {elapsed_time:.2f} seconds.") # Show sample predictions from the validation set show_predictions(model, val_loader, device) # Optional: plot training and validation accuracies epochs_arr = np.arange(1, num_epochs + 1) plt.figure(figsize=(10, 6)) plt.plot(epochs_arr, train_accuracies, label='Training Accuracy', marker='o') plt.plot(epochs_arr, val_accuracies, label='Validation Accuracy', marker='x') plt.xlabel('Epoch') plt.ylabel('Accuracy (%)') plt.title('Training and Validation Accuracies') plt.legend() plt.grid(True) plt.savefig('pytorch_accuracy.png') plt.show() def show_predictions(model, data_loader, device, num_images=6): """ Displays a few sample images from the data_loader along with the model's predictions. """ model.eval() images_shown = 0 plt.figure(figsize=(12, 8)) # Get one batch of images from the validation dataset for inputs, labels in data_loader: inputs, labels = inputs.to(device), labels.to(device) with torch.no_grad(): outputs = model(inputs) _, predicted = torch.max(outputs, 1) # Loop through the batch and plot images for i in range(inputs.size(0)): if images_shown >= num_images: break # Move the image to cpu and convert to numpy for plotting img = inputs[i].cpu().squeeze() plt.subplot(2, num_images // 2, images_shown + 1) plt.imshow(img, cmap='gray') plt.title(f"Pred: {predicted[i].item()}") plt.axis('off') images_shown += 1 if images_shown >= num_images: break plt.suptitle("Sample Predictions from the Validation Set") plt.tight_layout() plt.show() if __name__ == '__main__': train_and_validate(num_epochs=5, batch_size=64, learning_rate=0.01, split_ratio=0.8)


Esta implementación sigue un proceso de aprendizaje profundo convencional donde las multiplicaciones y sumas se manejan mediante aritmética FP.


A continuación se presenta un tutorial detallado de esta implementación de PyTorch de un perceptrón multicapa (MLP) para el conjunto de datos MNIST.

  1. Arquitectura del modelo (clase MLP):
 class MLP(nn.Module): def __init__(self): super(MLP, self).__init__() self.fc1 = nn.Linear(28 * 28, 100) # First fully connected layer self.relu = nn.ReLU() # Activation function self.fc2 = nn.Linear(100, 10) # Output layer self.logsoftmax = nn.LogSoftmax(dim=1)
  1. Pase hacia adelante:
 def forward(self, x): x = x.view(x.size(0), -1) # Flatten: (batch_size, 1, 28, 28) -> (batch_size, 784) x = self.fc1(x) # First layer x = self.relu(x) # Activation x = self.fc2(x) # Output layer return self.logsoftmax(x) # Final activation
  1. Configuración del entrenamiento:
 def train_and_validate(num_epochs=5, batch_size=64, learning_rate=0.01, split_ratio=0.8): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") # Data preprocessing transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) # Normalize to [-1, 1] ])

Componentes clave:

  • Compatibilidad con GPU mediante selección de dispositivo

  • Normalización de datos para un mejor entrenamiento

  • Hiperparámetros configurables


  1. Gestión de conjuntos de datos:
 train_dataset_full = torchvision.datasets.MNIST( root='./data', train=True, transform=transform, download=True ) # Split into train/validation n_train = int(split_ratio * n_total) train_dataset, val_dataset = torch.utils.data.random_split(train_dataset_full, [n_train, n_val])
  • Descarga el conjunto de datos MNIST si no está presente

  • Divide los datos en conjuntos de entrenamiento (80 %) y validación (20 %)


  1. Bucle de entrenamiento:
 for epoch in range(num_epochs): model.train() for inputs, labels in train_loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() # Clear gradients outputs = model(inputs) # Forward pass loss = criterion(outputs, labels)# Calculate loss loss.backward() # Backward pass optimizer.step() # Update weights

Procedimiento de entrenamiento clásico:

  • Gradientes cero

  • Pase hacia adelante

  • Cálculo de pérdidas

  • Pase hacia atrás

  • Actualizaciones de peso


  1. Validación:
 model.eval() with torch.no_grad(): for inputs, labels in val_loader: outputs = model(inputs) _, predicted = torch.max(outputs.data, 1) total_val += labels.size(0) correct_val += (predicted == labels).sum().item()

Características principales:

  • Modelo establecido en modo de evaluación

  • No es necesario calcular gradiente

  • Cálculo de precisión


  1. Visualización:
 def show_predictions(model, data_loader, device, num_images=6): model.eval() plt.figure(figsize=(12, 8)) # Display predictions vs actual labels
  • Muestra predicciones de muestra del conjunto de validación

  • Útil para la evaluación cualitativa


  1. Seguimiento del rendimiento:
 # Training metrics train_accuracies.append(train_accuracy) val_accuracies.append(val_accuracy) # Plot learning curves plt.plot(epochs_arr, train_accuracies, label='Training Accuracy') plt.plot(epochs_arr, val_accuracies, label='Validation Accuracy')
  • Realiza un seguimiento de la precisión del entrenamiento y la validación.

  • Traza curvas de aprendizaje

  • Mide el tiempo de entrenamiento


Esto proporciona una base sólida para comparar con implementaciones basadas en LNS, ya que implementa todos los componentes estándar de una cadena de aprendizaje profundo utilizando aritmética de punto flotante tradicional.

Sistema de numeración logarítmica (LNS) MLP

Para LNS, necesitamos usar la biblioteca xlns . A diferencia de FP, LNS reemplaza las operaciones que requieren mucha multiplicación por sumas en el dominio logarítmico. Sin embargo, PyTorch no admite esto de forma nativa, por lo que debemos aplicar manualmente las operaciones LNS cuando corresponda.

 import numpy as np import matplotlib.pyplot as plt import os import time import argparse import xlns as xl from tensorflow.keras.datasets import mnist # Use Keras's MNIST loader # If you are using fractional normalized LNS, make sure the following are uncommented import xlnsconf.xlnsudFracnorm xlnsconf.xlnsudFracnorm.ilog2 = xlnsconf.xlnsudFracnorm.ipallog2 xlnsconf.xlnsudFracnorm.ipow2 = xlnsconf.xlnsudFracnorm.ipalpow2 # Set global parameter in xlns xl.xlnssetF(10) def softmax(inp): max_vals = inp.max(axis=1) max_vals = xl.reshape(max_vals, (xl.size(max_vals), 1)) u = xl.exp(inp - max_vals) v = u.sum(axis=1) v = v.reshape((xl.size(v), 1)) u = u / v return u def main(main_params): print("arbitrary base np LNS. Also xl.hstack, xl routines in softmax") print("testing new softmax and * instead of @ for delta") print("works with type " + main_params['type']) is_training = bool(main_params['is_training']) leaking_coeff = float(main_params['leaking_coeff']) batchsize = int(main_params['minibatch_size']) lr = float(main_params['learning_rate']) num_epoch = int(main_params['num_epoch']) _lambda = float(main_params['lambda']) ones = np.array(list(np.ones((batchsize, 1)))) if is_training: # Load the MNIST dataset from Keras (x_train, y_train), (x_test, y_test) = mnist.load_data() # Normalize images to [0, 1] x_train = x_train.astype(np.float64) / 255.0 x_test = x_test.astype(np.float64) / 255.0 # One-hot encode the labels (assume 10 classes for MNIST digits 0-9) num_classes = 10 y_train = np.eye(num_classes)[y_train] y_test = np.eye(num_classes)[y_test] # Flatten the images from (28, 28) to (784,) x_train = x_train.reshape(x_train.shape[0], -1) x_test = x_test.reshape(x_test.shape[0], -1) # Use a portion of the training data for validation (the 'split' index) split = int(main_params['split']) x_val = x_train[split:] y_val = y_train[split:] x_train = x_train[:split] y_train = y_train[:split] # If available, load pretrained weights; otherwise, initialize new random weights. if os.path.isfile("./weightin.npz"): print("using ./weightin.npz") randfile = np.load("./weightin.npz", "r") W1 = randfile["W1"] W2 = randfile["W2"] randfile.close() else: print("using new random weights") # Note: The input layer now has 785 neurons (784 features + 1 bias). W1 = np.array(list(np.random.normal(0, 0.1, (785, 100)))) # The first hidden layer has 100 neurons; add bias so 101 W2 = np.array(list(np.random.normal(0, 0.1, (101, 10)))) np.savez_compressed("./weightout.npz", W1=W1, W2=W2) delta_W1 = np.array(list(np.zeros(W1.shape))) delta_W2 = np.array(list(np.zeros(W2.shape))) # Convert weights to desired type (xlns variants or float) if main_params['type'] == 'xlnsnp': lnsW1 = xl.xlnsnp(np.array(xl.xlnscopy(list(W1)))) lnsW2 = xl.xlnsnp(np.array(xl.xlnscopy(list(W2)))) lnsones = xl.xlnsnp(np.array(xl.xlnscopy(list(np.ones((batchsize, 1)))))) lnsdelta_W1 = xl.xlnsnp(np.array(xl.xlnscopy(list(np.zeros(W1.shape))))) lnsdelta_W2 = xl.xlnsnp(np.array(xl.xlnscopy(list(np.zeros(W2.shape))))) elif main_params['type'] == 'xlnsnpv': lnsW1 = xl.xlnsnpv(np.array(xl.xlnscopy(list(W1))), 6) lnsW2 = xl.xlnsnpv(np.array(xl.xlnscopy(list(W2))), 6) lnsones = xl.xlnsnpv(np.array(xl.xlnscopy(list(np.ones((batchsize, 1)))))) lnsdelta_W1 = xl.xlnsnpv(np.array(xl.xlnscopy(list(np.zeros(W1.shape))))) lnsdelta_W2 = xl.xlnsnpv(np.array(xl.xlnscopy(list(np.zeros(W2.shape))))) elif main_params['type'] == 'xlnsnpb': lnsW1 = xl.xlnsnpb(np.array(xl.xlnscopy(list(W1))), 2**2**-6) lnsW2 = xl.xlnsnpb(np.array(xl.xlnscopy(list(W2))), 2**2**-6) lnsones = xl.xlnsnpb(np.array(xl.xlnscopy(list(np.ones((batchsize, 1))))), 2**2**-xl.xlnsF) lnsdelta_W1 = xl.xlnsnpb(np.array(xl.xlnscopy(list(np.zeros(W1.shape)))), 2**2**-xl.xlnsF) lnsdelta_W2 = xl.xlnsnpb(np.array(xl.xlnscopy(list(np.zeros(W2.shape)))), 2**2**-xl.xlnsF) elif main_params['type'] == 'xlns': lnsW1 = np.array(xl.xlnscopy(list(W1))) lnsW2 = np.array(xl.xlnscopy(list(W2))) lnsones = np.array(xl.xlnscopy(list(np.ones((batchsize, 1))))) lnsdelta_W1 = np.array(xl.xlnscopy(list(np.zeros(W1.shape)))) lnsdelta_W2 = np.array(xl.xlnscopy(list(np.zeros(W2.shape)))) elif main_params['type'] == 'xlnsud': lnsW1 = np.array(xl.xlnscopy(list(W1), xl.xlnsud)) lnsW2 = np.array(xl.xlnscopy(list(W2), xl.xlnsud)) lnsones = np.array(xl.xlnscopy(list(np.ones((batchsize, 1))), xl.xlnsud)) lnsdelta_W1 = np.array(xl.xlnscopy(list(np.zeros(W1.shape)), xl.xlnsud)) lnsdelta_W2 = np.array(xl.xlnscopy(list(np.zeros(W2.shape)), xl.xlnsud)) elif main_params['type'] == 'xlnsv': lnsW1 = np.array(xl.xlnscopy(list(W1), xl.xlnsv, 6)) lnsW2 = np.array(xl.xlnscopy(list(W2), xl.xlnsv, 6)) lnsones = np.array(xl.xlnscopy(list(np.ones((batchsize, 1))), xl.xlnsv)) lnsdelta_W1 = np.array(xl.xlnscopy(list(np.zeros(W1.shape)), xl.xlnsv)) lnsdelta_W2 = np.array(xl.xlnscopy(list(np.zeros(W2.shape)), xl.xlnsv)) elif main_params['type'] == 'xlnsb': lnsW1 = np.array(xl.xlnscopy(list(W1), xl.xlnsb, 2**2**-6)) lnsW2 = np.array(xl.xlnscopy(list(W2), xl.xlnsb, 2**2**-6)) lnsones = np.array(xl.xlnscopy(list(np.ones((batchsize, 1))), xl.xlnsb, 2**2**-xl.xlnsF)) lnsdelta_W1 = np.array(xl.xlnscopy(list(np.zeros(W1.shape)), xl.xlnsb, 2**2**-xl.xlnsF)) lnsdelta_W2 = np.array(xl.xlnscopy(list(np.zeros(W2.shape)), xl.xlnsb, 2**2**-xl.xlnsF)) elif main_params['type'] == 'float': lnsW1 = np.array(list(W1)) lnsW2 = np.array(list(W2)) lnsones = np.array(list(np.ones((batchsize, 1)))) lnsdelta_W1 = np.array(list(np.zeros(W1.shape))) lnsdelta_W2 = np.array(list(np.zeros(W2.shape))) performance = {} performance['lnsacc_train'] = np.zeros(num_epoch) performance['lnsacc_val'] = np.zeros(num_epoch) start_time = time.process_time() # Training loop for epoch in range(num_epoch): print('At Epoch %d:' % (1 + epoch)) # Loop through training batches for mbatch in range(int(split / batchsize)): start = mbatch * batchsize x = np.array(x_train[start:(start + batchsize)]) y = np.array(y_train[start:(start + batchsize)]) # At this point, each x is already flattened (batchsize x 784) # Conversion based on type if main_params['type'] == 'xlnsnp': lnsx = xl.xlnsnp(np.array(xl.xlnscopy(np.array(x, dtype=np.float64)))) lnsy = xl.xlnsnp(np.array(xl.xlnscopy(np.array(y, dtype=np.float64)))) elif main_params['type'] == 'xlnsnpv': lnsx = xl.xlnsnpv(np.array(xl.xlnscopy(np.array(x, dtype=np.float64)))) lnsy = xl.xlnsnpv(np.array(xl.xlnscopy(np.array(y, dtype=np.float64)))) elif main_params['type'] == 'xlnsnpb': lnsx = xl.xlnsnpb(np.array(xl.xlnscopy(np.array(x, dtype=np.float64))), 2**2**-xl.xlnsF) lnsy = xl.xlnsnpb(np.array(xl.xlnscopy(np.array(y, dtype=np.float64))), 2**2**-xl.xlnsF) elif main_params['type'] == 'xlns': lnsx = np.array(xl.xlnscopy(np.array(x, dtype=np.float64))) lnsy = np.array(xl.xlnscopy(np.array(y, dtype=np.float64))) elif main_params['type'] == 'xlnsud': lnsx = np.array(xl.xlnscopy(np.array(x, dtype=np.float64), xl.xlnsud)) lnsy = np.array(xl.xlnscopy(np.array(y, dtype=np.float64), xl.xlnsud)) elif main_params['type'] == 'xlnsv': lnsx = np.array(xl.xlnscopy(np.array(x, dtype=np.float64), xl.xlnsv)) lnsy = np.array(xl.xlnscopy(np.array(y, dtype=np.float64), xl.xlnsv)) elif main_params['type'] == 'xlnsb': lnsx = np.array(xl.xlnscopy(np.array(x, dtype=np.float64), xl.xlnsv, 2**2**-xl.xlnsF)) lnsy = np.array(xl.xlnscopy(np.array(y, dtype=np.float64), xl.xlnsv, 2**2**-xl.xlnsF)) elif main_params['type'] == 'float': lnsx = np.array(x, dtype=np.float64) lnsy = np.array(y, dtype=np.float64) # Concatenate the bias "ones" with input features for the first layer lnss1 = xl.hstack((lnsones, lnsx)) @ lnsW1 lnsmask = (lnss1 > 0) + (leaking_coeff * (lnss1 < 0)) lnsa1 = lnss1 * lnsmask lnss2 = xl.hstack((lnsones, lnsa1)) @ lnsW2 lnsa2 = softmax(lnss2) lnsgrad_s2 = (lnsa2 - lnsy) / batchsize lnsgrad_a1 = lnsgrad_s2 @ xl.transpose(lnsW2[1:]) lnsdelta_W2 = xl.transpose(xl.hstack((lnsones, lnsa1))) * lnsgrad_s2 lnsgrad_s1 = lnsmask * lnsgrad_a1 lnsdelta_W1 = xl.transpose(xl.hstack((lnsones, lnsx))) * lnsgrad_s1 lnsW2 -= (lr * (lnsdelta_W2 + (_lambda * lnsW2))) lnsW1 -= (lr * (lnsdelta_W1 + (_lambda * lnsW1))) print('#= ', split, ' batch=', batchsize, ' lr=', lr) lnscorrect_count = 0 # Evaluate accuracy on training set for mbatch in range(int(split / batchsize)): start = mbatch * batchsize x = x_train[start:(start + batchsize)] y = y_train[start:(start + batchsize)] if main_params['type'] == 'xlnsnp': lnsx = xl.xlnsnp(np.array(xl.xlnscopy(np.array(x, dtype=np.float64)))) elif main_params['type'] == 'xlnsnpv': lnsx = xl.xlnsnpv(np.array(xl.xlnscopy(np.array(x, dtype=np.float64)))) elif main_params['type'] == 'xlnsnpb': lnsx = xl.xlnsnpb(np.array(xl.xlnscopy(np.array(x, dtype=np.float64))), 2**2**-xl.xlnsF) elif main_params['type'] == 'xlns': lnsx = np.array(xl.xlnscopy(np.array(x, dtype=np.float64))) elif main_params['type'] == 'xlnsud': lnsx = np.array(xl.xlnscopy(np.array(x, dtype=np.float64), xl.xlnsud)) elif main_params['type'] == 'xlnsv': lnsx = np.array(xl.xlnscopy(np.array(x, dtype=np.float64), xl.xlnsv)) elif main_params['type'] == 'xlnsb': lnsx = np.array(xl.xlnscopy(np.array(x, dtype=np.float64), xl.xlnsv, 2**2**-xl.xlnsF)) elif main_params['type'] == 'float': lnsx = np.array(x, dtype=np.float64) lnss1 = xl.hstack((lnsones, lnsx)) @ lnsW1 lnsmask = (lnss1 > 0) + (leaking_coeff * (lnss1 < 0)) lnsa1 = lnss1 * lnsmask lnss2 = xl.hstack((lnsones, lnsa1)) @ lnsW2 lnscorrect_count += np.sum(np.argmax(y, axis=1) == xl.argmax(lnss2, axis=1)) lnsaccuracy = lnscorrect_count / split print("train-set accuracy at epoch %d: %f" % (1 + epoch, lnsaccuracy)) performance['lnsacc_train'][epoch] = 100 * lnsaccuracy lnscorrect_count = 0 # Evaluate on the validation set for mbatch in range(int(split / batchsize)): start = mbatch * batchsize x = x_val[start:(start + batchsize)] y = y_val[start:(start + batchsize)] if main_params['type'] == 'xlnsnp': lnsx = xl.xlnsnp(np.array(xl.xlnscopy(np.array(x, dtype=np.float64)))) elif main_params['type'] == 'xlnsnpv': lnsx = xl.xlnsnpv(np.array(xl.xlnscopy(np.array(x, dtype=np.float64)))) elif main_params['type'] == 'xlnsnpb': lnsx = xl.xlnsnpb(np.array(xl.xlnscopy(np.array(x, dtype=np.float64))), 2**2**-xl.xlnsF) elif main_params['type'] == 'xlns': lnsx = np.array(xl.xlnscopy(np.array(x, dtype=np.float64))) elif main_params['type'] == 'xlnsud': lnsx = np.array(xl.xlnscopy(np.array(x, dtype=np.float64), xl.xlnsud)) elif main_params['type'] == 'xlnsv': lnsx = np.array(xl.xlnscopy(np.array(x, dtype=np.float64), xl.xlnsv)) elif main_params['type'] == 'xlnsb': lnsx = np.array(xl.xlnscopy(np.array(x, dtype=np.float64), xl.xlnsv, 2**2**-xl.xlnsF)) elif main_params['type'] == 'float': lnsx = np.array(x, dtype=np.float64) lnss1 = xl.hstack((lnsones, lnsx)) @ lnsW1 lnsmask = (lnss1 > 0) + (leaking_coeff * (lnss1 < 0)) lnsa1 = lnss1 * lnsmask lnss2 = xl.hstack((lnsones, lnsa1)) @ lnsW2 lnscorrect_count += np.sum(np.argmax(y, axis=1) == xl.argmax(lnss2, axis=1)) lnsaccuracy = lnscorrect_count / split print("Val-set accuracy at epoch %d: %f" % (1 + epoch, lnsaccuracy)) performance['lnsacc_val'][epoch] = 100 * lnsaccuracy print("elapsed time=" + str(time.process_time() - start_time)) fig = plt.figure(figsize=(16, 9)) ax = fig.add_subplot(111) x_axis = range(1, 1 + performance['lnsacc_train'].size) ax.plot(x_axis, performance['lnsacc_train'], 'y') ax.plot(x_axis, performance['lnsacc_val'], 'm') ax.set_xlabel('Number of Epochs') ax.set_ylabel('Accuracy') plt.suptitle(main_params['type'] + ' ' + str(split) + ' Validation and Training MNIST Accuracies F=' + str(xl.xlnsF), fontsize=14) ax.legend(['train', 'validation']) plt.grid(which='both', axis='both', linestyle='-.') plt.savefig('genericaccuracy.png') plt.show() # Now, show predictions on a few test images num_examples = 5 # Number of test images to display selected_indices = np.arange(num_examples) # choose the first few images for demo x_sample = x_test[selected_indices] y_sample = y_test[selected_indices] # For prediction, create a bias vector matching the sample size ones_sample = np.ones((x_sample.shape[0], 1)) z1_sample = np.hstack((ones_sample, x_sample)) @ lnsW1 mask_sample = (z1_sample > 0) + (leaking_coeff * (z1_sample < 0)) a1_sample = z1_sample * mask_sample z2_sample = np.hstack((ones_sample, a1_sample)) @ lnsW2 pred_probs = softmax(z2_sample) predictions = np.argmax(pred_probs, axis=1) true_labels = np.argmax(y_sample, axis=1) # Plot each test image along with its prediction and true label plt.figure(figsize=(10, 2)) for i in range(num_examples): plt.subplot(1, num_examples, i + 1) # Reshape the flattened image back to 28x28 for display plt.imshow(x_sample[i].reshape(28, 28), cmap='gray') plt.title(f"Pred: {predictions[i]}\nTrue: {true_labels[i]}") plt.axis('off') plt.tight_layout() plt.show() if __name__ == '__main__': # In a Kaggle notebook, set parameters manually using a dictionary. main_params = { 'is_training': True, 'split': 50, 'learning_rate': 0.01, 'lambda': 0.000, 'minibatch_size': 1, 'num_epoch': 5, 'leaking_coeff': 0.0078125, 'type': 'float' } main(main_params)


Te guiaré a través de este código que implementa un perceptrón multicapa (MLP) del sistema de numeración logarítmica (LNS) para la clasificación de dígitos MNIST. Permíteme dividirlo en secciones clave:


  1. Configuración e importaciones:
  • El código utiliza la biblioteca xlns para operaciones del sistema numérico logarítmico.

  • Ofrece múltiples variantes de LNS (xlnsnp, xlnsnpv, xlnsud, etc.) para diferentes compensaciones de precisión y rendimiento.

  • El conjunto de datos MNIST se carga a través de Keras


  1. Funciones principales:
 def softmax(inp): max_vals = inp.max(axis=1) max_vals = xl.reshape(max_vals, (xl.size(max_vals), 1)) u = xl.exp(inp - max_vals) v = u.sum(axis=1) v = v.reshape((xl.size(v), 1)) u = u / v return u

Esta es una implementación de softmax numéricamente estable adaptada para operaciones LNS.


  1. Arquitectura de red:
  • Capa de entrada: 784 neuronas (imágenes MNIST aplanadas de 28x28) + 1 sesgo = 785

  • Capa oculta: 100 neuronas + 1 sesgo = 101

  • Capa de salida: 10 neuronas (una por dígito)


  1. Inicialización de peso:
  • Los pesos se cargan desde un archivo ("weightin.npz") o se inicializan aleatoriamente

  • Los pesos aleatorios utilizan una distribución normal con media = 0, desviación estándar = 0,1

  • Las diferentes variantes de LNS requieren diferentes métodos de inicialización (xlnsnp, xlnsnpv, etc.)


  1. Bucle de entrenamiento:
 for epoch in range(num_epoch): for mbatch in range(int(split / batchsize)): # Forward pass lnss1 = xl.hstack((lnsones, lnsx)) @ lnsW1 lnsmask = (lnss1 > 0) + (leaking_coeff * (lnss1 < 0)) lnsa1 = lnss1 * lnsmask lnss2 = xl.hstack((lnsones, lnsa1)) @ lnsW2 lnsa2 = softmax(lnss2) # Backward pass lnsgrad_s2 = (lnsa2 - lnsy) / batchsize lnsgrad_a1 = lnsgrad_s2 @ xl.transpose(lnsW2[1:]) lnsdelta_W2 = xl.transpose(xl.hstack((lnsones, lnsa1))) * lnsgrad_s2 lnsgrad_s1 = lnsmask * lnsgrad_a1 lnsdelta_W1 = xl.transpose(xl.hstack((lnsones, lnsx))) * lnsgrad_s1


Aspectos claves de la formación:

  • Utiliza activación ReLU con fugas (controlada por leaking_coeff)

  • Implementa retropropagación estándar pero con operaciones LNS

  • Incluye regularización L2 (parámetro lambda)

  • Actualiza los pesos utilizando el descenso de gradiente con la tasa de aprendizaje 'lr'


  1. Evaluación:
  • Realiza un seguimiento de la precisión tanto del entrenamiento como de la validación.

  • Traza curvas de aprendizaje que muestran la precisión a lo largo de las épocas.

  • Muestra predicciones de muestra en imágenes de prueba.


  1. Hiperparámetros:
 main_params = { 'is_training': True, 'split': 50, 'learning_rate': 0.01, 'lambda': 0.000, 'minibatch_size': 1, 'num_epoch': 5, 'leaking_coeff': 0.0078125, 'type': 'float' }
  • Utiliza descenso de gradiente de minilotes (tamaño de lote predeterminado = 1)

  • Implementa la detención temprana a través de la división del conjunto de validación

  • El coeficiente Leaky ReLU se establece en 0,0078125


  1. Visualización:
  • Crea gráficos que muestran la precisión del entrenamiento y la validación.
  • Muestra imágenes de prueba de muestra con predicciones y etiquetas verdaderas.
  • Guarda el gráfico de precisión como 'genericaccuracy.png'


La innovación clave aquí es el uso de la aritmética LNS, que reemplaza las multiplicaciones por sumas en el dominio logarítmico, lo que potencialmente ofrece una mejor eficiencia computacional para ciertas implementaciones de hardware. El código admite múltiples variantes de LNS, lo que permite diferentes compensaciones entre precisión y rendimiento.

Comparación básica del rendimiento

Rendimiento del modelo de punto flotante

 Training on device: cuda Epoch [1/5], Loss: 0.8540, Train Acc: 79.60%, Val Acc: 88.22% Epoch [2/5], Loss: 0.3917, Train Acc: 88.97%, Val Acc: 89.92% Epoch [3/5], Loss: 0.3380, Train Acc: 90.29%, Val Acc: 90.60% Epoch [4/5], Loss: 0.3104, Train Acc: 90.96%, Val Acc: 91.12% Epoch [5/5], Loss: 0.2901, Train Acc: 91.60%, Val Acc: 91.62% Training completed in 57.76 seconds. 

Predicciones del modelo MLP basado en FP

Curva de entrenamiento y validación para el modelo MLP basado en FP


Rendimiento del modelo del sistema de numeración logarítmica

 At Epoch 1: train-set accuracy at epoch 1: 52.00% Val-set accuracy at epoch 1: 24.00% At Epoch 2: train-set accuracy at epoch 2: 74.00% Val-set accuracy at epoch 2: 40.00% At Epoch 3: train-set accuracy at epoch 3: 86.00% Val-set accuracy at epoch 3: 58.00% At Epoch 4: train-set accuracy at epoch 4: 94.00% Val-set accuracy at epoch 4: 70.00% At Epoch 5: train-set accuracy at epoch 5: 96.00% Val-set accuracy at epoch 5: 68.00% elapsed time = 0.35 seconds. 

Predicciones del modelo MLP basado en LNS

Curva de entrenamiento y validación para el modelo MLP basado en LNS


FP vs. LNS: Comparaciones clave

Aspecto

Punto flotante (FP)

Sistema de numeración logarítmica (LNS)

Tiempo de entrenamiento

57,76 s

0,35 s

Precisión del tren

91,60%

96,00%

Precisión de Val

91,62%

68,00%

Precisión

Alto

Inferior (errores de aproximación)

Eficiencia de la memoria

Mayor uso

Menor consumo de memoria

Manejo de multiplicaciones

Multiplicación nativa

Simplificaciones basadas en la adición

Conclusión

Las ventajas y desventajas entre el sistema de numeración logarítmica (LNS) y la aritmética de punto flotante (FP) presentan un caso de estudio interesante en el diseño conjunto de hardware y software para redes neuronales. Si bien el LNS ofrece ventajas significativas en ciertas áreas:

Velocidad de entrenamiento

  • Reemplaza la multiplicación por la suma en el dominio logarítmico.
  • Reduce operaciones complejas a operaciones aritméticas más simples.
  • Particularmente eficiente para multiplicaciones de matrices en redes neuronales.
  • Puede lograr una aceleración de 2 a 3 veces en algunas implementaciones

Beneficios de la memoria

  • Generalmente requiere menos bits para representar números
  • Puede comprimir pesos y activaciones de manera más eficiente.
  • Reduce los requisitos de ancho de banda de memoria
  • Menor consumo de energía para el acceso a la memoria


Sin embargo, los desafíos en materia de precisión son significativos:

  • Pérdida de precisión durante la acumulación de valores pequeños
  • Dificultad para representar números muy cercanos al cero
  • Posible inestabilidad en los cálculos de gradientes
  • Puede requerir un ajuste cuidadoso de los hiperparámetros

Direcciones futuras

Varios enfoques prometedores podrían mejorar la aplicabilidad de LNS:

1. Aritmética específica de capas

  • Utilice FP para capas sensibles (como la clasificación final)
  • Aplicar LNS en capas ocultas con gran capacidad computacional
  • Cambiar dinámicamente según requisitos numéricos

2. Computación adaptativa de precisión

  • Empieza a entrenar con FP para lograr estabilidad
  • Transición gradual a LNS a medida que los pesos convergen
  • Mantener rutas críticas con mayor precisión

3. Co-diseño de hardware

  • Aceleradores personalizados con unidades FP y LNS
  • Programación inteligente entre tipos aritméticos
  • Jerarquías de memoria especializadas para cada formato

4. Innovaciones algorítmicas

  • Nuevas funciones de activación optimizadas para LNS
  • Algoritmos de optimización modificados que mantienen la estabilidad
  • Representaciones numéricas híbridas

Posible compatibilidad con PyTorch

Para integrar LNS en marcos de aprendizaje profundo, se podría explorar lo siguiente:

1. Funciones personalizadas de Autograd

  • Implementar operaciones LNS como funciones de autograd personalizadas
  • Mantener el cálculo del gradiente en el dominio logarítmico
  • Proporcionar kernels CUDA eficientes para la aceleración

2. Extensiones de tipos de números

  • Agregar tipos de tensores LNS nativos
  • Implementar operaciones centrales (*+, -, , / ) en el dominio de registro
  • Proporcionar utilidades de conversión a/desde punto flotante

3. Modificaciones de capas

  • Crear versiones LNS de capas comunes (Lineal, Conv2d)
  • Optimizar los pases hacia atrás para el cálculo de LNS
  • Admite entrenamiento de precisión mixto


La comunidad de aprendizaje profundo podría beneficiarse enormemente de la integración de estas capacidades en los marcos convencionales, lo que permitiría redes neuronales más eficientes, de bajo consumo y alta velocidad .


¿Qué piensas sobre el equilibrio entre precisión numérica y eficiencia computacional? ¿Has encontrado casos de uso específicos en los que LNS podría ser particularmente beneficioso?


Déjame saber tu opinión sobre esto.

Referencias


[1] G. Alsuhli, et al., “Sistemas numéricos para arquitecturas de redes neuronales profundas: una encuesta”, arXiv:2307.05035 , 2023.

[2] M. Arnold, E. Chester, et al., “Entrenamiento de redes neuronales utilizando solo una ALU LNS aproximada sin tablas”. 31.ª Conferencia internacional sobre sistemas, arquitecturas y procesadores específicos de la aplicación, IEEE , 2020, págs. 69-72. DOI

[3] O. Kosheleva, et al., “El sistema de numeración logarítmica es óptimo para los cálculos de IA: explicación teórica del éxito empírico”, artículo

[4] D. Miyashita, et al., “Redes neuronales convolucionales utilizando representación de datos logarítmicos”, arXiv:1603.01025 , marzo de 2016.

[5] J. Zhao et al., “LNS-Madam: Entrenamiento de baja precisión en el sistema de numeración logarítmica mediante actualización de peso multiplicativo”, IEEE Transactions on Computers , vol. 71, núm. 12, págs. 3179–3190, diciembre de 2022. DOI