Pull to refresh

Человеческий мозг на Python

Level of difficultyEasy
Reading time8 min
Views3.8K

Приветствую любителей порассуждать о том, как скоро нейросети отнимут работу у человека и захватят мир. А также тех, кто этой темой никогда не интересовался. В общем, устраивайтесь по удобней.

Я планирую выпустить несколько статей, в которых расскажу о своих попытках воссоздать нейросеть в оригинальном ее виде. Т.е. повторить функционал настоящего нейрона, а затем и целой нейросети в коде.

С чего все началось

Нейросетями я пользовался давно. С ностальгией вспоминаю насколько убогой была первая версия 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.

Заключение

Мне удалось реализовать изначальную задумку. Все работает так, как я и хотел. Был удивлен, насколько математические модели нейросетей похожи на то, как они выглядят на самом деле.

В следующих статьях покажу, как я проектирую нейросеть. Попробую привязать ее к объектам и взаимодействовать с ними. А также вывести инструкцию по тонкой настройке нейросети(показателей, чьи значения можно изменять там ого-го).

Хочу создать механизм для эволюционного развития нейросети. Сложно представить, как можно вручную создать тысячи нейронов. Пусть они сами генерируются рандомно, а я лишь буду задавать условия естественного отбора.

В общем, идей еще очень много. По мере их реализации, буду писать новые статьи. Спасибо за прочтение!

Tags:
Hubs:
+19
Comments12

Articles