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 (determinando valor positivo ou negativo) bit de sinal Um (fator de escala) expoente Uma (precisão do número) mantissa (significando) 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: : ( x_1 * x_2 = b^{(\log_b x_1 + \log_b x_2)} ) 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 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: : o valor logarítmico. x : um bit de sinal (0 ou 1). s 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 é crucial porque: dtype=torch.int64 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 e , então . a = log(x) b = log(y) 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 em vez de direto para melhor estabilidade numérica. log1p log(1 + x) 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: Converter de float → representação interna do LNS (inteiros). Execute operações LNS usando aritmética de inteiros. 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 para adição e subtração. Multiplicação e divisão são simplificadas com aritmética de ponto fixo. Ampla faixa dinâmica para certas aplicações. Potencialmente mais eficiente Limitações . Adição e subtração são operações mais complexas entre ponto flutuante e LNS. Sobrecarga de conversão para números zero e negativos. Requer tratamento especial requer um gerenciamento cuidadoso de tipos. A compatibilidade do tensor PyTorch 6. Possibilidades de otimização Para melhorar o desempenho, pode-se: Implemente uma para operações LNS. função de autograd personalizada do PyTorch Crie um que suporte LNS nativamente. tipo de tensor personalizado Use para operações LNS eficientes na GPU. kernels CUDA A implementação atual faz compensações práticas: Prioriza em detrimento do desempenho máximo. clareza e manutenibilidade Utiliza do PyTorch, preservando a precisão do LNS. a infraestrutura de tensor existente Mantém por meio de um gerenciamento cuidadoso de tipos. a estabilidade numérica Minimiza . conversões entre representações 7. Exemplo de fluxo de dados As etapas a seguir demonstram o pipeline completo usando valores de exemplo e : [2.0, 3.0] [-1.0, 0.0] Converta floats de entrada em representação interna do LNS. Crie tensores inteiros para armazenar valores LNS. Executar operações aritméticas no domínio LNS. Converta os resultados novamente para ponto flutuante. 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: 784 neurônios (imagens achatadas de 28x28) Camada de entrada: Duas camadas com 256 e 128 neurônios, ativações ReLU Camadas ocultas: 10 neurônios (um para cada dígito, usando softmax) Camada de saída: Entropia cruzada Função de perda: Adam Otimizador: 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 , que implementa representações numéricas logarítmicas e aritmética, tornando possível usar o LNS em redes neurais. xlns 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. 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) 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 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 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%) 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 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 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 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 . 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. xlns 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: Configuração e importações: O código usa a biblioteca para operações do sistema numérico logarítmico xlns 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 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. 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) 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.) 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' 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 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 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. 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. 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 e apresentam um estudo de caso interessante em co-design de hardware-software para redes neurais. Enquanto o LNS oferece vantagens significativas em certas áreas: Logarithmic Number System (LNS) a aritmética de ponto flutuante (FP) 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 em algumas implementações uma aceleração de 2 a 3x Benefícios para a memória Normalmente requer para representar números menos bits Pode de forma mais eficiente comprimir pesos e ativações Reduz os requisitos de largura de banda da memória Menor para acesso à memória consumo de energia No entanto, os são significativos: desafios de precisão durante a acumulação de pequenos valores Perda de precisão muito próximos de zero Dificuldade em representar números em cálculos de gradiente Instabilidade potencial 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 (como classificação final) FP para camadas sensíveis Aplique LNS em camadas ocultas com uso intensivo de computação com base em requisitos numéricos Alterne dinamicamente 2. Computação Adaptativa de Precisão para estabilidade Comece a treinar com FP conforme os pesos convergem Transição gradual para LNS com maior precisão Manter caminhos críticos 3. Co-design de hardware com unidades FP e LNS Aceleradores personalizados entre tipos aritméticos Agendamento inteligente para cada formato Hierarquias de memória especializadas 4. Inovações Algorítmicas otimizadas para LNS Novas funções de ativação que mantêm a estabilidade Algoritmos de otimização modificados 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 para aceleração kernels CUDA eficientes 2. Extensões de tipo numérico Adicionar tipos de tensor LNS nativos Implementar (*+, -, ) no domínio de log operações principais , / Fornece de/para ponto flutuante utilitários de conversão 3. Modificações de Camadas Crie (Linear, Conv2d) versões LNS de camadas comuns Otimizar para computação LNS passagens para trás 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”, , 2023. arXiv:2307.05035 [2] M. Arnold, E. Chester, et al., “Treinamento de redes neurais usando apenas uma ALU LNS sem tabela aproximada.” , 2020, pp. 69–72. 31ª Conferência Internacional sobre Sistemas, Arquiteturas e Processadores Específicos de Aplicação, IEEE 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”, , março de 2016. arXiv:1603.01025 [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”, , vol. 71, no. 12, pp. 3179–3190, dezembro de 2022. IEEE Transactions on Computers DOI