Искусственные нейронные сети находятся на волне популярности. Самые современные модели ИИ способны творить чудеса: поддерживать видимость общения на уровне человека, создавать реалистичные изображения, писать музыкальные сочинения.
Сейчас никого не удивить заявлениями, что искусственный интеллект превзошёл человеческий. Справедливости ради, способности простого калькулятора тоже давно их превзошли. Например, в скорости умножения чисел — даже двузначных. Опередить человека в некоторых аспектах — задача не сложная.
Совершенно другое дело — повторить живой разум. Или хотя бы приблизиться к нему. В прессе мы часто встречаем заголовки вида «ИИ научился думать как человек! Сэм Альтман не смог с этим смириться и скрылся в ужасе!». Но будем честны: признаков сознания у любых моделей ИИ пока не наблюдается.
В то же время, создать что-нибудь эдакое хочется. Поэтому работа продолжается не только в магистральных, но и в альтернативных направлениях развития ИИ. В тот числе и с так называемыми спайковыми нейронными сетями (Spiking Neural Networks, SNN).
Скромность успехов в попытках научить машину думать ставит вопросы: а не упускаем ли мы что-то важное? Какую-то особенность, присущую биологическим нейронам? Такую, которой не обладают распространённые нейросетевые модели?
В этом плане SNN стремятся точнее подражать именно биологическим нейронным сетям. В надежде добиться, что однажды искусственная сеть заработает как настоящая.
В этой и последующих статьях мы будем поэтапно рассматривать SNN. Начиная с принципов работы одиночного нейрона до целых сетей на их основе. Всё моделирование будет выполняться "с нуля". К доступным фреймворкам (snnTorch и другие) прибегать не будем. А пока, чтобы легче было понять суть SNN, разберёмся в характерных особенностях.
Различия SNN и классических сетей
Традиционные искусственные нейронные сети (далее — ANN) вдохновлены устройством нервной ткани. Однако, искусственный нейрон ANN наследует лишь малую часть способностей своих биологических собратьев.
Первая способность — интегративная. Искусственный нейрон может объединять несколько сигналов в единый выходной сигнал. Он формируется некоторой непрерывной функцией — "функцией активации". Значение этой функции передаётся следующим нейронам в сети или используется как результат.
Вторая — способность к обучению. Входные сигналы объединяются согласно "весу" каждого входа, которые можно настраивать. Настройка выполняется в ходе отдельной обучающей процедуры.
Другие свойства биологического нейрона в ANN не наследуются. Хотя свойств таких достаточно много.
Отметим также, что нейроны в архитектуре ANN — абстрактная идея. Они не существуют отдельно. ANN описывается матрицами смежности, каждый элемент которой определяет вес нейронной связи. Так, всю ANN можно рассматривать как одну очень сложную математическую функцию: пользователь передаёт ей входные данные, затем выполняется расчёт и выдаётся результат. И так до следующего model.fit()
или нажатия клавиши "равно", вспоминая аналогию с калькулятором.
Теперь перейдём к деталям SNN. Но сперва коснёмся устройства биологического мозга. На макроскопическом уровне мозг выглядит незамысловато — как сгусток жира (больше 60%). Но на клеточном уровне он представляет собой организованную колонию живых организмов — нейронов. При этом, каждый нейрон в такой колонии довольно самостоятелен.
Нейроны в мозге общаются между собой способом, похожим на морзянку. То есть, короткими импульсами примерно одинаковой формы — "спайками" (spike).
Искусственные нейроны SNN наследуют свойство генерации спайков. Таким образом, сигналы в них кодируются сериями спайков (spike train), а не конкретными значениями функций активации. Это первое, самое важное отличие спайковых нейронов от ANN.
Спайковый нейрон как динамическая система
Следующее отличие — каждый искусственный спайковый нейрон это отдельная динамическая система. Она описывается дифференциальными уравнениями, которые определяют эволюцию системы во времени. То есть, в отличие от ANN, нейрон в SNN моделируется явно.
Хеббовский принцип обучения нейронов SNN
Естественным для SNN является так называемое Хеббовское обучение. Этот принцип можно сформулировать так: "нейроны, которые активируются вместе, связываются сильнее" (cells that fire together, wire together). Обучение происходит не в рамках отдельного процесса, а сразу в рабочем режиме. Если один нейрон постоянно раздражает соседа своими спайками, тот становится чувствительнее к ним.
В то же время, большинство ANN обучаются методом обратного распространения (backpropagation). К сожалению, в спайковых сетях этот метод работает плохо.
Множество разновидностей нейронов
Заключительное свойство нейронов SNN, которое мы рассмотрим, тоже начинается в биологическом мозге. Нейроны в нём неодинаковы, а сортов — сотни. Каждый сорт имеет свой паттерн генерации спайков. Некоторые способны генерировать спайки как из пулемёта, а другие — короткими сериями высокой частоты. Помимо этого, нейроны отличаются характером воздействия на соседей: некоторые своими спайками подавляют (ингибируют) активность других, некоторые, наоборот, возбуждают.

В заключение скажем, что искусственные нейроны SNN перенимают больше свойств реальных нейронов, чем ANN. Благодаря этому их поведение богаче. Потенциально, такие сети способны показывать эффекты, которые в ANN не проявляются. С другой стороны — спайковые нейроны вычислительно дороже, работать с ними в целом сложнее. Поэтому SNN имеют исследовательский интерес, а промышленное применение пока за ANN.
Далее проиллюстрируем принципы работы SNN на небольшом примере.
Иллюстрация работы простой SNN
На диаграмме ниже цифрами обозначены рецепторы. Эти элементы принимают входной сигнал и генерируют короткую серию спайков в ответ. Буквами обозначены остальные нейроны сети, а стрелками — направления распространения спайков.
Синий цвет обозначает тормозящий (ингибирующий) нейрон, а красный — возбуждающий. Ярко-красным обозначен нейрон, активность которого нас будет интересовать. Установим, что "красные" и "синие" будут иметь разные паттерны активации. "Красных" нужно возбуждать дольше, а частота спайков невелика.

Если начать подавать сигналы 0 и 1 в любом порядке, то пока сеть не научилась, нейрон J активироваться не будет. Его подавляют тормозящие нейроны B и I. Однако, со временем ситуация изменится. Возбуждающий сигнал 0 проложит себе дорогу по траектории ACDEFG. Тогда каждый сигнал 0 станет активировать нейрон H, который является тормозящим. В свою очередь, он станет подавлять другой тормозящий нейрон I, снимая оковы с J. Если сигнал 1 поступит в это окно возможностей, J активируется.
Примечательно, что J активируется только в таком порядке — сначала сигнал 0, потом через определённое время 1, ни раньше, ни позже. В других комбинациях, а также с другими интервалами между 0 и 1, активации не будет. Сигнал или не успеет добраться до I, или прибудет слишком поздно.
Также любопытно, что если добавить тормозящий нейрон между J и F, то для активации станет важен не только порядок 0 и 1. Теперь будут учитываться интервалы между последовательностями 0-1.
Как видно, даже столь простая модель способна показывать нетривиальное поведение. В том числе, реагировать на временную динамику. Теперь перейдём к моделированию спайкового нейрона.
Моделирование спайкового нейрона
Сначала опишем формальную модель одиночного нейрона. Подходящих моделей, которые могут генерировать спайки, несколько. Я назову несколько популярных.
Ходжкина-Хаксли (Hodgkin-Huxley). Подробно моделирует электрохимию каждого нейрона. Состоит из нескольких дифференциальных уравнений. Для практических задач, не связанных с изучением поведения нервной клетки, мало применима.
Интеграция с утечкой (Leaky Integrate&Fire, LIF). Описывается одним линейным дифференциальным уравнением
. Самая быстрая из всех, но и самая бедная — позволяет эмулировать лишь самую простую динамику спайков.
Ижикевича (Izhikevich). Представлена системой из двух дифференциальных уравнений. Позволяет моделировать поведение нейронов разных типов. Приемлема с точки зрения вычислительной сложности. Является компромиссом между первыми двумя, поэтому будем использовать её.
Уравнения Ижикевича описывают эволюцию мембранного потенциала — свойства биологического нейрона, от которого зависит генерация спайков. Разбирать физический смысл мембранного потенциала не будем. Допустимо воспринимать его как степень возбуждения нейрона. Сами уравнения выглядят следующим образом:
Здесь — мембранный потенциал,
— переменная восстановления, которая противодействует росту потенциала. Динамика
зависит как от самого потенциала, так и дополнительных параметров. Параметр
определяет силу такого противодействия,
— чувствительность
к флуктуациям потенциала
.
При достижении мембранным потенциалом определённого порога происходит сброс. Этот момент считается моментом возникновения спайка. Переменная
задаёт значение, на которое сбрасывается потенциал, а
корректирует
после спайка.
Внешние токи, заряжающие мембрану, заданы переменной . Это "вход" модели. Если передать ей единичный импульс
достаточной силы, это может породить спайк. После него мембранный потенциал стабилизируется на уровне около -70mV (зависит от параметров). Можно сказать, что нейроны "заряжают" друг друга своими спайками. Общий вклад определяется
.
Если , где
— некоторая константа, то модель начнёт осциллировать, производя постоянный поток спайков. Частота и интервалы между сериями будут зависеть от параметров модели.
Также важно отметить, что "мощность" спайка в этой модели не определяется ни , ни другими параметрами. Спайк — это факт резкого изменения мембранного потенциала и у него нет какой-то определённой величины. При этом сам потенциал другим нейронам напрямую не транслируется* и является внутренним состоянием системы.
В заключение отмечу, что данная версия модели Ижикевича является упрощённой. Числовые коэффициенты в нелинейном уравнении подобраны Ижикевичем так, чтобы они подходили большинству нейронов. Также, они соответствуют временному разрешению . То есть, 60 циклов в течении 1 секунды будут моделировать 60мс "жизни" нейрона. Подробнее можно ознакомиться в статьях автора. В реализации мы будем использовать упрощённую модель.
Реализация спайкового нейрона
Теперь приступим к реализации. Поскольку я не профессиональный разработчик, то буду делать это на том ЯП, который мне лучше всего знаком — на Swift. Весть дальнейший код — это фрагменты из экспериментального фреймворка, который я делал несколько лет назад для себя.
Этот фреймворк не является ни оптимальным, ни эффективным с точки зрения производительности. Некоторые алгоритмы и структуры сознательно реализованы не самым лучшим образом в угоду наглядности.
Первое, что сделаем — определим несколько протоколов. Excitable
будет абстрактно описывать любую структуру, которая управляет состоянием отдельного нейрона. У нас такая структура будет только одна — Izhikevich
. Аналогично, ExcitableParams
будет описывать структуры с параметрами моделей.
protocol Excitable: Sendable {
associatedtype P: ExcitableParams
init( params: P)
mutating func updateState( Isyn: Double) -> Bool
func stabilityCheck() -> Bool
func v() -> Double
}
protocol ExcitableParams: Sendable {
var type: NeuronType { get }
var code: String { get }
}
Далее определим две вспомогательные структуры. Перечисление NeuronType
будет определять тип нейрона — тормозящий или возбуждающий. NeuronKey
станет ключом (идентификатором) нейрона, но пока он нам не потребуется.
enum NeuronType {
case excitatory, inhibitory
}
struct NeuronKey: Hashable {
var x: UInt16
var y: UInt16
var z: UInt8 = 0
var L: UInt8 = 0
}
Теперь определим структуру IzhikevichParams
со всеми параметрами, нужными для описания нейрона Ижикевича. Согласуем её с протоколом ExcitableParams
.
struct IzhikevichParams: ExcitableParams {
var a: Double = 0.02
var b: Double = 0.2
var c: Double = -65
var d: Double = 2
let type: NeuronType
var code: String = ""
var peak: Double = 30.0
var v0: Double = -65
}
Такие значения по умолчанию описывают один из самых простых типов нейронов с регулярными спайками. Здесь peak
это порог для генерации спайка, а v0
— значения мембранного потенциала, с которым инициализируется нейрон. Смысл остальных значений описан выше.
Теперь определим основную структуру Izhikevich
, которая будет управлять состоянием нейрона:
struct Izhikevich: Sendable {
private var isStable: Bool = true
private var state: (v: Double, u: Double)
private let params: IzhikevichParams
init(_ params: IzhikevichParams) {
self.state = (v: params.v0, u: params.v0*params.b)
self.params = params
}
}
И согласуем её с протоколом Excitable
:
extension Izhikevich: Excitable {
func stabilityCheck() -> Bool {
isStable
}
func v() -> Double { state.v }
mutating func updateState(_ Isyn: Double) -> Bool {
var isFired = false
if state.v >= params.peak {
state.v = params.c
state.u += params.d
isFired = true
}
let (v, u) = state
let dv = 0.04*v*v + 5*v + 140 - u + Isyn
let du = params.a*(params.b*v - u)
state = (v: v+dv*dt, u: u+du*dt)
isStable = abs(dv) < EPSILON && !isFired
&& abs(du) < EPSILON
return isFired
}
}
Функция updateState()
будет делать расчёты, обновлять внутреннее состояние и возвращать факт возникновения спайка в качестве результата. Эту функцию можно считать дальним аналогом "активации" в ANN.
Здесь стоит отметить переменную isStable
. Она станет true
, когда dv
и du
окажутся меньше глобальной переменной EPSILON = 1e-7
. Переменная isStable
потребуется, чтобы не делать лишнюю работу. Когда система находится в стабильном состоянии и нейрон неактивен, холостые расчёты нам не нужны.
Параметр Isyn
будем получать извне. Это скалярный вход нашего нейрона.
Как видно, в updateState()
применяется самый простой метод решения дифференциальных уравнений — Эйлера с постоянным dt
. Значение dt = 1
также задаётся глобальной переменной. Использовать более точные методы (Рунге-Кутты и других) представляется нецелесообразным.
Ижикевич в своих работах предлагает дополнительные методы, улучшающих численную стабильность системы. Например интерполяцию при обновлении u
. Но мы отложим их на потом.
Пока с моделью всё. Такой нейрон пока не умеет обучаться, да и интегрировать сигналы он не может. Да, это немного. Зато мы можем увидеть, как работает спайковый нейрон, уже сейчас.
Далее определим класс-модель для SwiftUI — Playground
.
struct ChartPoint {
var t = Date.now
var v: Double
}
@MainActor final class Playground: ObservableObject {
private var neuron: Izhikevich
private let params: IzhikevichParams
private var task: Task<Void, Never>?
@Published private(set) var isRun: Bool = false
@Published private(set) var V: [ChartPoint] = []
@Published private(set) var spikes: [Int] = []
@Published var Iconst: Double = 3
@Published var pulse: Double = 0
func stop() {
if let task { task.cancel() }
}
private func I() async -> Double {
let I = Iconst + pulse; pulse = 0
return I
}
func start() {
guard !isRun else { return }
V.removeAll()
task = Task {
isRun = true
while !Task.isCancelled {
if neuron.updateState(await I()) {
spikes.append(spikes.count)
}
V.append(ChartPoint(v: neuron.v()))
if V.count > 1000 { V.removeFirst() }
try? await Task.sleep(for: .microseconds(1))
}
isRun = false
}
}
init() {
params = IzhikevichParams(type: .excitatory)
neuron = Izhikevich(params)
}
}
Этот простейший класс реализует два полезных метода. start()
для запуска задачи, внутри которой в цикле обновляется состояние. А также stop()
— для остановки активной задачи. Этим методы можно вызывать из View.
Код представления (View) SwiftUI я здесь приводить не буду. Его несложно написать самостоятельно по своему вкусу: все нужные свойства для графика и для работы кнопок вынесены в @Published
.
Результат выглядит примерно так:

Как и положено, определяет частоту спайков, но не их форму. Не следует путать пики на графике с дискретными спайками — диаграмма самих спайков выглядела бы как расчёска. Этот график отображает значение мембранного потенциала
. Момент резкого изменения
совпадает с временем спайка.
Пока на этом всё. В следующей части рассмотрим другой важный сетевой элемент — синапсы. Также, научим наши нейроны взаимодействовать с другими и интегрировать сигналы.
Спасибо за внимание! :-)