Год назад у меня впервые зародилось желание написать свою нейросеть и поэкспериментировать с ней, с тех пор я собирал попадающуюся мне информацию, но до дела у меня дошли руки только сейчас. Я твердо решил написать свою нейросеть с блекджеком, обучением с подкреплением и без сторонних библиотек. Собственно это я и сделал, а так как у меня самого опыта в этом еще не было, я подумал, что это может быть полезно и для других людей, которые хотят в этом разобраться. Хочу сказать, что смысл этой статьи не в правильном способе создания нейросетей, таких статей сотни, а в способе понять, что такое нейросети и наконец перейти к практике. Итак, поехали.
Сначала немного необходимой теории
Вероятно вы уже множество раз прочитали что-нибудь подобное, так что постараюсь покороче. Говоря простым языком: нейронная сеть – несколько слоев, состоящих из искусственных нейронов и синапсов, которые их соединяют. Значение нейрона формируется из активированной суммы дочерних нейронов, умноженных на вес их синапсов. Первый (следующий после нулевого) слой формируется из активированных входных данных, тоже умноженных на веса синапсов. Обычно веса синапсов изначально генерируются случайно, а потом корректируются в зависимости от процесса обучения. «Активированное значение» - значение, которое преобразовано с помощью выбранной функции активации.
Почти переходим к практике
Дело в том, что когда я "твердо решил написать свою нейросеть", я совершенно не подумал о том, какую задачу эта нейросеть будет решать, так что это я решил на ходу:
Задумавшись над задачей для нейронной сети , я решил выбрать что-нибудь подходящее под три критерия: наглядность, чтобы на выходом было какое-то графическое действие, обучение с подкреплением, потому что мне больше всего нравится этот метод, и количество нейронов не более 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)
Эта функция работает следующим образом: Если на нейрон, из которого вызвали эту функцию находится не на предпоследнем слое – его выход вычисляется по формуле иначе - .
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+="\n"
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) + " изменения весов\n Изменение правил произошло на ходу "+str(revv))
master.title( "Среда обучения: "+" i:"+ str(iterat)+" good:"+str(good))
master.update()
plt.show()
master.mainloop()
Итак, первый запуск:
Все работает и даже показывается график, который обновляется каждые 200 ходов.
Я проделывал еще несколько эксспериментов, но их результат совпал с предсказанным, так что они неинтересны.
Гитхаб репозиторий: ссылка
Заключение
Нейросети - очень интересная тема, с которой я возможно буду еще работать и если сделаю что-нибудь интересное - напишу, если этот мой опыт минимально зайдет.
Чуть-чуть обо мне в самом конце:
тык во избежание предвзятости
На самом деле мне 15 и я новичок на Хабре так что, пожалуйста, не сильно ругайтесь на ошибки, это мой первый опыт в написании статей. Если вам понравилось или было полезно, пожалуйста поделитесь этим в комментариях, мне очень важна эта информация. Также если что-то не корректным, прошу обратить мое внимание. Спасибо за прочтение, всего хорошего!
Комментарии (20)
CopterSpace
11.09.2021 19:59+1Наверно, есть смысл прочитать книгу Тарика Рашида "Создаём нейронную сеть".
Там всё очень просто, подробно и с примерами кода.Чтоб и в тему вникнуть, и велосипед не изобретать...
eigrad
11.09.2021 21:33+2Смысл создавать отдельную функцию, а не просто использовать math.tanh(), в том, чтобы удобнее было ее заменить, в случае, если я решу, что другая будет эффективней.
self.act = math.tanh
kitaisky
13.09.2021 11:59+1Очень круто, что с таких основ происходит вход в тему - а то все лепят фит-предикт и типа датасайентисты)
da-nie
13.09.2021 18:29kitaisky
13.09.2021 18:57Спасибо, но уже начитался - своим комментарием я просто выражал одобрение автору :)
da-nie
13.09.2021 19:47Из этой статьи новичок не поймёт вообще, что такое нейросеть.
Но учитывая возраст автора, было бы странно требовать от него хорошей статьи.kitaisky
18.09.2021 08:29Да, сама статья не особо полезна - новичку сложно, остальные и так наверное уже разобрались, тут скорее просто порадоваться за подход автора :)
da-nie
Пожалуйста, не надо. Не нужны там эти классы. Там всё отлично и просто описывается обычными матрицами.
pzrnqt1vrss Автор
Основываясь на личном опыте, могу сказать, что так гораздо проще подступиться к теме человеку, который до этого не занимался ничем подобным, а потом уже, с полным пониманием, делать общепринятыми и рациональными методами. Но в любом случае спасибо за совет и комментарий!
da-nie
Матричная запись очень естественна в данном случае. И очень наглядна. Вместо всех этих циклов с достаточным объёмом текста вы пишете просто умножение матриц.
Причина вовсе не в рациональности. Причина как раз в наглядности. 90% вашего кода просто выбрасывается при использовании матриц. Потому что просто не нужна. Нет классов с их методами. Тем не менее, каждый первый почему-то считает своим долгом завести класс нейрона, совершенно не читая даже самую первую попавшуюся в интернете статью про нейронным сетям. Вот и вы не стали исключением (кстати, с десяток месяцев назад уже была статья с ровно тем же подходом :) ).
yatanai
Проблема когда ты вообще не осознаёшь математику а в описании мат матриц видишь проблески сатанинского письма. (до сих пор вижу там каракули а не алгоритмы. Хотя если написать это всё в виде листа с шагами действий то сразу доходит )
Я сам тоже делал в начале всё на классах, но когда захотелось "увеличить кусок изображения в 2 раза" увидел насколько медленно оно работает, а там и пошло поехало... Благо я нашёл непонятного деда в ютубе с 5К просмотров, который в формате презентации пояснил за каждый нюанс при настройки нейронок на человеческом.
Да, все эти правила очень простые и понятные, и их можно перевести на "понятный язык", но в "надёжных источниках" столько страшного матана и непонятных слов что мозги текут. ,,Приходится говнокодить из того шо есть чтоб понять что ты делаешь,,
da-nie
Так можно взять любую другую статью. Их дофига, где математики почти и нет.
yatanai
Ага, но объясняют её ужасно. В лучшем случае тебе дадут реально работающий код со словами "оно работает не трож". И дальше уже на основе этого пилишь свои приблуды. Работало ктож спорит, но объяснить так и не объяснили. (И там нету матриц, все так же классами объясняют, в основном)
Самое интересное, что некоторые всё же пытаются объяснить как оно то работает, но делают это просто ужасно. Вот по сути очень лёгкая мысль которую легко объяснить графиками и рисунками - 1 нейрон с 2 связями обучается быть похожей на функцию которой его обучают (картиночки+примеры). Но почему-то её объяснить могут дай бог 1\5 чуваков кто статьи пишет. Не говорить умными словами с терминами матана которые "должны же в школе проходить", не пилить "мега абстракции" из образов и псевдонаучных объяснений, а именно на человеческом показать и объяснить почему тут математика эта и почему она работает.
***
Может сейчас с "горы" накопленного опыта и знаний о матане мне даются такие статьи уже легко, но тогда это было настоящей пыткой. Единственное что мне могли "порекомендовать" так это "пойти в школе поучиться", вздор. Тема то элементарная. (по началу, естественно)
Просто нету нормальных обучалок в лёгком гуглении, которые бы сразу на всё тебе давали ответ. (Боги, та даже тема о многомерности, вроде её элементарно объяснить на примере одного нейрона, но сколько обучалок не смотри, не поймёшь при чём тут это и как вообще оно выглядит. А термины уже вводят, без объяснения, ага)
ЗЫ Конечно, ты можешь напрягать извилины и пытаться всё понять сам, но у меня подход в обучении просто "кушоть дохрена информации и встраивай в своё понимание", и чем понятнее инфа тем быстрее до меня доходит.
***Пока писал подгорало слегка, решил не исправлять.
***Это всё лично мой опыт, может быть я просто гуглом не умею пользоваться)
***И ещё я тогда не так хорошо английский знал, а читал в основном топики гугла, если искал на англ. (хотя там тоже мусора по первой выдаче полно, глубоко только CNN гуглил и нашёл собственно, спустя седой волос)
***Почитал что ты скинул ниже, тоже самое - Статья с кодом но без объяснений, работает не трогай. ("Тогда читайте такое.")
da-nie
У меня для вас плохие новости… ;)
leshabirukov
Я подозреваю, что выбранный метод обучения (https://ru.wikipedia.org/wiki/Альфа-система_подкрепления ) не так хорошо описывается матрицами как обратное распространение ошибки. Если же всё так просто, приведите формулы или код (если где-то есть готовый).
da-nie
Прямой проход легко описывается матрицами? Описывается. Что мешает модифицировать матрицу коэффициентов путём либо непосредственного изменения конкретного коэффициента или свести к сложению матриц?