Основы построения нейронных сетей на Python (numpy) в Visual Studio (Python numpy NN in Visual Studio)

Автор: | 11.08.2019

Tags:  Python numpy neural networks Visual Studio

Введение
Создание нейронной сети в Visual Studio (версия кода 1)
Простая нейронная сеть (версия кода 2)
Простая нейронная сеть (версия кода 3)
Примеры хорошо структурированных программ
Нежное введение в тензоры для машинного обучения с NumPy
Полезные ссылки

Введение

Из статьи вы узнаете, как написать простую нейросеть на Python с подключением библиотеки numpy в среде Visual Studio (VS). Для более глубокого понимания проблематики приводятся примеры различных  версий кода — от простого из 7 строк до более профессионального.

Любая искусственная нейронная сеть состоит из слоёв. Первый слой — входной, последний — выходной. Любой слой, расположенный между входным и выходным — скрытый. Количество слоев и нейронов в них может быть разное.

Во входном слое расположены нейроны, которые принимают сигнал, но не обрабатывают его.

Стрелочки, передающие сигналы — синапсы. Они  умножают входной сигнал xi на синаптический вес wi.  В каждом из нейронов определяется сумма значений входящих сигналов

d = w0 +  w1 x1+ … + wn xn 

 где w0 — параметр смещения.

Результат d приводится к диапазону [0 … 1] при помощи функции активации  y=S(d) (нелинейной сигмоидальной функции).

alpha – параметр наклона сигмоидальной функции S(d). Чем больше этот параметр, тем круче функция (угол касательной в точке перегиба функции будет больше).

Обучить нейронную сеть — значит, сообщить ей, чего мы от нее добиваемся. Допустим, мы хотим, чтобы для нашей сети при сигналах на входе 0 и 1  на выходе была 1.

В общем виде алгоритм обучения с учителем будет выглядеть следующим образом:

  1. Инициализировать синаптические веса маленькими случайными значениями.
  2. Выбрать очередную обучающую пару из обучающего множества; подать входной вектор на вход сети.
  3. Вычислить выход сети.
  4. Вычислить разность между выходом сети и требуемым выходом (целевым вектором обучающей пары).
  5. Подкорректировать веса сети для минимизации ошибки.
  6. Повторять шаги с 2 по 5 для каждой пары обучающего множества до тех пор, пока ошибка на всем множестве не достигнет приемлемого уровня.

Конкретный вид математических операций, выполняемых на этапе 5, определяет разновидность алгоритма обучения. Например, для однослойных сетей применяют простейший алгоритм, основанный на т. н. дельта-правиле (см. Обучение персептрона. Дельта-правило), для сетей с любым количеством слоев широко используется алгоритм обратного распространения ошибки.

Дельта-правило — метод обучения перцептрона по принципу градиентного спуска по поверхности ошибки. Его дальнейшее развитие привело к созданию метода обратного распространения ошибки.

Создание нейронной сети в Visual Studio (версия кода 1)

Запускаем редактор  VS и создаем проект (File>New>Project>Python>Python Application)

Вставляем в исходный файл (.py) код:

import numpy as np
# обучающая выборка входных и выходных данных
X = np.array([[1,2],[4,5]])
y = np.array([[3, 6]]).T   # T-транспонирование матрицы
# нормализация данных (чтобы меньше 1)
X = X / 10
y = y / 10
# инициализация случайных весов (от -1 до +1) синапсов
np.random.seed(1)
synapses_hidden = 2 * np.random.random((2,3)) - 1  # 2*3 - hidden слой 
synapses_output = 2 * np.random.random((3,1)) - 1  # 3*1 - output слой 
# обучение сети - цикл из 10000 повторений
for j in range(10000):
    # Входной слой ( 2 входа )
    l0 = X
    # Скрытый слой ( 3 скрытых нейрона )
    l1 = 1 / (1 + np.exp(-(l0.dot(synapses_hidden))))
    # Выходной слой ( 1 выходной нейрон )
    l2 = 1 / (1 + np.exp(-(l1.dot(synapses_output))))
    # вычисляем ошибку (используем дельта-правило)
    l2_delta = (y - l2) * (l2 * (1 - l2))
    # получаем ошибку на скрытом слое (используем дельта-правило)
    l1_delta = l2_delta.dot(synapses_output.T) * (l1 * (1 - l1))
    # корректируем веса от скрытых нейронов к выходу
    synapses_output += l1.T.dot(l2_delta)
    # корректируем веса от входов к скрытым нейронам
    synapses_hidden += l0.T.dot(l1_delta)
# Печать сигналов на выходе после последнего цикла обучения
# Сигналы на выходе умножаем на коэффициент нормализации (10)   
print("Обучение [1,2],[4,5]; Результат:",l2 * 10)
# Тестируем уже обученную сеть
X = np.array([[7,8]])
X = X / 10
l0 = X
l1 = 1 / (1 + np.exp(-(l0.dot(synapses_hidden))))
l2 = 1 / (1 + np.exp(-(l1.dot(synapses_output))))
print("\nТест[7,8]; Результат:",l2 * 10)

Программа решает простенькую задачку «Математическая разминка»:

В этой задаче весь весь датасет был разбит на 2 батча, в каждом из которых было по 3 числа — 2 на входе и одно на выходе. Для реализации задачи используется нейронная сеть с 2-я входами, 3-я нейронами на скрытом слое и одним нейроном на выходе.

Через такую нейронную сеть за одну эпоху пропускается  датасет  за 2 итерации ( см. Эпоха, батч, итерация — в чем различия?). Подробное описание кода, на базе которого написана программа, см. Нейронные сети. Краткое введение  (см. также как работает функция numpy.dot).

Запускаем приложение. Получаем после обучения числа на выходе такие же, как и были заданы (3 для пары  (1,2) и 6 для пары (4,5)).  При тестировании для пары входных чисел (7,8) на выходе получено 7.51…:

Результат тестирования не совсем точный (ожидался результат 9). Это можно объяснить слишком маленькой выборкой для обучения.

Изменим пару входных чисел для тестирования обученной модели на (2,3). При этом получаем более точный результат на выходе 4.09…(ожидался результат 4) по сравнению с тестированием предыдущей пары.

Это можно объяснить тем, что входные числа находятся в промежутке между числами, которые использовались для обучения.

Если отладчик подчеркивает 1-й рядок кода, значит, у Вас еще не инсталлирован пакет (package) numpy. Его можно инсталлировать не выходя из проекта (Шаг 5. Установка пакетов в окружении Python). Для этого открываете контекстное меню в Solution Explore и выбираете в нем «Install Python Package»:

Откроется окно «Python Environments». В поисковом окошке набираете «numpy» и, затем, запускаете «Install numpy».

Если инсталляция  происходит впервые, откроется окно, в котором выбираете права администратора:

Инсталляция длится не более 5 минут.

Задание: Перестройте программу, чтобы она решала следующую задачу:

Решение:

# обучающая выборка входных и выходных данных
X = np.array([[1,2],[3,4],[5,6]])
y = np.array([[3,4],[5,6],[7,8]]) 
# нормализация данных (чтобы меньше 1)
X = X / 10
y = y / 10 
...
synapses_output = 2 * np.random.random((3,2)) - 1 # 3*2 - output слой
# Тестируем уже обученную сеть
X = np.array([[4,5]])
...
print("Обучение [1,2],[3,4],[5,6]; Результат:",l2 * 10)
...
print("\nТест[4,5]; Результат:",l2 * 10)

Простая нейронная сеть (версия кода 2)

Постановка задачи. Обучить однослойную нейронную сеть с 3-х входами и с одним выходом на основе  тренировочной выборки из 4-х примеров. После обучения системы определить результат на выходе для тестового примера.

Подробное описание приложения см. в первоисточнике Простая нейронная сеть в 9 строчек кода на Python

Вставляем в исходный файл следующий код:

from numpy import exp, array, random, dot
training_set_inputs = array([[0, 0, 1], [1, 1, 1], [1, 0, 1], [0, 1, 1]])
training_set_outputs = array([[0, 1, 1, 0]]).T
random.seed(1)
synaptic_weights = 2 * random.random((3, 1)) - 1
for iteration in range(10000):
    output = 1 / (1 + exp(-(dot(training_set_inputs, synaptic_weights))))
    synaptic_weights += dot(training_set_inputs.T, (training_set_outputs - output) * output * (1 - output))
print (1 / (1 + exp(-(dot(array([1, 0, 0]), synaptic_weights)))))

Запускаем приложение, получаем следующий результат:

Усложняем приложение. Вставляем в исходный файл следующий код:

from numpy import exp, array, random, dot

class NeuralNetwork():
    def __init__(self):
        random.seed(1)
        self.synaptic_weights = 2 * random.random((3, 1)) - 1
    def __sigmoid(self, x):
        return 1 / (1 + exp(-x))

    def __sigmoid_derivative(self, x):
        return x * (1 - x)

    def train(self, training_set_inputs, training_set_outputs, number_of_training_iterations):
        for iteration in range(number_of_training_iterations):
            output = self.think(training_set_inputs)
            error = training_set_outputs - output          
            adjustment = dot(training_set_inputs.T, error * self.__sigmoid_derivative(output))
            self.synaptic_weights += adjustment

    def think(self, inputs):
        return self.__sigmoid(dot(inputs, self.synaptic_weights))

if __name__ == "__main__":

    #Intialise a single neuron neural network.
    neural_network = NeuralNetwork()

    print ("Random starting synaptic weights: ")
    print (neural_network.synaptic_weights)

    training_set_inputs = array([[0, 0, 1], [1, 1, 1], [1, 0, 1], [0, 1, 1]])
    training_set_outputs = array([[0, 1, 1, 0]]).T

    # Train the neural network using a training set. Do it 10,000 times.
    neural_network.train(training_set_inputs, training_set_outputs, 10000)

    print ("New synaptic weights after training: ")
    print (neural_network.synaptic_weights)

    # Test the neural network with a new situation.
    print ("Considering new situation [1, 0, 0] -> ?: ")
    print (neural_network.think(array([1, 0, 0])))

Запускаем приложение, получаем следующий результат:

Простая нейронная сеть (версия кода 3)

Ниже рассмотрены коды приложений для 2-х  и 3-х уровневых нейронных сетей. Подробное описание обоих приложений и алгоритма к ним  см. в первоисточнике Нейронная сеть в 11 строках Python (часть 1).

Код к 2-х уровневой нейронной сети:

import numpy as np

# sigmoid function
def nonlin(x,deriv=False):
    if(deriv==True):
        return x*(1-x)
    return 1/(1+np.exp(-x))
 
# input dataset
X = np.array([ [0,0,1],
               [0,1,1],
               [1,0,1],
               [1,1,1] ]) 
# output dataset 
y = np.array([[0,0,1,1]]).T

np.random.seed(1)
syn0 = 2*np.random.random((3,1)) - 1

for i in range(10000):
    l0 = X
    l1 = nonlin(np.dot(l0,syn0))
    l1_error = y - l1
    l1_delta = l1_error * nonlin(l1,True)
    # update weights
    syn0 += np.dot(l0.T,l1_delta)

print ("Output After Training:")
print (l1)

Запускаем приложение, получаем следующий результат:

Код к 3-х уровневой нейронной сети:

import numpy as np

def nonlin(x,deriv=False):
    if(deriv==True):
        return x*(1-x)
    return 1/(1+np.exp(-x)) 

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

np.random.seed(1)
syn0 = 2*np.random.random((3,4)) - 1
syn1 = 2*np.random.random((4,1)) - 1

for j in range(60000):
    l0 = X
    l1 = nonlin(np.dot(l0,syn0))
    l2 = nonlin(np.dot(l1,syn1))
    l2_error = y - l2 
    if (j% 10000) == 0:
        print ("Error:" + str(np.mean(np.abs(l2_error))))
    l2_delta = l2_error*nonlin(l2,deriv=True)   
    l1_error = l2_delta.dot(syn1.T)
    l1_delta = l1_error * nonlin(l1,deriv=True)

    syn1 += l1.T.dot(l2_delta)
    syn0 += l0.T.dot(l1_delta)

Запускаем приложение, получаем следующий результат:

Примеры хорошо структурированных программ

Пример 1
Пример 2
Пример 3

 

Пример 1 (см. первоисточник: IMPLEMENTING A FLEXIBLE NEURAL NETWORK WITH BACKPROPAGATION FROM SCRATCH). Реализация программы, которая позволяет добавлять / удалять слои без изменения кода

Код модифицирован под задание в подразделе  Создание нейронной сети в Visual Studio (версия кода 1) 

import numpy as np
import matplotlib.pyplot as plt
np.random.seed(100)


class Layer:
    """
    Represents a layer (hidden or output) in our neural network.
    """

    def __init__(self, n_input, n_neurons, activation=None, weights=None, bias=None):
        """
        :param int n_input: The input size (coming from the input layer or a previous hidden layer)
        :param int n_neurons: The number of neurons in this layer.
        :param str activation: The activation function to use (if any).
        :param weights: The layer's weights.
        :param bias: The layer's bias.
        """

        self.weights = weights if weights is not None else np.random.rand(n_input, n_neurons)
        self.activation = activation
        self.bias = bias if bias is not None else np.random.rand(n_neurons)
        self.last_activation = None
        self.error = None
        self.delta = None

    def activate(self, x):
        """
        Calculates the dot product of this layer.
        :param x: The input.
        :return: The result.
        """

        r = np.dot(x, self.weights) + self.bias
        self.last_activation = self._apply_activation(r)
        return self.last_activation

    def _apply_activation(self, r):
        """
        Applies the chosen activation function (if any).
        :param r: The normal value.
        :return: The "activated" value.
        """

        # In case no activation function was chosen
        if self.activation is None:
            return r

        # tanh
        if self.activation == 'tanh':
            return np.tanh(r)

        # sigmoid
        if self.activation == 'sigmoid':
            return 1 / (1 + np.exp(-r))

        return r

    def apply_activation_derivative(self, r):
       """
       Applies the derivative of the activation function (if any).
       :param r: The normal value.
       :return: The "derived" value.
       """

       # We use 'r' directly here because its already activated, the only values that
       # are used in this function are the last activations that were saved.

       if self.activation is None:
           return r

       if self.activation == 'tanh':
           return 1 - r ** 2

       if self.activation == 'sigmoid':
           return r * (1 - r)

       return r


class NeuralNetwork:
    """
    Represents a neural network.
    """

    def __init__(self):
        self._layers = []

    def add_layer(self, layer):
        """
        Adds a layer to the neural network.
        :param Layer layer: The layer to add.
        """
        self._layers.append(layer)

    def feed_forward(self, X):
        """
        Feed forward the input through the layers.
        :param X: The input values.
        :return: The result.
        """

        for layer in self._layers:
            X = layer.activate(X)

        return X

    def predict(self, X):
        """
        Predicts a class (or classes).
        :param X: The input values.
        :return: The predictions.
        """

        ff = self.feed_forward(X)

        # One row
        if ff.ndim == 1:
            return np.argmax(ff)

        # Multiple rows
        return np.argmax(ff, axis=1)

    def backpropagation(self, X, y, learning_rate):
        """
        Performs the backward propagation algorithm and updates the layers weights.
        :param X: The input values.
        :param y: The target values.
        :param float learning_rate: The learning rate (between 0 and 1).
        """

        # Feed forward for the output
        output = self.feed_forward(X)

        # Loop over the layers backward
        for i in reversed(range(len(self._layers))):
            layer = self._layers[i]

            # If this is the output layer
            if layer == self._layers[-1]:
                layer.error = y - output
                # The output = layer.last_activation in this case
                layer.delta = layer.error * layer.apply_activation_derivative(output)
            else:
                next_layer = self._layers[i + 1]
                layer.error = np.dot(next_layer.weights, next_layer.delta)
                layer.delta = layer.error * layer.apply_activation_derivative(layer.last_activation)

        # Update the weights
        for i in range(len(self._layers)):
            layer = self._layers[i]
            # The input is either the previous layers output or X itself (for the first hidden layer)
            input_to_use = np.atleast_2d(X if i == 0 else self._layers[i - 1].last_activation)
            layer.weights += layer.delta * input_to_use.T * learning_rate

    def train(self, X, y, learning_rate, max_epochs):
        """
        Trains the neural network using backpropagation.
        :param X: The input values.
        :param y: The target values.
        :param float learning_rate: The learning rate (between 0 and 1).
        :param int max_epochs: The maximum number of epochs (cycles).
        :return: The list of calculated MSE errors.
        """

        mses = []

        for i in range(max_epochs):
            for j in range(len(X)):
                self.backpropagation(X[j], y[j], learning_rate)
                y1=nn.feed_forward(X)
        """
            if i % 10 == 0:
                #mse = np.mean(np.square(y - nn.feed_forward(X)))
                mse = np.mean(np.square((y - y1)**2))
                mses.append(mse)
        return mses
        """
        return y1

nn = NeuralNetwork()
nn.add_layer(Layer(3, 3, 'tanh'))
nn.add_layer(Layer(3, 3, 'sigmoid'))
nn.add_layer(Layer(3, 3, 'sigmoid'))
nn.add_layer(Layer(3, 3, 'sigmoid'))

# Define dataset
X = np.array([[1,2,3]])
y = np.array([[3,4,5]]) 
# нормализация данных (чтобы меньше 1)
X = X / 10
y = y / 10

# Train the neural network and outputting the result
print('y1=',10*nn.train(X, y, 0.3, 300))

"""
errors = nn.train(X, y, 0.3, 300)
# Plot changes in mse
plt.plot(errors)
plt.title('Changes in MSE')
plt.xlabel('Epoch (every 10th)')
plt.ylabel('MSE')
plt.show()
"""

Пример 2 (см. первоисточник: Build a Neural Network)

import numpy as np
import matplotlib.pyplot as plt

x_all = np.array(([2, 9], [1, 5], [3, 6], [5, 10]), dtype=float) # input data
y = np.array(([92], [86], [89]), dtype=float) # output

# scale units
x_all = x_all/np.amax(x_all, axis=0) # scaling input data
y = y/100 # scaling output data (max test score is 100)

# split data
X = np.split(x_all, [3])[0] # training data
x_predicted = np.split(x_all, [3])[1] # testing data

class neural_network(object):
  def __init__(self):
  #Network parameters (number of neurons in layers)
    self.inputSize = 2
    self.outputSize = 1
    self.hiddenSize = 3

  #Initializing the network weights
    self.W1 = np.random.randn(self.inputSize, self.hiddenSize) # (3x2) weight matrix from input to hidden layer
    self.W2 = np.random.randn(self.hiddenSize, self.outputSize) # (3x1) weight matrix from hidden to output layer

  def forward(self, X):
    #forward propagation through our network
    self.z = np.dot(X, self.W1) # dot product of X (input) and first set of 3x2 weights
    self.z2 = self.sigmoid(self.z) # activation function
    self.z3 = np.dot(self.z2, self.W2) # dot product of hidden layer (z2) and second set of 3x1 weights
    o = self.sigmoid(self.z3) # final activation function
    return o
  
  # Activation function (sigmoid) and its derivative
  def sigmoid(self, s):
    # activation function
    return 1/(1+np.exp(-s))

  def sigmoidPrime(self, s):
    #derivative of sigmoid
    return s * (1 - s)

  def backward(self, X, y, o):
    # backward propagate through the network
    self.o_error = y - o # error in output
    self.o_delta = self.o_error*self.sigmoidPrime(o) # applying derivative of sigmoid to error

    self.z2_error = self.o_delta.dot(self.W2.T) # z2 error: how much our hidden layer weights contributed to output error
    self.z2_delta = self.z2_error*self.sigmoidPrime(self.z2) # applying derivative of sigmoid to z2 error

    self.W1 += X.T.dot(self.z2_delta) # adjusting first set (input --> hidden) weights
    self.W2 += self.z2.T.dot(self.o_delta) # adjusting second set (hidden --> output) weights

  def train(self, X, y):
    o = self.forward(X)
    self.backward(X, y, o)

  def saveWeights(self):
    np.savetxt("w1.txt", self.W1, fmt="%s")
    np.savetxt("w2.txt", self.W2, fmt="%s")

  def predict(self):
    print("Predicted data based on trained weights: ")
    print("Input (scaled): \n" + str(x_predicted))
    print("Output: \n" + str(self.forward(x_predicted)))

nn = neural_network()
count = [] # list to store iteration count
loss = [] # list to store loss values
for i in range(50): # trains the nn 50 times
   #print("# " + str(i) + "\n")
   #print("Input (scaled): \n" + str(X))
   #print("Actual Output: \n" + str(y))
   loss1 = str(np.mean(np.square(y - nn.forward(X))))
   #print("Loss: \n" + loss1 ) # mean squared error
   #print("\n")
   count.append(i)
   loss.append(np.round(float(loss1), 6))
   plt.cla()
   plt.title("Loss over Iterations")
   plt.xlabel("Iterations")
   plt.ylabel("Loss")
   plt.plot(count, loss)
   plt.pause(.01)
   nn.train(X, y)

nn.saveWeights()
nn.predict()

Пример 3 (см. первоисточник: Writing Python Code for Neural Networks from Scratch)

import numpy as np 
# Activation Functions and their derivatives
def tanh(x): 
     return np.tanh(x) 
def d_tanh(x): 
       return 1 - np.square(np.tanh(x)) 
def sigmoid(x): 
      return 1/(1 + np.exp(-x)) 
def d_sigmoid(x): 
      return (1 - sigmoid(x)) * sigmoid(x)

# Loss Functions 
def logloss(y, a): 
      return -(y*np.log(a) + (1-y)*np.log(1-a)) 
def d_logloss(y, a): 
      return (a - y)/(a*(1 - a))

# The layer class
class Layer: 
    activationFns = { 
                 'tanh': (tanh, d_tanh), 
                 'sigmoid': (sigmoid, d_sigmoid) 
          } 
    learning_rate = 0.1 
 
    def __init__(self, inputs, neurons, activation): 
              self.W = np.random.randn(neurons, inputs) 
              self.b = np.zeros((neurons, 1)) 
              self.act, self.d_act=self.activationFns.get(activation)
    def feedforward(self, A1): 
             self.A1 = A1 
             self.Z = np.dot(self.W, self.A1) + self.b 
             self.A = self.act(self.Z) 
             return self.A
    def backprop(self, dA): 
            dZ = np.multiply(self.d_act(self.Z), dA) 
            dW = 1/dZ.shape[1] * np.dot(dZ, self.A1.T) 
            db = 1/dZ.shape[1] * np.sum(dZ, axis=1, keepdims=True) 
            dA1 = np.dot(self.W.T, dZ) 
            self.W = self.W - self.learning_rate * dW 
            self.b = self.b - self.learning_rate * db 
            return dA1

x = np.array([[0, 0, 1, 1], [0, 1, 0, 1]]) # dim x m
y = np.array([[0, 1, 1, 0]]) # 1 x m 
m = 4
epochs = 1500 
lyr = [Layer(2, 3, 'tanh'), Layer(3, 1, 'sigmoid')]
costs = [] 
# to plot graph 
for epoch in range(epochs): 
        # Feedforward 
        A = x 
        for layer in lyr: 
              A = layer.feedforward(A) 
       # Calulate cost to plot graph 
        cost = 1/m * np.sum(logloss(y, A)) 
        costs.append(cost) 
        dA = d_logloss(y, A) 
        for layer in reversed(lyr): 
             dA = layer.backprop(dA) 
# Making predictions 
A = x
for layer in lyr: 
       A = layer.feedforward(A)
print(A)

Полезные ссылки:

 

 

Автор: Николай Свирневский