Pull to refresh

Создание простого и работоспособного генетического алгоритма для нейросети с Python и NumPy

Level of difficultyMedium
Reading time6 min
Views12K

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

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

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

Вначале нам потребуется импортировать модули:

import numpy as np
import random

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


x = np.array([[1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 0, 0], [1, 0, 0], [0, 1, 0], [1, 1, 0], [0, 1, 1], [1, 1, 1]])
y = np.array([[0],[1],[1], [0], [0], [0], [0], [1], [1]])

#x = np.array([[0, 1, 1], [0, 0, 1], [1, 0, 1], [0, 1, 0], [1, 0, 0], [1, 1, 0], [0, 0, 0], [1, 1, 0], [1, 1, 1]])
#y = np.array([[1],[0], [0], [1], [0], [1], [0], [1], [1]])

#x = np.array([[1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 0], [1, 0, 0], [0, 0, 0], [1, 1, 0], [0, 1, 1], [1, 1, 1]])
#y = np.array([[1],[0],[1], [0], [1], [0], [1], [0], [1]])

Добавляем списки и функции активации. Смысл списков станет понятен позже. Первая функция активации это сигмоида, а вторая - пороговая

listNet = []
NewNet = []
goodNET = []
GoodNet0 = []
GoodNet1 = []
GoodNet2 = []
GoodNet3 = []
GoodNet4 = []
GoodNet5 = []
GoodNet6 = []
good = 0
epoch = 0

good = 0
epoch = 0

def sigmoid(x):
    return 1/(1 + np.exp(-x)) 
def finfunc(x):
    if x[0] >= 0.5:
        x[0] = 1
        return x[0]

    else:
        x[0] = 0
        return x[0]

Дальше нам потребуется создать два класса, первый нужен для создания начальной популяции, а второй для всех последующих, так как в первый раз нам потребуется рандомно создавать веса, а потом только их скрещивать и мутировать. Функция init() мы создаем веса или добавляем, predict() нужен для самого алгоритма и подсчета лучших вариантов, а функция Fredict() отличается возвращением ответа и фитнесс функции, чтобы выводить числа на экран и видеть этапы обучения. На выходном слое вначале используется функция сигмоида, чтобы приблизить ответ к одному из вариантов и только потом - пороговая.

class Network():
    def __init__(self):
        self.H1 = np.random.randn(3, 6)
        self.O1 = np.random.randn(6, 1)

    def predict(self, x, y):
        t1 = x @ self.H1
        t1 = sigmoid(t1)
        t2 = t1 @ self.O1
        t2 = sigmoid(t2)
        t2 = finfunc(t2)
        if t2 == y[0]:
            global good
            good += 1

    def Fpredict(self, x, y):
        t1 = x @ self.H1
        t1 = sigmoid(t1)
        t2 = t1 @ self.O1
        t2 = sigmoid(t2)
        t2 = finfunc(t2)
        if t2 == y[0]:
            global good
            good += 1
        return t2, good
class Network1():
    def __init__(self, H1, O1):
        self.H1 = H1
        self.O1 = O1


    def predict(self, x, y):
        t1 = x @ self.H1
        t1 = sigmoid(t1)
        t2 = t1 @ self.O1
        t2 = sigmoid(t2)
        t2 = finfunc(t2)
        if t2 == y[0]:
            global good
            good += 1
    def Fpredict(self, x, y):
        t1 = x @ self.H1
        t1 = sigmoid(t1)
        t2 = t1 @ self.O1
        t2 = sigmoid(t2)
        t2 = finfunc(t2)
        if t2 == y[0]:
            global good
            good += 1
        return t2, good

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

s = Network()
print(s.Fpredict(x[0], y[0]))
print(s.Fpredict(x[1], y[1]))
print(s.Fpredict(x[2], y[2]))
print(s.Fpredict(x[3], y[3]))
print("wait0")
good = 0

Проходит первый цикл, здесь и во всех последующих циклах мы даем, только шесть вопросов, чтобы проверить насколько хорошо она справится с задачей, которую она не встречала, то есть мы проверяем её на зубрежку, а это иногда встречается. А теперь давайте поподробнее: от того насколько ответов она ответила правильно, мы определяем её в один из классов, если большое кол-во правильно, то мы должны поддержать такую нейросеть и увеличить её количество, чтобы при последующей мутации более умных стало больше, чтобы это понять, Вы можете представить, что на 100 человек есть один гений, но его на всех не хватит, и значит его гений угаснет в следующих поколениях, это значит, либо нейросеть будет учиться очень медленно, или вообще не будет, чтобы такого не было мы в цикле увеличиваем количество нейросетей с большим количеством правильных ответов. В конце мы опустошаем основной список listNet присваиваем ему новые значения списков GoodNet в порядке от лучших к худшим, делаем срез на 100 лучших особей, для последующей мутации.

for s in range (1000):
    s = Network()
    good = 0
    s.predict(x[0], y[0])
    s.predict(x[1], y[1])
    s.predict(x[2], y[2])
    s.predict(x[3], y[3])
    s.predict(x[4], y[4])
    s.predict(x[5], y[5])
    if good == 6:
        GoodNet6.append(s)
        for r in range(15):
            GoodNet4.append(s)
    elif good == 5:
        GoodNet5.append(s)
        for r in range(10):
            GoodNet4.append(s)
    elif good == 4:
        GoodNet4.append(s)
        for r in range(5):
            GoodNet4.append(s)
    elif good == 3:
        GoodNet3.append(s)
    elif good == 2:
        GoodNet2.append(s)
    elif good == 1:
        GoodNet1.append(s)
    elif good == 0:
        GoodNet0.append(s)
    good = 0
listNet = []
listNet.extend(GoodNet6)
listNet.extend(GoodNet5)
listNet.extend(GoodNet4)
listNet.extend(GoodNet3)
listNet.extend(GoodNet2)
listNet.extend(GoodNet1)
GoodNet1 = []
GoodNet2 = []
GoodNet3 = []
GoodNet4 = []
GoodNet5 = []
GoodNet6 = []
goodNET = listNet[:100]
listNet = goodNET
goodNET = []

Само скрещивание и мутация: мы берём одну часть первого родителя, вторую от второго, мутируем и у нас получился ребёнок в списке NewNet, так 1000 раз.

for g in range(1000):
    parent1 = random.choice(listNet)
    parent2 = random.choice(listNet)
    ch1H = np.vstack((parent1.H1[:1], parent2.H1[1:])) * random.uniform(-0.2, 0.2)
    ch1O = parent1.O1 * random.uniform(-0.2, 0.2)
    g = Network1(ch1H, ch1O)
    NewNet.append(g)
listNet = NewNet
NewNet = []

Начиная с предыдущей части кода, мы используем Network1(), так как мы сейчас скрещиваем и мутируем, но не создаем рандомно. Так нужно повторить 1000 раз(это гиперпараметр, поэтому можете выбрать количество эпох самостоятельно, мне хватало 15), мы показываем ответы на первый эпохе и 1000-й - финальный вариант(если у вас, например, 20, то указываете 20). Здесь код повторяется, поэтому я не буду его описывать, там всё очень понятно.

for i in range(1000):
    good = 0
    epoch += 1
    for s in listNet:
      good = 0
      s.predict(x[0], y[0])
      s.predict(x[1], y[1])
      s.predict(x[2], y[2])
      s.predict(x[3], y[3])
      s.predict(x[4], y[4])
      s.predict(x[5], y[5])
      if good == 6:
          GoodNet6.append(s)
          for r in range(15):
              GoodNet4.append(s)
      elif good == 5:
          GoodNet5.append(s)
          for r in range(10):
              GoodNet4.append(s)
      elif good == 4:
          GoodNet4.append(s)
          for r in range(5):
              GoodNet4.append(s)
      elif good == 3:
          GoodNet3.append(s)
      elif good == 2:
          GoodNet2.append(s)
      elif good == 1:
          GoodNet1.append(s)
      elif good == 0:
          GoodNet0.append(s)
      good = 0
    listNet = []
    listNet.extend(GoodNet6)
    listNet.extend(GoodNet5)
    listNet.extend(GoodNet4)
    listNet.extend(GoodNet3)
    listNet.extend(GoodNet2)
    listNet.extend(GoodNet1)
    GoodNet1 = []
    GoodNet2 = []
    GoodNet3 = []
    GoodNet4 = []
    GoodNet5 = []
    GoodNet6 = []
    goodNET = listNet[:100]
    listNet = goodNET
    goodNET = []
    if epoch == 1000:

        print(listNet[0].Fpredict(x[0], y[0]))
        print(listNet[0].Fpredict(x[1], y[1]))
        print(listNet[0].Fpredict(x[2], y[2]))
        print(listNet[0].Fpredict(x[3], y[3]))
        print(listNet[0].Fpredict(x[4], y[4]))
        print(listNet[0].Fpredict(x[5], y[5]))
        print(listNet[0].Fpredict(x[6], y[6]))
        print(listNet[0].Fpredict(x[7], y[7]))
        print(listNet[0].Fpredict(x[8], y[8]))

        good = 0
        print('wait')
    elif epoch == 1:

        good = 0
        print(listNet[0].Fpredict(x[0], y[0]))
        print(listNet[0].Fpredict(x[1], y[1]))
        print(listNet[0].Fpredict(x[2], y[2]))
        print(listNet[0].Fpredict(x[3], y[3]))
        print('wait1')
    for g in range(1000):
        parent1 = random.choice(listNet)

        parent2 = random.choice(listNet)
        ch1H = np.vstack((parent1.H1[:1], parent2.H1[1:])) * random.uniform(-2, 2)
        ch1O = parent1.O1 * random.uniform(2, 2)
        g = Network1(ch1H, ch1O)
        NewNet.append(g)
    listNet = NewNet

Это всё, закономерность, которую должна найти нейросеть, это от какого числа(первого, второго, третьего) зависит окончательный вариант и на остальные не обращать внимание. Вы можете делать, например, логические операции(XOR, NOT, AND…), только в этом случае в классе network изменить входные данные на два, я также придерживался правила нейроны в скрытом слое равны входным данным умноженным на два, это работало, но вы можете попробовать свои варианты, также очень важно предоставить нейросети одинаковое количество одних ответов и других ответов, чтобы количество правильных ответе, например "a", было бы равно "b", а иначе нейросеть будет отвечать на все ответы одинаково, то есть, если больше a, то она на все ответит a и ничего не выйдет, также давать ей в обучающей выборке совершенно разные варианты, чтобы она поняла закономерность, например если вы делаете блок XOR, то надо обязательно добавить вариант c двумя единичками, но в случае логических операций, там придется давать все варианты, ибо их слишком мало и она ничего не поймет.

Всё!!!

Tags:
Hubs:
Total votes 7: ↑4 and ↓3+1
Comments4

Articles