Приветствую любителей порассуждать о том, как скоро нейросети отнимут работу у человека и захватят мир. А также тех, кто этой темой никогда не интересовался. В общем, устраивайтесь по удобней.
Я планирую выпустить несколько статей, в которых расскажу о своих попытках воссоздать нейросеть в оригинальном ее виде. Т.е. повторить функционал настоящего нейрона, а затем и целой нейросети в коде.
Нейросетями я пользовался давно. С ностальгией вспоминаю насколько убогой была первая версия ChatGPT... И меня всегда интересовал вопрос о подкапотном устройстве хотя бы самой простой из них. Но как только я начинал лезть в эту тему, меня тут же спускала на землю ОНА. Да, да - МАТЕМАТИКА. Так как математику я мягко говоря не люблю, а нейросети не отпускают мое любопытство, я решил пойти другим путем.
Во первых, возник вопрос, оправдывает ли "нейросеть" свое название? Действительно ли математические модели нейронных сетей так похожи на то, что происходит в нашей черепной коробке.
Во вторых, было бы интересно именно воссоздать нейросеть в оригинальном ее смысле. И посмотреть что из этого выйдет.
Наверняка, я не первый, кто это делает. Но смотреть на результаты других довольно скучно... поэтому поехали!
Как можно программировать нейрон, не зная что он себя представляет? Поэтому, прочитав пару статей из интернета, пообщавшись с ChatGPT и отсмотрев десяток лекций из этого замечательного плейлиста, я пришел к следующим выводам.
На первом этапе нужно забыть о мозге целиком и его структурах, потому что до сих пор точно не изучено каким образом они все взаимодействуют
Существуют разные типы нейронов, но, применив главный принцип человека разумного - абстракцию, я могу создать только одну модель нейрона и получать любой другой, изменяя характеристики исходного
Существует большое количество клеток, которые поддерживают жизнь нейрона. Их тоже можно исключить из модели
Так как самые примитивные нейросети не предоставляют организмам более чем возможность передвигаться и питаться, то и мне не стоит сразу смотреть в сторону решения сложных задач. Пусть нейросеть хотя бы позволит оживить модельку червя
Внешне нейрон выглядит так:
Внутреннее устройство нейрона. Подробнее смотрите лекции: первую и вторую.
Работа нейрона делится на четыре части.
Часть 1: Создание потенциала
Тут мы вводим новое понятие: "нейротрансмиттер", или же "нейромедиатор". Это некоторое вещество, которое способно открывать специальные каналы в дендритах. Через эти каналы в нейрон попадают положительно и отрицательно заряженные ионы.
Ионы в свою очередь создают электрический потенциал. Когда потенциал достигает порогового значения, происходит передача потенциала по аксону.
Часть 2: Передача потенциала
Благодаря электрическому потенциалу открываются каналы, которые впускают ионы натрия, которые в свою очередь открывают следующие каналы. Получается цепная реакция, которая проходит по аксону.
Когда ионы натрия доходят до конца аксона, они открывают другие каналы, которые впускают ионы кальция.
Часть 3: Выброс нейромедиаторов
В процессе жизни нейрона вырабатывается большое количество веществ. В том числе и нейромедиаторы. Это тоже довольно увлекательный процесс, но описывать его я сейчас не буду.
Эти вещества в пузырьках передаются к концу аксона.
Ионы кальция позволяют этим пузырькам слиться с телом нейрона и выбросить их в СИНАПС. Синапс - это место, где связаны дендриты одного нейрона и отростки аксона другого.
Часть 4: Реполяризация
Далее идет волна реполяризации. Она движется в обратную сторону и выравнивает электрический потенциал. После чего нейрон снова готов принимать нейромедиаторы. Цикл завершен.
Надеюсь из моего краткого описания у вас появилось хотя бы малейшее понимание работы нейрона. Переходим к коду.
Общая схема работы нейросети выглядит так:
Допустим, что класс контроллера это кожа. Она генерирует импульс на нейрон. Далее нейросеть обрабатывает этот импульс. И через другие нейроны импульс возвращается на класс контроллер. Например, на мышцы, чтобы существо сжалось от внешнего раздражителя.
Рассчет нейросети будет происходить в простом цикле. Нейросеть будет инициировать действия внутри нейрона, а также передавать нейротрансмиттеры между нейронами. Для начала запрограммируем сам нейрон.
Применяя абстракцию от сложных химических и биологических терминов в ходе длительных размышлений, я пришел к следующему
class Neuron:
def __init__(self, name):
self.name = name
self.treshold = 10 # treshold of activation
self.returnablity = 0.1 # percent of remaining transmitters
self.speed = 5 # the less, the faster signal will be sent after activation
self.recovery = 5 # if 0, neuron ready to send signal. -=1 on each step after
self.sta = 5 # sta - steps to activation. Set > 0 when created to autostart
self.str = 0 # str - steps to recovery
# outer tm
self.dendrite = [0, 0] # recieve from another synaps
self.synapse = [0, 0] # send to another neuron and reset to zero
# inner tm
self.reproductivity = [0.5, -0.1] # amount of transmitters + on each step
self.accumulated = [0, 0] # move to synapse and set accumulated * returnability
self.current_state = [0, 0] # how many transmitters in synapse; before calculations complete
self.last_state = [0, 0] # after calculations; [activator, ingibitor]
Давайте по порядку.
# outer tm
self.dendrite = [0, 0] # recieve from another synaps
self.synapse = [0, 0] # send to another neuron and reset to zero
Нейромедиаторы в общем бывают двух видов: возбуждающие и подавляющие. В зависимости от того, положительные или отрицательные ионы они впускают. Поэтому абстрагируемся от конкретных веществ. Пусть нулевой элемент всегда отвечает за положительные, а первый за отрицательные.
dendrite - отвечает за хранение возбуждающих и подавляющих нт, получаемых на дендритах
synapse - отвечает за хранение их же, но в синапсе(между дендритами одного и аксоном другого)
self.treshold = 10 # treshold of activation
self.sta = 5 # sta - steps to activation. Set > 0 when created to autostart
self.speed = 5 # the less, the faster signal will be sent after activation
Когда потенциал превышает пороговое значение treshold, происходит передача потенциала. Она отражена в переменной-счетчике sta. То есть через sta итераций произойдет выброс нт в синапс. При каждой активации нейрона, она устанавливается в константное значение speed.
self.recovery = 5 # if 0, neuron ready to send signal. -=1 on each step after
self.str = 0 # str - steps to recovery
После передачи нт нейрон восстанавливается. str еще одна переменная-счетчик.
# inner tm
self.reproductivity = [0.5, -0.1] # amount of transmitters + on each step
self.accumulated = [0, 0] # move to synapse and set accumulated * returnability
Все время существования нейрона в нем вырабатываются нейротрансмиттеры. reproductivity показывает сколько будет создано на каждой итерации. А accumulated - сколько уже содержится в нейроне.
Да, это довольно прямолинейная и топорная логика, но в будущем при желании можно использовать какие-либо функции для динамического рассчета нт.
Когда счетчик sta оказывается в нуле, значения из accumulated назначаются в synapse, а accumulated обнуляется.
Следующие связанные нейроны будут брать нт из synapse, а часть нт будет возвращена из synapse в accumulated. (Да, часть нт возвращается обратно в нейрон)
self.current_state = [0, 0] # how many transmitters in synapse; before calculations complete
self.last_state = [0, 0] # after calculations; [activator, ingibitor]
current_state хранит уровень потенциала в нейроне. Так как рассчет происходит последовательно в цикле, то может возникнуть такая ситуация, что нейрон должен принять нт из множества других, но один уже был рассчитан, а остальные еще нет. Поэтому для каждого нейрона введен дополнительный атрибут last_state, который будет обновлен для каждого нейрона после завершения рассчетов. Т.е. в процессе рассчета новые данные записываются в current_state, а используются last_state.
На схеме это выглядит следующим образом
А это уже целая нейросеть!
Хорошо, класс нейрона есть. Но сам по себе он бесполезен. Его нужно заставить работать.
class Network:
def __init__(self):
# replace axons and dendrites with it
self.neurons: {Neuron: [Neuron]} = {}
self.run = False
Словарь neurons хранит все нейроны в нейросети в качестве ключей, а также список нейронов с которыми он связан в качестве значений.
А также есть флаг run, который помогает останавливать нейросеть, когда она запущена в отдельном потоке.
Далее пару методов для создания и редактирования сети
# first to second (one way communication)
def link(self, n1, n2):
if n2 in self.neurons[n1]:
self.neurons[n1].remove(n2)
else:
self.neurons[n1].append(n2)
def add(self, n: Neuron):
self.neurons[n] = []
И наконец, главная логика ее работы
def maincycle(self):
while self.run:
for neuron in self.neurons.keys():
neuron.step()
tm = neuron.synapse
neuron.synapse[0] = neuron.synapse[0] * 0.1
neuron.synapse[1] = neuron.synapse[1] * 0.1
amount = len(self.neurons[neuron])
for dendrite in self.neurons[neuron]:
dendrite.dendrites((tm[0]/amount, tm[1]/amount))
for neuron in self.neurons.keys():
neuron.last_state = neuron.current_state
time.sleep(0.01)
Здесь мы проходим по всем нейронам в словаре. Распределяем нейротрансмиттеры из синапса поровну по всем связанным с ним нейронам. А затем обновляем состояния каждого.
Как вы могли заметить, в цикле вызывается метод step нейрона. Он реализует логику его работы
def step(self):
self.accumulated[0] += self.reproductivity[0]
self.accumulated[1] += self.reproductivity[1]
if self.str > 0:
print(pcns(), self.name, 'ВОССТАНАВЛИВАЮСЬ')
self.str -= 1
elif self.sta == 1:
print(pcns(), self.name, 'ВЫБРАСЫВАЮ')
self.sta = 0
self.synapse[0] += self.accumulated[0]
self.synapse[1] += self.accumulated[1]
self.accumulated[0] = self.accumulated[0] * self.returnablity
self.accumulated[1] = self.accumulated[1] * self.returnablity
self.str = self.recovery
elif self.sta > 0:
print(pcns(), self.name, 'ПЕРЕДАЮ')
self.sta -= 1
elif self.last_state[0] + self.last_state[1] > self.treshold:
print(pcns(), self.name, 'АКТИВИРУЮСЬ')
self.current_state = [0, 0]
self.sta = self.speed
else:
print(pcns(), self.name, 'НАКАПЛИВАЮ')
self.current_state[0] += self.dendrite[0]
self.current_state[1] += self.dendrite[1]
self.dendrite[0] = 0
self.dendrite[1] = 0
И метод dendrites для приема нт из синапса
def dendrites(self, tm):
print(pcns(), self.name, 'ПРИНИМАЮ')
self.dendrite[0] += tm[0]
self.dendrite[1] += tm[1]
Теперь попробуем это все запустить
if __name__ == '__main__':
net = Network()
net.run = True
threading.Thread(target=net.maincycle).start()
n1 = Neuron('ПЕРВЫЙ')
n2 = Neuron('ВТОРОЙ')
net.add(n1)
net.add(n2)
net.link(n1, n2)
Часть вывода в консоли-----------1------------
386500223981600 ПЕРВЫЙ ПЕРЕДАЮ
386500224013300 ВТОРОЙ ПРИНИМАЮ
386500224025400 ВТОРОЙ ПЕРЕДАЮ
-----------2------------
386500727221500 ПЕРВЫЙ ПЕРЕДАЮ
386500727240500 ВТОРОЙ ПРИНИМАЮ
386500727253100 ВТОРОЙ ПЕРЕДАЮ
-----------3------------
386501230345700 ПЕРВЫЙ ПЕРЕДАЮ
386501230378100 ВТОРОЙ ПРИНИМАЮ
386501230395500 ВТОРОЙ ПЕРЕДАЮ
-----------4------------
386501739995900 ПЕРВЫЙ ПЕРЕДАЮ
386501740041700 ВТОРОЙ ПРИНИМАЮ
386501740061700 ВТОРОЙ ПЕРЕДАЮ
-----------5------------
386502247167700 ПЕРВЫЙ ВЫБРАСЫВАЮ
386502247208500 ВТОРОЙ ПРИНИМАЮ
386502247231500 ВТОРОЙ ВЫБРАСЫВАЮ
-----------6------------
386502761955400 ПЕРВЫЙ ВОССТАНАВЛИВАЮСЬ
386502761996500 ВТОРОЙ ПРИНИМАЮ
386502762028300 ВТОРОЙ ВОССТАНАВЛИВАЮСЬ
-----------7------------
386503265130200 ПЕРВЫЙ ВОССТАНАВЛИВАЮСЬ
386503265159800 ВТОРОЙ ПРИНИМАЮ
386503265181800 ВТОРОЙ ВОССТАНАВЛИВАЮСЬ
Все работает именно так, как и было задумано! (хотя при первом прочтении вряд ли это можно понять)
Но ведь ничего не понятно! Скажете вы. И я с вами полностью согласен. Поэтому посидев часок с ChatGPT я смог получить графический интерфейс на pygame.
С помощью этого интерфейса можно добавлять нейроны, удалять, создавать и удалять связи. Сохранять и загружать модели, перемещаться по экрану и масштабировать. А также выводить показатели в реальном времени. (Я был приятно удивлен качеством работы ChatGPT 4.0)
Исходный код можно найти в моем github.
Мне удалось реализовать изначальную задумку. Все работает так, как я и хотел. Был удивлен, насколько математические модели нейросетей похожи на то, как они выглядят на самом деле.
В следующих статьях покажу, как я проектирую нейросеть. Попробую привязать ее к объектам и взаимодействовать с ними. А также вывести инструкцию по тонкой настройке нейросети(показателей, чьи значения можно изменять там ого-го).
Хочу создать механизм для эволюционного развития нейросети. Сложно представить, как можно вручную создать тысячи нейронов. Пусть они сами генерируются рандомно, а я лишь буду задавать условия естественного отбора.
В общем, идей еще очень много. По мере их реализации, буду писать новые статьи. Спасибо за прочтение!