Operácia XOR a ekvivalencia

Blog Admin

V nadväznosti na blogový príspevok Pohľad do hĺbky neurónovej siete cítime potrebu uzavrieť operáciu XOR. Operácie AND, OR a IMPLIKÁCIA (neplatí len v prípade [1,0], teda, keď je predpoklad pravdivý a tvrdenie nepravdivé; v prípadoch [0,0], [0,1] a [1,1] platí) sú lineárne separovateľné a vystačíme si pri nich s elementárnym perceptrónom, ktorý s matematickou istotou oddelí body v rovine tak, že pravdivostné hodnoty 1 budú v jednej polrovine a nepravdivé pravdivostné hodnoty budú v polrovine opačnej. Operácia XOR a aj EKVIVALENCIA (platí, keď sú oba výroky pravdivé alebo oba nepravdivé; neplatí, ak nemajú výroky rovnakú pravdivostnú hodnotu, teda v prípadoch [0,1] a [1,0]) nie sú lineárne separovateľné a v týchto prípadoch sme nútení už použiť aj ďalšiu vrstvu, ktorú umiestnime medzi input a output layer a ktorú nazývame hidden layer (skrytá vrstva). Aby sme boli čo najviac univerzálni, použijeme model/program neurónovej siete s vami zvoleným počtom skrytých vrstiev a aj neurónov v nej ležiacich. Týmto konštrukciu neurónovej siete vlastne úplne zovšeobecníme. Napr. architektúra siete [2,2,1] bude znamenať 2 neuróny v input layer, 2 neuróny v prvej a zároveň poslednej hidden layer a 1 neurón v output layer. Architektúra neurónovej siete [2,4,3,1] znamená 2 neuróny v input layer, 4 neuróny v 1. hidden layer, 3 neuróny v 2. hidden layer a 1 neurón v output layer. Opäť je možné spustiť skript pomocou buttonu Spustiť skript nižšie, nad ktorým sa nachádza aj vstupné pole, kde zadáte architektúru siete. Architektúra (topológia) práve zadanej neurónovej siete sa po spustení skriptu zobrazí aj v grafickej podobe. Logicky a podľa vašich očakávaní výpočtový čas bude závislý na počte vrstiev a na počte neurónov v jednotlivých vrstvách. Všimnite si výpočet a graf pri architektúre [2,2,1] (oproti perceptrónu sme pridali len 1 hidden layer s 2 neurónmi), kde je už separovaná dvojica bodov [0,0] a [1,1] od dvojice [0,1] a [1,0]. Podobne môžete vyskúšať zadať architektúry [2,4,1] (do input poľa nižšie zadáte len 2,4,1, lebo hranaté zátvorky sú už tam) alebo [2,4,3,1]. Na grafoch budete vidieť separáciu bodov XOR troška iným spôsobom. Na záver treba pododknúť, že len malé zmeny v tomto programe je potrebné urobiť, aby sme ukázali, že pomocou 1 alebo viacerých hidden layers vieme separovať aj body pri ekvivalencii.

Poznámky

V kóde programu nižšie si môžete všimnúť, že namiesto aktivačnej funkcie tanh môžeme použiť aj sigmoid, ktorého definíciu aj s jeho deriváciou vidíte na riadkoch 107-111. Ďalšou dôležitou poznámkou je to, že keď zadáme architektúru [2,1], tak sme v situácii bez hidden layer a teda pracujeme len s perceptrónom. Toto dokazuje univerzálnosť konštrukcie neurónovej siete, ale aj potvrdzuje výsledky z predchádzajúceho blogu tu. Zobrazený graf v prípade siete s architektúrou [2,1] neseparuje body [0,0] a [1,1] od [0,1] a [1,0].

Záver

Čo viete teraz? Nuž, je toho dosť veľa. Predsa viete zostaviť ľubovoľnú neurónovú sieť a viete, že to slúži na kategorizáciu, ale aj na regresiu (čo je opäť len špeciálny prípad kategorizácie). Pri štandardnej kategorizácii máme počet kategórií z množiny prirodzených čísliel, ktoré reprezentujú diskrétnu množinu, (napr. kategorizácia videí do povedzme 100 kategórií, aby vám mohli ponúknuť video podobné tomu, aké práve pozeráte napr. na Youtube) a pri regresii odhadujeme hodnotu z nekonečnej množiny reálnych čísiel (akoby sme robili kategorizáciu s nekonečným množstvom kategórií), ktoré reprezentujú spojitú množinu, (napr. odhad ceny akcie v budúcich obdobiach alebo odhad počtu postihnutých ľudí nejakou chorobou na základe doterajšieho priebehu výskytu tejto nemoci v čase).

lineárna regresia

Na obrázku vyššie je zobrazená regresná priamka, ktorá, zhruba povedané, rozdeľuje/oddeľuje červené body reprezentujúce údaje o nejakom jave na 2 polovice, čiže opäť sa jedná o akúsi separáciu. Keby sme chceli byť presnejší, tak cez červené body sme preložili priamku (lineárnu funkciu), ktorá má od týchto bodov minimálnu vzdialenosť. Toto sa nazýva aj „vyrovnanie“ bodov/dát. V štatistike sa často snažíme o rozklad časového radu na trend, sezónnu a cyklickú zložku a náhodnú chybu. Ak z dát vidno, že rastú/klesajú lineárne, môžeme použiť práve lineárnu regresiu na odhad trendu, ktorý sa považuje za dlhodobejšiu informáciu. Tento postup je typický pre stacionarizovanie dát, keď po odčítaní lineárneho trendu dostávame stacionárny časový rad (obyčajne sa stacionarizuje diferenciou radu, teda, ak máme rad x(1), x(2), ..., x(t), kde t je čas, tak diferencia je rad x(2)-x(1), x(3)-x(2), ..., x(t)-x(t-1)), s ktorým sa lepšie pracuje napríklad v ekonometrických modeloch pri vysvetlení krátkodobej dynamiky. Samozrejme, že nie vždy sa dá lineárna regresia použiť. Preto existuje aj kvadratická regresia, exponenciálna regresia... , ktoré sa použijú, ak z grafu vidno, že dáta sú zobrazené okolo nejakej paraboly, exponenciálnej funkcie a podobne.

Ale v porovnaní s tým, čo by mal vedieť vyskumník/vedec, ktorý sa snaží vymyslieť niečo nové, čo by nám uľahčilo život, je to stále dosť málo. Ako inšpiráciu vám teraz podsunieme softvér, ktorý vyrobili v susednej Českej republike a ktorý zatiaľ najlepšie rieši čítanie faktúr (invoices). Členovia firmy Rossum (pomenované podľa drámy R.U.R. K. Čapka, kde sa spomína Rossumov univerzálny robot, pričom tu bolo slovo robot prvýkrát použité) zostrojili sústavu neurónových sietí na čítanie dokumentov v rôznych formátoch s faktúrami. Dnes sa o tento softvér zaujímajú už firmy z americkej Fortune 100. Kto si chce tento softvér vyskúšať, môže si uploadovať vlastnú faktúru tu (do 300 faktúr mesačne je to zdarma).

Čítajte aj Pohľad do hĺbky neurónovej siete, Ako si vytvoriť prvý model neurónovej siete

Zdrojový kód v jazyku Python 3.x

1
2
#!/opt/python/Python-3.6.5/python
# -*- coding: utf-8 -*-
import cgi
import cgitb
import sys
import os
cgitb.enable()
print ("Content-Type: text/html; Charset='UTF-8'")
print ("")#use this double quote print statement to add a blank line in the script

# xor-neural-network.py in python 3.x
import time
import numpy as np
from numpy import exp, array, random, dot
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
from matplotlib.colors import ListedColormap

import pylab
from pylab import figure, axes, title

# The following code is used for hiding the warnings and make this notebook clearer.
import warnings
if not sys.warnoptions:
    warnings.simplefilter("ignore")

# Create instance of FieldStorage 
data = cgi.FieldStorage() 

# Get data from fields
runnn = data.getvalue('runnn')
filename = data.getvalue('filename')
filename1 = data.getvalue('filename1')
arch = data.getvalue('arch')

mydir = '/var/www/masternn/masternn/collect_static/blog/test/'
myfile="/var/www/masternn/masternn/collect_static/blog/test/" + filename
myfile1="/var/www/masternn/masternn/collect_static/blog/test/" + filename1
now = time.time()
old = now - 7200

for f in os.listdir(mydir):
    path = os.path.join(mydir, f)
    if os.path.isfile(path):
        stat = os.stat(path)
        if stat.st_ctime < old:
            os.remove(path) 

ciarka = 0
zatvorka = 0
for i in range(0,len(arch)):
#    f.write(str(arch[i]) + ' ')
    if arch[i] == ',':
        ciarka = ciarka + 1
    if arch[i] == '[' or arch[i] == ']':
        zatvorka = zatvorka + 1  

arch1 = np.zeros(len(arch)-int(ciarka)-int(zatvorka), dtype=int)
k=0
for i in range(0,len(arch)-1):
    if arch[i] != ',' and arch[i] != '[' and arch[i] != ']': 
        arch1[k] = int(arch[i])              
        k = k+1 

def plot_decision_regions(X, y, classifier, test_idx=None, resolution=0.02):
    # setup marker generator and color map
    markers = ('s', 'x', 'o', '^', 'v')
    colors = ('red', 'blue', 'lightgreen', 'gray', 'cyan')
    cmap = ListedColormap(colors[:len(np.unique(y))])
    #cmap = mcolors.ListedColormap(['red', 'blue', 'lightgreen', 'gray', 'cyan'])
    # plot the decision surface
    x1_min, x1_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    x2_min, x2_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, resolution),
                           np.arange(x2_min, x2_max, resolution))
    Z = classifier.predict(np.array([xx1.ravel(), xx2.ravel()]).T)
    Z = Z.reshape(xx1.shape)
    plt.contourf(xx1, xx2, Z, alpha=0.4, cmap=cmap)
    plt.xlim(xx1.min(), xx1.max())
    plt.ylim(xx2.min(), xx2.max())
    # plot class samples
    for idx, cl in enumerate(np.unique(y)):
        plt.scatter(x=X[y == cl, 0], y=X[y == cl, 1],
                    alpha=0.8, c=cmap(idx),
                    marker=markers[idx], label=cl)

    # highlight test samples
    if test_idx:
        # plot all samples
        X_test, y_test = X[test_idx, :], y[test_idx]

        plt.scatter(X_test[:, 0],
                    X_test[:, 1],
                    c='',
                    alpha=1.0,
                    linewidths=1,
                    marker='o',
                    s=55, label='test set')

def tanh(x):
    return (1.0 - np.exp(-2*x))/(1.0 + np.exp(-2*x))

def tanh_derivative(x):
    return (1 + tanh(x))*(1 - tanh(x))

def sigmoid(x):
    return 1 / (1 + np.exp(-x))

def sigmoid_derivative(y):
    return y * (1 - y)

class NeuralNetwork:
    #########
    # parameters
    # ----------
    # self:      the class object itself
    # net_arch:  consists of a list of integers, indicating
    #            the number of neurons in each layer, i.e. the network architecture
    #########
    def __init__(self, net_arch):
        #np.random.seed(0)

        # Initialized the weights, making sure we also 
        # initialize the weights for the biases that we will add later
        self.activity = tanh
        #self.activity = sigmoid
        self.activity_derivative = tanh_derivative
        #self.activity_derivative = sigmoid_derivative
        self.layers = len(net_arch)
        self.steps_per_epoch = 1
        self.arch = net_arch
        self.weights = []

        # Random initialization with range of weight values (-1,1)
        for layer in range(self.layers - 1):
            w = 2*np.random.rand(net_arch[layer] + 1, net_arch[layer+1]) - 1
            print('Zvolene nahodne vahy na uvod ')
            print('<br>')
            print('layer: ', layer)
            print('<br>')
            print(w)
            print ('<br>')
            self.weights.append(w)
            print('<br>')

    def _forward_prop(self, x):
        y = x

        for i in range(len(self.weights)-1):
            activation = np.dot(y[i], self.weights[i])
            activity = self.activity(activation)

            # add the bias for the next layer
            activity = np.concatenate((np.ones(1), np.array(activity)))
            y.append(activity)

        # last layer
        activation = np.dot(y[-1], self.weights[-1])
        activity = self.activity(activation)
        y.append(activity)

        return y

    def _back_prop(self, y, target, learning_rate):
        error = target - y[-1]
        delta_vec = [error * self.activity_derivative(y[-1])]

        # we need to begin from the back, from the next to last layer
        for i in range(self.layers-2, 0, -1):
            error = delta_vec[-1].dot(self.weights[i][1:].T)
            error = error*self.activity_derivative(y[i][1:])
            delta_vec.append(error)

        # Now we need to set the values from back to front
        delta_vec.reverse()

        # Finally, we adjust the weights, using the backpropagation rules
        for i in range(len(self.weights)):
            layer = y[i].reshape(1, self.arch[i]+1)
            delta = delta_vec[i].reshape(1, self.arch[i+1])
            self.weights[i] += learning_rate*layer.T.dot(delta)

    #########
    # parameters
    # ----------
    # self:    the class object itself
    # data:    the set of all possible pairs of booleans True or False indicated by the integers 1 or 0
    # labels:  the result of the logical operation 'xor' on each of those input pairs
    #########
    def fit(self, data, labels, learning_rate=0.1, epochs=100):

        # Add bias units to the input layer - 
        # add a "1" to the input data (the always-on bias neuron)
        ones = np.ones((1, data.shape[0]))
        Z = np.concatenate((ones.T, data), axis=1)

        for k in range(epochs):
            if (k+1) % 10000 == 0:
                print('vykonane epochy treningu: {}'.format(k+1))
                print('<br>')

            sample = np.random.randint(X.shape[0])

            # We will now go ahead and set up our feed-forward propagation:
            x = [Z[sample]]
            y = self._forward_prop(x)

            # Now we do our back-propagation of the error to adjust the weights:
            target = labels[sample]
            self._back_prop(y, target, learning_rate)

    #########
    # the predict function is used to check the prediction result of
    # this neural network.
    # 
    # parameters
    # ----------
    # self:   the class object itself
    # x:      single input data
    #########
    def predict_single_data(self, x):
        val = np.concatenate((np.ones(1).T, np.array(x)))
        for i in range(0, len(self.weights)):
            val = self.activity(np.dot(val, self.weights[i]))
            val = np.concatenate((np.ones(1).T, np.array(val)))
        return val[1]

    #########
    # the predict function is used to check the prediction result of
    # this neural network.
    # 
    # parameters
    # ----------
    # self:   the class object itself
    # X:      the input data array
    #########
    def predict(self, X):
        Y = np.array([]).reshape(0, self.arch[-1])
        for x in X:
            y = np.array([[self.predict_single_data(x)]])
            Y = np.vstack((Y,y))
        return Y

def draw_neural_net(ax, left, right, bottom, top, layer_sizes):
    '''
    Draw a neural network cartoon using matplotilb.

    :usage:
        >>> fig = plt.figure(figsize=(12, 12))
        >>> draw_neural_net(fig.gca(), .1, .9, .1, .9, [4, 7, 2])

    :parameters:
        - ax : matplotlib.axes.AxesSubplot
            The axes on which to plot the cartoon (get e.g. by plt.gca())
        - left : float
            The center of the leftmost node(s) will be placed here
        - right : float
            The center of the rightmost node(s) will be placed here
        - bottom : float
            The center of the bottommost node(s) will be placed here
        - top : float
            The center of the topmost node(s) will be placed here
        - layer_sizes : list of int
            List of layer sizes, including input and output dimensionality
    '''
    n_layers = len(layer_sizes)
    v_spacing = (top - bottom)/float(max(layer_sizes))
    h_spacing = (right - left)/float(len(layer_sizes) - 1)
    # Input-Arrows
    layer_top_0 = v_spacing*(layer_sizes[0] - 1)/2. + (top + bottom)/2.
    for m in range(layer_sizes[0]):
        plt.arrow(left-0.18, layer_top_0 - m*v_spacing, 0.12, 0,  lw =1, head_width=0.01, head_length=0.02)
    # Nodes
    for n, layer_size in enumerate(layer_sizes):
        layer_top = v_spacing*(layer_size - 1)/2. + (top + bottom)/2.
        for m in range(layer_size):
            circle = plt.Circle((n*h_spacing + left, layer_top - m*v_spacing), v_spacing/8.,color='w', ec='k', zorder=4)

            #plt.plot(n*h_spacing + left, layer_top - m*v_spacing, 'o', mfc='w', mec='k', ls= '-', markersize = 40)
            # Add texts
            if n == 0:
                plt.text(left-0.125, layer_top - m*v_spacing, r'$X_{'+str(m+1)+'}$', fontsize=15)
            elif (n_layers == 3) & (n == 1):
                plt.text(n*h_spacing + left+0.00, layer_top - m*v_spacing+ (v_spacing/8.+0.01*v_spacing), r'$H_{'+str(m+1)+'}$', fontsize=15)
            elif n == n_layers -1:
                plt.text(n*h_spacing + left+0.10, layer_top - m*v_spacing, r'$y_{'+str(m+1)+'}$', fontsize=15)
            ax.add_artist(circle) 
    # Bias-Nodes
    for n, layer_size in enumerate(layer_sizes):
        if n < n_layers -1:
            x_bias = (n+0.5)*h_spacing + left
            y_bias = top + 0.005
            circle = plt.Circle((x_bias, y_bias), v_spacing/8., color='w', ec='k', zorder=4)
            # Add texts
            plt.text(x_bias-(v_spacing/8.+0.10*v_spacing+0.01), y_bias, r'$1$', fontsize=15)
            ax.add_artist(circle) 
    # Edges between nodes
    for n, (layer_size_a, layer_size_b) in enumerate(zip(layer_sizes[:-1], layer_sizes[1:])):
        layer_top_a = v_spacing*(layer_size_a - 1)/2. + (top + bottom)/2.
        layer_top_b = v_spacing*(layer_size_b - 1)/2. + (top + bottom)/2.
        for m in range(layer_size_a):
            print(m)
            for o in range(layer_size_b):
                line = plt.Line2D([n*h_spacing + left, (n + 1)*h_spacing + left], [layer_top_a - m*v_spacing, layer_top_b - o*v_spacing], c='k')
                ax.add_artist(line)
                xm = (n*h_spacing + left)
                xo = ((n + 1)*h_spacing + left)
                ym = (layer_top_a - m*v_spacing)
                yo = (layer_top_b - o*v_spacing)
                rot_mo_rad = np.arctan((yo-ym)/(xo-xm))
                rot_mo_deg = rot_mo_rad*180./np.pi
                xm1 = xm + (v_spacing/8.+0.05)*np.cos(rot_mo_rad)
                if n == 0:
                    if yo > ym:
                        ym1 = ym + (v_spacing/8.+0.12)*np.sin(rot_mo_rad)
                    else:
                        ym1 = ym + (v_spacing/8.+0.05)*np.sin(rot_mo_rad)
                else:
                    if yo > ym:
                        ym1 = ym + (v_spacing/8.+0.12)*np.sin(rot_mo_rad)
                    else:
                        ym1 = ym + (v_spacing/8.+0.04)*np.sin(rot_mo_rad)
                #plt.text( xm1, ym1,str(round(coefs_[n][m, o],4)),rotation = rot_mo_deg,fontsize = 10)
    # Edges between bias and nodes
    for n, (layer_size_a, layer_size_b) in enumerate(zip(layer_sizes[:-1], layer_sizes[1:])):
        if n < n_layers-1:
            layer_top_a = v_spacing*(layer_size_a - 1)/2. + (top + bottom)/2.
            layer_top_b = v_spacing*(layer_size_b - 1)/2. + (top + bottom)/2. 
        for m in range(layer_size_a): 
            x_bias = (n+0.5)*h_spacing + left
            y_bias = top + 0.005 
            for o in range(layer_size_b):
                print(o)
                line = plt.Line2D([x_bias, (n + 1)*h_spacing + left], [y_bias, layer_top_b - o*v_spacing], c='k')
                ax.add_artist(line)
                xo = ((n + 1)*h_spacing + left)
                yo = (layer_top_b - o*v_spacing)
                rot_bo_rad = np.arctan((yo-y_bias)/(xo-x_bias))
                rot_bo_deg = rot_bo_rad*180./np.pi
                xo2 = xo - (v_spacing/8.+0.01)*np.cos(rot_bo_rad)
                yo2 = yo - (v_spacing/8.+0.01)*np.sin(rot_bo_rad)
                xo1 = xo2 -0.05 *np.cos(rot_bo_rad)
                yo1 = yo2 -0.05 *np.sin(rot_bo_rad)
                #plt.text( xo1, yo1,str(round(intercepts_[n][o],4)),rotation = rot_bo_deg,fontsize = 10)    
    # Output-Arrows
    layer_top_0 = v_spacing*(layer_sizes[-1] - 1)/2. + (top + bottom)/2.
    for m in range(layer_sizes[-1]):
        plt.arrow(right+0.015, layer_top_0 - m*v_spacing, 0.16*h_spacing, 0,  lw =1, head_width=0.01, head_length=0.02)
    # Record the n_iter_ and loss
    #plt.text(left + (right-left)/3., bottom - 0.005*v_spacing,'Steps:'+str(n_iter_)+'    Loss: ' + str(round(loss_, 6)), fontsize = 15)     
        # Edges
        #for n, (layer_size_a, layer_size_b) in enumerate(zip(layer_sizes[:-1], layer_sizes[1:])):
        #    layer_top_a = v_spacing*(layer_size_a - 1)/2. + (top + bottom)/2.
        #    layer_top_b = v_spacing*(layer_size_b - 1)/2. + (top + bottom)/2.
        #    for m in range(layer_size_a):
        #        for o in range(layer_size_b):
        #            line = plt.Line2D([n*h_spacing + left, (n + 1)*h_spacing + left], [layer_top_a - m*v_spacing, layer_top_b - o*v_spacing], c='k')
        #            ax.add_artist(line)



np.random.seed(0)

# Initialize the NeuralNetwork with
# 2 input neurons
# 2 hidden neurons
# 1 output neuron
print('<br>')
nn = NeuralNetwork(arch1)
print('<br>')
print('Architektura siete: NeuralNetwork('+ str(arch) + ')')
print('<br>')
print ('Aktivacna funkcia: tanh')
print('<br>')

# Set the input data
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])

# Set the labels, the correct results for the xor operation
y = np.array([0, 1, 1, 0])

# Call the fit function and train the network for a chosen number of epochs
print('<br>')
nn.fit(X, y, epochs=100000)
print('<br>')
# Show the prediction results
print('<br>')
print('Predikcia XOR')
print('<br>')
for s in X:
    print(s, nn.predict_single_data(s))
    print('<br>')
print('<br>')
fig = plt.figure()
ax = fig.add_subplot(111)
title('XOR', bbox={'facecolor': '0.8', 'pad': 5}) 
A = 0, 1, 0, 1
B = 0, 0, 1, 1
for xy in zip(A, B):                                       
    ax.annotate('(%s, %s)' % xy, xy=xy, textcoords='data')  
plot_decision_regions(X, y, nn)
plt.xlabel('x-os')
plt.ylabel('y-os')
plt.legend(loc='upper left')
plt.tight_layout()
pylab.savefig(myfile)  

fig = plt.figure(figsize=(12, 12))
ttl = 'Architektura zvolenej neuronovej siete: ' + arch
title(ttl, fontsize=20, bbox={'facecolor': '0.8', 'pad': 5}) 
ax = fig.gca()
ax.axis('off')
draw_neural_net(ax, .1, .9, .1, .9, arch1)
pylab.savefig(myfile1)

Autor príspevku: F. Gachulinec Dr., PhD.

S pomocou Chih-Ling Hsu, tu a tu

Zadajte architektúru neurónovej siete: []

vrstvy oddeľte čiarkami; nepoužívajte medzery; za poslednou vrstvou nedávajte čiarku; prvá vrstva je input layer; posledná je output layer; medzitým sú skryté vrstvy; čísla oddelené čiarkami znamenajú počet neurónov v príslušnej vrstve; môžete zadať maximálne 9 neurónov; ak spustíte skript bez zadania vstupnej architektúry, spustí sa architektúra [2,2,1]; architektúru zadávajte už bez hranatých zátvoriek; nie všetky kombinácie počtov neurónov vyhovujú podmienkam tohto programu, nakoľko musia byť splnené určité podmienky - napr. počet neurónov vo vstupnej vrstve je 2, lebo máme 2 súradnice, a preto, keď program ohlási chybu, pokúste sa zadať inú kombináciu

Tágy:
umelá inteligencia xor ekvivalencia Python neurónová sieť