Год назад у меня впервые зародилось желание написать свою нейросеть и поэкспериментировать с ней, с тех пор я собирал попадающуюся мне информацию, но до дела у меня дошли руки только сейчас. Я твердо решил написать свою нейросеть с блекджеком, обучением с подкреплением и без сторонних библиотек. Собственно это я и сделал, а так как у меня самого опыта в этом еще не было, я подумал, что это может быть полезно и для других людей, которые хотят в этом разобраться. Хочу сказать, что смысл этой статьи не в правильном способе создания нейросетей, таких статей сотни, а в способе понять, что такое нейросети и наконец перейти к практике. Итак, поехали.
Сначала немного необходимой теории
Вероятно вы уже множество раз прочитали что-нибудь подобное, так что постараюсь покороче. Говоря простым языком: нейронная сеть – несколько слоев, состоящих из искусственных нейронов и синапсов, которые их соединяют. Значение нейрона формируется из активированной суммы дочерних нейронов, умноженных на вес их синапсов. Первый (следующий после нулевого) слой формируется из активированных входных данных, тоже умноженных на веса синапсов. Обычно веса синапсов изначально генерируются случайно, а потом корректируются в зависимости от процесса обучения. «Активированное значение» - значение, которое преобразовано с помощью выбранной функции активации.
Почти переходим к практике
Дело в том, что когда я "твердо решил написать свою нейросеть", я совершенно не подумал о том, какую задачу эта нейросеть будет решать, так что это я решил на ходу:
Задумавшись над задачей для нейронной сети , я решил выбрать что-нибудь подходящее под три критерия: наглядность, чтобы на выходом было какое-то графическое действие, обучение с подкреплением, потому что мне больше всего нравится этот метод, и количество нейронов не более 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 и я новичок на Хабре так что, пожалуйста, не сильно ругайтесь на ошибки, это мой первый опыт в написании статей. Если вам понравилось или было полезно, пожалуйста поделитесь этим в комментариях, мне очень важна эта информация. Также если что-то не корректным, прошу обратить мое внимание. Спасибо за прочтение, всего хорошего!