Твій мозок споживає 20 ватів. Менше, ніж лампочка. При цьому він обробляє інформацію, на яку GPU витрачає кіловати. Розпізнає обличчя за мілісекунди. Розуміє мову в шумному середовищі. Координує рухи всього тіла.
Секрет? Мозок не рахує постійно. Нейрони мовчать більшість часу. Вони «стріляють» (spike) тільки коли є щось важливе. Spike — і тиша. Spike — і тиша. Інформація кодується не в амплітуді сигналу, а в часі спайків.
Spiking Neural Networks (SNN) копіюють цей принцип. А нейроморфні чіпи — Intel Loihi, IBM TrueNorth, BrainChip Akida — це hardware, який робить це ефективно. В 100-1000 разів менше енергії, ніж традиційні нейромережі. Це не покращення. Це інша парадигма обчислень.
Чому традиційні нейронки енергетично неефективні
Проблема continuous activation:
class TraditionalNeuralNet:
"""Чому звичайні мережі споживають багато енергії"""
def forward(self, x):
# КОЖЕН нейрон активується на КОЖНОМУ кроці
for layer in self.layers:
x = layer(x) # матричне множення
x = relu(x) # activation
# Навіть якщо input не змінився — все одно обчислюємо
# Мільйони neurons × мільйони операцій = мільярди FLOPs
return x
# Проблеми:
# 1. Dense computation — всі нейрони активні
# 2. Synchronous updates — все обчислюється кожен timestep
# 3. High precision — float32 для кожної операції
# 4. Memory bandwidth — постійне читання/запис ваг
Біологічна реальність:
| Властивість | Традиційна NN | Біологічний мозок |
|-------------|---------------|-------------------|
| Активність нейронів | 100% (dense) | ~1-5% (sparse) |
| Обробка | Synchronous | Asynchronous, event-driven |
| Енергія на операцію | ~pJ (GPU) | ~fJ (біологія) |
| Інформаційне кодування | Rate (amplitude) | Temporal (spike timing) |
| Пластичність | Тільки training | Continuous |
Моделі спайкових нейронів
Leaky Integrate-and-Fire (LIF) — найпопулярніша модель:
import torch
import torch.nn as nn
import numpy as np
class LeakyIntegrateAndFire(nn.Module):
"""
Leaky Integrate-and-Fire neuron — основа SNN
Динаміка:
τ·dV/dt = -(V - V_rest) + R·I(t)
Якщо V > V_threshold: emit spike, V = V_reset
"""
def __init__(self,
tau_mem: float = 20.0, # membrane time constant (ms)
v_rest: float = -65.0, # resting potential (mV)
v_threshold: float = -50.0, # spike threshold (mV)
v_reset: float = -70.0, # reset potential after spike (mV)
dt: float = 1.0): # time step (ms)
super().__init__()
self.tau_mem = tau_mem
self.v_rest = v_rest
self.v_threshold = v_threshold
self.v_reset = v_reset
self.dt = dt
# Decay factor
self.beta = np.exp(-dt / tau_mem)
def forward(self, input_current: torch.Tensor,
membrane: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""
Single timestep update
input_current: (batch, neurons) - weighted input spikes
membrane: (batch, neurons) - membrane potential
returns: (spikes, new_membrane)
"""
# Leaky integration
membrane = self.beta * membrane + (1 - self.beta) * input_current
# Spike generation
spikes = (membrane > self.v_threshold).float()
# Reset after spike
membrane = membrane * (1 - spikes) + self.v_reset * spikes
return spikes, membrane
class LIFLayer(nn.Module):
"""Повний LIF layer з ваговими зв'язками"""
def __init__(self, in_features: int, out_features: int,
beta: float = 0.9):
super().__init__()
self.fc = nn.Linear(in_features, out_features, bias=False)
self.beta = beta # membrane decay
self.threshold = 1.0
def forward(self, spikes: torch.Tensor,
mem: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
"""
spikes: input spikes (batch, in_features)
mem: membrane potential (batch, out_features)
"""
# Weighted input
cur = self.fc(spikes)
# Leaky integration
mem = self.beta * mem + cur
# Threshold and fire
out_spikes = (mem > self.threshold).float()
# Soft reset (subtract threshold)
mem = mem - out_spikes * self.threshold
return out_spikes, mem
Інші моделі нейронів:
class AdaptiveLIF(nn.Module):
"""
Adaptive LIF — нейрон з адаптацією
Після спайку threshold тимчасово підвищується
"""
def __init__(self, beta: float = 0.9, rho: float = 0.95):
super().__init__()
self.beta = beta # membrane decay
self.rho = rho # threshold adaptation decay
self.base_threshold = 1.0
self.adaptation_strength = 0.5
def forward(self, x: torch.Tensor, mem: torch.Tensor,
threshold: torch.Tensor) -> tuple:
# Leaky integration
mem = self.beta * mem + x
# Dynamic threshold
spike = (mem > threshold).float()
# Soft reset
mem = mem - spike * threshold
# Threshold adaptation (increases after spike, then decays)
threshold = self.rho * threshold + (1 - self.rho) * self.base_threshold
threshold = threshold + spike * self.adaptation_strength
return spike, mem, threshold
class IzhikevichNeuron(nn.Module):
"""
Izhikevich model — біологічно реалістичніший
Може відтворювати різні паттерни: regular, bursting, chattering
"""
def __init__(self, a=0.02, b=0.2, c=-65, d=8, dt=0.5):
super().__init__()
self.a = a # time scale of recovery
self.b = b # sensitivity of recovery to V
self.c = c # reset voltage
self.d = d # reset recovery
self.dt = dt
def forward(self, I: torch.Tensor, v: torch.Tensor,
u: torch.Tensor) -> tuple:
"""
I: input current
v: membrane potential
u: recovery variable
"""
# Dynamics
dv = 0.04 * v**2 + 5*v + 140 - u + I
du = self.a * (self.b*v - u)
v = v + self.dt * dv
u = u + self.dt * du
# Spike and reset
spike = (v >= 30).float()
v = torch.where(spike.bool(), torch.tensor(self.c), v)
u = u + spike * self.d
return spike, v, u
Spike Encoding — як перетворити дані в спайки
Три основні методи кодування:
import torch
import numpy as np
class SpikeEncoding:
"""Методи перетворення аналогових даних у спайки"""
@staticmethod
def rate_encoding(data: torch.Tensor, num_steps: int,
max_rate: float = 1.0) -> torch.Tensor:
"""
Rate encoding — частота спайків пропорційна інтенсивності
Простий, але не оптимальний для енергоефективності
"""
# Normalize to [0, 1]
data_norm = (data - data.min()) / (data.max() - data.min() + 1e-8)
# Generate spikes based on probability
spikes = torch.zeros(num_steps, *data.shape)
for t in range(num_steps):
spikes[t] = (torch.rand_like(data_norm) < data_norm * max_rate).float()
return spikes # (time, batch, features)
@staticmethod
def latency_encoding(data: torch.Tensor, num_steps: int,
tau: float = 5.0) -> torch.Tensor:
"""
Latency encoding — час спайку кодує інтенсивність
Сильніший сигнал → раніший спайк
Дуже енергоефективний (один спайк на нейрон)
"""
# Normalize to [0, 1]
data_norm = (data - data.min()) / (data.max() - data.min() + 1e-8)
# Inverse: higher value → earlier spike
spike_times = ((1 - data_norm) * (num_steps - 1)).long()
# Generate spike train
spikes = torch.zeros(num_steps, *data.shape)
for t in range(num_steps):
spikes[t] = (spike_times == t).float()
return spikes
@staticmethod
def delta_encoding(data: torch.Tensor,
threshold: float = 0.1) -> torch.Tensor:
"""
Delta encoding — спайк при зміні сигналу
Ідеально для event-driven sensors (DVS cameras)
"""
# Compute differences
diff = torch.zeros_like(data)
diff[1:] = data[1:] - data[:-1]
# Positive and negative changes
pos_spikes = (diff > threshold).float()
neg_spikes = (diff < -threshold).float()
# Two channels: ON and OFF
return torch.stack([pos_spikes, neg_spikes], dim=-1)
class DVSEventStream:
"""
Dynamic Vision Sensor (DVS) — event camera
Виводить події тільки при зміні яскравості
Природне джерело spike-based даних
"""
def __init__(self, threshold: float = 0.15):
self.threshold = threshold
self.reference = None
def process_frame(self, frame: np.ndarray) -> list[dict]:
"""Перетворює кадр у потік подій"""
if self.reference is None:
self.reference = frame.copy().astype(np.float32)
return []
events = []
log_frame = np.log(frame.astype(np.float32) + 1)
log_ref = np.log(self.reference + 1)
diff = log_frame - log_ref
# ON events (brightness increase)
on_mask = diff > self.threshold
on_coords = np.argwhere(on_mask)
for y, x in on_coords:
events.append({'x': x, 'y': y, 'polarity': 1, 'timestamp': 0})
# OFF events (brightness decrease)
off_mask = diff < -self.threshold
off_coords = np.argwhere(off_mask)
for y, x in off_coords:
events.append({'x': x, 'y': y, 'polarity': 0, 'timestamp': 0})
# Update reference where events occurred
self.reference[on_mask | off_mask] = frame[on_mask | off_mask]
return events
Навчання SNN — проблема недиференційованості
Проблема: Spike function — step function, градієнт = 0 або ∞
class SurrogateGradient(torch.autograd.Function):
"""
Surrogate gradient — ключ до backprop через спайки
Forward: справжня spike function (hard threshold)
Backward: smooth surrogate (дозволяє градієнту проходити)
"""
@staticmethod
def forward(ctx, membrane: torch.Tensor, threshold: float = 1.0):
ctx.save_for_backward(membrane)
ctx.threshold = threshold
# Hard threshold: справжні спайки
return (membrane > threshold).float()
@staticmethod
def backward(ctx, grad_output: torch.Tensor):
membrane, = ctx.saved_tensors
threshold = ctx.threshold
# Surrogate gradient: fast sigmoid
grad = grad_output / (1 + torch.abs(membrane - threshold))**2
return grad, None
class FastSigmoidSurrogate:
"""Альтернативні surrogate functions"""
@staticmethod
def rectangular(x: torch.Tensor, width: float = 0.5) -> torch.Tensor:
"""Rectangular window"""
return (torch.abs(x) < width).float() / (2 * width)
@staticmethod
def triangular(x: torch.Tensor, width: float = 1.0) -> torch.Tensor:
"""Triangular window"""
return torch.clamp(1 - torch.abs(x) / width, min=0) / width
@staticmethod
def gaussian(x: torch.Tensor, sigma: float = 0.5) -> torch.Tensor:
"""Gaussian window"""
return torch.exp(-x**2 / (2 * sigma**2)) / (sigma * np.sqrt(2*np.pi))
@staticmethod
def fast_sigmoid(x: torch.Tensor, slope: float = 25) -> torch.Tensor:
"""Fast sigmoid — найпопулярніший"""
return slope / (1 + slope * torch.abs(x))**2
class SNNWithSurrogateGrad(nn.Module):
"""Повна SNN з surrogate gradient training"""
def __init__(self, input_size: int, hidden_size: int, output_size: int,
num_steps: int = 100, beta: float = 0.9):
super().__init__()
self.num_steps = num_steps
self.beta = beta
# Layers
self.fc1 = nn.Linear(input_size, hidden_size)
self.fc2 = nn.Linear(hidden_size, output_size)
# Spike function with surrogate gradient
self.spike_fn = SurrogateGradient.apply
def forward(self, x: torch.Tensor) -> torch.Tensor:
"""
x: (batch, time, features) or (batch, features) for static input
"""
batch_size = x.shape[0]
# Initialize membrane potentials
mem1 = torch.zeros(batch_size, self.fc1.out_features, device=x.device)
mem2 = torch.zeros(batch_size, self.fc2.out_features, device=x.device)
# Output accumulator
out_sum = torch.zeros(batch_size, self.fc2.out_features, device=x.device)
for t in range(self.num_steps):
# Get input (time-varying or static)
if x.dim() == 3:
x_t = x[:, t, :]
else:
x_t = x
# Layer 1
cur1 = self.fc1(x_t)
mem1 = self.beta * mem1 + cur1
spk1 = self.spike_fn(mem1)
mem1 = mem1 - spk1 # soft reset
# Layer 2
cur2 = self.fc2(spk1)
mem2 = self.beta * mem2 + cur2
spk2 = self.spike_fn(mem2)
mem2 = mem2 - spk2
# Accumulate output spikes
out_sum += spk2
# Rate decoding: spike count / time steps
return out_sum / self.num_steps
ANN-to-SNN Conversion
Альтернативний підхід: Навчаємо звичайну мережу, потім конвертуємо.
class ANNtoSNNConverter:
"""
Конвертація навченої ANN у SNN
Плюси: використовуємо існуючі trained models
Мінуси: suboptimal — не використовуємо temporal dynamics
"""
def __init__(self, percentile: float = 99.9):
self.percentile = percentile
self.scale_factors = {}
def calibrate(self, model: nn.Module, data_loader,
num_batches: int = 100):
"""Калібрує scale factors на representative data"""
activations = {}
def hook_fn(name):
def hook(module, input, output):
if name not in activations:
activations[name] = []
activations[name].append(output.detach().cpu())
return hook
# Register hooks
hooks = []
for name, module in model.named_modules():
if isinstance(module, nn.ReLU):
hooks.append(module.register_forward_hook(hook_fn(name)))
# Run calibration
model.eval()
with torch.no_grad():
for i, (x, _) in enumerate(data_loader):
if i >= num_batches:
break
model(x)
# Remove hooks
for h in hooks:
h.remove()
# Compute scale factors
for name, acts in activations.items():
all_acts = torch.cat(acts)
self.scale_factors[name] = np.percentile(
all_acts.numpy().flatten(),
self.percentile
)
def convert_layer(self, layer: nn.Linear,
scale_in: float, scale_out: float) -> nn.Linear:
"""Конвертує один linear layer"""
# Scale weights
new_layer = nn.Linear(layer.in_features, layer.out_features,
bias=layer.bias is not None)
new_layer.weight.data = layer.weight.data * (scale_out / scale_in)
if layer.bias is not None:
new_layer.bias.data = layer.bias.data / scale_in
return new_layer
def convert_model(self, ann_model: nn.Module) -> nn.Module:
"""Конвертує всю модель"""
# Implementation depends on model architecture
# Basic idea: replace ReLU with IF neurons
# Scale weights based on calibration
pass
class ConvertedSNN(nn.Module):
"""SNN отримана з конвертації ANN"""
def __init__(self, converted_layers: list, num_steps: int = 100):
super().__init__()
self.layers = nn.ModuleList(converted_layers)
self.num_steps = num_steps
def forward(self, x: torch.Tensor) -> torch.Tensor:
batch_size = x.shape[0]
# Initialize membranes
mems = [torch.zeros(batch_size, l.out_features, device=x.device)
for l in self.layers if isinstance(l, nn.Linear)]
out_spikes = torch.zeros(batch_size, self.layers[-1].out_features,
device=x.device)
for t in range(self.num_steps):
spk = x / self.num_steps # Rate encoding
for i, layer in enumerate(self.layers):
if isinstance(layer, nn.Linear):
mems[i] += layer(spk)
spk = (mems[i] > 1.0).float()
mems[i] = mems[i] - spk # reset
out_spikes += spk
return out_spikes
Нейроморфні чіпи
Intel Loihi 2 (2021):
class Loihi2Architecture:
"""Характеристики Intel Loihi 2"""
SPECS = {
"neurons": 1_000_000,
"synapses": 120_000_000,
"cores": 128,
"process": "Intel 4",
"features": [
"Programmable neuron models",
"On-chip learning (STDP, 3-factor rules)",
"Graded spikes (multi-bit activations)",
"Programmable synaptic delays",
"Sparse event-driven computation"
],
"power": "~1W typical workload",
"latency": "~1ms for inference"
}
@staticmethod
def compare_to_gpu():
"""Порівняння з GPU на SNN workloads"""
return {
"energy_ratio": "100-1000x more efficient",
"latency": "10-100x faster for sparse",
"throughput": "Lower than GPU for dense"
}
class LoihiDeployment:
"""Приклад deployment на Loihi через Lava"""
def create_network(self):
"""
Lava framework для Loihi
Примітка: потребує Intel INRC access
"""
# Псевдокод — реальний API вимагає Lava installation
"""
from lava.proc.lif.process import LIF
from lava.proc.dense.process import Dense
# Define layers
input_layer = InputProcess(shape=(784,))
hidden = LIF(shape=(128,), du=0.9, dv=0.9, vth=1.0)
output = LIF(shape=(10,), du=0.9, dv=0.9, vth=1.0)
# Connections
conn1 = Dense(weights=w1)
conn2 = Dense(weights=w2)
# Connect
input_layer.out_ports.s_out.connect(conn1.in_ports.s_in)
conn1.out_ports.a_out.connect(hidden.in_ports.a_in)
hidden.out_ports.s_out.connect(conn2.in_ports.s_in)
conn2.out_ports.a_out.connect(output.in_ports.a_in)
# Run
run_cfg = Loihi2HwCfg()
output.run(condition=RunSteps(100), run_cfg=run_cfg)
"""
pass
IBM TrueNorth:
class TrueNorthArchitecture:
"""IBM TrueNorth характеристики"""
SPECS = {
"neurons": 1_000_000,
"synapses": 256_000_000,
"cores": 4096,
"process": "Samsung 28nm",
"power": "70mW at typical workload",
"features": [
"Fixed neuron model (simplified LIF)",
"No on-chip learning",
"256 neurons per core",
"Tile-based architecture"
]
}
BrainChip Akida:
class AkidaDeployment:
"""BrainChip Akida — комерційний нейроморфний чіп"""
def deploy_model(self, keras_model):
"""
Akida підтримує пряму конвертацію з Keras
pip install akida
"""
"""
from akida import Model as AkidaModel
from cnn2snn import convert
# Convert Keras model to Akida
akida_model = convert(keras_model)
# Quantize for Akida hardware
akida_model.quantize(...)
# Map to hardware
akida_model.map(...)
# Inference on Akida chip
predictions = akida_model.predict(x_test)
"""
pass
APPLICATIONS = [
"Keyword spotting (always-on voice)",
"Gesture recognition",
"Object detection (edge cameras)",
"Anomaly detection (industrial IoT)",
"Vibration analysis (predictive maintenance)"
]
STDP — біологічне навчання
Spike-Timing-Dependent Plasticity:
class STDPLearning:
"""
STDP — локальне unsupervised learning rule
If pre-spike comes BEFORE post-spike: strengthen synapse (LTP)
If pre-spike comes AFTER post-spike: weaken synapse (LTD)
"""
def __init__(self, tau_plus: float = 20.0, tau_minus: float = 20.0,
a_plus: float = 0.01, a_minus: float = 0.01):
self.tau_plus = tau_plus
self.tau_minus = tau_minus
self.a_plus = a_plus
self.a_minus = a_minus
def compute_weight_change(self, pre_times: np.ndarray,
post_times: np.ndarray) -> float:
"""
Обчислює зміну ваги на основі часів спайків
pre_times: масив часів pre-synaptic спайків
post_times: масив часів post-synaptic спайків
"""
delta_w = 0.0
for t_pre in pre_times:
for t_post in post_times:
dt = t_post - t_pre
if dt > 0:
# Pre before post: potentiation
delta_w += self.a_plus * np.exp(-dt / self.tau_plus)
else:
# Post before pre: depression
delta_w -= self.a_minus * np.exp(dt / self.tau_minus)
return delta_w
class STDPLayer(nn.Module):
"""Layer з online STDP learning"""
def __init__(self, in_features: int, out_features: int,
lr: float = 0.01):
super().__init__()
self.weights = nn.Parameter(torch.rand(out_features, in_features) * 0.1)
self.lr = lr
# Trace variables for STDP
self.pre_trace = None
self.post_trace = None
def forward(self, pre_spikes: torch.Tensor,
post_spikes: torch.Tensor = None) -> torch.Tensor:
"""
Forward pass + online STDP update
"""
# Decay traces
tau = 20.0
decay = np.exp(-1 / tau)
if self.pre_trace is None:
self.pre_trace = torch.zeros_like(pre_spikes)
self.post_trace = torch.zeros(pre_spikes.shape[0],
self.weights.shape[0])
# Update traces
self.pre_trace = decay * self.pre_trace + pre_spikes
if post_spikes is not None:
self.post_trace = decay * self.post_trace + post_spikes
# Compute output
output = torch.matmul(pre_spikes, self.weights.T)
# STDP update (if training)
if self.training and post_spikes is not None:
# LTP: post-spike × pre-trace
ltp = torch.outer(post_spikes.mean(0), self.pre_trace.mean(0))
# LTD: pre-spike × post-trace
ltd = torch.outer(self.post_trace.mean(0), pre_spikes.mean(0))
# Weight update
with torch.no_grad():
self.weights += self.lr * (ltp - ltd.T)
self.weights.clamp_(0, 1) # Keep weights bounded
return output
Benchmark результати
Порівняння енергоефективності:
| Task | Platform | Accuracy | Power | Energy/inf |
|------|----------|----------|-------|------------|
| Gesture (DVS128) | GPU (RTX 3090) | 97.2% | 350W | 70mJ |
| Gesture (DVS128) | Loihi 2 | 96.5% | 1W | 0.2mJ |
| Keyword spotting | GPU | 94% | 200W | 40mJ |
| Keyword spotting | Akida | 92% | 50mW | 0.1mJ |
Висновок: 100-1000x енергоефективність за 1-2% accuracy trade-off
Ідеї для дослідження
Для бакалаврської роботи:
- Конвертація простої CNN (MNIST) у SNN, порівняння accuracy
- Реалізація LIF neurons на snnTorch, visualization spike patterns
- Gesture recognition з DVS dataset (DVS128 Gesture)
Для магістерської:
- Surrogate gradient training для specific application
- SNN для anomaly detection в IoT (energy comparison)
- Deployment на BrainChip Akida через MetaTF
- Hybrid SNN-ANN architectures
Для PhD:
- Novel learning rules для SNN (beyond STDP)
- Theoretical expressiveness: що можуть SNN, чого не можуть ANN?
- Scaling SNN до vision transformers
- Bio-plausible credit assignment
Чому це майбутнє edge AI
Moore's Law сповільнюється. GPU споживають сотні ватів. Dennard scaling закінчився. А потреба в edge AI росте експоненційно.
SNN + нейроморфні чіпи — це не інкрементальне покращення. Це інша парадигма. Обчислення, що імітують мозок не метафорично, а буквально. Event-driven. Sparse. Energy-efficient.
Коли твій сенсор має працювати рік від батарейки — CNN не варіант. Коли потрібна мікросекундна latency — GPU не варіант. Коли треба always-on voice detection без drain battery — традиційні мережі не варіант.
SNN — варіант. І нейроморфні чіпи вже доступні комерційно.
Для тих, хто готує наукову роботу з SNN або нейроморфних обчислень — від курсової до дисертації — команда SKP-Degree на skp-degree.com.ua готова допомогти з дослідженням та реалізацією. Пишіть у Telegram: @kursovi_diplomy — маємо досвід супроводу проєктів у галузі neuromorphic computing.
Ключові слова: spiking neural networks, SNN, neuromorphic computing, Intel Loihi, IBM TrueNorth, BrainChip Akida, surrogate gradient, STDP, event-driven, edge AI, енергоефективність, наукова робота, дипломна, магістерська.