paint-brush
Deep Learning roda em matemática de ponto flutuante. E se isso for um erro?por@abhiyanampally_kob9nse8
549 leituras
549 leituras

Deep Learning roda em matemática de ponto flutuante. E se isso for um erro?

por 40m2025/02/11
Read on Terminal Reader

Muito longo; Para ler

O Logarithmic Number System (LNS) é uma representação numérica alternativa à aritmética de ponto flutuante (FP). O LNS representa números em uma escala de logaritmo, convertendo multiplicações em adições, o que pode ser computacionalmente mais barato em certas arquiteturas de hardware. No entanto, adição e subtração no LNS exigem aproximações, levando a uma precisão reduzida. Usamos o LNS para treinar um Perceptron Multicamadas simples totalmente conectado no MNIST.
featured image - Deep Learning roda em matemática de ponto flutuante. E se isso for um erro?
undefined HackerNoon profile picture
0-item
1-item

Quando me deparei pela primeira vez com a ideia de usar o Logarithmic Number System (LNS) em aprendizado profundo, fiquei intrigado, mas também cético. Como a maioria de nós, sempre trabalhei com aritmética de ponto flutuante (FP) — o padrão para computação numérica em aprendizado profundo. O FP fornece um bom equilíbrio entre precisão e alcance, mas vem com compensações: maior uso de memória, maior complexidade computacional e maior consumo de energia. Então, decidi experimentar e ver por mim mesmo — como o LNS se compara ao FP ao treinar um Multi-Layer Perceptron (MLP) simples totalmente conectado no MNIST?

Por que considerar o LNS?

O LNS representa números em uma escala logarítmica, convertendo multiplicações em adições, o que pode ser computacionalmente mais barato em certas arquiteturas de hardware. Essa eficiência vem ao custo da precisão, especialmente em operações de adição e subtração, que são mais complexas no LNS. No entanto, os benefícios potenciais — menor pegada de memória, cálculos mais rápidos e menor consumo de energia — me deixaram curioso o suficiente para experimentá-lo.

Contexto: Sistema de numeração de ponto flutuante vs. logarítmico

Representação de ponto flutuante (FP)

A aritmética de ponto flutuante é a representação numérica padrão na maioria das estruturas de aprendizado profundo, como PyTorch e TensorFlow. Os números FP têm:


  • Um bit de sinal (determinando valor positivo ou negativo)
  • Um expoente (fator de escala)
  • Uma mantissa (significando) (precisão do número)


FP32 (precisão simples) é comumente usado em aprendizado profundo, oferecendo um equilíbrio entre precisão numérica e eficiência computacional. Formatos mais eficientes como FP16 e BF16 estão ganhando popularidade para acelerar o treinamento.

Sistema de Numeração Logarítmica (LNS)

LNS é uma representação numérica alternativa onde os números são armazenados como logaritmos: [ x = \log_b (y) ] onde ( b ) é a base do logaritmo. LNS tem várias vantagens:


  • A multiplicação é simplificada para adição : ( x_1 * x_2 = b^{(\log_b x_1 + \log_b x_2)} )
  • A divisão é simplificada para subtração : ( x_1 / x_2 = b^{(\log_b x_1 - \log_b x_2)} )
  • Funções de crescimento exponencial tornam-se lineares


Entretanto, adição e subtração no LNS exigem aproximações, o que leva à redução da precisão.

Operações Aritméticas LNS

Para explorar mais o LNS, implementei operações aritméticas básicas, como adição, subtração, multiplicação e divisão, usando representações internas do 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)


Aqui está uma análise da minha implementação experimental do Sistema de Numeração Logarítmica (LNS).

1. Conceito básico de LNS e desafios no PyTorch

No LNS, os números são representados como logaritmos, o que transforma multiplicação e divisão em adição e subtração. No entanto, implementar isso com PyTorch apresenta desafios específicos, pois os tensores PyTorch usam representações de ponto flutuante internamente. Isso cria vários requisitos:


  • Manter representação logarítmica durante todos os cálculos.
  • Garantir estabilidade numérica.
  • Lide com conversões com cuidado.
  • Gerencie a representação interna usando dois componentes:
    • x : o valor logarítmico.
    • s : um bit de sinal (0 ou 1).

2. Representação interna e conversão

O primeiro passo é converter números de ponto flutuante para sua representação 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)


O uso de dtype=torch.int64 é crucial porque:

  • Ele preserva a representação interna exata do LNS sem erros de arredondamento de ponto flutuante.
  • Empacota o valor logarítmico e o bit de sinal em um único inteiro.
  • Evita que operações de ponto flutuante não intencionais corrompam a representação do LNS.

3. Operações Aritméticas Básicas

a) Multiplicação

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

A multiplicação em LNS se torna adição:

  • Se a = log(x) e b = log(y) , então log(x×y) = log(x) + log(y) .

b) Divisão

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

Divisão se torna subtração:

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

c) Adição

 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


A adição é mais complexa e numericamente sensível porque:

  • Envolve operações exponenciais e logarítmicas.
  • A implementação direta de ponto flutuante pode levar a estouro/subfluxo.
  • Usa a equação: log(x + y) = log(max(x,y)) + log(1 + exp(log(min(x,y)) - log(max(x,y)))) .
  • Usa log1p em vez de log(1 + x) direto para melhor estabilidade numérica.

4. Segurança de Tipo e Gerenciamento de Conversão

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


O pipeline de conversão mantém uma separação clara:

  1. Converter de float → representação interna do LNS (inteiros).
  2. Execute operações LNS usando aritmética de inteiros.
  3. Converta novamente para float somente quando necessário.
 # 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. Principais vantagens e limitações

Vantagens

  • Multiplicação e divisão são simplificadas para adição e subtração.
  • Ampla faixa dinâmica com aritmética de ponto fixo.
  • Potencialmente mais eficiente para certas aplicações.

Limitações

  • Adição e subtração são operações mais complexas .
  • Sobrecarga de conversão entre ponto flutuante e LNS.
  • Requer tratamento especial para números zero e negativos.
  • A compatibilidade do tensor PyTorch requer um gerenciamento cuidadoso de tipos.

6. Possibilidades de otimização

Para melhorar o desempenho, pode-se:

  1. Implemente uma função de autograd personalizada do PyTorch para operações LNS.
  2. Crie um tipo de tensor personalizado que suporte LNS nativamente.
  3. Use kernels CUDA para operações LNS eficientes na GPU.


A implementação atual faz compensações práticas:

  • Prioriza clareza e manutenibilidade em detrimento do desempenho máximo.
  • Utiliza a infraestrutura de tensor existente do PyTorch, preservando a precisão do LNS.
  • Mantém a estabilidade numérica por meio de um gerenciamento cuidadoso de tipos.
  • Minimiza conversões entre representações .

7. Exemplo de fluxo de dados

As etapas a seguir demonstram o pipeline completo usando valores de exemplo [2.0, 3.0] e [-1.0, 0.0] :

  1. Converta floats de entrada em representação interna do LNS.
  2. Crie tensores inteiros para armazenar valores LNS.
  3. Executar operações aritméticas no domínio LNS.
  4. Converta os resultados novamente para ponto flutuante.
  5. Crie tensores PyTorch finais para processamento posterior.


Esta implementação preenche com sucesso a lacuna entre o sistema tensor de ponto flutuante do PyTorch e a aritmética LNS, mantendo a estabilidade numérica e a precisão.


Treinando um MLP totalmente conectado no conjunto de dados MNIST Digit com FP e LNS

Configuração do experimento

Treinei um MLP totalmente conectado no conjunto de dados MNIST usando representações FP e LNS. A arquitetura do modelo era simples:

  • Camada de entrada: 784 neurônios (imagens achatadas de 28x28)
  • Camadas ocultas: Duas camadas com 256 e 128 neurônios, ativações ReLU
  • Camada de saída: 10 neurônios (um para cada dígito, usando softmax)
  • Função de perda: Entropia cruzada
  • Otimizador: Adam


Para a implementação do LNS, tive que sair do meu fluxo de trabalho usual do PyTorch. Ao contrário do FP, que o PyTorch suporta nativamente, o PyTorch não fornece operações LNS integradas. Encontrei um projeto do GitHub chamado xlns , que implementa representações numéricas logarítmicas e aritmética, tornando possível usar o LNS em redes neurais.

MLP de ponto flutuante em PyTorch

Começamos implementando um MLP totalmente conectado baseado em FP padrão 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 implementação segue um pipeline de aprendizado profundo convencional, onde multiplicações e adições são tratadas pela aritmética FP.


Aqui está um passo a passo detalhado desta implementação do PyTorch de um Perceptron Multicamadas (MLP) para o conjunto de dados MNIST.

  1. Arquitetura do modelo (classe 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. Passe para frente:
 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. Configuração de treinamento:
 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 principais:

  • Suporte de GPU por meio da seleção de dispositivos

  • Normalização de dados para melhor treinamento

  • Hiperparâmetros configuráveis


  1. Gerenciamento de conjunto de dados:
 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])
  • Baixa o conjunto de dados MNIST se não estiver presente

  • Divide os dados em conjuntos de treinamento (80%) e validação (20%)


  1. Circuito de treinamento:
 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

Procedimento de treinamento clássico:

  • Gradientes zero

  • Passe para frente

  • Cálculo de perdas

  • Passe para trás

  • Atualizações de peso


  1. Validação:
 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()

Principais características:

  • Modelo definido para modo de avaliação

  • Não é necessário cálculo de gradiente

  • Cálculo de precisão


  1. Visualização:
 def show_predictions(model, data_loader, device, num_images=6): model.eval() plt.figure(figsize=(12, 8)) # Display predictions vs actual labels
  • Mostra previsões de amostra do conjunto de validação

  • Útil para avaliação qualitativa


  1. Acompanhamento de desempenho:
 # 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')
  • Acompanha a precisão do treinamento e da validação

  • Traça curvas de aprendizagem

  • Mede o tempo de treinamento


Isso fornece uma base sólida para comparação com implementações baseadas em LNS, pois implementa todos os componentes padrão de um pipeline de aprendizado profundo usando aritmética de ponto flutuante tradicional.

Sistema de numeração logarítmica (LNS) MLP

Para LNS, precisamos usar a biblioteca xlns . Ao contrário do FP, o LNS substitui operações pesadas de multiplicação por adição no domínio logarítmico. No entanto, o PyTorch não oferece suporte nativo para isso, então temos que aplicar manualmente as operações LNS quando aplicável.

 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)


Vou guiá-lo por este código que implementa um Logarithmic Number System (LNS) Multi-Layer Perceptron (MLP) para classificação de dígitos MNIST. Deixe-me dividi-lo em seções principais:


  1. Configuração e importações:
  • O código usa a biblioteca xlns para operações do sistema numérico logarítmico

  • Ele oferece várias variantes de LNS (xlnsnp, xlnsnpv, xlnsud, etc.) para diferentes compensações de precisão e desempenho

  • O conjunto de dados MNIST é carregado por meio do Keras


  1. Funções principais:
 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 é uma implementação softmax numericamente estável adaptada para operações LNS.


  1. Arquitetura de rede:
  • Camada de entrada: 784 neurônios (imagens MNIST achatadas 28x28) + 1 viés = 785

  • Camada oculta: 100 neurônios + 1 viés = 101

  • Camada de saída: 10 neurônios (um por dígito)


  1. Inicialização de peso:
  • Os pesos são carregados de um arquivo ("weightin.npz") ou inicializados aleatoriamente

  • Pesos aleatórios usam distribuição normal com média = 0, dp = 0,1

  • Diferentes variantes de LNS requerem diferentes métodos de inicialização (xlnsnp, xlnsnpv, etc.)


  1. Loop de treinamento:
 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 principais do treinamento:

  • Usa ativação ReLU com vazamento (controlada por leaking_coeff)

  • Implementa retropropagação padrão, mas com operações LNS

  • Inclui regularização L2 (parâmetro lambda)

  • Atualiza pesos usando descida de gradiente com taxa de aprendizado 'lr'


  1. Avaliação:
  • Rastreia a precisão do treinamento e da validação

  • Traça curvas de aprendizagem mostrando precisão ao longo de épocas

  • Exibe previsões de amostra em imagens de teste


  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' }
  • Usa descida de gradiente de mini-lote (tamanho de lote padrão = 1)

  • Implementa a parada antecipada por meio da divisão do conjunto de validação

  • O coeficiente Leaky ReLU é definido como 0,0078125


  1. Visualização:
  • Cria gráficos que mostram a precisão do treinamento e da validação
  • Exibe imagens de teste de amostra com previsões e rótulos verdadeiros
  • Salva o gráfico de precisão como 'genericaccuracy.png'


A principal inovação aqui é o uso da aritmética LNS que substitui multiplicações por adições no domínio de log, potencialmente oferecendo melhor eficiência computacional para certas implementações de hardware. O código suporta múltiplas variantes LNS permitindo diferentes compensações de precisão-desempenho.

Comparação de desempenho básico

Desempenho do modelo de ponto flutuante

 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. 

Previsões do modelo MLP baseado em FP

Curva de treinamento e validação para modelo MLP baseado em FP


Desempenho do modelo do sistema numérico logarítmico

 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. 

Previsões do modelo MLP baseado em LNS

Curva de treinamento e validação para modelo MLP baseado em LNS


FP vs. LNS: Principais comparações

Aspecto

Ponto Flutuante (FP)

Sistema de Numeração Logarítmica (LNS)

Tempo de treinamento

57,76s

0,35s

Precisão do trem

91,60%

96,00%

Precisão Val

91,62%

68,00%

Precisão

Alto

Inferior (erros de aproximação)

Eficiência de memória

Maior uso

Menor consumo de memória

Manipulação de multiplicação

Multiplicação nativa

Simplificações baseadas em adição

Conclusão

As compensações entre o Logarithmic Number System (LNS) e a aritmética de ponto flutuante (FP) apresentam um estudo de caso interessante em co-design de hardware-software para redes neurais. Enquanto o LNS oferece vantagens significativas em certas áreas:

Velocidade de treinamento

  • Substitui multiplicação por adição no domínio de log
  • Reduz operações complexas a aritméticas mais simples
  • Particularmente eficiente para multiplicações de matrizes em redes neurais
  • Pode atingir uma aceleração de 2 a 3x em algumas implementações

Benefícios para a memória

  • Normalmente requer menos bits para representar números
  • Pode comprimir pesos e ativações de forma mais eficiente
  • Reduz os requisitos de largura de banda da memória
  • Menor consumo de energia para acesso à memória


No entanto, os desafios de precisão são significativos:

  • Perda de precisão durante a acumulação de pequenos valores
  • Dificuldade em representar números muito próximos de zero
  • Instabilidade potencial em cálculos de gradiente
  • Pode exigir um ajuste cuidadoso do hiperparâmetro

Direções futuras

Várias abordagens promissoras podem melhorar a aplicabilidade do LNS:

1. Aritmética específica de camada

  • Use FP para camadas sensíveis (como classificação final)
  • Aplique LNS em camadas ocultas com uso intensivo de computação
  • Alterne dinamicamente com base em requisitos numéricos

2. Computação Adaptativa de Precisão

  • Comece a treinar com FP para estabilidade
  • Transição gradual para LNS conforme os pesos convergem
  • Manter caminhos críticos com maior precisão

3. Co-design de hardware

  • Aceleradores personalizados com unidades FP e LNS
  • Agendamento inteligente entre tipos aritméticos
  • Hierarquias de memória especializadas para cada formato

4. Inovações Algorítmicas

  • Novas funções de ativação otimizadas para LNS
  • Algoritmos de otimização modificados que mantêm a estabilidade
  • Representações numéricas híbridas

Suporte potencial do PyTorch

Para integrar o LNS em estruturas de aprendizado profundo, o seguinte poderia ser explorado:

1. Funções de Autograd personalizadas

  • Implementar operações LNS como funções de autograd personalizadas
  • Manter a computação de gradiente no domínio do log
  • Fornecer kernels CUDA eficientes para aceleração

2. Extensões de tipo numérico

  • Adicionar tipos de tensor LNS nativos
  • Implementar operações principais (*+, -, , / ) no domínio de log
  • Fornece utilitários de conversão de/para ponto flutuante

3. Modificações de Camadas

  • Crie versões LNS de camadas comuns (Linear, Conv2d)
  • Otimizar passagens para trás para computação LNS
  • Suporte para treinamento de precisão mista


A comunidade de aprendizado profundo poderia se beneficiar muito da integração desses recursos em estruturas tradicionais, permitindo redes neurais mais eficientes, de baixo consumo de energia e alta velocidade .


Quais são seus pensamentos sobre o equilíbrio entre precisão numérica e eficiência computacional? Você encontrou casos de uso específicos em que o LNS pode ser particularmente benéfico?


Deixe-me saber o que você pensa sobre isso.

Referências


[1] G. Alsuhli, et al., “Sistemas numéricos para arquiteturas de redes neurais profundas: uma pesquisa”, arXiv:2307.05035 , 2023.

[2] M. Arnold, E. Chester, et al., “Treinamento de redes neurais usando apenas uma ALU LNS sem tabela aproximada.” 31ª Conferência Internacional sobre Sistemas, Arquiteturas e Processadores Específicos de Aplicação, IEEE , 2020, pp. 69–72. DOI

[3] O. Kosheleva, et al., “O sistema de numeração logarítmica é ótimo para computações de IA: explicação teórica do sucesso empírico”, artigo

[4] D. Miyashita, et al., “Redes neurais convolucionais usando representação de dados logarítmicos”, arXiv:1603.01025 , março de 2016.

[5] J. Zhao et al., “LNS-Madam: Treinamento de baixa precisão em sistema de numeração logarítmica usando atualização de peso multiplicativo”, IEEE Transactions on Computers , vol. 71, no. 12, pp. 3179–3190, dezembro de 2022. DOI