sciBASIC# knot logo sciBASIC#

12 Tutorial — Spiking NN

Spiking neurons learn with surrogate gradients — LIF dynamics, trained end to end with Adam.

Module DeepLearning · SpikingNeuralNetwork Network 8 → LIF×64 → LIF×3 · T=30 Training Adam · 40 epochs · batch 36 Accuracy 100% · 90/90 test

The series closes with the newest learner in the stack: a spiking neural network. Continuous features enter as rate-coded spike trains over 30 timesteps, leaky integrate-and-fire neurons carry membrane state across the steps, and the non-differentiable spikes are trained with FastSigmoid surrogate gradients through Adam. Three Gaussian clusters in 8-D space separate perfectly (90/90 on the test set) — and the script finishes by replaying one inference step-by-step, drawing the spike raster, the membrane-potential sparklines and the output spike counts that decode the answer.

02 Pipeline

Spikes in, spikes out

Step 1

Encode

SpikeEncoders.RateEncode turns each 8-dimensional sample into a 30-timestep spike raster (rate coding) — brighter inputs fire more often. The script prints sample #0's raster before any learning starts.

Step 2

Build

SpikingNetwork(8, 30, SpikeEncoding.RateCoding) stacks two LIF layers, 8 → 64 → 3, each with membrane decay beta := 0.85 and threshold theta = 1.0. Training uses AdamOptimizer(0.002) with gradient clipping and the FastSigmoid surrogate (α = 2).

Step 3

Train

40 epochs of mini-batches (36) over the 360 training samples. Loss falls 3.2745 → 0.0002 and the test curve reaches 100% by epoch 5 — printed every 5 epochs from a held-out set.

Step 4

Evaluate

net.Predict classifies all 90 test samples by spike-count decoding: 90 / 90 correct.

Step 5

Look inside

Report.VizInference replays one forward pass with a fixed random source: input raster, membrane-potential sparklines of the first hidden neurons, and the output-layer spike grid whose counts pick the predicted class.

01 The Script

Full demo source

The complete script exactly as executed by the sciBASIC# script engine (vbs.exe) — nothing elided.

spiking_nn.vb · 207 linesDownload spiking_nn.vb
#include "Microsoft.VisualBasic.MachineLearning.TensorFlow.dll"
#include "Microsoft.VisualBasic.DeepLearning.SpikingNeuralNetwork.dll"

Imports System.Text
Imports Microsoft.VisualBasic.Data
Imports Microsoft.VisualBasic.DeepLearning.SpikingNeuralNetwork
Imports Microsoft.VisualBasic.Linq
Imports Microsoft.VisualBasic.MachineLearning.TensorFlow
Imports randf = Microsoft.VisualBasic.Math.RandomExtensions
Imports std = System.Math

' =========================================================================
' Part 1:替代梯度监督学习
' =========================================================================
Console.WriteLine("  Spiking Neural Network  ·  LIF + SurrogateGrad + STDP")
Console.WriteLine()
Console.WriteLine("  --- 替代梯度监督学习(3 类高斯团簇分类)---")
Console.WriteLine()

Friend Class Data

    ''' <summary>3 个类别在 8 维空间中的中心(各维取值 [0,1])</summary>
    Shared ReadOnly ClassCenters As Double()() = {
        New Double() {0.2, 0.2, 0.75, 0.75, 0.5, 0.5, 0.2, 0.8},
        New Double() {0.75, 0.75, 0.2, 0.2, 0.8, 0.2, 0.5, 0.5},
        New Double() {0.5, 0.8, 0.5, 0.8, 0.2, 0.75, 0.8, 0.2}
    }

    Public Shared Function SampleClass(rng As Random, cls As Integer) As Double()
        Dim f = ClassCenters(0).Length
        Dim v(f - 1) As Double
        For i = 0 To f - 1
            v(i) = std.Max(0.02, std.Min(0.98, ClassCenters(cls)(i) + 0.09 * randf.NextGaussian))
        Next
        Return v
    End Function

    Public Shared Function MakeData(rng As Random, perClass As Integer) As NumericTable
        Dim xs As New List(Of Double())()
        Dim ys As New List(Of Integer)()
        For c As Integer = 0 To ClassCenters.Length - 1
            For n = 1 To perClass
                xs.Add(SampleClass(rng, c))
                ys.Add(c)
            Next
        Next
        Return New NumericTable(xs, ys.ToArray())
    End Function
End Class



Dim rng As New Random(2024)
Dim train = Data.MakeData(rng, 120)      ' 360 样本
Dim test = Data.MakeData(rng, 30)        ' 90 样本
Console.WriteLine($"  数据: 3 类 × 8 维高斯团簇(σ=0.09), 训练 {train.nsamples}, 测试 {test.nsamples}")

' ---- 展示一个样本的脉冲编码 ----
Dim demoX = New Tensor(train.features(0), 1, 8)
Dim demoSeq = SpikeEncoders.RateEncode(demoX, 30, New Random(99))
Console.WriteLine()
Console.WriteLine("  样本 #0 的频率编码脉冲栅格(行=输入神经元, 列=时间步, █=脉冲):")
Report.PrintRaster(demoSeq, 8)

' ---- 网络构建 ----
Dim net As New SpikingNetwork(8, 30, SpikeEncoding.RateCoding)
net.Rng = New Random(31415)
net.AddLayer(64, beta:=0.85, seed:=11)
net.AddLayer(3, beta:=0.85, seed:=22)
Dim opt As New AdamOptimizer(0.002, 1.0)

Console.WriteLine()
Console.WriteLine("  网络结构: 8 → [LIF×64 β=0.85 θ=1.0] → [LIF×3 β=0.85 θ=1.0], T=30")
Console.WriteLine("  训练配置: Adam(lr=0.002) + 梯度裁剪(1.0) + FastSigmoid 替代梯度(α=2)")
Console.WriteLine()

' ---- 训练 ----
Dim batchSize = 36
Dim epochs = 40
Console.WriteLine("  epoch |   loss   | test-acc")
Dim testX = AllTensor(test)

For epoch = 1 To epochs
    Dim order = Enumerable.Range(0, train.nsamples).Shuffle.ToArray
    Dim lossSum = 0.0
    Dim nb = 0
    Dim train_label = train.GetLabel(0)

    For start = 0 To order.Length - 1 Step batchSize
        Dim cnt = std.Min(batchSize, order.Length - start)
        Dim bx = BatchTensor(train.features, order, start, cnt)
        Dim by(cnt - 1) As Integer
        For b = 0 To cnt - 1
            by(b) = train_label(order(start + b))
        Next
        lossSum += net.TrainStep(bx, by, opt)
        nb += 1
    Next

    If epoch = 1 OrElse epoch Mod 5 = 0 Then
        Dim acc = SpikingNetwork.Accuracy(net.Predict(testX), test.GetClassLabel(0))
        Console.WriteLine($"   {epoch,4} | {lossSum / nb,8:F4} | {acc,8:P1}")
    End If
Next

' ---- 测试评估 ----
Dim pred = net.Predict(testX)
Dim hit = 0
Dim test_labels = test.GetLabel(0)
For i = 0 To pred.Length - 1
    If pred(i) = test_labels(i) Then
        hit += 1
    End If
Next
Console.WriteLine()
Console.WriteLine($"  [结果] 测试集准确率: {hit / CDbl(pred.Length):P1} ({hit}/{pred.Length})")

' ---- 单样本推理轨迹可视化 ----
Console.WriteLine()
Console.WriteLine("  —— 单样本推理轨迹(测试样本 #0)——")
Report.VizInference(net, test, 0)

Friend Class Report

Public Shared Sub VizInference(net As SpikingNetwork, test As NumericTable, sample As Integer)
    Dim x0 = New Tensor(test.features(sample), 1, 8)
    Dim label = test.labels(sample)(0)

    ' 用独立随机源复现一次前向(与训练一致的 Rate 编码)
    Dim seq = SpikeEncoders.RateEncode(x0, net.TimeSteps, New Random(777))
    For Each l In net.Layers
        l.ResetState(1)
    Next

    Dim outS As New List(Of Tensor)()
    For t = 0 To net.TimeSteps - 1
        Dim sig = seq(t)
        For Each l In net.Layers
            sig = l.ForwardStep(sig)
        Next
        outS.Add(sig)
    Next

    Console.WriteLine()
    Console.WriteLine("  输入脉冲栅格:")
    PrintRaster(seq, 8)
    Console.WriteLine()
    Console.WriteLine("  隐藏层前 4 个神经元的膜电位轨迹(▁▂▃▄▅▆▇█ 相对幅度):")
    Console.WriteLine()
    Dim h1 = net.Layers(0)
    For n = 0 To 3
        Dim u(net.TimeSteps - 1) As Double
        For tt = 0 To net.TimeSteps - 1
            u(tt) = h1.UHistory(tt).Data(n)
        Next
        Console.WriteLine($"    h{n}: {Sparkline(u)}")
        Console.WriteLine($"    " & New String("-"c, 36))
        Console.WriteLine()
    Next

    Console.WriteLine("  输出层脉冲栅格与计数(计数解码):")
    Dim counts(2) As Integer
    For j = 0 To 2
        Dim line = ""
        For t = 0 To outS.Count - 1
            Dim v = outS(t).Data(j)
            If v > 0 Then counts(j) += 1
            line &= If(v > 0, "█", "·")
        Next
        Dim mark = If(j = label, " ← 真实类别", "")
        Console.WriteLine($"    out{j} |{line}| 计数={counts(j)}{mark}")
    Next

    Dim best = 0
    For j = 1 To 2
        If counts(j) > counts(best) Then best = j
    Next
    Console.WriteLine($"    → 预测类别: {best}(真实: {label}) {If(best = label, "[正确]", "[错误]")}")
End Sub

' =========================================================================
' 可视化辅助
' =========================================================================
Public Shared Sub PrintRaster(seq As List(Of Tensor), rows As Integer)
    For r = 0 To rows - 1
        Dim line = ""
        For t = 0 To seq.Count - 1
            line &= If(seq(t).Data(r) > 0, "█", "·")
        Next
        Console.WriteLine($"    in{r,-2}|{line}|")
    Next
End Sub

Private Shared Function Sparkline(v As Double()) As String
    Const blocks = "▁▂▃▄▅▆▇█"
    Dim mx = v.Max()
    Dim mn = v.Min()
    Dim span = std.Max(mx - mn, 0.0000001)
    Dim sb As New StringBuilder()
    For Each x In v
        Dim k = CInt(std.Floor((x - mn) / span * 7.999))
        If k < 0 Then k = 0
        If k > 7 Then k = 7
        sb.Append(blocks(k))
    Next
    Return sb.ToString()
End Function
End Class

03 Results

The full session, verbatim

Spike rasters, the training curve, the sparkline membrane traces and the final inference walkthrough — all rendered by the script itself, in plain console text:

spiking_nn.vb — console output
  Spiking Neural Network  ·  LIF + SurrogateGrad + STDP

  --- 替代梯度监督学习(3 类高斯团簇分类)---

  数据: 3 类 × 8 维高斯团簇(σ=0.09), 训练 360, 测试 90

  样本 #0 的频率编码脉冲栅格(行=输入神经元, 列=时间步, █=脉冲):
    in0 |········██··█···█·············|
    in1 |···█·█···██····█··█··█···█····|
    in2 |·█████·████·██████·███·██··███|
    in3 |██████████████·███████████████|
    in4 |····██·█·█··████·████████·█·██|
    in5 |█·█····█████·█·█████··█·█··███|
    in6 |·····██····█···██···█·····█···|
    in7 |█·███████████·██████·█·██·····|

  网络结构: 8 → [LIF×64 β=0.85 θ=1.0] → [LIF×3 β=0.85 θ=1.0], T=30
  训练配置: Adam(lr=0.002) + 梯度裁剪(1.0) + FastSigmoid 替代梯度(α=2)

  epoch |   loss   | test-acc
      1 |   3.2745 |    46.7%
      5 |   0.0028 |   100.0%
     10 |   0.0014 |   100.0%
     15 |   0.0006 |   100.0%
     20 |   0.0006 |   100.0%
     25 |   0.0007 |   100.0%
     30 |   0.0002 |   100.0%
     35 |   0.0003 |   100.0%
     40 |   0.0002 |   100.0%

  [结果] 测试集准确率: 100.0% (90/90)

  —— 单样本推理轨迹(测试样本 #0)——

  输入脉冲栅格:
    in0 |·█···██·········█·····█·······|
    in1 |······························|
    in2 |·██··██████·████··███████·████|
    in3 |████████████·██·██████·███████|
    in4 |█·██·██···██···█·██·█···███···|
    in5 |·····█···██··█···██··█····█···|
    in6 |█··█···█······█·█··█·██·█·····|
    in7 |██████████·████████·███████·██|

  隐藏层前 4 个神经元的膜电位轨迹(▁▂▃▄▅▆▇█ 相对幅度):

    h0: ▄▅▇▄▄▅▇▄▅▅▇▅▅▅▅▆▄▄▅▆█▂▁▃▄▆▇▆▇▅
    ------------------------------------

    h1: ██▆▆▆▅▅▄▄▃▃▃▂▂▂▁▂▂▁▃▂▂▂▁▁▂▁▂▂▂
    ------------------------------------

    h2: █▁▃█▄▅▆▆▄▃▄█▁▁▁▃▅█▅▅▆▄▃▂▅█▅▃▂▁
    ------------------------------------

    h3: █▇▆▆▅▅▄▃▃▂▂▃▃▂▁▂▂▃▂▂▂▁▁▁▁▁▁▁▁▁
    ------------------------------------

  输出层脉冲栅格与计数(计数解码):
    out0 |··█··██·██··███··█·█·█·█··████| 计数=16 ← 真实类别
    out1 |···█··························| 计数=1
    out2 |······························| 计数=0
    → 预测类别: 0(真实: 0) [正确]
Two engine features deserve a second look. First, the surrogate gradient: LIF firing is a non-differentiable threshold operation, so the backward pass rides a FastSigmoid curve over the spike — that is what makes net.TrainStep end-to-end trainable with Adam at all. Second, the script mixes top-level statements with top-level Friend Class blocks (Data, Report) — the engine hoists class definitions next to the main flow, exactly like the Public Function rule from the earlier tutorials. And the banner names the engine's full repertoire — LIF + SurrogateGrad + STDP — while this demo exercises the supervised path.