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

a0302ddf108f912e94a898a364b2e104

Генетический алгоритм нужен, когда ты знаешь параметры своей нейросети, но не знаешь, что должно получиться на выходе, например, этот алгоритм можно использовать для игры в 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([[1, 1, 0], [0, 0, 1], [1, 0, 1], [0, 1, 0], [1, 0, 0], [1, 1, 0], [0, 0, 0], [0, 1, 1], [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
        if x[1] >= 0.5:
            x[1] = 1
            return x[1]
        else:
            x[1] = 0
            return x[1]

    else:
        x[0] = 0
        if x[1] >= 0.5:
            x[1] = 1
            return x[1]
        else:
            x[1] = 0
            return x[1]

Дальше нам потребуется создать два класса, первый нужен для создания начальной популяции, а второй для всех последующих, так как в первый раз нам потребуется рандомно создавать веса, а потом только их скрещивать и мутировать. Функция 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 раз, мы показываем ответы на первый эпохе и 1000-й — финальный вариант. Здесь код повторяется, поэтому я не буду его описывать, там всё очень понятно.

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 двумя единичками, но в случае логических операций, там придется давать все варианты, ибо их слишком мало и она ничего не поймет.

Многие могут спросить, а как с помощью этого сделать нейросеть для Google динозаврика и тому подобное. Ответ: используйте три списка, первый: listNet, второй: NewNet, третий: фитнесс функция.

a = []#listNet
b = []#NewNet
с = []# фитнесс функция
d = 0# переменная
e = 0# счетчик
f = 100
#создавая нейросеть мы добавляем в listNet нейросеть, а в c - время
a.append(нейросеть)
c.append(время)
'''далаете так 100 нейросетй(поэтому в f - 100), в каждой проверяете сколько 
она продержалась добавляете время в c, потом выбор 10 самых лучших:
'''
for r in range(10):
  for i in c:
    e += 1
    if d < i:
      d = i
    if e == f:
      n = c.index(d)
      b.append(a[n])
      c.remove(d)
      e = 0
      f -= 1
a = b
b = []
c = []
'''Срез списка не нужен, дальше скрещиваете, мутируете, повторяете и готово!!!'''   
  

© Habrahabr.ru