
基于LIF模型的神经元动态、将信息编码为时间脉冲序列、在时间维度上进行前向传播、以及将脉冲率作为信号进行网络训练。使用实现脉冲神经网络(Spiking Neural Network, SNN) 用于分类任务,由神经元模型构建开始,逐步实现了网络层、完整模型、训练逻辑以及可视化功能。
一、 漏电积分-发放(LIF)
这段代码的目标是构建并训练一个基于漏电积分-发放(LIF)模型的脉冲神经网络,用于解决一个简单的合成数据分类问题。它完整展示了SNN的关键组成部分:神经元动态、时间编码、基于脉冲率的传播以及一个简单的监督训练过程。
二、 核心组件深度解读
1. LIFNeuron 类:漏电积分-发放神经元
这是模拟生物神经元电生理特性的计算模型,是整个SNN的基础。
-
数学模型 :其核心是差分方程
dv/dt = (-v + I) / tau_m。它模拟了:- 积分 :输入电流
I对膜电位v的充电作用。 - 漏电 :
-v / tau_m项模拟了膜电位通过离子通道的漏电,tau_m控制漏电速度。 - 发放与重置 :当
v达到阈值v_th时,神经元"发放"一个脉冲(spike=1),随后v被重置为v_reset。 - 不应期 :发放后进入短暂的
refractory_period,在此期间神经元不响应输入,模拟生物神经元的不应期。
- 积分 :输入电流
-
状态与历史:
v:当前膜电位(主要状态变量)。spike:当前时间步是否发放脉冲(输出)。refractory:不应期剩余时间步。*_history列表:用于记录整个模拟时间内电位、脉冲和输入的变化,便于分析和可视化。
2. SNNLayer 类:脉冲神经网络层
将多个LIFNeuron组织成一层,并管理其与输入的连接。
- 结构 :包含
n_neurons个LIFNeuron实例。 - 前向传播 :
- 线性变换 :
weighted_input = dot(inputs, weights) + bias。这与传统人工神经网络(ANN)的线性计算相同,inputs是脉冲或编码后的模拟值。 - 脉冲生成 :将
weighted_input作为电流I,传递给该层的每个LIF神经元。每个神经元根据自身的动力学历程独立决定是否发放脉冲。该时间步所有神经元的脉冲输出构成该层的输出向量spikes。
- 线性变换 :
- 脉冲率编码 :
get_spike_rate方法计算神经元在最近一个时间窗口内的平均发放率。在SNN中,信息通常编码在脉冲的时间序列或发放率中,而非单个时间步的模拟值。
3. SNN 类:完整的脉冲神经网络模型
这是最主要的类,集成了编码、解码、训练和评估流程。
- 网络架构 :一个简单的双层前馈网络(输入→隐藏层→输出)。输入层是隐式的,负责将数据编码为脉冲。
- 关键方法 :
encode_input(data):将静态的模拟数据转换为时间上的脉冲序列 。这是SNN处理静态数据的关键步骤。本代码采用一种简单的"速率编码 "加噪声的方式:将输入特征值叠加噪声后,通过固定阈值(0.5)生成二进制脉冲序列。time_steps定义了模拟的时间长度。forward(input_sequence):时间驱动的前向传播。循环处理每个时间步的输入,逐层传递脉冲。它返回隐藏层和输出层在整个时间序列上的脉冲记录。train(...):基于脉冲率的监督训练 。这是代码的核心逻辑之一。- 前向传播:对每个样本,先编码,再进行前向传播,得到输出脉冲序列。
- 解码与损失计算 :计算输出层神经元在时间上的平均脉冲率
output_rate。将分类标签转换为目标脉冲率向量(如对于类别2,目标为[0, 1])。损失函数定义为均方误差(MSE) 介于输出脉冲率和目标脉冲率之间。 - 权重更新 :采用一种简化的梯度下降 ,近似地通过脉冲率误差来更新权重。
- 输出层权重更新:
ΔW_output ∝ hidden_rate · error - 隐藏层权重更新:
ΔW_hidden ∝ input_rate · (W_output · error)
这本质上是将脉冲率视为可微分的激活值,并沿用了传统ANN的反向传播思想,但这是一个非常简化的近似,并非严格基于脉冲时间的反向传播(如BPTT)。
- 输出层权重更新:
visualize_activity和visualize_weights:强大的诊断和教学工具,可以直观展示输入脉冲、膜电位变化、各层脉冲发放、训练曲线和权重矩阵,极大地帮助理解SNN的动态行为。
三、 工作流程 (main函数)
- 数据准备 :使用
make_blobs生成合成二维数据集,标准化后分割为训练集、验证集和测试集。 - 模型初始化 :创建
SNN实例,定义各层尺寸。 - 模型训练 :调用
train方法,在训练集上迭代优化权重,并在验证集上监控性能。 - 模型评估:在测试集上评估最终准确率。
- 结果可视化 :
- 随机选取一个测试样本,详细可视化其在前向传播过程中的所有活动:输入脉冲、隐藏神经元膜电位变化、各层脉冲发放图、训练损失和准确率曲线。
- 可视化训练后的权重矩阵,观察网络学到了什么。
- 统计与预测:输出训练时间、准确率等统计信息,并展示几个测试样本的预测结果。
四、 完整代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_blobs
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
import time
class LIFNeuron:
"""漏电积分-发放神经元"""
def __init__(self, tau_m=20.0, v_th=1.0, v_reset=0.0, dt=1.0):
"""
初始化LIF神经元
参数:
tau_m: 膜时间常数 (ms)
v_th: 阈值电位
v_reset: 重置电位
dt: 时间步长 (ms)
"""
self.tau_m = tau_m
self.v_th = v_th
self.v_reset = v_reset
self.dt = dt
# 状态变量
self.v = v_reset
self.spike = 0
self.refractory = 0
self.refractory_period = 5
# 记录历史
self.v_history = []
self.spike_history = []
self.input_history = []
def update(self, I):
"""
更新神经元状态
参数:
I: 输入电流
返回:
spike: 当前时间步是否发放脉冲
"""
# 记录输入
self.input_history.append(I)
# 检查不应期
if self.refractory > 0:
self.v = self.v_reset
self.refractory -= 1
self.spike = 0
else:
# 计算膜电位变化
dv = (-self.v + I) / self.tau_m
self.v += dv * self.dt
# 检查是否发放脉冲
if self.v >= self.v_th:
self.spike = 1
self.v = self.v_reset
self.refractory = self.refractory_period
else:
self.spike = 0
# 记录历史
self.v_history.append(self.v)
self.spike_history.append(self.spike)
return self.spike
def reset(self):
"""重置神经元状态"""
self.v = self.v_reset
self.spike = 0
self.refractory = 0
self.v_history = []
self.spike_history = []
self.input_history = []
class SNNLayer:
"""脉冲神经网络层"""
def __init__(self, n_neurons, n_inputs, tau_m=20.0, v_th=1.0, name=""):
"""
初始化SNN层
参数:
n_neurons: 神经元数量
n_inputs: 输入维度
tau_m: 膜时间常数
v_th: 阈值电位
name: 层名称
"""
self.name = name
self.n_neurons = n_neurons
self.n_inputs = n_inputs
# 初始化神经元
self.neurons = [LIFNeuron(tau_m=tau_m, v_th=v_th) for _ in range(n_neurons)]
# 初始化权重
self.weights = np.random.randn(n_inputs, n_neurons) * 0.1
self.bias = np.zeros(n_neurons)
# 记录脉冲历史
self.spike_train = []
def forward(self, inputs):
"""
前向传播
参数:
inputs: 输入向量 (n_inputs,)
返回:
spikes: 输出脉冲 (n_neurons,)
"""
# 计算加权输入
weighted_input = np.dot(inputs, self.weights) + self.bias
# 更新每个神经元
spikes = np.zeros(self.n_neurons)
for i, neuron in enumerate(self.neurons):
spikes[i] = neuron.update(weighted_input[i])
# 记录脉冲
self.spike_train.append(spikes.copy())
return spikes
def reset(self):
"""重置层状态"""
for neuron in self.neurons:
neuron.reset()
self.spike_train = []
def get_spike_rate(self, window_size=10):
"""
计算脉冲率
参数:
window_size: 时间窗口大小
返回:
spike_rate: 平均脉冲率
"""
if len(self.spike_train) == 0:
return np.zeros(self.n_neurons)
# 计算最近时间窗口的平均脉冲率
recent_spikes = np.array(self.spike_train[-window_size:])
return np.mean(recent_spikes, axis=0)
class SNN:
"""脉冲神经网络模型"""
def __init__(self, input_size, hidden_size, output_size, time_steps=50):
"""
初始化SNN模型
参数:
input_size: 输入维度
hidden_size: 隐藏层神经元数量
output_size: 输出层神经元数量
time_steps: 时间步数
"""
self.input_size = input_size
self.hidden_size = hidden_size
self.output_size = output_size
self.time_steps = time_steps
# 创建网络层
self.hidden_layer = SNNLayer(hidden_size, input_size, tau_m=15.0, v_th=0.8, name="hidden")
self.output_layer = SNNLayer(output_size, hidden_size, tau_m=20.0, v_th=1.0, name="output")
# 训练历史
self.loss_history = []
self.accuracy_history = []
self.train_time = 0
def encode_input(self, data):
"""
编码输入数据为脉冲序列
参数:
data: 输入数据 (batch_size, input_size)
返回:
spike_sequence: 脉冲序列 (time_steps, batch_size, input_size)
"""
batch_size = data.shape[0] if len(data.shape) > 1 else 1
if len(data.shape) == 1:
data = data.reshape(1, -1)
batch_size = 1
# 将输入数据转换为脉冲序列
spike_sequence = np.zeros((self.time_steps, batch_size, self.input_size))
for t in range(self.time_steps):
# 添加噪声模拟脉冲不确定性
noise = np.random.normal(0, 0.1, (batch_size, self.input_size))
encoded = data + noise
# 通过阈值函数转换为脉冲
spikes = (encoded > 0.5).astype(float)
spike_sequence[t] = spikes
return spike_sequence
def forward(self, input_sequence):
"""
前向传播
参数:
input_sequence: 输入脉冲序列 (time_steps, input_size)
返回:
output_spikes: 输出脉冲序列 (time_steps, output_size)
hidden_spikes: 隐藏层脉冲序列 (time_steps, hidden_size)
"""
# 重置网络状态
self.hidden_layer.reset()
self.output_layer.reset()
time_steps = len(input_sequence)
hidden_spikes_seq = []
output_spikes_seq = []
# 时间步循环
for t in range(time_steps):
# 获取当前时间步输入
current_input = input_sequence[t]
# 隐藏层前向传播
hidden_spikes = self.hidden_layer.forward(current_input)
hidden_spikes_seq.append(hidden_spikes)
# 输出层前向传播
output_spikes = self.output_layer.forward(hidden_spikes)
output_spikes_seq.append(output_spikes)
return (np.array(hidden_spikes_seq),
np.array(output_spikes_seq))
def train(self, X_train, y_train, X_val, y_val, epochs=50, lr=0.01):
"""
训练SNN
参数:
X_train: 训练数据
y_train: 训练标签
X_val: 验证数据
y_val: 验证标签
epochs: 训练轮数
lr: 学习率
"""
start_time = time.time()
for epoch in range(epochs):
epoch_loss = 0
correct = 0
total = 0
# 打乱训练数据
indices = np.random.permutation(len(X_train))
for idx in indices:
# 获取单个样本
sample = X_train[idx]
label = y_train[idx]
# 编码输入
input_seq = self.encode_input(sample)
input_seq = input_seq.reshape(self.time_steps, -1)
# 前向传播
hidden_spikes, output_spikes = self.forward(input_seq)
# 计算输出脉冲率
output_rate = np.mean(output_spikes, axis=0)
# 创建目标向量
target = np.zeros(self.output_size)
if self.output_size > 1:
target[label] = 1.0
else:
target[0] = label
# 计算损失
loss = np.mean((output_rate - target) ** 2)
epoch_loss += loss
# 预测
predicted = np.argmax(output_rate) if self.output_size > 1 else (output_rate[0] > 0.5)
if predicted == label:
correct += 1
total += 1
# 简单梯度下降更新权重
error = output_rate - target
# 计算隐藏层脉冲率
hidden_rate = np.mean(hidden_spikes, axis=0)
# 更新输出层权重
dw_output = np.outer(hidden_rate, error)
self.output_layer.weights -= lr * dw_output
# 更新隐藏层权重
input_rate = np.mean(input_seq, axis=0)
error_hidden = np.dot(self.output_layer.weights, error)
dw_hidden = np.outer(input_rate, error_hidden)
self.hidden_layer.weights -= lr * dw_hidden
# 计算验证集准确率
val_acc = self.evaluate(X_val, y_val)
train_acc = correct / total
# 记录历史
epoch_loss /= len(X_train)
self.loss_history.append(epoch_loss)
self.accuracy_history.append((train_acc, val_acc))
print(f"Epoch {epoch+1:3d}/{epochs} | "
f"Loss: {epoch_loss:.4f} | "
f"Train Acc: {train_acc:.2%} | "
f"Val Acc: {val_acc:.2%}")
self.train_time = time.time() - start_time
print(f"\n训练完成!总时间: {self.train_time:.2f}秒")
def evaluate(self, X, y):
"""评估模型性能"""
correct = 0
for i in range(len(X)):
# 编码输入
input_seq = self.encode_input(X[i])
input_seq = input_seq.reshape(self.time_steps, -1)
# 前向传播
_, output_spikes = self.forward(input_seq)
# 计算输出脉冲率
output_rate = np.mean(output_spikes, axis=0)
# 预测
if self.output_size > 1:
predicted = np.argmax(output_rate)
else:
predicted = 1 if output_rate[0] > 0.5 else 0
if predicted == y[i]:
correct += 1
return correct / len(X)
def predict(self, X):
"""预测"""
predictions = []
for i in range(len(X)):
# 编码输入
input_seq = self.encode_input(X[i])
input_seq = input_seq.reshape(self.time_steps, -1)
# 前向传播
_, output_spikes = self.forward(input_seq)
# 计算输出脉冲率
output_rate = np.mean(output_spikes, axis=0)
# 预测
if self.output_size > 1:
predicted = np.argmax(output_rate)
else:
predicted = 1 if output_rate[0] > 0.5 else 0
predictions.append(predicted)
return np.array(predictions)
def visualize_activity(self, sample, label=None, timesteps=30):
"""可视化网络活动"""
# 编码输入
input_seq = self.encode_input(sample)
input_seq = input_seq.reshape(self.time_steps, -1)
# 前向传播
hidden_spikes, output_spikes = self.forward(input_seq)
# 创建图形
fig = plt.figure(figsize=(15, 10))
# 1. 输入脉冲
ax1 = plt.subplot(3, 2, 1)
ax1.imshow(input_seq[:timesteps].T, aspect='auto', cmap='binary')
ax1.set_title('输入脉冲序列')
ax1.set_xlabel('时间步')
ax1.set_ylabel('输入神经元')
ax1.set_yticks(range(self.input_size))
# 2. 隐藏层膜电位
ax2 = plt.subplot(3, 2, 2)
for i in range(min(5, self.hidden_size)):
v_history = self.hidden_layer.neurons[i].v_history[:timesteps]
ax2.plot(v_history, label=f'神经元 {i+1}')
ax2.axhline(y=self.hidden_layer.neurons[0].v_th, color='r',
linestyle='--', label='阈值')
ax2.set_title('隐藏层膜电位')
ax2.set_xlabel('时间步')
ax2.set_ylabel('膜电位')
ax2.legend(loc='upper right')
ax2.grid(True, alpha=0.3)
# 3. 隐藏层脉冲
ax3 = plt.subplot(3, 2, 3)
ax3.imshow(hidden_spikes[:timesteps].T, aspect='auto', cmap='binary')
ax3.set_title('隐藏层脉冲')
ax3.set_xlabel('时间步')
ax3.set_ylabel('隐藏神经元')
ax3.set_yticks(range(min(20, self.hidden_size)))
# 4. 输出层脉冲
ax4 = plt.subplot(3, 2, 4)
ax4.imshow(output_spikes[:timesteps].T, aspect='auto', cmap='binary')
ax4.set_title('输出层脉冲')
ax4.set_xlabel('时间步')
ax4.set_ylabel('输出神经元')
ax4.set_yticks(range(self.output_size))
# 5. 训练损失
ax5 = plt.subplot(3, 2, 5)
ax5.plot(self.loss_history, 'b-', linewidth=2)
ax5.set_title('训练损失')
ax5.set_xlabel('轮次')
ax5.set_ylabel('损失')
ax5.grid(True, alpha=0.3)
# 6. 准确率
ax6 = plt.subplot(3, 2, 6)
if self.accuracy_history:
train_acc = [acc[0] for acc in self.accuracy_history]
val_acc = [acc[1] for acc in self.accuracy_history]
ax6.plot(train_acc, 'b-', linewidth=2, label='训练集')
ax6.plot(val_acc, 'r--', linewidth=2, label='验证集')
ax6.set_title('准确率曲线')
ax6.set_xlabel('轮次')
ax6.set_ylabel('准确率')
ax6.legend()
ax6.grid(True, alpha=0.3)
title = 'SNN网络活动'
if label is not None:
prediction = self.predict(sample.reshape(1, -1))[0]
title += f' (标签: {label}, 预测: {prediction})'
plt.suptitle(title, fontsize=14, fontweight='bold')
plt.tight_layout()
plt.show()
def visualize_weights(self):
"""可视化权重矩阵"""
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
# 输入-隐藏层权重
im1 = axes[0].imshow(self.hidden_layer.weights.T, cmap='RdBu_r',
aspect='auto', vmin=-1, vmax=1)
axes[0].set_title('输入 → 隐藏层权重')
axes[0].set_xlabel('输入特征')
axes[0].set_ylabel('隐藏神经元')
plt.colorbar(im1, ax=axes[0])
# 隐藏-输出层权重
im2 = axes[1].imshow(self.output_layer.weights.T, cmap='RdBu_r',
aspect='auto', vmin=-1, vmax=1)
axes[1].set_title('隐藏层 → 输出层权重')
axes[1].set_xlabel('隐藏神经元')
axes[1].set_ylabel('输出神经元')
plt.colorbar(im2, ax=axes[1])
plt.tight_layout()
plt.show()
def generate_synthetic_data(n_samples=200, n_features=5, random_state=42):
"""生成合成数据"""
np.random.seed(random_state)
# 生成二分类数据
X, y = make_blobs(n_samples=n_samples, n_features=n_features,
centers=2, cluster_std=1.5, random_state=random_state)
# 标准化
scaler = StandardScaler()
X = scaler.fit_transform(X)
return X, y
def main():
"""主函数"""
print("=" * 60)
print("脉冲神经网络(SNN)分类器")
print("=" * 60)
# 1. 生成数据
print("\n1. 生成数据...")
X, y = generate_synthetic_data(n_samples=200, n_features=5)
# 分割数据集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
X_train, X_val, y_train, y_val = train_test_split(X_train, y_train, test_size=0.125, random_state=42)
print(f" 训练集: {len(X_train)} 样本")
print(f" 验证集: {len(X_val)} 样本")
print(f" 测试集: {len(X_test)} 样本")
# 2. 创建SNN模型
print("\n2. 创建SNN模型...")
input_size = X_train.shape[1]
hidden_size = 20
output_size = 2 # 二分类
snn = SNN(input_size=input_size,
hidden_size=hidden_size,
output_size=output_size,
time_steps=50)
print(f" 输入层: {input_size} 神经元")
print(f" 隐藏层: {hidden_size} 神经元")
print(f" 输出层: {output_size} 神经元")
# 3. 训练模型
print("\n3. 训练模型...")
snn.train(X_train, y_train, X_val, y_val, epochs=30, lr=0.01)
# 4. 测试模型
print("\n4. 测试模型...")
test_acc = snn.evaluate(X_test, y_test)
print(f" 测试集准确率: {test_acc:.2%}")
# 5. 可视化
print("\n5. 可视化结果...")
# 随机选择一个样本可视化
sample_idx = np.random.randint(len(X_test))
sample = X_test[sample_idx]
label = y_test[sample_idx]
snn.visualize_activity(sample, label, timesteps=30)
snn.visualize_weights()
# 6. 显示统计信息
print("\n" + "=" * 60)
print("模型统计信息")
print("=" * 60)
print(f"训练时间: {snn.train_time:.2f}秒")
print(f"训练轮次: {len(snn.loss_history)}")
print(f"最终训练损失: {snn.loss_history[-1]:.4f}")
if snn.accuracy_history:
train_acc, val_acc = snn.accuracy_history[-1]
print(f"最终训练准确率: {train_acc:.2%}")
print(f"最终验证准确率: {val_acc:.2%}")
print(f"测试集准确率: {test_acc:.2%}")
# 7. 预测示例
print("\n" + "=" * 60)
print("预测示例")
print("=" * 60)
n_examples = 5
indices = np.random.choice(len(X_test), n_examples, replace=False)
for i, idx in enumerate(indices):
sample = X_test[idx]
true_label = y_test[idx]
prediction = snn.predict(sample.reshape(1, -1))[0]
correct = "✓" if prediction == true_label else "✗"
print(f"样本 {i+1}: 真实标签={true_label}, 预测={prediction} {correct}")
if __name__ == "__main__":
main()