DEV Community

Кирилл Ланской
Кирилл Ланской

Posted on

Creating a simple and efficient genetic algorithm for a neural network with Python and NumPy

It is the first article from course about evolution algorithms in ML.

A genetic algorithm is needed when you know the parameters of your neural network, but do not know what the output should be, for example, this algorithm can be used to play Google Dinosaur or Flappy Bird, because there you do not know what the output should be, but you have the ability to sort the most viable options, for example by time, this is called fitness functions.

I have never been able to find such an algorithm that would work, be simple, and be usable, so I started creating my own lightweight, simple, perfectly working Genetic Algorithm.

My goal is not to drag out the writing of this article, and to torture readers with its length, so let’s get straight to the code. As already mentioned, the code is simple, so most of it does not need to be described in entire essays.

First we need to import the modules:

import numpy as np
import random
Enter fullscreen mode Exit fullscreen mode

Then we add Dataset and the answers to it, but not to use the backpropagation algorithm, but simply to count the number of correct answers. Then you can test it on other variants, which are now commented out

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]])

Enter fullscreen mode Exit fullscreen mode

Add lists and activation functions. The meaning of the lists will become clear later. The first activation function is the sigmoid, and the second is the threshold.

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]
Enter fullscreen mode Exit fullscreen mode

Next, we will need to create two classes, the first one is needed to create the initial population, and the second one for all subsequent ones, since the first time we will need to randomly create weights, and then only cross and mutate them. The init() function is used to create or add weights, predict() is needed for the algorithm itself and for calculating the best options, and the Fredict() function is different in that it returns the answer and the fitness function to display numbers on the screen and see the training stages. At the output layer, the sigmoid function is first used to bring the answer closer to one of the options, and only then the threshold function.

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
Enter fullscreen mode Exit fullscreen mode

We output the first answers and the variable good, which is the fitness function here, then we reset it for the next neural network, the print 'wait0' (you can write whatever you want here) is necessary so as not to get confused about where the answers of different neural networks begin.

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
Enter fullscreen mode Exit fullscreen mode

The first cycle passes, here and in all subsequent cycles we give only six questions to check how well it will cope with the task, which it has not met, that is, we check it for cramming, and this sometimes happens. And now let's go into more detail: depending on how many answers it answered correctly, we assign it to one of the classes, if a large number are correct, then we must support such a neural network and increase its number, so that with the subsequent mutation there will be more smarter ones, to understand this, you can imagine that for 100 people there is one genius, but it is not enough for everyone, and this means that his genius will fade away in the next generations, this means that either the neural network will learn very slowly, or will not exist at all, to avoid this, we increase the number of neural networks with a large number of correct answers in the cycle. At the end, we empty the main listNet list, assign it new values ​​​​of the GoodNet lists in order from best to worst, make a cut for the 100 best individuals, for the subsequent mutation.

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 = []
Enter fullscreen mode Exit fullscreen mode

The crossing and mutation itself: we take one part from the first parent, the second from the second, mutate and we get a child in the NewNet list, so 1000 times.

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 = []
Enter fullscreen mode Exit fullscreen mode

Starting from the previous part of the code, we use Network1(), since we are now crossing and mutating, but not creating randomly. So we need to repeat 1000 times (this is a hyperparameter, so you can choose the number of epochs yourself, 15 was enough for me), we show the answers on the first epoch and the 1000th is the final version (if you have, for example, 20, then specify 20). Here the code is repeated, so I will not describe it, everything is very clear there.

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
Enter fullscreen mode Exit fullscreen mode

That's all, the pattern that the neural network should find, this is what number (first, second, third) the final version depends on and ignore the rest. You can do, for example, logical operations (XOR, NOT, AND ...), only in this case in the network class change the input data by two, I also followed the rule neurons in the hidden layer are equal to the input data multiplied by two, it worked, but you can try your options, it is also very important to provide the neural network with the same number of some answers and other answers, so that the number of correct answers, for example "a", would be equal to "b", otherwise the neural network will answer all answers the same way, that is, if there is more a, then it will answer a to everything and nothing will come of it, also give it completely different options in the training sample so that it understands the pattern, for example, if you make an XOR block, then you must add an option with two ones, but in the case of logical operations, you will have to give all the options, because there are too few of them and it will not understand anything.
That's it!!! Next article (must read!): Soon…
Code: https://github.com/LanskoyKirill/GenNumPy.git

My site(it may be undergoing rework): selfrobotics.space

Top comments (0)