СОЗДАНИЕ СЕТИ ПРЯМОГО РАСПРОСТРАНЕНИЯ - Студенческий научный форум

XI Международная студенческая научная конференция Студенческий научный форум - 2019

СОЗДАНИЕ СЕТИ ПРЯМОГО РАСПРОСТРАНЕНИЯ

 Комментарии
Текст работы размещён без изображений и формул.
Полная версия работы доступна во вкладке "Файлы работы" в формате PDF

В нашей статье мы создадим нейронную сеть прямого распространения на языке программирования Python. В качестве библиотеки работы с векторами и матрицами будет использоваться библиотека Numpy. Для работы с изображениями потребуется библиотека PLT. Данная работа выполнена в рамках курсового проекта по дисциплине «Machine Learning. Обучающиеся технические системы [1].

Введение

Нейронная сеть — математическая модель, а также её программное или аппаратное воплощение, построенная по принципу организации и функционирования биологических нейронных сетей — сетей нервных клеток живого организма [3].

Искусственный нейрон — это всего лишь взвешенная сумма значений входного вектора элементов, которая передаётся на нелинейную функцию активации f: z = f(y), где y = w0·x0 + w1·x1 + ... + wm - 1·xm - 1. Здесь w0, ..., wm - 1 — коэффициенты, веса каждого элемента вектора, x0, ..., xm - 1 — значения входного вектора X, y — взвешенная сумма элементов X, а z— результат применения функции активации.

Функция активации — это функция, которая добавляет в нейронную сеть нелинейность, благодаря чему нейроны могут довольно точно интерполировать произвольную функцию. В настоящее время существует довольно много различных функций активации, но наиболее распространёнными функциями являются следующие:

Сигмоида: f(x) = 1 / (1 + e-x)

Гиперболический тангенс: f(x) = tanh(x)

ReLU: f(x) = max(x, 0)

Написание кода нейронной сети

Для реализации сети прямого назначения нам потребуется:

Вектор (входные, выходные) – из библиотеки Numpy;

Матрица (каждый слой содержит матрицу весовых коэффициентов) – из библиотеки Numpy;

Нейросеть.

Напишем метод, который будет получать выход нейронной сети по заданному входному вектору input:

# инициализация матрицы случайными значениями

def InitWeight(n, m):

w = np.zeros((n, m))

# заполняем матрицу случайными числами из интервала (-0.5, 0.5)

for i in range(n):

for j in range(m):

w[i][j] = random.uniform(-0.5, 0.5)

return w

# инициализация списка матриц весов случайными значениями

def InitWeights(struct):

W = []

# для каждого из весов задаём матрицу

for k in range(1, len(struct)):

w = InitWeight(struct[k], struct[k - 1])

W.append(w)

return W

def Sigmoid(x):

return 1 / (1 + np.exp(-x))

# прямое распространение сигнала по сети

def Forward (weights, input):

z = input;

# проходимся по всем слоям

for k in range(len(weights))

# создаём вектора для текущего слоя

y = weights[k].dot(z) # производим умножения матриц весов на входное значение

z = Sigmoid(y) # применяем сигмоидальную активационную функцию

return z # возвращаем активированный выход последнего слоя

Рис. 1. Код входных значений

Создали нейронную сеть. Теперь необходимо её обучить. Обучать нейросеть мы будем с помощью алгоритма обратного распространения ошибки, который работает следующим образом:

Подать на вход сети обучающий пример (один входной вектор)

Распространить сигнал по сети вперёд (получить выход сети)

Вычислить ошибку (разница получившегося и ожидаемого векторов)

Распространить ошибку на предыдущие слои

Обновить весовые коэффициенты для уменьшения ошибки

Обучаем сеть

Для обратного распространения ошибки нам потребуется знать значения входов, выходов и значения производных функции активации сети на каждом из слоёв, поэтому будем хранить список из структур 3 векторов: x — вход слоя, z — выход слоя, df — производная функции активации. Также для каждого слоя потребуются векторы дельт, поэтому добавим ещё и их. С учётом вышесказанного прямое распространение сигнала по сети будет выглядеть следующим образом:

# инициализация матрицы случайными значениями

def InitWeight(n, m):

w = np.zeros((n, m))

# заполняем матрицу случайными числами из интервала (-0.5, 0.5)

for i in range(n):

for j in range(m):

w[i][j] = random.uniform(-0.5, 0.5)

return w

# инициализация списка матриц весов случайными значениями

def InitWeights(struct):

W = []

# для каждого из весов задаём матрицу

for k in range(1, len(struct)):

w = InitWeight(struct[k], struct[k - 1])

W.append(w)

return W

def Sigmoid(x):

return 1 / (1 + np.exp(-x))

def SigmoidDerivative(y):

return y * (1 - y)

# прямое распространение сигнала по сети

def Forward (weights, input):

X = [] # входные значения слоёв

Z = [] # активированные значения слоёв

D = [] # значения производной функции активации

# проходимся по всем слоям

for k in range(len(weights)):

if k == 0: # если в первом слое, то входом является входной вектор

X.append(np.array(input))

else: # иначе входом является выход предыдущего слоя

X.append(Z[k - 1])

# создаём вектора для текущего слоя

y = weights[k].dot(X[k]) # производим умножения матриц весов на входное значение

z = Sigmoid(y) # применяем сигмоидальную активационную функцию

df = SigmoidDerivative(z) # вычисляем производную функции активации через уже известное значение выхода

Z.append(z)

D.append(df);

return X, Z, D # возвращаем список входов, выходов, активированных выходов и производных

Рис. 2. Код прямого распространения сигнала

Обратное распространение ошибки

В качестве функции оценки сети E(W) возьмём минимальное квадратичное отклонение: E = 0.5 · Σ(y1i - y2i)2. Чтобы найти значение ошибки E, нам нужно найти сумму квадратов разности значений вектора, который выдала сеть в качестве ответа, и вектора, который мы ожидаем увидеть при обучении. Также нам потребуется найти дельту для каждого слоя, причём для последнего слоя она будет равна вектору разности полученного и ожидаемого векторов, умноженному (покомпонентно) на вектор значений производных последнего слоя: δlast = (zlast - d)·f'last, где zlast — выход последнего слоя сети, d — ожидаемый вектор сети, f'last — вектор значений производной функции активации последнего слоя.

Теперь, зная дельту последнего слоя, мы можем найти дельты всех предыдущих слоёв. Для этого нужно умножить транспонированную матрицы текущего слоя на дельту текущего слоя и затем умножить полученный вектор на вектор производных функции активации предыдущего слоя: δk-1 = WT· δ· f'k. Добавим это в код:

# обратное распространение ошибки

def Backward(weights, Z, df, output):

deltas = []

layersN = len(weights)

last = layersN - 1

error = 0

for k in range(layersN):

deltas.append([])

# расчитываем ошибку на выходном слое

e = Z[last] - output

deltas[last] = np.multiply(e, df[last])

error += np.sum(e**2)

# распространяем ошибку выше по слоям

for k in range(last, 0, -1):

deltas[k - 1] = np.multiply(weights[k].T.dot(deltas[k]), df[k - 1])

return deltas, error # возвращаем дельты и величину ошибки

Рис. 3. Код обратного распространения ошибки

Изменение весов

Для того чтобы уменьшить ошибку сети нужно изменить весовые коэффициенты каждого слоя. Для этого используется метод градиентного спуска. Градиент по весам равен перемножению входного вектора и вектора дельт (не покомпонентно). Поэтому, чтобы обновить весовые коэффициенты и уменьшить тем самым ошибку сети нужно вычесть из матрицы весов результат перемножения дельт и входных векторов, умноженный на скорость обучения. Записываем в виде: Wt+1 = Wt – η · δ · X, где Wt+1 — новая матрица весов, Wt — текущая матрица весов, X — входное значение слоя, δ — дельта этого слоя.

# обновлениевесов

def UpdateWeights(weights, X, deltas, alpha):

for k in range(len(weights)):

weights[k] -= alpha * np.outer(deltas[k], X[k])

Рис. 4. Код изменения весов

Обучение сети

Теперь, имея методы прямого распространения сигнала, обратного распространения ошибки и изменения весовых коэффициентов, нам остаётся лишь соединить всё вместе в один метод обучения.

# обучениесети

def Train(weights, inputData, outputData, alpha, eps, epochs):

print('\nStart train process:')

epoch = 1

while True:

error = 0

for i in range(len(inputData)):

X, Z, D = Forward(weights, inputData[i]) # прямое распространение сигнала

deltas, err = Backward(weights, Z, D, outputData[i]) # обратное распространение ошибки

UpdateWeights(weights, X, deltas, alpha) # обновление весовых коэффициентов

error += err / 2

if epoch % 1 == 0:

print("epoch:", epoch, "alpha:", alpha, "error:", error)

# выходим, если ошибка стала меньше точности

if error < eps:

break;

# выходим, если достигли большого числа эпох

if epoch >= epochs:

print('Warning! Max epochs reached')

break

epoch += 1# увеличиваемчислоэпохна 1

alpha *= 0.999

Рис. 5. Код обучения сети

Тренируем сеть

Создаем массив векторов X и Y с обучающими данными и саму нейросеть:

# массив входных обучающих векторов

X = [

[ 0, 0 ],

[ 0, 1 ],

[ 1, 0 ],

[ 1, 1 ],

];

# массив выходных обучающих векторов

Y = {

[ 0 ], # 0 ^ 0 = 0

[ 1 ], # 0 ^ 1 = 1

[ 1 ], # 1 ^ 0 = 1

[ 0 ] # 1 ^ 1 = 0

}

struct = [ 2, 3, 1 ] # структура сети: 2 входных нейрона, 3 нейрона в скрытом слое, 1 выходной нейрон

W = InitWeights(struct) # матрицы весовых коэффициентов

Рис. 6. Код функции XOR

Запускаем обучение со следующими параметрами: скорость обучения - 0.5, число эпох - 100000, величина ошибки - 1e-7:

Train(W, x, y, 0.5, 1e-7, 100000)

После обучения посмотрим на результаты, выполнив прямой проход для всех элементов:

for i in range(len(X)):

output = Forward(X[i]);

print"X: ", X[i][0], X[i][1], "Y: ", Y[i][0], "output: ", output[0]);

}

В результате вывод может быть таким:

X: 0 0, Y: 0, output: 0,00503439463431083

X: 0 1, Y: 1, output: 0,996036009216668

X: 1 0, Y: 1, output: 0,996036033202241

X: 1 1, Y: 0, output: 0,00550270947767007

Далее будем учить сеть распознавать цифры на картинках с рукописными цифрами из выборки MNIST.

Получаем тренировочную выборку

Файл с тренировочной выборкой train.csv находится по ссылке: http://www.pjreddie.com/media/files/mnist_train.csv [2]

# получение обучающей выборки

def GetTrainData(path, maxLen):

f = open(path, 'r')

lines = f.readlines() # считываемстрокифайла

trainInput = []

trainOutput = []

# проходимся по строкам файла, но не более, чем maxLen

for i in range(1, min(maxLen + 1, len(lines))):

splited = lines[i].split(',')

x = []

y = []

# сохраняем нормализованные значения в вектор

for j in range(1, len(splited)):

v = 0.01 + float(splited[j]) / 255 * 0.99

x.append(v)

for j in range(10):

y.append(0);

y[int(splited[0])] = 1 # задаём выходной вектор

# добавляем в обучающую выборку цифру

trainInput.append(x)

trainOutput.append(y)

print('\nReaded train data:', len(trainInput), 'elements'); # выводиминформациюосчитанныхданных

return trainInput, trainOutput

Рис. 7. Код с обучающей выборкой

Получив обучающее множество, состоящее из 42000 примеров, запустим обучений нейронной сети на 20 эпох обучения со скоростью 0.65.

struct = [ 784, 100, 10 ] # структура сети: 784 входных нейрона, 100 в скрытом слое, 10 выходных

W = InitWeights(struct) # матрицы весовых коэффициентов

learningRate = 0.65 # скорость обучения

maxEpoch = 20 # максимальное число эпох

eps = 1e-7 # точность

trainPath = 'train.csv' # путь к файлу с обучающей выборкой

testPath = 'test.csv' # путь к файлу с тестовой выборкой

answersPath = 'answers.csv' # путь к файлу с ответами

print('Network structure:', struct)

print('Learning rate (alpha):', learningRate)

print('Max epochs: ', maxEpoch)

print('Accuracy (eps):', eps)

x, y = GetTrainData(trainPath, 42000) # считываем обучающую выборку

Train(W, x, y, learningRate, eps, maxEpoch)

Рис. 8. Код запуска обучения

Тестируем сеть

Файл с тестовой выборкой находится по ссылке:

http://www.pjreddie.com/media/files/mnist_test.csv [2]

# проверка точности на обучающей выборке

def Test(weights, testPath, answersPath):

print('\nStart test process:');

test = open(testPath, 'r')

answers = open(answersPath, 'r')

testLine = test.readlines()

answersLine = answers.readlines()

correct = 0

total = 0

# проходимся по всем тестовым строкам кроме первой

for i in range(1, len(testLine)):

splited = testLine[i].split(',') # разбиваем строку по запятым

x = []

# сохраняем нормализованные значения в вектор

for j in range(len(splited)):

v = 0.01 + float(splited[j]) / 255 * 0.99

x.append(v)

d = np.argmax(GetOutput(weights, x)); # получаемцифру, предсказаннуюсетью

y = int(answersLine[i].split(',')[1]) # получаем цифру из ответов

if y == d: # если цифры совпали, увеличиваем на 1 число корректно распознанных

correct += 1

total += 1 # увеличиваем число обработанных строк

if total % 1000 == 0:

print('accuracy:', correct, " / ", total, ": ", (100.0 * correct / total), '%') # выводиминформациюпопредсказаниям

print('accuracy:', correct, " / ", total, ": ", (100.0 * correct / total), '%') # выводим информацию по предсказаниям

Рис. 8. Код с тестовой выборкой

В результате запуска теста мы получили точность около 97%. Таким образом мы научились распознавать изображения.

# предсказание по картинке

def Predict(weights, path):

img = plt.imread(path)

x = 0.01 + img.flatten() * 256 / 255 * 0.99

return GetOutput(weights, x)

while True:

path = input('Enter path to image:');

if path == '':

break;

d = Predict(W, path)

print('For image \'', path, '\' probably it is', np.argmax(d))

Рис. 8. Код распознавания

Выводы

Нейронная сеть успешно обучилась функции исключающего ИЛИ (XOR), а также смогла распознавать изображения рукописных цифр с точностью более 97%.

СПИСОК ИСТОЧНИКОВ И ЛИТЕРАТУРЫ

Воронова Л.И., Воронов В.И.. Machine Learning: регрессионные методы интеллектуального анализа данных: учебное пособие / МТУСИ.– М., 2017.- 92с.

Рашид Т. Создаем нейронную сеть.: Пер. с англ. – СПб.: ООО «Диалектика», 2018 – 272 с.: ил. – Парал. тит. англ.

Википедия [Электронный ресурс]. Режим доступа: https://ru.wikipedia.org

Просмотров работы: 21