paint-brush
Дубоко учење ради на математици са плутајућим зарезом. Шта ако је то грешка?од стране@abhiyanampally_kob9nse8
549 читања
549 читања

Дубоко учење ради на математици са плутајућим зарезом. Шта ако је то грешка?

од стране 40m2025/02/11
Read on Terminal Reader

Предуго; Читати

Логаритамски бројевни систем (ЛНС) је алтернативна нумеричка репрезентација аритметици са плутајућим зарезом (ФП). ЛНС представља бројеве у логаритамској скали, претварајући множења у сабирања, што може бити рачунарски јефтиније у одређеним хардверским архитектурама. Међутим, сабирање и одузимање у ЛНС захтевају апроксимације, што доводи до смањене прецизности. Користимо ЛНС за обуку једноставног потпуно повезаног вишеслојног перцептрона на МНИСТ-у.
featured image - Дубоко учење ради на математици са плутајућим зарезом. Шта ако је то грешка?
undefined HackerNoon profile picture
0-item
1-item

Када сам први пут наишао на идеју да користим логаритамски бројевни систем (ЛНС) у дубоком учењу, био сам заинтригиран, али и скептичан. Као и већина нас, одувек сам радио са аритметиком са плутајућим зарезом (ФП) — стандардом за нумеричко рачунање у дубоком учењу. ФП пружа добар баланс између прецизности и домета, али долази са компромисима: већом употребом меморије, повећаном сложеношћу рачунара и већом потрошњом енергије. Дакле, одлучио сам да експериментишем и уверим се – како се ЛНС пореди са ФП-ом када тренирате једноставан потпуно повезан вишеслојни перцептрон (МЛП) на МНИСТ-у?

Зашто узети у обзир ЛНС?

ЛНС представља бројеве у логаритамској скали, претварајући множења у сабирања, што може бити рачунарски јефтиније у одређеним хардверским архитектурама. Ова ефикасност долази по цену прецизности, посебно у операцијама сабирања и одузимања, које су сложеније у ЛНС-у. Међутим, потенцијалне предности — смањени меморијски отисак, бржи прорачуни и мања потрошња енергије — учинили су ме довољно радозналим да га испробам.

Позадина: Флоатинг-Поинт вс. Логаритамски бројни систем

Представљање у покретном зарезу (ФП).

Аритметика са плутајућим зарезом је стандардна нумеричка репрезентација у већини оквира за дубоко учење, као што су ПиТорцх и ТенсорФлов. ФП бројеви имају:


  • Бит знака (одређивање позитивне или негативне вредности)
  • Експонент (фактор скалирања)
  • Мантиса (значај) (прецизност броја)


ФП32 (сингле прецисион) се обично користи у дубоком учењу, нудећи равнотежу између нумеричке прецизности и рачунарске ефикасности. Ефикаснији формати као што су ФП16 и БФ16 постају све популарнији како би убрзали обуку.

Логаритамски систем бројева (ЛНС)

ЛНС је алтернативни нумерички приказ где се бројеви чувају као логаритми: [ к = \лог_б (и) ] где је ( б ) база логаритма. ЛНС има неколико предности:


  • Множење је поједностављено сабирањем : ( к_1 * к_2 = б^{(\лог_б к_1 + \лог_б к_2)})
  • Дељење је поједностављено на одузимање : ( к_1 / к_2 = б^{(\лог_б к_1 - \лог_б к_2)})
  • Функције експоненцијалног раста постају линеарне


Међутим, сабирање и одузимање у ЛНС захтевају апроксимације, што доводи до смањене прецизности.

ЛНС аритметичке операције

Да бих даље истражио ЛНС, имплементирао сам основне аритметичке операције као што су сабирање, одузимање, множење и дељење користећи ЛНС интерне репрезентације.


 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)


Ево рашчлањавања моје експерименталне имплементације логаритамског бројевног система (ЛНС).

1. Основни ЛНС концепт и изазови у ПиТорцх-у

У ЛНС-у бројеви су представљени као логаритми, што претвара множење и дељење у сабирање и одузимање. Међутим, имплементација овога са ПиТорцх-ом представља специфичне изазове јер ПиТорцх тензори интерно користе репрезентације с помичним зарезом. Ово ствара неколико захтева:


  • Одржавајте логаритамску репрезентацију током рачунања.
  • Обезбедите нумеричку стабилност.
  • Пажљиво поступајте са конверзијама.
  • Управљајте интерним представљањем користећи две компоненте:
    • к : логаритамска вредност.
    • с : бит знака (0 или 1).

2. Интерно заступање и конверзија

Први корак је претварање бројева са покретним зарезом у њихову интерну ЛНС репрезентацију.

 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)


Употреба dtype=torch.int64 је кључна јер:

  • Чува тачну ЛНС интерну репрезентацију без грешака заокруживања у покретном зарезу.
  • Пакује и логаритамску вредност и бит знака у један цео број.
  • Спречава ненамерне операције са покретним зарезом од оштећења ЛНС репрезентације.

3. Основне аритметичке операције

а) Множење

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

Множење у ЛНС постаје сабирање:

  • Ако је a = log(x) и b = log(y) , онда log(x×y) = log(x) + log(y) .

б) Дивизија

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

Дељење постаје одузимање:

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

в) Сабирање

 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


Сабирање је сложеније и нумерички осетљивије јер:

  • Укључује експоненцијалне и логаритамске операције.
  • Директна имплементација са помичним зарезом може довести до преливања/недовољног тока.
  • Користи једначину: log(x + y) = log(max(x,y)) + log(1 + exp(log(min(x,y)) - log(max(x,y)))) .
  • Користи log1p уместо директног log(1 + x) за бољу нумеричку стабилност.

4. Откуцајте Управљање безбедношћу и конверзијом

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


Цевовод конверзије одржава јасно раздвајање:

  1. Претвори из флоат → ЛНС интерна репрезентација (цели бројеви).
  2. Извршите ЛНС операције користећи целобројну аритметику.
  3. Претворите назад у плутајући само када је потребно.
 # 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. Кључне предности и ограничења

Предности

  • Множење и дељење су поједностављени на сабирање и одузимање.
  • Широк динамички опсег са аритметиком фиксне тачке.
  • Потенцијално ефикасније за одређене апликације.

Ограничења

  • Сабирање и одузимање су сложеније операције.
  • Прекомерни трошкови конверзије између помичног зареза и ЛНС-а.
  • Захтева посебно руковање нултим и негативним бројевима.
  • Компатибилност ПиТорцх тензора захтева пажљиво управљање типовима.

6. Могућности оптимизације

Да бисте побољшали перформансе, могло би се:

  1. Имплементирајте прилагођену ПиТорцх аутоград функцију за ЛНС операције.
  2. Креирајте прилагођени тип тензора који изворно подржава ЛНС.
  3. Користите ЦУДА језгра за ефикасне ЛНС операције на ГПУ-у.


Тренутна имплементација доноси практичне компромисе:

  • Даје приоритет јасноћи и одржавању у односу на максималне перформансе.
  • Користи постојећу инфраструктуру тензора ПиТорцх-а уз очување ЛНС тачности.
  • Одржава нумеричку стабилност кроз пажљиво управљање типовима.
  • Минимизира конверзије између репрезентација .

7. Пример тока података

Следећи кораци показују комплетан цевовод користећи пример вредности [2.0, 3.0] и [-1.0, 0.0] :

  1. Конвертујте улазне вредности у ЛНС интерну репрезентацију.
  2. Креирајте тензоре целих бројева за чување ЛНС вредности.
  3. Извршити аритметичке операције у ЛНС домену.
  4. Конвертујте резултате назад у покретни зарез.
  5. Креирајте коначне ПиТорцх тензоре за даљу обраду.


Ова имплементација успешно премошћује јаз између ПиТорцх-овог тензорског система са плутајућим зарезом и ЛНС аритметике уз одржавање нумеричке стабилности и тачности.


Обука потпуно повезаног МЛП-а на скупу података МНИСТ Дигит са ФП и ЛНС

Подешавање експеримента

Обучио сам потпуно повезан МЛП на МНИСТ скупу података користећи и ФП и ЛНС репрезентације. Архитектура модела је била једноставна:

  • Улазни слој: 784 неурона (спљоштене слике 28к28)
  • Скривени слојеви: Два слоја са 256 и 128 неурона, РеЛУ активације
  • Излазни слој: 10 неурона (по један за сваку цифру, користећи софтмак)
  • Функција губитка: Унакрсна ентропија
  • Оптимизатор: Адам


За имплементацију ЛНС-а, морао сам да изађем из свог уобичајеног ПиТорцх тока посла. За разлику од ФП-а, који ПиТорцх изворно подржава, ПиТорцх не обезбеђује уграђене ЛНС операције. Пронашао сам ГитХуб пројекат под називом xlns , који имплементира логаритамске репрезентације бројева и аритметику, што омогућава коришћење ЛНС-а у неуронским мрежама.

МЛП са плутајућим зарезом у ПиТорцх-у

Почињемо са имплементацијом стандардног ФП-базираног потпуно повезаног МЛП-а користећи ПиТорцх:

 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)


Ова имплементација прати конвенционални цевовод дубоког учења где се множењем и сабирањем рукује помоћу ФП аритметике.


Овде је детаљно објашњење ове ПиТорцх имплементације вишеслојног перцептрона (МЛП) за МНИСТ скуп података.

  1. Архитектура модела (МЛП класа):
 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. Пролаз унапред:
 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. Подешавање обуке:
 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] ])

Кључне компоненте:

  • Подршка за ГПУ кроз избор уређаја

  • Нормализација података за бољу обуку

  • Конфигурабилни хиперпараметри


  1. Управљање скупом података:
 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])
  • Преузима МНИСТ скуп података ако није присутан

  • Дели податке у скупове за обуку (80%) и валидацију (20%)


  1. Петља за обуку:
 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

Класична процедура тренинга:

  • Нулти градијенти

  • Пас напред

  • Прорачун губитака

  • Пас уназад

  • Ажурирања тежине


  1. Валидација:
 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()

Кључне карактеристике:

  • Модел је подешен на режим евалуације

  • Није потребно израчунавање градијента

  • Прорачун тачности


  1. Визуелизација:
 def show_predictions(model, data_loader, device, num_images=6): model.eval() plt.figure(figsize=(12, 8)) # Display predictions vs actual labels
  • Приказује узорке предвиђања из скупа за валидацију

  • Корисно за квалитативну процену


  1. Праћење учинка:
 # 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')
  • Прати обуку и тачност валидације

  • Исцртава криве учења

  • Мери време тренинга


Ово пружа солидну основу за поређење са имплементацијама заснованим на ЛНС-у, јер имплементира све стандардне компоненте цевовода дубоког учења користећи традиционалну аритметику са покретним зарезом.

Логаритамски систем бројева (ЛНС) МЛП

За ЛНС, морамо да користимо 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)


Провешћу вас кроз овај код који имплементира вишеслојни перцептрон (МЛП) система логаритамских бројева (ЛНС) за МНИСТ класификацију цифара. Дозволите ми да га поделим на кључне одељке:


  1. Подешавање и увоз:
  • Код користи xlns библиотеку за операције логаритамског бројевног система

  • Нуди више ЛНС варијанти (клнснп, клнснпв, клнсуд, итд.) за различите уступке прецизности и перформанси

  • МНИСТ скуп података се учитава преко Кераса


  1. Основне функције:
 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

Ово је нумерички стабилна софтмак имплементација прилагођена за ЛНС операције.


  1. Архитектура мреже:
  • Улазни слој: 784 неурона (28к28 спљоштених МНИСТ слика) + 1 пристрасност = 785

  • Скривени слој: 100 неурона + 1 пристрасност = 101

  • Излазни слој: 10 неурона (један по цифри)


  1. Иницијализација тежине:
  • Тежине се или учитавају из датотеке („веигхтин.нпз“) или се насумично иницијализирају

  • Случајне тежине користе нормалну дистрибуцију са средња вредност=0, стд=0,1

  • Различите ЛНС варијанте захтевају различите методе иницијализације (клнснп, клнснпв, итд.)


  1. Петља за обуку:
 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


Кључни аспекти обуке:

  • Користи непропусну РеЛУ активацију (контролисано леакинг_цоефф)

  • Имплементира стандардно ширење уназад, али са ЛНС операцијама

  • Укључује Л2 регуларизацију (ламбда параметар)

  • Ажурира тежине користећи градијентно спуштање са стопом учења 'лр'


  1. Евалуација:
  • Прати и обуку и тачност валидације

  • Исцртава криве учења које показују тачност током епоха

  • Приказује узорке предвиђања на тестним сликама


  1. Хиперпараметри:
 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' }
  • Користи спуштање низа у низу (подразумевана величина групе = 1)

  • Имплементира рано заустављање кроз подјелу скупа валидације

  • Коефицијент цурења РеЛУ је постављен на 0,0078125


  1. Визуелизација:
  • Прави графиконе који показују тачност обуке и валидације
  • Приказује узорке тестних слика са предвиђањима и тачним ознакама
  • Чува графикон тачности као 'генерицаццураци.пнг'


Кључна иновација овде је употреба ЛНС аритметике која замењује множење са сабирањем у домену дневника, потенцијално нудећи бољу рачунарску ефикасност за одређене хардверске имплементације. Код подржава више ЛНС варијанти које омогућавају различите компромисе између прецизности и перформанси.

Основно поређење перформанси

Перформансе модела са помичним зарезом

 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. 

Предвиђања МЛП модела заснованог на ФП

Крива обуке и валидације за МЛП модел заснован на ФП


Перформансе модела логаритамског система бројева

 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. 

Предвиђања ЛНС заснованог МЛП модела

Крива обуке и валидације за ЛНС заснован МЛП модел


ФП вс. ЛНС: Кључна поређења

Аспецт

Покретни зарез (ФП)

Логаритамски систем бројева (ЛНС)

Време обуке

57.76с

0.35с

Траин Аццураци

91,60%

96,00%

Вал Аццураци

91,62%

68,00%

Прецизност

Високо

Ниже (грешке апроксимације)

Ефикасност меморије

Већа употреба

Мањи меморијски отисак

Руковање множењем

Нативно множење

Поједностављења заснована на сабирању

Закључак

Компромис између логаритамског система бројева (ЛНС) и аритметике са плутајућим зарезом (ФП) представља занимљиву студију случаја у ко-дизајну хардвера и софтвера за неуронске мреже. Док ЛНС нуди значајне предности у одређеним областима:

Брзина тренинга

  • Замењује множење са сабирањем у домену евиденције
  • Своди сложене операције на једноставнију аритметику
  • Посебно ефикасан за множење матрица у неуронским мрежама
  • Може постићи 2–3 пута убрзање у неким имплементацијама

Предности меморије

  • Обично је потребно мање битова за представљање бројева
  • Може ефикасније компресовати тежине и активације
  • Смањује захтеве за пропусни опсег меморије
  • Мања потрошња енергије за приступ меморији


Међутим, изазови у вези са прецизношћу су значајни:

  • Губитак прецизности током акумулације малих вредности
  • Потешкоће у представљању бројева веома близу нуле
  • Потенцијална нестабилност у прорачунима градијента
  • Може захтевати пажљиво подешавање хиперпараметара

Футуре Дирецтионс

Неколико обећавајућих приступа могло би побољшати применљивост ЛНС-а:

1. Аритметика специфична за слој

  • Користите ФП за осетљиве слојеве (попут коначне класификације)
  • Примените ЛНС у скривеним слојевима са тешким рачунима
  • Динамички пребацивање на основу нумеричких захтева

2. Прецизно прилагодљиво рачунарство

  • Почните да тренирате са ФП за стабилност
  • Постепено пређите на ЛНС како се тежине приближавају
  • Одржавајте критичне путање у већој прецизности

3. Ко-дизајн хардвера

  • Прилагођени акцелератори са ФП и ЛНС јединицама
  • Паметно распоређивање између аритметичких типова
  • Специјализоване меморијске хијерархије за сваки формат

4. Алгоритамске иновације

  • Нове функције активације оптимизоване за ЛНС
  • Модификовани алгоритми оптимизације који одржавају стабилност
  • Хибридне репрезентације бројева

Потенцијална подршка за ПиТорцх

Да би се ЛНС интегрисао у оквире дубоког учења, могло би се истражити следеће:

1. Прилагођене функције Аутоград

  • Имплементирајте ЛНС операције као прилагођене аутоград функције
  • Одржавајте рачунање градијента у домену дневника
  • Обезбедите ефикасне ЦУДА кернеле за убрзање

2. Екстензије типа броја

  • Додајте изворне типове ЛНС тензора
  • Имплементирајте основне операције (*+, -, , / ) у домену евиденције
  • Обезбедите услужне програме за конверзију у/из помичног зареза

3. Модификације слоја

  • Креирајте ЛНС верзије уобичајених слојева (Линеар, Цонв2д)
  • Оптимизујте пролазе уназад за ЛНС рачунање
  • Подржите мешовиту прецизну обуку


Заједница дубоког учења могла би имати велике користи од интеграције ових могућности у главне оквире, омогућавајући ефикасније неуронске мреже мале снаге и велике брзине .


Шта мислите о равнотежи између нумеричке прецизности и рачунарске ефикасности? Да ли сте наишли на специфичне случајеве употребе у којима би ЛНС могао бити посебно користан?


Јавите ми своје мишљење о овоме.

Референце


[1] Г. Алсухли, ет ал., „Системи бројева за архитектуре дубоких неуронских мрежа: Преглед“, арКсив:2307.05035 , 2023.

[2] М. Арнолд, Е. Цхестер, ет ал., „Тренинг неуронских мрежа користећи само приближни ЛНС АЛУ без табеле.“ 31. међународна конференција о системима, архитектурама и процесорима специфичним за апликације, ИЕЕЕ , 2020, стр. 69–72. ДОИ

[3] О. Косхелева, ет ал., „Логаритамски бројевни систем је оптималан за АИ прорачуне: теоријско објашњење емпиријског успеха,” Рад

[4] Д. Мииасхита, ет ал., „Конволуционе неуронске мреже које користе логаритамску репрезентацију података“, арКсив:1603.01025 , мар 2016.

[5] Ј. Зхао ет ал., “ЛНС-Мадам: Лов-Прецисион Траининг ин Логаритхмиц Нумбер Систем Усинг Мултиплицативе Веигхт Упдате,” ИЕЕЕ Трансацтионс он Цомпутерс , вол. 71, бр. 12, стр. 3179–3190, децембар 2022. ДОИ