paint-brush
ディープラーニングは浮動小数点演算で実行されます。それが間違いだったらどうなるでしょうか?@abhiyanampally_kob9nse8
549 測定値
549 測定値

ディープラーニングは浮動小数点演算で実行されます。それが間違いだったらどうなるでしょうか?

40m2025/02/11
Read on Terminal Reader

長すぎる; 読むには

対数数システム (LNS) は、浮動小数点 (FP) 演算の代替となる数値表現です。LNS は数値を対数スケールで表し、乗算を加算に変換します。これにより、特定のハードウェア アーキテクチャでは計算コストが削減されます。ただし、LNS での加算と減算には近似値が必要なため、精度が低下します。LNS を使用して、MNIST で単純な完全接続型多層パーセプトロンをトレーニングします。
featured image - ディープラーニングは浮動小数点演算で実行されます。それが間違いだったらどうなるでしょうか?
undefined HackerNoon profile picture
0-item
1-item

ディープラーニングで対数数システム (LNS) を使用するというアイデアに初めて出会ったとき、私は興味をそそられると同時に懐疑的でした。ほとんどの人と同じように、私はこれまでずっと、ディープラーニングの数値計算の標準である浮動小数点 (FP) 演算を使用してきました。FP は精度と範囲のバランスが取れていますが、トレードオフとして、メモリ使用量の増加、計算の複雑さの増加、消費電力の増加が伴います。そこで、MNIST で単純な完全接続型多層パーセプトロン (MLP) をトレーニングする場合、LNS と FP を比較して自分で実験してみることにしました。

LNS を検討する理由

LNS は数値を対数スケールで表し、乗算を加算に変換します。これにより、特定のハードウェア アーキテクチャでは計算コストが削減されます。この効率性は、特に LNS ではより複雑な加算と減算の演算で精度を犠牲にして実現されます。しかし、メモリ フットプリントの削減、計算の高速化、消費電力の低減といった潜在的なメリットに、私は興味をそそられ、試してみることにしました。

背景: 浮動小数点数システムと対数数システム

浮動小数点数 (FP) 表現

浮動小数点演算は、PyTorch や TensorFlow などのほとんどのディープラーニング フレームワークにおける標準的な数値表現です。浮動小数点数値には次の特徴があります。


  • 符号ビット(正または負の値を決定する)
  • 指数(スケーリング係数)
  • 仮数部(数値の精度)


FP32 (単精度) はディープラーニングでよく使用され、数値精度と計算効率のバランスが取れています。トレーニングを高速化するために、FP16 や BF16 などのより効率的な形式が人気を集めています。

対数数システム (LNS)

LNS は、数値を対数として保存する別の数値表現です: [ x = \log_b (y) ] ( b ) は対数の底です。LNS にはいくつかの利点があります:


  • 掛け算は足し算に簡略化されます: ( x_1 * x_2 = b^{(\log_b x_1 + \log_b x_2)} )
  • 割り算は引き算に簡略化されます: ( x_1 / x_2 = b^{(\log_b x_1 - \log_b x_2)} )
  • 指数関数的成長関数は線形になる


ただし、LNS での加算と減算には近似値が必要なので、精度が低下します。

LNS算術演算

LNS をさらに調査するために、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)


以下は、対数数システム (LNS) の実験的な実装の詳細です。

1. PyTorchにおけるLNSの基本概念と課題

LNS では、数値は対数として表され、乗算と除算が加算と減算に変換されます。ただし、PyTorch テンソルは内部で浮動小数点表現を使用するため、これを PyTorch で実装するには特定の課題があります。これにより、いくつかの要件が生じます。


  • 計算全体を通して対数表現を維持します。
  • 数値の安定性を確保します。
  • 変換は慎重に行ってください。
  • 次の 2 つのコンポーネントを使用して内部表現を管理します。
    • x : 対数値。
    • s : 符号ビット(0 または 1)。

2. 内部表現と変換

最初のステップは、浮動小数点数を 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)


dtype=torch.int64の使用は、次の理由で重要です。

  • 浮動小数点の丸め誤差なしに、正確な LNS 内部表現を保持します。
  • 対数値と符号ビットの両方を 1 つの整数にパックします。
  • 意図しない浮動小数点演算によって LNS 表現が破損するのを防ぎます。

3. コア算術演算

a) 掛け算

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

LNS での乗算は加算になります。

  • a = log(x)かつb = log(y)の場合、 log(x×y) = log(x) + log(y)となります。

b) 部門

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

割り算は引き算になります:

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

c) 追加

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))))
  • 数値の安定性を高めるために、直接のlog(1 + x)ではなくlog1p使用します。

4. 型安全性と変換管理

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


変換パイプラインは明確な分離を維持します。

  1. float → LNS 内部表現 (整数) に変換します。
  2. 整数演算を使用して LNS 操作を実行します。
  3. 必要な場合にのみ float に戻します。
 # 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. 主な利点と制限

利点

  • 乗算と除算は加算と減算に簡略化されます
  • 固定小数点演算による広いダイナミック レンジ
  • 特定のアプリケーションではより効率的になる可能性があります

制限事項

  • 加算と減算はより複雑な演算です
  • 浮動小数点と LNS 間の変換オーバーヘッド
  • ゼロと負の数には特別な処理が必要です
  • PyTorch テンソルの互換性には慎重な型管理が必要です。

6. 最適化の可能性

パフォーマンスを向上させるには、次の方法があります。

  1. LNS 操作用のカスタム PyTorch autograd 関数を実装します。
  2. LNS をネイティブにサポートするカスタム テンソル タイプを作成します。
  3. GPU 上で効率的な LNS 操作を実行するには、 CUDA カーネルを使用します。


現在の実装では、実用的なトレードオフが行われます。

  • 最大のパフォーマンスよりも、明瞭性と保守性を優先します。
  • LNS の精度を維持しながら、PyTorch の既存のテンソル インフラストラクチャを使用します。
  • 慎重な型管理により数値の安定性を維持します。
  • 表現間の変換を最小限に抑えます。

7. データフローの例

次の手順では、サンプル値[2.0, 3.0][-1.0, 0.0]を使用して完全なパイプラインを示します。

  1. 入力浮動小数点数を LNS 内部表現に変換します。
  2. LNS 値を格納するための整数テンソルを作成します。
  3. LNS ドメインで算術演算を実行します。
  4. 結果を浮動小数点に戻します。
  5. さらに処理するための最終的な PyTorch テンソルを作成します。


この実装は、数値の安定性と精度を維持しながら、PyTorch の浮動小数点テンソル システムと LNS 演算の間のギャップをうまく埋めます。


FP と LNS を使用して MNIST Digit データセットで完全に接続された MLP をトレーニングする

実験のセットアップ

FP と LNS の両方の表現を使用して、MNIST データセットで完全に接続された MLP をトレーニングしました。モデルのアーキテクチャはシンプルでした。

  • 入力層: 784 ニューロン (平坦化された 28x28 画像)
  • 隠れ層: 256 と 128 のニューロンを持つ 2 つの層、ReLU 活性化
  • 出力層: 10 個のニューロン (各桁に 1 つ、ソフトマックスを使用)
  • 損失関数:クロスエントロピー
  • オプティマイザー: Adam


LNS 実装では、通常の PyTorch ワークフローから外れなければなりませんでした。PyTorch がネイティブにサポートする FP とは異なり、PyTorch は組み込みの LNS 操作を提供していません。私はxlnsという GitHub プロジェクトを見つけました。これは対数表現と演算を実装し、ニューラル ネットワークで LNS を使用できるようにします。

PyTorch の浮動小数点 MLP

まず、PyTorch を使用して標準的な FP ベースの完全接続 MLP を実装します。

 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)


この実装は、乗算と加算が FP 演算によって処理される従来のディープラーニング パイプラインに従います。


ここでは、MNIST データセット用の多層パーセプトロン (MLP) の PyTorch 実装の詳細なチュートリアルを示します。

  1. モデルアーキテクチャ(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. フォワードパス:
 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] ])

主なコンポーネント:

  • デバイス選択によるGPUサポート

  • より良いトレーニングのためのデータ正規化

  • 設定可能なハイパーパラメータ


  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])
  • MNISTデータセットが存在しない場合はダウンロードします

  • データをトレーニング(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')
  • トレーニングと検証の精度を追跡する

  • 学習曲線をプロットする

  • トレーニング時間を測定


これは、従来の浮動小数点演算を使用してディープラーニング パイプラインのすべての標準コンポーネントを実装するため、LNS ベースの実装と比較するための強固な基盤を提供します。

対数数システム (LNS) MLP

LNS の場合、 xlnsライブラリを使用する必要があります。FP とは異なり、LNS は乗算の多い演算を対数領域での加算に置き換えます。ただし、PyTorch はこれをネイティブにサポートしていないため、該当する場合は LNS 演算を手動で適用する必要があります。

 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)


MNIST 数字分類用の対数数システム (LNS) 多層パーセプトロン (MLP) を実装するこのコードについて説明します。主要なセクションに分けて説明します。


  1. セットアップとインポート:
  • このコードは対数演算にxlnsライブラリを使用しています。

  • さまざまな精度とパフォーマンスのトレードオフに対応する複数のLNSバリアント(xlnsnp、xlnsnpv、xlnsudなど)を提供します。

  • MNISTデータセットはKerasを通じて読み込まれます


  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

これは、LNS 操作に適合した数値的に安定したソフトマックス実装です。


  1. ネットワークアーキテクチャ:
  • 入力層: 784 ニューロン (28x28 の平坦化された MNIST 画像) + 1 バイアス = 785

  • 隠れ層: 100 ニューロン + 1 バイアス = 101

  • 出力層: 10 ニューロン (各指に 1 つ)


  1. 重みの初期化:
  • 重みはファイル(「weightin.npz」)から読み込まれるか、ランダムに初期化されます。

  • ランダム重みは、平均=0、標準偏差=0.1の正規分布を使用します。

  • 異なる LNS バリアントには異なる初期化方法 (xlnsnp、xlnsnpv など) が必要です。


  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


トレーニングの主な側面:

  • リーキー ReLU アクティベーションを使用する (leaking_coeff によって制御)

  • 標準的なバックプロパゲーションをLNS操作で実装します

  • L2正規化(ラムダパラメータ)を含む

  • 学習率 'lr' で勾配降下法を使用して重みを更新します


  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)

  • 検証セットの分割による早期停止を実装

  • リーキーReLU係数は0.0078125に設定されている


  1. 視覚化:
  • トレーニングと検証の精度を示すプロットを作成します
  • 予測と実際のラベルを含むサンプルテスト画像を表示します
  • 精度プロットを 'genericaccuracy.png' として保存します。


ここでの重要な革新は、対数領域での乗算を加算に置き換える LNS 演算の使用であり、特定のハードウェア実装で計算効率が向上する可能性があります。コードは複数の LNS バリアントをサポートしており、さまざまな精度とパフォーマンスのトレードオフが可能です。

基本性能比較

浮動小数点モデルのパフォーマンス

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. 

FPベースのMLPモデルの予測

FP ベースの MLP モデルのトレーニングと検証曲線


対数システムモデルのパフォーマンス

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. 

LNSベースのMLPモデルの予測

LNS ベースの MLP モデルのトレーニングと検証曲線


FP vs. LNS: 主な比較

側面

浮動小数点数 (FP)

対数数システム (LNS)

トレーニング時間

57.76秒

0.35秒

トレーニングの精度

91.60%

96.00%

値精度

91.62%

68.00%

精度

高い

下限(近似誤差)

メモリ効率

使用頻度の増加

メモリ使用量の削減

乗算処理

ネイティブ乗算

加算ベースの簡略化

結論

対数数システム (LNS)浮動小数点 (FP) 演算のトレードオフは、ニューラル ネットワークのハードウェアとソフトウェアの共同設計における興味深いケース スタディです。LNS は特定の領域で大きな利点を提供します。

トレーニング速度

  • 対数領域で乗算を加算に置き換える
  • 複雑な演算をより単純な算術演算に減らす
  • ニューラルネットワークにおける行列乗算に特に効果的
  • いくつかの実装では2~3倍の高速化を実現可能

記憶のメリット

  • 通常、数値を表すのに必要なビット数は少なくなります
  • 重みとアクティベーションをより効率的に圧縮できる
  • メモリ帯域幅の要件を削減
  • メモリアクセス時の消費電力を低減


しかし、精度の課題は重大です。

  • 小さな値の蓄積中に精度が失われる
  • ゼロに非常に近い数字を表すのが難しい
  • 勾配計算における潜在的な不安定性
  • ハイパーパラメータの慎重な調整が必要になる場合があります

今後の方向性

いくつかの有望なアプローチにより、LNS の適用性を高めることができます。

1. レイヤー固有の演算

  • 敏感なレイヤー(最終分類など)には FP を使用する
  • 計算負荷の高い隠れ層にLNSを適用する
  • 数値要件に基づいて動的に切り替える

2. 高精度適応コンピューティング

  • 安定性のためにFPでトレーニングを始める
  • 重みが収束するにつれて徐々にLNSに移行する
  • クリティカルパスをより高い精度で維持する

3. ハードウェアの共同設計

  • FPとLNSユニットの両方を備えたカスタムアクセラレータ
  • 算術型間のスマートなスケジューリング
  • 各フォーマットに特化したメモリ階層

4. アルゴリズムの革新

  • LNS向けに最適化された新しい活性化関数
  • 安定性を維持する改良された最適化アルゴリズム
  • ハイブリッド数値表現

潜在的な PyTorch サポート

LNS をディープラーニング フレームワークに統合するには、次のことが考えられます。

1. カスタム Autograd 関数

  • LNS操作をカスタムautograd関数として実装する
  • 対数領域での勾配計算を維持する
  • 高速化のための効率的なCUDAカーネルを提供する

2. 数値型の拡張

  • ネイティブLNSテンソル型を追加する
  • ログドメインにコア操作(*+、-、、 / )を実装する
  • 浮動小数点間の変換ユーティリティを提供する

3. レイヤーの変更

  • 共通レイヤーの LNS バージョンを作成する (Linear、Conv2d)
  • LNS計算のための逆方向パスを最適化する
  • 混合精度トレーニングをサポート


ディープラーニング コミュニティは、これらの機能を主流のフレームワークに統合することで大きなメリットを得ることができ、より効率的で低消費電力、高速なニューラル ネットワークを実現できます。


数値精度と計算効率のバランスについてどうお考えですか? LNS が特に有益となる可能性がある特定のユースケースに遭遇したことがありますか?


これについてあなたの考えを教えてください。

参考文献


[1] G. Alsuhli他「ディープニューラルネットワークアーキテクチャの数値システム:調査」 arXiv:2307.05035 、2023年。

[2] M. Arnold、E. Chester、他「近似テーブルレスLNS ALUのみを使用したニューラルネットのトレーニング」第31回国際特定用途向けシステム、アーキテクチャ、プロセッサ会議、IEEE 、2020年、pp.69-72。DOI

[3] O. Kosheleva, et al.、「対数数はAI計算に最適:実証的成功の理論的説明」論文

[4] D. Miyashita, et al., “Convolutional Neural Networks using Logarithmic Data Representation,” arXiv:1603.01025 , 2016年3月.

[5] J. Zhao他「LNS-Madam: 乗法重み更新を用いた対数数システムの低精度トレーニング」 IEEE Transactions on Computers 、vol. 71、no. 12、pp. 3179–3190、2022年12月。DOI