polystep 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- polystep/__init__.py +176 -0
- polystep/_compiled.py +334 -0
- polystep/_step_blockwise.py +994 -0
- polystep/_step_momentum.py +128 -0
- polystep/_step_monolithic.py +1070 -0
- polystep/adaptive_subspace.py +762 -0
- polystep/api.py +307 -0
- polystep/ask_tell.py +164 -0
- polystep/benchmarks/__init__.py +62 -0
- polystep/benchmarks/baselines.py +604 -0
- polystep/benchmarks/rl/__init__.py +13 -0
- polystep/benchmarks/rl/cartpole.py +260 -0
- polystep/benchmarks/rl/gym_evaluator.py +304 -0
- polystep/benchmarks/rl/metrics.py +53 -0
- polystep/benchmarks/rl/policies.py +133 -0
- polystep/benchmarks/utils.py +1262 -0
- polystep/blockwise.py +549 -0
- polystep/cma.py +400 -0
- polystep/cma_subspace.py +404 -0
- polystep/cost_nn.py +633 -0
- polystep/costs.py +86 -0
- polystep/dynamics.py +138 -0
- polystep/epsilon.py +217 -0
- polystep/geometry.py +410 -0
- polystep/hybrid_subspace.py +1335 -0
- polystep/layers/__init__.py +16 -0
- polystep/layers/attention.py +243 -0
- polystep/layers/rnn.py +276 -0
- polystep/objectives/__init__.py +25 -0
- polystep/objectives/base.py +77 -0
- polystep/objectives/synthetic.py +232 -0
- polystep/optimizer.py +1405 -0
- polystep/projection/__init__.py +5 -0
- polystep/projection/sparse.py +291 -0
- polystep/py.typed +0 -0
- polystep/quadratic_model.py +285 -0
- polystep/solver.py +654 -0
- polystep/solvers/__init__.py +34 -0
- polystep/solvers/_prelude.py +129 -0
- polystep/solvers/base.py +77 -0
- polystep/solvers/greedy.py +159 -0
- polystep/solvers/kl_softmax.py +200 -0
- polystep/solvers/sinkhorn.py +522 -0
- polystep/solvers/softmax.py +151 -0
- polystep/solvers/tempered_softmax.py +97 -0
- polystep/subspace.py +694 -0
- polystep/transform.py +365 -0
- polystep-0.7.0.dist-info/METADATA +285 -0
- polystep-0.7.0.dist-info/RECORD +52 -0
- polystep-0.7.0.dist-info/WHEEL +5 -0
- polystep-0.7.0.dist-info/licenses/LICENSE +191 -0
- polystep-0.7.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1262 @@
|
|
|
1
|
+
"""Benchmark utilities: data loaders, model factories, metrics, and output formatting.
|
|
2
|
+
|
|
3
|
+
Shared utilities for all benchmark scripts:
|
|
4
|
+
- Data loading (MNIST, CIFAR-10, N-MNIST, DVS-Gesture) without torchvision dependency
|
|
5
|
+
- Model factories for consistent architectures (MLP, SNN with LIF neurons)
|
|
6
|
+
- Evaluation and metrics utilities
|
|
7
|
+
- Environment info collection
|
|
8
|
+
- JSON output and console table formatting
|
|
9
|
+
|
|
10
|
+
SNN utilities require snnTorch for optimal experience:
|
|
11
|
+
pip install snntorch
|
|
12
|
+
|
|
13
|
+
If snnTorch is not installed, falls back to:
|
|
14
|
+
- Pure PyTorch LIF neurons (truly non-differentiable)
|
|
15
|
+
- Synthetic spike data for neuromorphic datasets
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import gzip
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import pickle
|
|
24
|
+
import platform
|
|
25
|
+
import struct as pystruct
|
|
26
|
+
import tarfile
|
|
27
|
+
from collections import defaultdict
|
|
28
|
+
from dataclasses import dataclass, asdict, field
|
|
29
|
+
from datetime import datetime
|
|
30
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
31
|
+
from urllib.request import urlretrieve
|
|
32
|
+
|
|
33
|
+
import numpy as np
|
|
34
|
+
import torch
|
|
35
|
+
import torch.nn as nn
|
|
36
|
+
from torch.utils.data import DataLoader, TensorDataset
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Standard validation seeds
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
# Benchmark validation seeds (3 seeds). Paper experiments use 5 seeds
|
|
44
|
+
# defined in paper/experiments/common.py instead.
|
|
45
|
+
SEEDS = [42, 123, 456]
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
# ---------------------------------------------------------------------------
|
|
49
|
+
# MNIST data loading (no torchvision required)
|
|
50
|
+
# ---------------------------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
MNIST_URL = "https://storage.googleapis.com/cvdf-datasets/mnist/"
|
|
53
|
+
MNIST_FILES = {
|
|
54
|
+
"train_images": "train-images-idx3-ubyte.gz",
|
|
55
|
+
"train_labels": "train-labels-idx1-ubyte.gz",
|
|
56
|
+
"test_images": "t10k-images-idx3-ubyte.gz",
|
|
57
|
+
"test_labels": "t10k-labels-idx1-ubyte.gz",
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def download_file(url: str, filepath: str) -> None:
|
|
62
|
+
"""Download a file from URL if not already present."""
|
|
63
|
+
if not os.path.exists(filepath):
|
|
64
|
+
print(f" Downloading {os.path.basename(filepath)}...")
|
|
65
|
+
urlretrieve(url, filepath)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _download_mnist(data_dir: str) -> None:
|
|
69
|
+
"""Download MNIST dataset if not already present."""
|
|
70
|
+
os.makedirs(data_dir, exist_ok=True)
|
|
71
|
+
for name, filename in MNIST_FILES.items():
|
|
72
|
+
filepath = os.path.join(data_dir, filename)
|
|
73
|
+
download_file(MNIST_URL + filename, filepath)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _load_mnist_images(filepath: str) -> np.ndarray:
|
|
77
|
+
"""Load MNIST images from gzipped IDX file."""
|
|
78
|
+
with gzip.open(filepath, "rb") as f:
|
|
79
|
+
_magic, num, rows, cols = pystruct.unpack(">IIII", f.read(16))
|
|
80
|
+
images = np.frombuffer(f.read(), dtype=np.uint8)
|
|
81
|
+
images = images.reshape(num, 1, rows, cols)
|
|
82
|
+
return images.astype(np.float32) / 255.0
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _load_mnist_labels(filepath: str) -> np.ndarray:
|
|
86
|
+
"""Load MNIST labels from gzipped IDX file."""
|
|
87
|
+
with gzip.open(filepath, "rb") as f:
|
|
88
|
+
_magic, _num = pystruct.unpack(">II", f.read(8))
|
|
89
|
+
labels = np.frombuffer(f.read(), dtype=np.uint8)
|
|
90
|
+
return labels.astype(np.int64)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def get_mnist_loaders(
|
|
94
|
+
data_dir: str = "/tmp/mnist",
|
|
95
|
+
batch_size: int = 512,
|
|
96
|
+
normalize: bool = True,
|
|
97
|
+
max_train: int = 0,
|
|
98
|
+
max_test: int = 0,
|
|
99
|
+
) -> Tuple[DataLoader, DataLoader]:
|
|
100
|
+
"""Load MNIST train/test as PyTorch DataLoaders.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
data_dir: Directory to store/load MNIST data
|
|
104
|
+
batch_size: Batch size for training
|
|
105
|
+
normalize: Whether to normalize with MNIST mean/std
|
|
106
|
+
max_train: Maximum training samples (0=full dataset)
|
|
107
|
+
max_test: Maximum test samples (0=full dataset)
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Tuple of (train_loader, test_loader)
|
|
111
|
+
"""
|
|
112
|
+
_download_mnist(data_dir)
|
|
113
|
+
|
|
114
|
+
train_images = _load_mnist_images(os.path.join(data_dir, MNIST_FILES["train_images"]))
|
|
115
|
+
train_labels = _load_mnist_labels(os.path.join(data_dir, MNIST_FILES["train_labels"]))
|
|
116
|
+
test_images = _load_mnist_images(os.path.join(data_dir, MNIST_FILES["test_images"]))
|
|
117
|
+
test_labels = _load_mnist_labels(os.path.join(data_dir, MNIST_FILES["test_labels"]))
|
|
118
|
+
|
|
119
|
+
if normalize:
|
|
120
|
+
mean, std = 0.1307, 0.3081
|
|
121
|
+
train_images = (train_images - mean) / std
|
|
122
|
+
test_images = (test_images - mean) / std
|
|
123
|
+
|
|
124
|
+
if max_train > 0:
|
|
125
|
+
train_images = train_images[:max_train]
|
|
126
|
+
train_labels = train_labels[:max_train]
|
|
127
|
+
if max_test > 0:
|
|
128
|
+
test_images = test_images[:max_test]
|
|
129
|
+
test_labels = test_labels[:max_test]
|
|
130
|
+
|
|
131
|
+
train_ds = TensorDataset(torch.from_numpy(train_images), torch.from_numpy(train_labels))
|
|
132
|
+
test_ds = TensorDataset(torch.from_numpy(test_images), torch.from_numpy(test_labels))
|
|
133
|
+
|
|
134
|
+
train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=0)
|
|
135
|
+
test_loader = DataLoader(test_ds, batch_size=256, shuffle=False, num_workers=0)
|
|
136
|
+
return train_loader, test_loader
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# CIFAR-10 data loading (no torchvision required)
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
CIFAR10_URL = "https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz"
|
|
144
|
+
CIFAR10_FILENAME = "cifar-10-python.tar.gz"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _download_cifar10(data_dir: str) -> None:
|
|
148
|
+
"""Download CIFAR-10 dataset if not already present."""
|
|
149
|
+
os.makedirs(data_dir, exist_ok=True)
|
|
150
|
+
tar_path = os.path.join(data_dir, CIFAR10_FILENAME)
|
|
151
|
+
extracted_dir = os.path.join(data_dir, "cifar-10-batches-py")
|
|
152
|
+
|
|
153
|
+
if not os.path.exists(extracted_dir):
|
|
154
|
+
download_file(CIFAR10_URL, tar_path)
|
|
155
|
+
print(f" Extracting {CIFAR10_FILENAME}...")
|
|
156
|
+
with tarfile.open(tar_path, "r:gz") as tar:
|
|
157
|
+
tar.extractall(data_dir)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _load_cifar10_batch(filepath: str) -> Tuple[np.ndarray, np.ndarray]:
|
|
161
|
+
"""Load a single CIFAR-10 batch file."""
|
|
162
|
+
with open(filepath, "rb") as f:
|
|
163
|
+
batch = pickle.load(f, encoding="bytes")
|
|
164
|
+
# Data is stored as (num_samples, 3072) where 3072 = 3*32*32
|
|
165
|
+
# Reshape to (num_samples, 3, 32, 32)
|
|
166
|
+
images = batch[b"data"].reshape(-1, 3, 32, 32).astype(np.float32) / 255.0
|
|
167
|
+
labels = np.array(batch[b"labels"], dtype=np.int64)
|
|
168
|
+
return images, labels
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_cifar10_loaders(
|
|
172
|
+
data_dir: str = "/tmp/cifar10",
|
|
173
|
+
batch_size: int = 512,
|
|
174
|
+
normalize: bool = True,
|
|
175
|
+
max_train: int = 0,
|
|
176
|
+
max_test: int = 0,
|
|
177
|
+
) -> Tuple[DataLoader, DataLoader]:
|
|
178
|
+
"""Load CIFAR-10 train/test as PyTorch DataLoaders.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
data_dir: Directory to store/load CIFAR-10 data
|
|
182
|
+
batch_size: Batch size for training
|
|
183
|
+
normalize: Whether to normalize with CIFAR-10 mean/std
|
|
184
|
+
max_train: Maximum training samples (0=full dataset)
|
|
185
|
+
max_test: Maximum test samples (0=full dataset)
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Tuple of (train_loader, test_loader)
|
|
189
|
+
"""
|
|
190
|
+
_download_cifar10(data_dir)
|
|
191
|
+
|
|
192
|
+
batch_dir = os.path.join(data_dir, "cifar-10-batches-py")
|
|
193
|
+
|
|
194
|
+
# Load all training batches
|
|
195
|
+
train_images_list = []
|
|
196
|
+
train_labels_list = []
|
|
197
|
+
for i in range(1, 6):
|
|
198
|
+
batch_path = os.path.join(batch_dir, f"data_batch_{i}")
|
|
199
|
+
images, labels = _load_cifar10_batch(batch_path)
|
|
200
|
+
train_images_list.append(images)
|
|
201
|
+
train_labels_list.append(labels)
|
|
202
|
+
|
|
203
|
+
train_images = np.concatenate(train_images_list, axis=0)
|
|
204
|
+
train_labels = np.concatenate(train_labels_list, axis=0)
|
|
205
|
+
|
|
206
|
+
# Load test batch
|
|
207
|
+
test_path = os.path.join(batch_dir, "test_batch")
|
|
208
|
+
test_images, test_labels = _load_cifar10_batch(test_path)
|
|
209
|
+
|
|
210
|
+
if normalize:
|
|
211
|
+
# CIFAR-10 normalization values (per channel)
|
|
212
|
+
mean = np.array([0.4914, 0.4822, 0.4465]).reshape(1, 3, 1, 1)
|
|
213
|
+
std = np.array([0.2470, 0.2435, 0.2616]).reshape(1, 3, 1, 1)
|
|
214
|
+
train_images = (train_images - mean) / std
|
|
215
|
+
test_images = (test_images - mean) / std
|
|
216
|
+
|
|
217
|
+
if max_train > 0:
|
|
218
|
+
train_images = train_images[:max_train]
|
|
219
|
+
train_labels = train_labels[:max_train]
|
|
220
|
+
if max_test > 0:
|
|
221
|
+
test_images = test_images[:max_test]
|
|
222
|
+
test_labels = test_labels[:max_test]
|
|
223
|
+
|
|
224
|
+
train_ds = TensorDataset(
|
|
225
|
+
torch.from_numpy(train_images.astype(np.float32)),
|
|
226
|
+
torch.from_numpy(train_labels),
|
|
227
|
+
)
|
|
228
|
+
test_ds = TensorDataset(
|
|
229
|
+
torch.from_numpy(test_images.astype(np.float32)),
|
|
230
|
+
torch.from_numpy(test_labels),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=0)
|
|
234
|
+
test_loader = DataLoader(test_ds, batch_size=256, shuffle=False, num_workers=0)
|
|
235
|
+
return train_loader, test_loader
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
# SST-2 data loading (HuggingFace with simple tokenization)
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
# Check for HuggingFace datasets at module load time
|
|
243
|
+
try:
|
|
244
|
+
from datasets import load_dataset as _hf_load_dataset
|
|
245
|
+
|
|
246
|
+
HAS_DATASETS = True
|
|
247
|
+
except ImportError:
|
|
248
|
+
HAS_DATASETS = False
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _simple_tokenize(
|
|
252
|
+
texts: List[str],
|
|
253
|
+
vocab: Dict[str, int],
|
|
254
|
+
max_seq_len: int,
|
|
255
|
+
pad_idx: int = 0,
|
|
256
|
+
unk_idx: int = 1,
|
|
257
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
258
|
+
"""Tokenize texts using simple word-to-index mapping.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
texts: List of text strings to tokenize.
|
|
262
|
+
vocab: Vocabulary mapping word -> index.
|
|
263
|
+
max_seq_len: Maximum sequence length (pad/truncate to this).
|
|
264
|
+
pad_idx: Index for padding token.
|
|
265
|
+
unk_idx: Index for unknown token.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
Tuple of (input_ids, attention_mask) as numpy arrays.
|
|
269
|
+
"""
|
|
270
|
+
input_ids = []
|
|
271
|
+
attention_masks = []
|
|
272
|
+
|
|
273
|
+
for text in texts:
|
|
274
|
+
# Simple word tokenization: lowercase, split on whitespace/punctuation
|
|
275
|
+
words = text.lower().replace(",", " ").replace(".", " ").replace("!", " ").replace("?", " ").split()
|
|
276
|
+
|
|
277
|
+
# Convert to indices
|
|
278
|
+
ids = [vocab.get(w, unk_idx) for w in words]
|
|
279
|
+
|
|
280
|
+
# Truncate if needed
|
|
281
|
+
if len(ids) > max_seq_len:
|
|
282
|
+
ids = ids[:max_seq_len]
|
|
283
|
+
|
|
284
|
+
# Create attention mask (1 for real tokens)
|
|
285
|
+
mask = [1] * len(ids)
|
|
286
|
+
|
|
287
|
+
# Pad if needed
|
|
288
|
+
padding_len = max_seq_len - len(ids)
|
|
289
|
+
if padding_len > 0:
|
|
290
|
+
ids = ids + [pad_idx] * padding_len
|
|
291
|
+
mask = mask + [0] * padding_len
|
|
292
|
+
|
|
293
|
+
input_ids.append(ids)
|
|
294
|
+
attention_masks.append(mask)
|
|
295
|
+
|
|
296
|
+
return np.array(input_ids, dtype=np.int64), np.array(attention_masks, dtype=np.int64)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _build_vocab(texts: List[str], vocab_size: int) -> Dict[str, int]:
|
|
300
|
+
"""Build vocabulary from texts.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
texts: List of text strings.
|
|
304
|
+
vocab_size: Maximum vocabulary size (including special tokens).
|
|
305
|
+
|
|
306
|
+
Returns:
|
|
307
|
+
Dictionary mapping word -> index.
|
|
308
|
+
"""
|
|
309
|
+
# Count word frequencies
|
|
310
|
+
word_counts: Dict[str, int] = defaultdict(int)
|
|
311
|
+
for text in texts:
|
|
312
|
+
words = text.lower().replace(",", " ").replace(".", " ").replace("!", " ").replace("?", " ").split()
|
|
313
|
+
for w in words:
|
|
314
|
+
word_counts[w] += 1
|
|
315
|
+
|
|
316
|
+
# Sort by frequency and take top vocab_size - 2 (reserve 0=PAD, 1=UNK)
|
|
317
|
+
sorted_words = sorted(word_counts.items(), key=lambda x: -x[1])
|
|
318
|
+
vocab = {"<PAD>": 0, "<UNK>": 1}
|
|
319
|
+
|
|
320
|
+
for word, _ in sorted_words[: vocab_size - 2]:
|
|
321
|
+
vocab[word] = len(vocab)
|
|
322
|
+
|
|
323
|
+
return vocab
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def get_sst2_loaders(
|
|
327
|
+
max_train: int = 0,
|
|
328
|
+
max_test: int = 0,
|
|
329
|
+
max_seq_len: int = 64,
|
|
330
|
+
batch_size: int = 32,
|
|
331
|
+
vocab_size: int = 10000,
|
|
332
|
+
data_dir: str = "/tmp/sst2",
|
|
333
|
+
) -> Tuple[DataLoader, DataLoader, int, str]:
|
|
334
|
+
"""Load SST-2 sentiment dataset as PyTorch DataLoaders.
|
|
335
|
+
|
|
336
|
+
Uses HuggingFace datasets with simple word-to-index tokenization.
|
|
337
|
+
Raises error if HuggingFace datasets is not installed (no synthetic fallback).
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
max_train: Maximum training samples (0=all available).
|
|
341
|
+
max_test: Maximum test samples (0=all available).
|
|
342
|
+
max_seq_len: Maximum sequence length for padding/truncation.
|
|
343
|
+
batch_size: Batch size for DataLoaders.
|
|
344
|
+
vocab_size: Maximum vocabulary size for tokenization.
|
|
345
|
+
data_dir: Directory for caching HuggingFace datasets.
|
|
346
|
+
|
|
347
|
+
Returns:
|
|
348
|
+
Tuple of (train_loader, test_loader, actual_vocab_size, data_type).
|
|
349
|
+
DataLoaders yield (input_ids, attention_mask, labels) tuples.
|
|
350
|
+
data_type is "real" for HuggingFace data.
|
|
351
|
+
|
|
352
|
+
Raises:
|
|
353
|
+
ImportError: If HuggingFace datasets is not installed.
|
|
354
|
+
"""
|
|
355
|
+
if not HAS_DATASETS:
|
|
356
|
+
raise ImportError("HuggingFace datasets required for SST-2 benchmark. Install with: pip install datasets")
|
|
357
|
+
|
|
358
|
+
print(" Loading SST-2 from HuggingFace datasets...")
|
|
359
|
+
dataset = _hf_load_dataset("glue", "sst2", cache_dir=data_dir)
|
|
360
|
+
|
|
361
|
+
# Get train/validation splits (SST-2 test set has no labels)
|
|
362
|
+
train_texts = dataset["train"]["sentence"]
|
|
363
|
+
train_labels = dataset["train"]["label"]
|
|
364
|
+
test_texts = dataset["validation"]["sentence"]
|
|
365
|
+
test_labels = dataset["validation"]["label"]
|
|
366
|
+
|
|
367
|
+
# Apply max samples
|
|
368
|
+
if max_train > 0:
|
|
369
|
+
train_texts = train_texts[:max_train]
|
|
370
|
+
train_labels = train_labels[:max_train]
|
|
371
|
+
if max_test > 0:
|
|
372
|
+
test_texts = test_texts[:max_test]
|
|
373
|
+
test_labels = test_labels[:max_test]
|
|
374
|
+
|
|
375
|
+
print(f" Building vocabulary from {len(train_texts)} training samples...")
|
|
376
|
+
vocab = _build_vocab(train_texts, vocab_size)
|
|
377
|
+
actual_vocab_size = len(vocab)
|
|
378
|
+
print(f" Vocabulary size: {actual_vocab_size}")
|
|
379
|
+
|
|
380
|
+
# Tokenize
|
|
381
|
+
print(f" Tokenizing with max_seq_len={max_seq_len}...")
|
|
382
|
+
train_ids, train_mask = _simple_tokenize(train_texts, vocab, max_seq_len)
|
|
383
|
+
test_ids, test_mask = _simple_tokenize(test_texts, vocab, max_seq_len)
|
|
384
|
+
train_labels_arr = np.array(train_labels, dtype=np.int64)
|
|
385
|
+
test_labels_arr = np.array(test_labels, dtype=np.int64)
|
|
386
|
+
|
|
387
|
+
# Convert to tensors and create DataLoaders
|
|
388
|
+
train_ds = TensorDataset(
|
|
389
|
+
torch.from_numpy(train_ids),
|
|
390
|
+
torch.from_numpy(train_mask),
|
|
391
|
+
torch.from_numpy(train_labels_arr),
|
|
392
|
+
)
|
|
393
|
+
test_ds = TensorDataset(
|
|
394
|
+
torch.from_numpy(test_ids),
|
|
395
|
+
torch.from_numpy(test_mask),
|
|
396
|
+
torch.from_numpy(test_labels_arr),
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=0)
|
|
400
|
+
test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False, num_workers=0)
|
|
401
|
+
|
|
402
|
+
data_type = "real"
|
|
403
|
+
print(f" Train: {len(train_ds)} samples, Test: {len(test_ds)} samples")
|
|
404
|
+
print(f" Data type: {data_type} (HuggingFace SST-2)")
|
|
405
|
+
return train_loader, test_loader, actual_vocab_size, data_type
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
# ---------------------------------------------------------------------------
|
|
409
|
+
# Model factories
|
|
410
|
+
# ---------------------------------------------------------------------------
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
class MNISTNet(nn.Module):
|
|
414
|
+
"""Two-layer MLP for MNIST digit classification.
|
|
415
|
+
|
|
416
|
+
Architecture: 784 -> hidden -> 10
|
|
417
|
+
Uses ReLU activation, ~101K params with hidden=128.
|
|
418
|
+
"""
|
|
419
|
+
|
|
420
|
+
def __init__(self, hidden: int = 128):
|
|
421
|
+
super().__init__()
|
|
422
|
+
self.flatten = nn.Flatten()
|
|
423
|
+
self.fc1 = nn.Linear(784, hidden)
|
|
424
|
+
self.relu = nn.ReLU()
|
|
425
|
+
self.fc2 = nn.Linear(hidden, 10)
|
|
426
|
+
|
|
427
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
428
|
+
x = self.flatten(x)
|
|
429
|
+
x = self.relu(self.fc1(x))
|
|
430
|
+
return self.fc2(x)
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
class CIFAR10Net(nn.Module):
|
|
434
|
+
"""Standard small CNN for CIFAR-10 classification (vmap-compatible).
|
|
435
|
+
|
|
436
|
+
Architecture: 3xConv2d + MaxPool -> FC(128) -> FC(10)
|
|
437
|
+
No BatchNorm (incompatible with vmap). ~189K parameters.
|
|
438
|
+
"""
|
|
439
|
+
|
|
440
|
+
def __init__(self):
|
|
441
|
+
super().__init__()
|
|
442
|
+
self.features = nn.Sequential(
|
|
443
|
+
nn.Conv2d(3, 32, 3, padding=1),
|
|
444
|
+
nn.ReLU(),
|
|
445
|
+
nn.MaxPool2d(2),
|
|
446
|
+
nn.Conv2d(32, 64, 3, padding=1),
|
|
447
|
+
nn.ReLU(),
|
|
448
|
+
nn.MaxPool2d(2),
|
|
449
|
+
nn.Conv2d(64, 64, 3, padding=1),
|
|
450
|
+
nn.ReLU(),
|
|
451
|
+
nn.MaxPool2d(2),
|
|
452
|
+
)
|
|
453
|
+
self.classifier = nn.Sequential(
|
|
454
|
+
nn.Flatten(),
|
|
455
|
+
nn.Linear(64 * 4 * 4, 128),
|
|
456
|
+
nn.ReLU(),
|
|
457
|
+
nn.Linear(128, 10),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
461
|
+
return self.classifier(self.features(x))
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def create_model(
|
|
465
|
+
dataset_name: str,
|
|
466
|
+
hidden: int = 128,
|
|
467
|
+
**kwargs,
|
|
468
|
+
) -> nn.Module:
|
|
469
|
+
"""Factory function to create models for different datasets.
|
|
470
|
+
|
|
471
|
+
Args:
|
|
472
|
+
dataset_name: One of 'mnist', 'cifar10'
|
|
473
|
+
hidden: Hidden layer size
|
|
474
|
+
**kwargs: Additional model-specific arguments
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
nn.Module: The created model
|
|
478
|
+
|
|
479
|
+
Raises:
|
|
480
|
+
ValueError: If dataset_name is not recognized
|
|
481
|
+
"""
|
|
482
|
+
dataset_name = dataset_name.lower()
|
|
483
|
+
|
|
484
|
+
if dataset_name == "mnist":
|
|
485
|
+
return MNISTNet(hidden=hidden)
|
|
486
|
+
elif dataset_name == "cifar10":
|
|
487
|
+
# CIFAR10Net has a fixed architecture; ``hidden`` is ignored.
|
|
488
|
+
return CIFAR10Net()
|
|
489
|
+
else:
|
|
490
|
+
raise ValueError(f"Unknown dataset: {dataset_name}. Supported: mnist, cifar10")
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
# ---------------------------------------------------------------------------
|
|
494
|
+
# Evaluation utilities
|
|
495
|
+
# ---------------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
@torch.no_grad()
|
|
499
|
+
def evaluate_accuracy(model: nn.Module, dataloader: DataLoader) -> float:
|
|
500
|
+
"""Compute classification accuracy on a DataLoader.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
model: The model to evaluate
|
|
504
|
+
dataloader: DataLoader with (inputs, labels) or (inputs, attention_mask, labels)
|
|
505
|
+
|
|
506
|
+
Returns:
|
|
507
|
+
Accuracy as a float between 0 and 1
|
|
508
|
+
"""
|
|
509
|
+
model.eval()
|
|
510
|
+
device = next(model.parameters()).device
|
|
511
|
+
correct = 0
|
|
512
|
+
total = 0
|
|
513
|
+
|
|
514
|
+
for batch in dataloader:
|
|
515
|
+
if len(batch) == 2:
|
|
516
|
+
inputs, targets = batch
|
|
517
|
+
inputs, targets = inputs.to(device), targets.to(device)
|
|
518
|
+
outputs = model(inputs)
|
|
519
|
+
elif len(batch) == 3:
|
|
520
|
+
# SST-2 format: (input_ids, attention_mask, labels)
|
|
521
|
+
input_ids, attention_mask, targets = batch
|
|
522
|
+
input_ids = input_ids.to(device)
|
|
523
|
+
attention_mask = attention_mask.to(device)
|
|
524
|
+
targets = targets.to(device)
|
|
525
|
+
outputs = model(input_ids, attention_mask=attention_mask)
|
|
526
|
+
else:
|
|
527
|
+
raise ValueError(f"Unexpected batch format with {len(batch)} elements")
|
|
528
|
+
|
|
529
|
+
preds = outputs.argmax(dim=-1)
|
|
530
|
+
correct += (preds == targets).sum().item()
|
|
531
|
+
total += targets.size(0)
|
|
532
|
+
|
|
533
|
+
model.train()
|
|
534
|
+
return correct / total if total > 0 else 0.0
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
# ---------------------------------------------------------------------------
|
|
538
|
+
# BenchmarkResult dataclass
|
|
539
|
+
# ---------------------------------------------------------------------------
|
|
540
|
+
|
|
541
|
+
|
|
542
|
+
@dataclass
|
|
543
|
+
class BenchmarkResult:
|
|
544
|
+
"""Single optimizer run result.
|
|
545
|
+
|
|
546
|
+
Attributes:
|
|
547
|
+
optimizer: Name of the optimizer (e.g., 'polystep', 'adam', 'cmaes')
|
|
548
|
+
seed: Random seed used
|
|
549
|
+
final_accuracy: Accuracy at end of training
|
|
550
|
+
best_accuracy: Best accuracy achieved during training
|
|
551
|
+
final_loss: Loss at end of training (None for optimizers that don't compute loss)
|
|
552
|
+
wall_time_seconds: Total wall clock time in seconds
|
|
553
|
+
peak_gpu_memory_mb: Peak GPU memory usage in MB
|
|
554
|
+
total_steps: Total optimization steps/iterations
|
|
555
|
+
function_evals: Total function evaluations (steps * popsize for ES)
|
|
556
|
+
convergence_epoch: Epoch when target accuracy first reached (None if never)
|
|
557
|
+
epoch_logs: List of per-epoch metrics dicts
|
|
558
|
+
"""
|
|
559
|
+
|
|
560
|
+
optimizer: str
|
|
561
|
+
seed: int
|
|
562
|
+
final_accuracy: float
|
|
563
|
+
best_accuracy: float
|
|
564
|
+
final_loss: Optional[float]
|
|
565
|
+
wall_time_seconds: float
|
|
566
|
+
peak_gpu_memory_mb: float
|
|
567
|
+
total_steps: int
|
|
568
|
+
function_evals: int
|
|
569
|
+
convergence_epoch: Optional[int]
|
|
570
|
+
epoch_logs: List[Dict[str, Any]] = field(default_factory=list)
|
|
571
|
+
|
|
572
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
573
|
+
"""Convert to dictionary for JSON serialization."""
|
|
574
|
+
return asdict(self)
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
# ---------------------------------------------------------------------------
|
|
578
|
+
# Environment info collection
|
|
579
|
+
# ---------------------------------------------------------------------------
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
def get_environment_info() -> Dict[str, Any]:
|
|
583
|
+
"""Collect environment info for reproducibility.
|
|
584
|
+
|
|
585
|
+
Returns:
|
|
586
|
+
Dict with torch_version, cuda_version, gpu_model, python_version, platform
|
|
587
|
+
"""
|
|
588
|
+
info = {
|
|
589
|
+
"torch_version": torch.__version__,
|
|
590
|
+
"python_version": platform.python_version(),
|
|
591
|
+
"platform": platform.system(),
|
|
592
|
+
"platform_release": platform.release(),
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if torch.cuda.is_available():
|
|
596
|
+
info["cuda_version"] = torch.version.cuda
|
|
597
|
+
info["gpu_model"] = torch.cuda.get_device_name(0)
|
|
598
|
+
info["gpu_count"] = torch.cuda.device_count()
|
|
599
|
+
else:
|
|
600
|
+
info["cuda_version"] = None
|
|
601
|
+
info["gpu_model"] = None
|
|
602
|
+
info["gpu_count"] = 0
|
|
603
|
+
|
|
604
|
+
return info
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
# ---------------------------------------------------------------------------
|
|
608
|
+
# Output utilities
|
|
609
|
+
# ---------------------------------------------------------------------------
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def compute_summary_stats(results: List[BenchmarkResult]) -> Dict[str, Dict[str, float]]:
|
|
613
|
+
"""Compute summary statistics grouped by optimizer.
|
|
614
|
+
|
|
615
|
+
Args:
|
|
616
|
+
results: List of BenchmarkResult objects
|
|
617
|
+
|
|
618
|
+
Returns:
|
|
619
|
+
Dict mapping optimizer names to stats dicts with mean_accuracy, std_accuracy, etc.
|
|
620
|
+
"""
|
|
621
|
+
by_optimizer: Dict[str, List[BenchmarkResult]] = defaultdict(list)
|
|
622
|
+
for r in results:
|
|
623
|
+
by_optimizer[r.optimizer].append(r)
|
|
624
|
+
|
|
625
|
+
summary = {}
|
|
626
|
+
for optimizer, runs in by_optimizer.items():
|
|
627
|
+
accs = [r.best_accuracy for r in runs]
|
|
628
|
+
times = [r.wall_time_seconds for r in runs]
|
|
629
|
+
mems = [r.peak_gpu_memory_mb for r in runs]
|
|
630
|
+
evals = [r.function_evals for r in runs]
|
|
631
|
+
|
|
632
|
+
summary[optimizer] = {
|
|
633
|
+
"mean_accuracy": float(np.mean(accs)),
|
|
634
|
+
"std_accuracy": float(np.std(accs)),
|
|
635
|
+
"min_accuracy": float(np.min(accs)),
|
|
636
|
+
"max_accuracy": float(np.max(accs)),
|
|
637
|
+
"mean_time": float(np.mean(times)),
|
|
638
|
+
"std_time": float(np.std(times)),
|
|
639
|
+
"mean_memory_mb": float(np.mean(mems)),
|
|
640
|
+
"mean_function_evals": float(np.mean(evals)),
|
|
641
|
+
"num_runs": len(runs),
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
return summary
|
|
645
|
+
|
|
646
|
+
|
|
647
|
+
def save_results_json(
|
|
648
|
+
results: List[BenchmarkResult],
|
|
649
|
+
output_dir: str,
|
|
650
|
+
benchmark_name: str,
|
|
651
|
+
config: Optional[Dict[str, Any]] = None,
|
|
652
|
+
) -> str:
|
|
653
|
+
"""Save benchmark results to JSON file.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
results: List of BenchmarkResult objects
|
|
657
|
+
output_dir: Directory to save results
|
|
658
|
+
benchmark_name: Name of the benchmark (e.g., 'mnist', 'cifar10')
|
|
659
|
+
config: Optional benchmark configuration dict
|
|
660
|
+
|
|
661
|
+
Returns:
|
|
662
|
+
Path to the saved JSON file
|
|
663
|
+
"""
|
|
664
|
+
os.makedirs(output_dir, exist_ok=True)
|
|
665
|
+
|
|
666
|
+
timestamp = datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
667
|
+
filename = f"{benchmark_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
|
668
|
+
filepath = os.path.join(output_dir, filename)
|
|
669
|
+
|
|
670
|
+
output = {
|
|
671
|
+
"benchmark": benchmark_name,
|
|
672
|
+
"timestamp": timestamp,
|
|
673
|
+
"environment": get_environment_info(),
|
|
674
|
+
"config": config or {},
|
|
675
|
+
"results": [r.to_dict() for r in results],
|
|
676
|
+
"summary": compute_summary_stats(results),
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
with open(filepath, "w") as f:
|
|
680
|
+
json.dump(output, f, indent=2)
|
|
681
|
+
|
|
682
|
+
return filepath
|
|
683
|
+
|
|
684
|
+
|
|
685
|
+
def format_results_table(results: List[BenchmarkResult]) -> str:
|
|
686
|
+
"""Format results as a console table using tabulate.
|
|
687
|
+
|
|
688
|
+
Args:
|
|
689
|
+
results: List of BenchmarkResult objects
|
|
690
|
+
|
|
691
|
+
Returns:
|
|
692
|
+
Formatted table string
|
|
693
|
+
"""
|
|
694
|
+
try:
|
|
695
|
+
from tabulate import tabulate
|
|
696
|
+
except ImportError:
|
|
697
|
+
# Fallback to simple formatting if tabulate not installed
|
|
698
|
+
return _format_results_simple(results)
|
|
699
|
+
|
|
700
|
+
headers = ["Optimizer", "Mean Acc", "Std", "Mean Time", "Peak Mem (MB)", "Func Evals"]
|
|
701
|
+
rows = []
|
|
702
|
+
|
|
703
|
+
# Group by optimizer
|
|
704
|
+
by_opt: Dict[str, List[BenchmarkResult]] = defaultdict(list)
|
|
705
|
+
for r in results:
|
|
706
|
+
by_opt[r.optimizer].append(r)
|
|
707
|
+
|
|
708
|
+
for opt, runs in sorted(by_opt.items()):
|
|
709
|
+
mean_acc = sum(r.best_accuracy for r in runs) / len(runs)
|
|
710
|
+
std_acc = (sum((r.best_accuracy - mean_acc) ** 2 for r in runs) / len(runs)) ** 0.5
|
|
711
|
+
mean_time = sum(r.wall_time_seconds for r in runs) / len(runs)
|
|
712
|
+
mean_mem = sum(r.peak_gpu_memory_mb for r in runs) / len(runs)
|
|
713
|
+
total_evals = sum(r.function_evals for r in runs) // len(runs) # Average
|
|
714
|
+
|
|
715
|
+
rows.append(
|
|
716
|
+
[
|
|
717
|
+
opt,
|
|
718
|
+
f"{mean_acc * 100:.1f}%",
|
|
719
|
+
f"+/-{std_acc * 100:.1f}%",
|
|
720
|
+
f"{mean_time:.1f}s",
|
|
721
|
+
f"{mean_mem:.0f}",
|
|
722
|
+
f"{total_evals:,}",
|
|
723
|
+
]
|
|
724
|
+
)
|
|
725
|
+
|
|
726
|
+
return tabulate(rows, headers=headers, tablefmt="github")
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
def _format_results_simple(results: List[BenchmarkResult]) -> str:
|
|
730
|
+
"""Simple fallback table formatting without tabulate."""
|
|
731
|
+
lines = []
|
|
732
|
+
lines.append("| Optimizer | Mean Acc | Std | Mean Time | Peak Mem (MB) | Func Evals |")
|
|
733
|
+
lines.append("|-----------|----------|-----|-----------|---------------|------------|")
|
|
734
|
+
|
|
735
|
+
# Group by optimizer
|
|
736
|
+
by_opt: Dict[str, List[BenchmarkResult]] = defaultdict(list)
|
|
737
|
+
for r in results:
|
|
738
|
+
by_opt[r.optimizer].append(r)
|
|
739
|
+
|
|
740
|
+
for opt, runs in sorted(by_opt.items()):
|
|
741
|
+
mean_acc = sum(r.best_accuracy for r in runs) / len(runs)
|
|
742
|
+
std_acc = (sum((r.best_accuracy - mean_acc) ** 2 for r in runs) / len(runs)) ** 0.5
|
|
743
|
+
mean_time = sum(r.wall_time_seconds for r in runs) / len(runs)
|
|
744
|
+
mean_mem = sum(r.peak_gpu_memory_mb for r in runs) / len(runs)
|
|
745
|
+
total_evals = sum(r.function_evals for r in runs) // len(runs)
|
|
746
|
+
|
|
747
|
+
lines.append(
|
|
748
|
+
f"| {opt:<9} | {mean_acc * 100:>6.1f}% | +/-{std_acc * 100:.1f}% | "
|
|
749
|
+
f"{mean_time:>8.1f}s | {mean_mem:>13.0f} | {total_evals:>10,} |"
|
|
750
|
+
)
|
|
751
|
+
|
|
752
|
+
return "\n".join(lines)
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
# ---------------------------------------------------------------------------
|
|
756
|
+
# snnTorch availability check
|
|
757
|
+
# ---------------------------------------------------------------------------
|
|
758
|
+
|
|
759
|
+
_HAS_SNNTORCH = False
|
|
760
|
+
try:
|
|
761
|
+
import snntorch as snn
|
|
762
|
+
|
|
763
|
+
_HAS_SNNTORCH = True
|
|
764
|
+
except ImportError:
|
|
765
|
+
pass
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
# ---------------------------------------------------------------------------
|
|
769
|
+
# Pure-PyTorch LIF neuron (fallback when snnTorch unavailable)
|
|
770
|
+
# ---------------------------------------------------------------------------
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
class LIFNeuron(nn.Module):
|
|
774
|
+
"""Leaky Integrate-and-Fire neuron with hard threshold spike.
|
|
775
|
+
|
|
776
|
+
This is truly non-differentiable: the spike function has zero
|
|
777
|
+
gradient almost everywhere, making backpropagation useless.
|
|
778
|
+
polystep sidesteps this entirely with gradient-free optimization.
|
|
779
|
+
|
|
780
|
+
Args:
|
|
781
|
+
beta: Membrane decay factor (0 < beta < 1). Higher values = longer memory.
|
|
782
|
+
threshold: Spike threshold for membrane potential.
|
|
783
|
+
"""
|
|
784
|
+
|
|
785
|
+
def __init__(self, beta: float = 0.95, threshold: float = 1.0):
|
|
786
|
+
super().__init__()
|
|
787
|
+
self.beta = beta
|
|
788
|
+
self.threshold = threshold
|
|
789
|
+
|
|
790
|
+
def forward(self, x: torch.Tensor, mem: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
791
|
+
"""One timestep of LIF dynamics.
|
|
792
|
+
|
|
793
|
+
Args:
|
|
794
|
+
x: Input current, shape (batch, features).
|
|
795
|
+
mem: Membrane potential, shape (batch, features).
|
|
796
|
+
|
|
797
|
+
Returns:
|
|
798
|
+
(spike, new_mem): Binary spikes and updated membrane.
|
|
799
|
+
"""
|
|
800
|
+
mem = self.beta * mem + x
|
|
801
|
+
# This is THE non-differentiable operation: d(spike)/d(mem) = 0
|
|
802
|
+
# almost everywhere. Backpropagation gives zero gradients through
|
|
803
|
+
# this line. polystep never differentiates through it.
|
|
804
|
+
spike = (mem >= self.threshold).float()
|
|
805
|
+
mem = mem * (1.0 - spike) # Reset after spike
|
|
806
|
+
return spike, mem
|
|
807
|
+
|
|
808
|
+
|
|
809
|
+
# ---------------------------------------------------------------------------
|
|
810
|
+
# SNN models
|
|
811
|
+
# ---------------------------------------------------------------------------
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
class SpikingNet(nn.Module):
|
|
815
|
+
"""SNN with LIF neurons for classification.
|
|
816
|
+
|
|
817
|
+
Uses snnTorch.Leaky if available, falls back to pure PyTorch LIF neurons.
|
|
818
|
+
|
|
819
|
+
Architecture: Linear -> LIF -> Linear -> LIF
|
|
820
|
+
Output: mean spike rate over num_steps timesteps.
|
|
821
|
+
|
|
822
|
+
Why gradient-free for SNNs?
|
|
823
|
+
SNNs use hard threshold spikes: d(spike)/d(membrane) = 0.
|
|
824
|
+
Backpropagation gives zero gradients through spikes.
|
|
825
|
+
Surrogate gradients are an approximation hack.
|
|
826
|
+
polystep needs NO gradients -- only forward passes!
|
|
827
|
+
|
|
828
|
+
Args:
|
|
829
|
+
input_dim: Input dimension (flattened).
|
|
830
|
+
hidden: Number of hidden neurons.
|
|
831
|
+
output: Number of output classes.
|
|
832
|
+
beta: Membrane decay factor (0.9-0.99 typical).
|
|
833
|
+
num_steps: Number of timesteps for spike integration.
|
|
834
|
+
use_snntorch: Use snnTorch if available (default: True).
|
|
835
|
+
|
|
836
|
+
Example:
|
|
837
|
+
>>> model = SpikingNet(input_dim=32*32*2, hidden=128, output=10, num_steps=25)
|
|
838
|
+
>>> x = torch.randn(32, 25, 2, 32, 32) # (batch, time, polarity, H, W)
|
|
839
|
+
>>> out = model(x) # (batch, output) spike rates
|
|
840
|
+
"""
|
|
841
|
+
|
|
842
|
+
def __init__(
|
|
843
|
+
self,
|
|
844
|
+
input_dim: int = 784,
|
|
845
|
+
hidden: int = 128,
|
|
846
|
+
output: int = 10,
|
|
847
|
+
beta: float = 0.95,
|
|
848
|
+
num_steps: int = 25,
|
|
849
|
+
use_snntorch: bool = True,
|
|
850
|
+
):
|
|
851
|
+
super().__init__()
|
|
852
|
+
self.input_dim = input_dim
|
|
853
|
+
self.hidden = hidden
|
|
854
|
+
self.output_dim = output
|
|
855
|
+
self.num_steps = num_steps
|
|
856
|
+
self.beta = beta
|
|
857
|
+
self.use_snntorch = use_snntorch and _HAS_SNNTORCH
|
|
858
|
+
|
|
859
|
+
self.fc1 = nn.Linear(input_dim, hidden)
|
|
860
|
+
self.fc2 = nn.Linear(hidden, output)
|
|
861
|
+
|
|
862
|
+
if self.use_snntorch:
|
|
863
|
+
self.lif1 = snn.Leaky(beta=beta)
|
|
864
|
+
self.lif2 = snn.Leaky(beta=beta)
|
|
865
|
+
else:
|
|
866
|
+
self.lif1 = LIFNeuron(beta=beta)
|
|
867
|
+
self.lif2 = LIFNeuron(beta=beta)
|
|
868
|
+
|
|
869
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
870
|
+
"""Forward pass: process spike data over multiple timesteps.
|
|
871
|
+
|
|
872
|
+
Supports two input formats:
|
|
873
|
+
1. Temporal spike data: (num_steps, batch, ...) or (batch, num_steps, ...)
|
|
874
|
+
2. Static input: (batch, ...) - presented at each timestep
|
|
875
|
+
|
|
876
|
+
Args:
|
|
877
|
+
x: Input tensor. If temporal, shape is (time, batch, ...) or (batch, time, ...)
|
|
878
|
+
If static, shape is (batch, input_dim) or (batch, channels, H, W).
|
|
879
|
+
|
|
880
|
+
Returns:
|
|
881
|
+
Spike rates, shape (batch, output_dim). Values in [0, 1].
|
|
882
|
+
"""
|
|
883
|
+
# Detect input format and normalize to (num_steps, batch, features)
|
|
884
|
+
if x.dim() >= 3 and x.shape[0] == self.num_steps:
|
|
885
|
+
# Temporal format: (num_steps, batch, ...)
|
|
886
|
+
num_steps = x.shape[0]
|
|
887
|
+
batch = x.shape[1]
|
|
888
|
+
x_seq = x.reshape(num_steps, batch, -1) # (T, B, features)
|
|
889
|
+
elif x.dim() >= 3 and x.shape[1] == self.num_steps:
|
|
890
|
+
# Alternate temporal: (batch, num_steps, ...)
|
|
891
|
+
batch = x.shape[0]
|
|
892
|
+
num_steps = x.shape[1]
|
|
893
|
+
x_seq = x.reshape(batch, num_steps, -1).permute(1, 0, 2) # (T, B, features)
|
|
894
|
+
else:
|
|
895
|
+
# Static input: repeat across timesteps
|
|
896
|
+
batch = x.shape[0]
|
|
897
|
+
num_steps = self.num_steps
|
|
898
|
+
x_flat = x.reshape(batch, -1)
|
|
899
|
+
x_seq = x_flat.unsqueeze(0).expand(num_steps, -1, -1) # (T, B, features)
|
|
900
|
+
|
|
901
|
+
# Initialize membrane potentials
|
|
902
|
+
if self.use_snntorch:
|
|
903
|
+
mem1 = self.lif1.init_leaky()
|
|
904
|
+
mem2 = self.lif2.init_leaky()
|
|
905
|
+
else:
|
|
906
|
+
mem1 = torch.zeros(batch, self.hidden, device=x.device, dtype=x.dtype)
|
|
907
|
+
mem2 = torch.zeros(batch, self.output_dim, device=x.device, dtype=x.dtype)
|
|
908
|
+
|
|
909
|
+
total_spikes = torch.zeros(batch, self.output_dim, device=x.device, dtype=x.dtype)
|
|
910
|
+
|
|
911
|
+
for t in range(num_steps):
|
|
912
|
+
cur1 = self.fc1(x_seq[t])
|
|
913
|
+
spk1, mem1 = self.lif1(cur1, mem1)
|
|
914
|
+
cur2 = self.fc2(spk1)
|
|
915
|
+
spk2, mem2 = self.lif2(cur2, mem2)
|
|
916
|
+
total_spikes = total_spikes + spk2
|
|
917
|
+
|
|
918
|
+
return total_spikes / num_steps # Spike rate in [0, 1]
|
|
919
|
+
|
|
920
|
+
|
|
921
|
+
# ---------------------------------------------------------------------------
|
|
922
|
+
# N-MNIST data loading
|
|
923
|
+
# ---------------------------------------------------------------------------
|
|
924
|
+
|
|
925
|
+
|
|
926
|
+
def get_nmnist_loaders(
|
|
927
|
+
data_dir: str = "/tmp/nmnist",
|
|
928
|
+
num_steps: int = 25,
|
|
929
|
+
batch_size: int = 64,
|
|
930
|
+
max_train: int = 0,
|
|
931
|
+
max_test: int = 0,
|
|
932
|
+
) -> Tuple[DataLoader, DataLoader]:
|
|
933
|
+
"""Load N-MNIST neuromorphic dataset.
|
|
934
|
+
|
|
935
|
+
N-MNIST is a neuromorphic version of MNIST created by displaying MNIST
|
|
936
|
+
digits on a monitor and recording with a DVS (Dynamic Vision Sensor) camera.
|
|
937
|
+
It contains ON and OFF polarity events encoding digit patterns.
|
|
938
|
+
|
|
939
|
+
Uses snnTorch spikevision if available, falls back to synthetic spike data.
|
|
940
|
+
|
|
941
|
+
Args:
|
|
942
|
+
data_dir: Directory to store/load N-MNIST data.
|
|
943
|
+
num_steps: Number of timesteps for spike binning.
|
|
944
|
+
batch_size: Batch size for DataLoader.
|
|
945
|
+
max_train: Maximum training samples (0=full dataset, ~60K).
|
|
946
|
+
max_test: Maximum test samples (0=full dataset, ~10K).
|
|
947
|
+
|
|
948
|
+
Returns:
|
|
949
|
+
Tuple of (train_loader, test_loader).
|
|
950
|
+
Data format: (batch, num_steps, 2, 34, 34) for snnTorch
|
|
951
|
+
(batch, num_steps, 2, 32, 32) for synthetic
|
|
952
|
+
Label format: (batch,) with values 0-9.
|
|
953
|
+
|
|
954
|
+
Note:
|
|
955
|
+
With snnTorch, the actual data shape depends on snnTorch version.
|
|
956
|
+
The SpikingNet model handles both formats automatically.
|
|
957
|
+
|
|
958
|
+
Example:
|
|
959
|
+
>>> train_loader, test_loader = get_nmnist_loaders(max_train=1000)
|
|
960
|
+
>>> for data, labels in train_loader:
|
|
961
|
+
... # data: (batch, time, polarity, H, W)
|
|
962
|
+
... pass
|
|
963
|
+
"""
|
|
964
|
+
if _HAS_SNNTORCH:
|
|
965
|
+
try:
|
|
966
|
+
return _load_nmnist_snntorch(data_dir, num_steps, batch_size, max_train, max_test)
|
|
967
|
+
except Exception as e:
|
|
968
|
+
print(f" snnTorch N-MNIST loading failed: {e}")
|
|
969
|
+
print(" Falling back to synthetic spike data...")
|
|
970
|
+
|
|
971
|
+
return _generate_synthetic_nmnist(num_steps, batch_size, max_train, max_test)
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def _load_nmnist_snntorch(
|
|
975
|
+
data_dir: str,
|
|
976
|
+
num_steps: int,
|
|
977
|
+
batch_size: int,
|
|
978
|
+
max_train: int,
|
|
979
|
+
max_test: int,
|
|
980
|
+
) -> Tuple[DataLoader, DataLoader]:
|
|
981
|
+
"""Load N-MNIST using snnTorch spikevision."""
|
|
982
|
+
from snntorch.spikevision import spikedata
|
|
983
|
+
|
|
984
|
+
print(f" Loading N-MNIST from snnTorch (data_dir={data_dir})...")
|
|
985
|
+
|
|
986
|
+
# snnTorch NMNIST returns data in format (time, polarity, H, W)
|
|
987
|
+
train_ds = spikedata.NMNIST(
|
|
988
|
+
root=data_dir,
|
|
989
|
+
train=True,
|
|
990
|
+
num_steps=num_steps,
|
|
991
|
+
dt=1000, # 1ms time bins
|
|
992
|
+
)
|
|
993
|
+
test_ds = spikedata.NMNIST(
|
|
994
|
+
root=data_dir,
|
|
995
|
+
train=False,
|
|
996
|
+
num_steps=num_steps,
|
|
997
|
+
dt=1000,
|
|
998
|
+
)
|
|
999
|
+
|
|
1000
|
+
# Apply sample limits using Subset
|
|
1001
|
+
if max_train > 0 and len(train_ds) > max_train:
|
|
1002
|
+
train_ds = torch.utils.data.Subset(train_ds, range(max_train))
|
|
1003
|
+
if max_test > 0 and len(test_ds) > max_test:
|
|
1004
|
+
test_ds = torch.utils.data.Subset(test_ds, range(max_test))
|
|
1005
|
+
|
|
1006
|
+
train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=0)
|
|
1007
|
+
test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False, num_workers=0)
|
|
1008
|
+
|
|
1009
|
+
return train_loader, test_loader
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
def _generate_synthetic_nmnist(
|
|
1013
|
+
num_steps: int,
|
|
1014
|
+
batch_size: int,
|
|
1015
|
+
max_train: int,
|
|
1016
|
+
max_test: int,
|
|
1017
|
+
) -> Tuple[DataLoader, DataLoader]:
|
|
1018
|
+
"""Generate synthetic N-MNIST-like spike data.
|
|
1019
|
+
|
|
1020
|
+
Creates synthetic spike patterns for digits 0-9. Each digit has a
|
|
1021
|
+
distinct activation pattern simulating rate-coded neural spikes.
|
|
1022
|
+
"""
|
|
1023
|
+
print(" Generating synthetic N-MNIST data...")
|
|
1024
|
+
|
|
1025
|
+
num_train = max_train if max_train > 0 else 5000
|
|
1026
|
+
num_test = max_test if max_test > 0 else 1000
|
|
1027
|
+
height, width = 32, 32
|
|
1028
|
+
num_classes = 10
|
|
1029
|
+
|
|
1030
|
+
def generate_digit_spikes(num_samples: int, seed: int) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
1031
|
+
rng = torch.Generator().manual_seed(seed)
|
|
1032
|
+
|
|
1033
|
+
# Create templates for each digit - different spatial patterns
|
|
1034
|
+
templates = torch.zeros(num_classes, 2, height, width)
|
|
1035
|
+
for c in range(num_classes):
|
|
1036
|
+
# Each digit activates different regions
|
|
1037
|
+
y_start = (c // 2) * (height // 5)
|
|
1038
|
+
x_start = (c % 5) * (width // 5)
|
|
1039
|
+
y_end = min(y_start + height // 4, height)
|
|
1040
|
+
x_end = min(x_start + width // 4, width)
|
|
1041
|
+
templates[c, 0, y_start:y_end, x_start:x_end] = 0.8 # ON polarity
|
|
1042
|
+
templates[c, 1, y_start:y_end, x_start:x_end] = 0.3 # OFF polarity
|
|
1043
|
+
|
|
1044
|
+
labels = torch.randint(0, num_classes, (num_samples,), generator=rng)
|
|
1045
|
+
|
|
1046
|
+
# Generate spike data: (samples, time, polarity, H, W)
|
|
1047
|
+
data = torch.zeros(num_samples, num_steps, 2, height, width)
|
|
1048
|
+
for i, label in enumerate(labels):
|
|
1049
|
+
template = templates[label.item()]
|
|
1050
|
+
# Spike with probability proportional to template
|
|
1051
|
+
for t in range(num_steps):
|
|
1052
|
+
noise = torch.rand(2, height, width, generator=rng)
|
|
1053
|
+
spikes = (noise < template).float()
|
|
1054
|
+
data[i, t] = spikes
|
|
1055
|
+
|
|
1056
|
+
return data, labels
|
|
1057
|
+
|
|
1058
|
+
train_data, train_labels = generate_digit_spikes(num_train, seed=42)
|
|
1059
|
+
test_data, test_labels = generate_digit_spikes(num_test, seed=123)
|
|
1060
|
+
|
|
1061
|
+
train_ds = TensorDataset(train_data, train_labels)
|
|
1062
|
+
test_ds = TensorDataset(test_data, test_labels)
|
|
1063
|
+
|
|
1064
|
+
train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=0)
|
|
1065
|
+
test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False, num_workers=0)
|
|
1066
|
+
|
|
1067
|
+
print(f" Generated {num_train} train, {num_test} test samples (synthetic)")
|
|
1068
|
+
return train_loader, test_loader
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
# ---------------------------------------------------------------------------
|
|
1072
|
+
# DVS-Gesture data loading
|
|
1073
|
+
# ---------------------------------------------------------------------------
|
|
1074
|
+
|
|
1075
|
+
|
|
1076
|
+
def get_dvs_gesture_loaders(
|
|
1077
|
+
data_dir: str = "/tmp/dvs_gesture",
|
|
1078
|
+
num_steps: int = 50,
|
|
1079
|
+
batch_size: int = 16,
|
|
1080
|
+
max_train: int = 0,
|
|
1081
|
+
max_test: int = 0,
|
|
1082
|
+
synthetic: bool = False,
|
|
1083
|
+
) -> Tuple[DataLoader, DataLoader]:
|
|
1084
|
+
"""Load DVS-Gesture neuromorphic dataset.
|
|
1085
|
+
|
|
1086
|
+
DVS-Gesture contains 11 hand gestures recorded with a DVS camera.
|
|
1087
|
+
The dataset is ~1.5GB, so fallback to synthetic is supported.
|
|
1088
|
+
|
|
1089
|
+
Uses snnTorch spikevision if available, falls back to synthetic spike data.
|
|
1090
|
+
|
|
1091
|
+
Args:
|
|
1092
|
+
data_dir: Directory to store/load DVS-Gesture data.
|
|
1093
|
+
num_steps: Number of timesteps for spike binning.
|
|
1094
|
+
batch_size: Batch size for DataLoader.
|
|
1095
|
+
max_train: Maximum training samples (0=full dataset, ~1077).
|
|
1096
|
+
max_test: Maximum test samples (0=full dataset, ~264).
|
|
1097
|
+
synthetic: Force synthetic data (skip download attempt).
|
|
1098
|
+
|
|
1099
|
+
Returns:
|
|
1100
|
+
Tuple of (train_loader, test_loader).
|
|
1101
|
+
Data format: (batch, num_steps, 2, 128, 128) or downsampled.
|
|
1102
|
+
Label format: (batch,) with values 0-10 (11 gesture classes).
|
|
1103
|
+
|
|
1104
|
+
Gesture classes:
|
|
1105
|
+
0: hand_clapping, 1: right_hand_wave, 2: left_hand_wave,
|
|
1106
|
+
3: right_arm_cw, 4: right_arm_ccw, 5: left_arm_cw,
|
|
1107
|
+
6: left_arm_ccw, 7: arm_roll, 8: air_drums,
|
|
1108
|
+
9: air_guitar, 10: other_gestures
|
|
1109
|
+
|
|
1110
|
+
Example:
|
|
1111
|
+
>>> train_loader, test_loader = get_dvs_gesture_loaders(synthetic=True)
|
|
1112
|
+
>>> for data, labels in train_loader:
|
|
1113
|
+
... # data: (batch, time, polarity, H, W)
|
|
1114
|
+
... pass
|
|
1115
|
+
"""
|
|
1116
|
+
if synthetic:
|
|
1117
|
+
return _generate_synthetic_dvs_gesture(num_steps, batch_size, max_train, max_test)
|
|
1118
|
+
|
|
1119
|
+
if _HAS_SNNTORCH:
|
|
1120
|
+
try:
|
|
1121
|
+
return _load_dvs_gesture_snntorch(data_dir, num_steps, batch_size, max_train, max_test)
|
|
1122
|
+
except Exception as e:
|
|
1123
|
+
print(f" snnTorch DVS-Gesture loading failed: {e}")
|
|
1124
|
+
print(" Falling back to synthetic spike data...")
|
|
1125
|
+
|
|
1126
|
+
return _generate_synthetic_dvs_gesture(num_steps, batch_size, max_train, max_test)
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def _load_dvs_gesture_snntorch(
|
|
1130
|
+
data_dir: str,
|
|
1131
|
+
num_steps: int,
|
|
1132
|
+
batch_size: int,
|
|
1133
|
+
max_train: int,
|
|
1134
|
+
max_test: int,
|
|
1135
|
+
) -> Tuple[DataLoader, DataLoader]:
|
|
1136
|
+
"""Load DVS-Gesture using snnTorch spikevision."""
|
|
1137
|
+
from snntorch.spikevision import spikedata
|
|
1138
|
+
|
|
1139
|
+
print(f" Loading DVS-Gesture from snnTorch (data_dir={data_dir})...")
|
|
1140
|
+
print(" Note: First download is ~1.5GB and may take several minutes.")
|
|
1141
|
+
|
|
1142
|
+
train_ds = spikedata.DVSGesture(
|
|
1143
|
+
root=data_dir,
|
|
1144
|
+
train=True,
|
|
1145
|
+
num_steps=num_steps,
|
|
1146
|
+
dt=1000,
|
|
1147
|
+
)
|
|
1148
|
+
test_ds = spikedata.DVSGesture(
|
|
1149
|
+
root=data_dir,
|
|
1150
|
+
train=False,
|
|
1151
|
+
num_steps=num_steps,
|
|
1152
|
+
dt=1000,
|
|
1153
|
+
)
|
|
1154
|
+
|
|
1155
|
+
if max_train > 0 and len(train_ds) > max_train:
|
|
1156
|
+
train_ds = torch.utils.data.Subset(train_ds, range(max_train))
|
|
1157
|
+
if max_test > 0 and len(test_ds) > max_test:
|
|
1158
|
+
test_ds = torch.utils.data.Subset(test_ds, range(max_test))
|
|
1159
|
+
|
|
1160
|
+
train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=0)
|
|
1161
|
+
test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False, num_workers=0)
|
|
1162
|
+
|
|
1163
|
+
return train_loader, test_loader
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def _generate_synthetic_dvs_gesture(
|
|
1167
|
+
num_steps: int,
|
|
1168
|
+
batch_size: int,
|
|
1169
|
+
max_train: int,
|
|
1170
|
+
max_test: int,
|
|
1171
|
+
) -> Tuple[DataLoader, DataLoader]:
|
|
1172
|
+
"""Generate synthetic DVS-Gesture-like spike data.
|
|
1173
|
+
|
|
1174
|
+
Creates synthetic gesture patterns. Each gesture has a distinct
|
|
1175
|
+
spatio-temporal activation pattern.
|
|
1176
|
+
"""
|
|
1177
|
+
print(" Generating synthetic DVS-Gesture data...")
|
|
1178
|
+
|
|
1179
|
+
num_train = max_train if max_train > 0 else 500
|
|
1180
|
+
num_test = max_test if max_test > 0 else 100
|
|
1181
|
+
height, width = 32, 32 # Downsampled from 128x128 for efficiency
|
|
1182
|
+
num_classes = 11
|
|
1183
|
+
|
|
1184
|
+
def generate_gesture_spikes(num_samples: int, seed: int) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
1185
|
+
rng = torch.Generator().manual_seed(seed)
|
|
1186
|
+
|
|
1187
|
+
labels = torch.randint(0, num_classes, (num_samples,), generator=rng)
|
|
1188
|
+
data = torch.zeros(num_samples, num_steps, 2, height, width)
|
|
1189
|
+
|
|
1190
|
+
for i, label in enumerate(labels):
|
|
1191
|
+
c = label.item()
|
|
1192
|
+
# Each gesture has different temporal pattern
|
|
1193
|
+
for t in range(num_steps):
|
|
1194
|
+
# Spatial pattern varies by gesture class
|
|
1195
|
+
phase = (t / num_steps + c / num_classes) * 2 * 3.14159
|
|
1196
|
+
center_y = int((height // 2) + (height // 4) * np.sin(phase * (c % 3 + 1)))
|
|
1197
|
+
center_x = int((width // 2) + (width // 4) * np.cos(phase * (c % 4 + 1)))
|
|
1198
|
+
|
|
1199
|
+
# Create activation region around center
|
|
1200
|
+
y_start = max(0, center_y - height // 8)
|
|
1201
|
+
y_end = min(height, center_y + height // 8)
|
|
1202
|
+
x_start = max(0, center_x - width // 8)
|
|
1203
|
+
x_end = min(width, center_x + width // 8)
|
|
1204
|
+
|
|
1205
|
+
# Spike probability in active region
|
|
1206
|
+
prob = 0.3 + 0.3 * np.sin(phase)
|
|
1207
|
+
noise = torch.rand(y_end - y_start, x_end - x_start, generator=rng)
|
|
1208
|
+
spikes = (noise < prob).float()
|
|
1209
|
+
|
|
1210
|
+
data[i, t, c % 2, y_start:y_end, x_start:x_end] = spikes
|
|
1211
|
+
|
|
1212
|
+
return data, labels
|
|
1213
|
+
|
|
1214
|
+
train_data, train_labels = generate_gesture_spikes(num_train, seed=42)
|
|
1215
|
+
test_data, test_labels = generate_gesture_spikes(num_test, seed=123)
|
|
1216
|
+
|
|
1217
|
+
train_ds = TensorDataset(train_data, train_labels)
|
|
1218
|
+
test_ds = TensorDataset(test_data, test_labels)
|
|
1219
|
+
|
|
1220
|
+
train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=True, num_workers=0)
|
|
1221
|
+
test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False, num_workers=0)
|
|
1222
|
+
|
|
1223
|
+
print(f" Generated {num_train} train, {num_test} test samples (synthetic gestures)")
|
|
1224
|
+
return train_loader, test_loader
|
|
1225
|
+
|
|
1226
|
+
|
|
1227
|
+
# ---------------------------------------------------------------------------
|
|
1228
|
+
# SNN evaluation utilities
|
|
1229
|
+
# ---------------------------------------------------------------------------
|
|
1230
|
+
|
|
1231
|
+
|
|
1232
|
+
@torch.no_grad()
|
|
1233
|
+
def evaluate_snn_accuracy(model: nn.Module, dataloader: DataLoader, scale_output: float = 10.0) -> float:
|
|
1234
|
+
"""Compute classification accuracy for SNN models.
|
|
1235
|
+
|
|
1236
|
+
Args:
|
|
1237
|
+
model: SNN model that outputs spike rates.
|
|
1238
|
+
dataloader: DataLoader with (spike_data, labels).
|
|
1239
|
+
scale_output: Scale factor for spike rates before argmax.
|
|
1240
|
+
SNNs output rates in [0,1], scaling helps with CrossEntropyLoss.
|
|
1241
|
+
|
|
1242
|
+
Returns:
|
|
1243
|
+
Accuracy as a float between 0 and 1.
|
|
1244
|
+
"""
|
|
1245
|
+
model.eval()
|
|
1246
|
+
device = next(model.parameters()).device
|
|
1247
|
+
correct = 0
|
|
1248
|
+
total = 0
|
|
1249
|
+
|
|
1250
|
+
for batch in dataloader:
|
|
1251
|
+
data, targets = batch
|
|
1252
|
+
data = data.to(device)
|
|
1253
|
+
targets = targets.to(device)
|
|
1254
|
+
|
|
1255
|
+
# SNN outputs spike rates
|
|
1256
|
+
outputs = model(data) * scale_output
|
|
1257
|
+
preds = outputs.argmax(dim=-1)
|
|
1258
|
+
correct += (preds == targets).sum().item()
|
|
1259
|
+
total += targets.size(0)
|
|
1260
|
+
|
|
1261
|
+
model.train()
|
|
1262
|
+
return correct / total if total > 0 else 0.0
|