Простая нейронка без библиотек и многомерных массивов

МЕНЮ


Главная страница
Поиск
Регистрация на сайте
Помощь проекту
Архив новостей

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


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

Сначала немного необходимой теории

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

Почти переходим к практике

Дело в том, что когда я "твердо решил написать свою нейросеть", я совершенно не подумал о том, какую задачу эта нейросеть будет решать, так что это я решил на ходу:

Задумавшись над задачей для нейронной сети , я решил выбрать что-нибудь подходящее под три критерия: наглядность, чтобы на выходом было какое-то графическое действие, обучение с подкреплением, потому что мне больше всего нравится этот метод, и количество нейронов не более 5—10 млн, ибо мой текущий компьютер с большим не справится. После длительного отбора идей, я вспомнил статью про эксперименты над обучением одноклеточных организмов и пришел к выводу, что правильным решением будет создать примитивную нейросеть, которая будет выполнять роль клетки в чашке Петри. Предварительный анализ задачи показал, что логичней будет ограничить поле зрения: я выбрал поле 5 на 5 вокруг клетки. В итоге я решил сделать нейронную сеть, имеющую входной слой в 25 нейрона, скрытый в 16 и выходной слой в 14. Почему именно столько? В конструировании нейросетей нет четких правил, но для нашей задачи больше одного слоя не требуется, а количество нейронов в скрытом слою, принято делать между количеством во входном и выходном, а дальше корректировать, в зависимости от эмпирических данных, так что спустя несколько попыток, я выбрал именно 16. Систему обучения я выбрал изначально - подкрепление для нашей задачи подходит идеально. Реализовать обучение с подкреплением для нейросети не сложно: для положительного подкрепления необходимо увеличивать веса синапсов активных нейронов, ответственных за правильное решение на n, а для отрицательного уменьшать. Ещё нужна функция активации, чтобы значение нейрона для удобства варьировалось между -1 и 1. Я выбираю стандартный гиперболический тангенс, который на самом деле является модифицированной экспонентой.

Пишем код

Писать я буду на python, хотя принцип остается тем же и для других языков. Обычно для нейронных сетей используют NumPy с его многомерными массивами, но мне показалось, что для первой нейросети это слишком не наглядно, так что, вдохновившись идеей о создании нейросети методами ООП, я решил реализовать ее через классы. Что я имею ввиду? Я создам класс нейросети и нейрона, а потом уже буду с этим работать. Сначала создаю класс нейрона. У нейрона должны быть 3 переменных: out – выход нейрона, weight – вес синапса, связывающий этот нейрон и родительский, childs – массив дочерних нейронов.

class Neuron(object):     def __init__(self, childs, weight,isultra):         self.childs = childs         self.weight = weight         self.isultra=isultra         self.out=None

Потом класс сети, в ней нужен только массив выходов. (название Mind я использовал в начале, а потом оно приелось, так что я не стал менять):

class Mind(object):     def __init__(self, childs):         self.outs = childs

Добавляем функции создания:

def create_neuron(layers):     if len(layers)==1:         neuron=Neuron([],random.uniform(-1,1),True)         for j in range(layers[-1]):             neuron.childs.append(Neuron(None,random.uniform(-1,1),False))         return neuron     else:         neuron=Neuron([],random.uniform(-1, 1),False)         for j in range(layers[-1]):             neuron.childs.append(create_neuron(layers[:-1]))         return neuron  def create_network(layers,p):     mind=Mind([])     for i in range(p):         mind.outs.append(create_neuron(layers))     return(mind) 

layers – массив слоев(точнее массив количеств нейронов в слою), не считая выходного

p – выходной слой

Эта функция – рекурсивная, это означает, что она вызывает сама себя, в этом случае она работает так: Если слой, который необходимо создать, – не предпоследний, то сначала создается нейрон со случайным весом синапса(random.uniform(-1,1) – функция, возвращающая псевдослучайное число от -1 до 1), а потом с помощью этой же функции создаются дочерние нейроны этого нейрона, иначе создается нейрон и сразу дочерние нейроны к нему.

Треть уже готова, осталось сделать функцию активации,

def act(num):     return(math.tanh(num))

Смысл создавать отдельную функцию, а не просто использовать math.tanh(), в том, чтобы удобнее было ее заменить, в случае, если я решу, что другая будет эффективней.

функции для получения выхода,

class Neuron(object):     def __init__(self, childs, weight,isultra):         self.childs = childs         self.weight = weight         self.isultra=isultra         self.out=None      def getout(self,input):         if self.isultra:             out=0             for i in range(len(input)):                 self.childs[i].out=input[i]                 out+=act(self.childs[i].out*self.childs[i].weight)             self.out=act(out)             return(self.out)         else:             out=0             for i in range(len(self.childs)):                 out+=act(self.childs[i].getout(input)*self.childs[i].weight)             self.out=act(out)             return(self.out)            

 Эта функция работает следующим образом: Если на нейрон, из которого вызвали эту функцию находится не на предпоследнем слое – его выход вычисляется по формуле th(?childs.getout*childs.weight)иначе - th(?input*childs.weight).

class Mind(object):     def __init__(self, childs):         self.outs = childs      def out(self,input):         maxx=-float('inf')         maxxlist=list()         for i in range(len(self.outs)):             now=self.outs[i].getout(input)             if now==maxx:                 maxxlist.append(i)             if now> maxx:                 maxx=now                 maxxlist=[i]         return(random.choice(maxxlist))  

Эта функция принимает параметр input – массив входных значений. По сути, эта функция возвращает номер самого активного выходного нейрона или случайного из самых активных. Выход дочернего нейрона возвращается функцией Neuron.getout(input).

и наконец обучение.

На данном этапе мы уже можем запустить нейросеть со случайным входным значением и увидеть, что все работает и нейросеть выдает случайное значение:

>>> from neurocell import * #так называется файл с нейросетью >>> mind=create_network([25,16],4) >>> print(mind.out([random.uniform(-1,1) for i in range(25)])) 0 >>> print(mind.out([random.uniform(-1,1) for i in range(25)])) 2 

Для обучения я реализую альфа-систему подкрепления. Надо оговориться, что у меня считаются «активными связями» все нейроны, модуль выхода которых, больше, либо равен 0.4, а вес синапса может быть отрицательным. Итак, представляю вашему вниманию полный код:

import random import math  global defch def okr(num):     #num = int(num + (0.5 if num > 0 else -0.5))     return num  def act(num):     return(math.tanh(num))   class Neuron(object):     def __init__(self, childs, weight,isultra):         self.childs = childs         self.weight = weight         self.isultra=isultra         self.out=None      def getout(self,input):         if self.isultra:             out=0             for i in range(len(input)):                 self.childs[i].out=input[i]                 out+=act(self.childs[i].out*self.childs[i].weight)             self.out=act(out)             return(self.out)         else:             out=0             for i in range(len(self.childs)):                 out+=act(self.childs[i].getout(input)*self.childs[i].weight)             self.out=act(out)             return(self.out)      def chweight(self,mlt):         if self.isultra:             for i in range(len(self.childs)):                 if self.childs[i].out>=0.4:                     self.childs[i].weight+=mlt                 if self.childs[i].out<=-0.4:                     self.childs[i].weight-=mlt             if self.out>=0.4:                 self.weight+=mlt             if self.out<=-0.4:                 self.weight-=mlt         else:             for i in range(len(self.childs)):                 self.childs[i].chweight(mlt)             if self.out>=0.4:                 self.weight+=mlt             if self.out<=-0.4:                 self.weight-=mlt         return  class Mind(object):     def __init__(self, childs):         self.outs = childs      def out(self,input):         maxx=-float('inf')         maxxlist=list()         for i in range(len(self.outs)):             now=self.outs[i].getout(input)             if now==maxx:                 maxxlist.append(i)             if now> maxx:                 maxx=now                 maxxlist=[i]         return(random.choice(maxxlist))      def bad(self,out,cof):         if out ==-1:             for i in range(len(self.outs)):                 self.outs[i].chweight(-defch*cof/len(self.outs))         else:             self.outs[out].chweight(-defch*cof)         return     def good(self,out,cof):         if out ==-1:             for i in range(len(self.outs)):                 self.outs[i].chweight(defch*cof/len(self.outs))         else:             self.outs[out].chweight(defch*cof)         return  def create_neuron(layers):     if len(layers)==1:         neuron=Neuron([],random.uniform(-1,1),True)         for j in range(layers[-1]):             neuron.childs.append(Neuron(None,random.uniform(-1,1),False))         return neuron     else:         neuron=Neuron([],random.uniform(-1, 1),False)         for j in range(layers[-1]):             neuron.childs.append(create_neuron(layers[:-1]))         return neuron  def create_network(layers,p):     mind=Mind([])     for i in range(p):         mind.outs.append(create_neuron(layers))     return(mind)

Функции good и bad меняют веса выбранного нейрона на определенное значение с помощью функции Neuron.chweight(). На практике, как следует из названия, good – положительное подкрепление, а bad – отрицательное.

На этом сама нейросеть окончательно закончена, пора приступать к разработке среды обучения. Подробное описание процесса разработки среды не имеет ценности для темы, так что я просто опишу принцип работы:

Изначально создается массив, который является картой среды. Массив изначально состоит из 0.1, а потом каждый ход наполняется 1 и -1 случайным образом. Также создается клетка, которая управляется нейросетью, которой на вход подается массив из значений полей в квадрате 5*5, а на выходе число от 1 до 4, обозначающие ход (1- шаг вверх, 2 - вниз, 3 - вправо, 4 – влево). Проверяется по одной клетке вокруг клетки и если находится 1 – то по этому направлению применяется положительное подкрепление, а если -1 – то отрицательное. Чтобы клетка не стояла на месте, если 0.1, то тоже применяется отрицательное подкрепление, но в меньшем количестве, чем при -1. Также я добавил к этому графический интерфейс.

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

код среды
from tkinter import * import matplotlib.pyplot as plt import neurocell import random  mind=neurocell.create_network([5*5,16],4)  canvas_size=640 realsize=16  pix=canvas_size/realsize  canvas=[[0.1 for x in range(realsize)] for y in range(realsize)] cellx,celly=15,15 aix,aiy,=15,15  def cellvision(vis): 	global cellx 	global celly 	global canvas 	inp=[] 	if vis !=-1: 		for i in range(vis): 			for j in range(vis): 				if int(cellx-vis//2+1+j) >= realsize-1 and int(celly-vis//2+1+i) >= realsize-1 and int(cellx-vis//2+1+j) <= 0  and int(celly-vis//2+1+i) <= 0: 					inp.append(canvas[int(cellx-vis//2+1+j)][int(celly-vis//2+1+i)]) 				else: 					inp.append(0) 			#print() 	else: 		if cellx >= realsize-1 and celly-1 >= realsize-1 and cellx <= 0 and celly-1 <= 0: 			inp.append(canvas[cellx][celly-1]) 		else: 			inp.append(0) 		if cellx >= realsize-1 and celly+1 >= realsize-1 and cellx <= 0 and celly+1 <= 0: 			inp.append(canvas[cellx][celly+1]) 		else: 			inp.append(0.1) 		if cellx+1 >= realsize-1 and celly >= realsize-1 and cellx+1 <= 0 and celly <= 0: 			inp.append(canvas[cellx+1][celly]) 		else: 			inp.append(0.1) 		if cellx-1 >= realsize-1 and celly >= realsize-1 and cellx-1 <= 0 and celly <= 0: 			inp.append(canvas[cellx-1][celly]) 		else: 			inp.append(0.1)  	return(inp)  def move(out): 	global cellx 	global celly 	if out==0: 		celly-=1 	if out==1: 		celly+=1 	if out==2: 		cellx+=1 	if out==3: 		cellx-=1 	if cellx==realsize: 		cellx=1 	if cellx==0: 		cellx=realsize-1 	if celly==realsize: 		celly=1 	if celly==0: 		celly=realsize-1 	cell(cellx,celly) 	return  def goodpoint(x,y): 	color = "#476042" 	x,y=x*pix,y*pix 	x1, y1 = ( x - pix/2 ), ( y - pix/2 ) 	x2, y2 = ( x + pix/2 ), ( y + pix/2 ) 	w.create_oval( x1, y1, x2, y2, outline=color,fill = color )  def badpoint(x,y): 	color = "#ff0000" 	x,y=x*pix,y*pix 	x1, y1 = ( x - pix/2 ), ( y - pix/2 ) 	x2, y2 = ( x + pix/2 ), ( y + pix/2 ) 	w.create_oval( x1, y1, x2, y2, outline=color,fill = color )  def cell(x,y): 	color = "#ffffff" 	x,y=x*pix,y*pix 	x1, y1 = ( x - pix/2 ), ( y - pix/2 ) 	x2, y2 = ( x + pix/2 ), ( y + pix/2 ) 	w.create_oval( x1, y1, x2, y2, outline=color,fill = color )  def canvas_print(): 	global canvas 	w.delete("all") 	ans='' 	for y in range(realsize): 		for x in range(realsize): 			ans+=str(canvas[x][y])+" " 			if canvas[x][y] == 1: 				goodpoint(x,y) 			if canvas[x][y] == -1: 				badpoint(x,y) 			if canvas[x][y] == 0: 				cell(x,y) 		ans+=" "  def usergoodpoint(event): 	x,y=int(event.x/pix),int(event.y/pix) 	canvas[x][y]=1  def userbadpoint(event): 	x,y=int(event.x/pix),int(event.y/pix) 	canvas[x][y]=-1  master = Tk() master.title( "Среда обучения" ) w = Canvas(master, bg="black",            width=canvas_size,            height=canvas_size) w.pack(expand = YES, fill = BOTH) w.bind( "<B1-Motion>", usergoodpoint ) w.bind( "<B3-Motion>", userbadpoint )  iterat=-1 allg=0 graphic=[] rev=True neurocell.defch=input("Введите число(дефолт - 0.01):") if neurocell.defch=="": 	neurocell.defch=0.01 else: 	neurocell.defch=float(neurocell.defch) end=input("кол-во ходов:") if end=="": 	end=-1 else: 	end=int(end)+1 revv=input("реверс на ходу:") if revv=="": 	revv=-1 else: 	revv=int(revv)+1  while True: 	iterat += 1 	if iterat == end: 		break 	if iterat==revv: 		rev=False 	if iterat%200==0: 		plt.plot(graphic) 		plt.pause(0.0000001) 	good=0 	if rev: 		canvas[random.randint(0,realsize-1)][random.randint(0,realsize-1)]=1 		canvas[random.randint(0,realsize-1)][random.randint(0,realsize-1)]=1 		canvas[random.randint(0,realsize-1)][random.randint(0,realsize-1)]=-1 	else: 		canvas[random.randint(0,realsize-1)][random.randint(0,realsize-1)]=1 		canvas[random.randint(0,realsize-1)][random.randint(0,realsize-1)]=-1 		canvas[random.randint(0,realsize-1)][random.randint(0,realsize-1)]=-1 	canvas_print() 	 	visn=cellvision(5) 	visnn=cellvision(-1) 	if rev: 		if iterat!=0: 			for i in range(len(visnn)): 				if visnn[i]==1: 					mind.good(i,50) 				if visnn[i]==-1: 					mind.bad(i,50) 				else: 					mind.bad(i,10) 	else: 		if iterat!=0: 			for i in range(len(visnn)): 				if visnn[i]==1: 					mind.bad(i, 50) 				if visnn[i]==-1: 					mind.good(i,50) 				else: 					mind.bad(i,10) 	out=mind.out(visn) 	move(out)  	if rev: 		if canvas[cellx][celly]==1: 			good+=50 			canvas[cellx][celly]=0.1 		elif canvas[cellx][celly]==-1: 			good-=50 			canvas[cellx][celly]=0.1 		else: 			good-=10 	else: 		if canvas[cellx][celly]==1: 			good-=50 			canvas[cellx][celly]=0.1 		elif canvas[cellx][celly]==-1: 			good+=50 			canvas[cellx][celly]=0.1 		else: 			good-=10 	#print(input()) 	allg+=good 	graphic.append(allg) 	if rev: 		plt.suptitle("График обучения при условии: 1 единица подкрепления = "+str(neurocell.defch)+" изменения весов") 	else: 		plt.suptitle("График обучения при условии: 1 единица подкрепления = " + str(neurocell.defch) + " изменения весов  Изменение правил произошло на ходу "+str(revv)) 	master.title( "Среда обучения: "+" i:"+ str(iterat)+" good:"+str(good)) 	master.update() plt.show() master.mainloop()

Итак, первый запуск:

Все работает и даже показывается график, который обновляется каждые 200 ходов.

Я проделывал еще несколько эксспериментов, но их результат совпал с предсказанным, так что они неинтересны.

Гитхаб репозиторий: ссылка

Заключение

Нейросети - очень интересная тема, с которой я возможно буду еще работать и если сделаю что-нибудь интересное - напишу, если этот мой опыт минимально зайдет.

Чуть-чуть обо мне в самом конце:

тык во избежание предвзятости

На самом деле мне 15 и я новичок на Хабре так что, пожалуйста, не сильно ругайтесь на ошибки, это мой первый опыт в написании статей. Если вам понравилось или было полезно, пожалуйста поделитесь этим в комментариях, мне очень важна эта информация. Также если что-то не корректным, прошу обратить мое внимание. Спасибо за прочтение, всего хорошего!


Источник: habr.com

Комментарии: