К книге
Рассказы о математике с примерами на языках Python и CСтраница 8
73%
Страница 8
8

import math, random

class MLP(object):

def __init__(self, n_in, n_hidden, n_out):

self.n_in = n_in

self.n_hidden = n_hidden

self.n_out = n_out

# Входные данные: массив [n_in + 1] инициализирован значениями 1.0

self.inputLayer = [1.0] * (n_in + 1)

# Веса входного-скрытого слоя: матрица [n_in+1]x[n_hidden] заполнена 0..1

self.wIH = [[random.random() for x in range(n_in+1)] for y in range(n_hidden)]

# Скрытые нейроны: [n_hidden + 1]

self.hiddenLayer = [1.0] * (n_hidden + 1)

# Веса нейронов скрытого-выходного слоя: [n_hidden+1]x[n_out], 0..1

self.wHO = [[random.random() for x in range(n_hidden+1)] for y in range(n_out)]

# Выходной слой: массив

self.outputLayer = [1.0] * n_out

def printValues(self):

print "Network: input/hidden/output: {}/{}/{}".format(self.n_in, self.n_hidden, self.n_out)

print "Вход", self.inputLayer

print "wIH", self.wIH

print "Скрытый слой", self.hiddenLayer

print "wHO", self.wHO

print "Выход", self.outputLayer

def printOutput(self):

print "Вход", self.inputLayer

print "Выход", self.outputLayer

def sigmoid(self, x):

# Функция активации

return 1.0/(1.0 + math.exp(-x))

def derivative(self, x):

# Производная функции активации

return x*(1 - x)

def forwardPass(self, x):

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

# Input

for p in range(len(x)):

self.inputLayer[p] = x[p]

# Input-Hidden

for h in range(self.n_hidden):

sum = 0.0

for i in range(len(self.inputLayer)):

sum += self.inputLayer[i]*self.wIH[h][i]

self.hiddenLayer[h] = self.sigmoid(sum)

# Hidden-Output

for o in range(self.n_out):

sum = 0.0

for h in range(len(self.hiddenLayer)):

sum += self.hiddenLayer[h]*self.wHO[o][h]

self.outputLayer[o] = self.sigmoid(sum)

def backPass(self, input, target):

# Обратное распространение

# Формулы основаны на статье

# https://mattmazur.com/2015/03/17/a-step-by-step-backpropagation-example/

learn_rate = 0.5

# Output->hidden layer adjust weights

for o in range(self.n_out):

for h in range(self.n_hidden):

deout = -(target[o] - self.outputLayer[o])

der = self.derivative(self.outputLayer[o])

hid = self.hiddenLayer[h]

der = deout*der*hid

prev = self.wHO[o][h]

self.wHO[o][h] -= learn_rate*der

# Hidden->input layer adjust weights

for h in range(self.n_hidden):

for i in range(self.n_in):

derSum = 0.0

for o in range(self.n_out):

deout = -(target[o] - self.outputLayer[o])

derOut = self.derivative(self.outputLayer[o])

der = deout*derOut*self.wHO[o][h]

derSum += der

derH = self.derivative(self.hiddenLayer[h])

derI = self.inputLayer[i]

der = derSum*derH*derI

prev = self.wIH[h][i]

self.wIH[h][i] -= learn_rate*der

def calcError(self, x, target):

# Вычисление ошибки: 0.5*Sum(dV^2)

err = 0.0

for p in range(self.n_out):

err += 0.5*((self.outputLayer[p] - target[p])**2)

return err

Как можно видеть, создан класс MLP, принимающий на входе 3 параметра: число входных нейронов, число скрытых нейронов, число выходных нейронов.

Рассмотрим пример использования этой сети для логической функции XOR. Как известно, она имеет следующую таблицу истинности:

Таким образом, в нашей сети будет 2 входных параметра (их еще называют “характеристиками”) и 1 выходной. Количество скрытых нейронов пусть будет 2.

Код на языке Python выглядит следующим образом.

mlp = MLP(n_in=2, n_hidden=2, n_out=1)

print("Тренировка сети")

inputs = [ [0.0, 0.0], [0.0, 1.0], [1.0, 0.0], [1.0, 1.0] ]

targets = [ [0.0], [1.0], [1.0], [0.0] ]

N = 500000

for p in range(N):

rand_index = random.randrange(0, len(inputs))

mlp.forwardPass(inputs[rand_index])

mlp.backPass(inputs[rand_index], targets[rand_index])

err = mlp.calcError(inputs[rand_index], targets[rand_index])

if p % 10000 == 0:

print("Step", p, "error", err)

print("")

print("Done")

mlp.printValues()

print("")

print("Результаты")

mlp.forwardPass([ 0.0, 0.0 ])

mlp.printOutput()

mlp.forwardPass([ 0.0, 1.0 ])

mlp.printOutput()

mlp.forwardPass([ 1.0, 0.0 ])

mlp.printOutput()

mlp.forwardPass([ 1.0, 1.0 ])

mlp.printOutput()

Как можно видеть, мы создаем сеть с заданными параметрами, затем выполняем большое количество раз функции прямого и обратного распространения. При каждом вызове функции backPass коэффициенты чуть корректируются в сторону уменьшения ошибки.

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

Wih = [[10.276084, 9.57721341, 0.11424834], [0.86694222, 0.86379, 0.728575]]

Who = [53.976871, -61.523507, 0.13521682]

Напомним, что весовые коэффициенты фактически соответствуют нейронным связям, возникающим в “настоящих” биологических сетях. Тестирование показывает, что обученная сеть действительно воспроизводит функцию XOR практически достоверно, лишь с небольшой погрешностью:

Input [0.0, 0.0], output [0.0000266]

Input [0.0, 1.0], output [0.95193190]

Input [1.0, 0.0], output [0.95074983]

Input [1.0, 1.0], output [0.07137252]

Как показывает тестирование, если увеличить число скрытых нейронов до 3х, обучение происходит гораздо быстрее, и не требует столь большого числа итераций. Уже 50000 итераций достаточно для приемлемой точности, лог программы выглядит примерно так:

Network: input/hidden/output: 2/3/1

Train phase:

Step 0 error 0.418338057819

Step 10000 error 0.00320960246906

Step 20000 error 0.00115714673178

Step 30000 error 0.00176164027913

Step 40000 error 4.34585435915e-05

wIH [[6.243515, -4.35533, 0.1199493], [5.040033, 5.132244, 0.35661004], [-4.4208235, 6.207919, 0.2522050]]

wHO [-11.2227881, 14.0531446, -11.2731065, 0.9389702]

Test phase:

Input [0.0, 0.0], output [0.0436959]

Input [0.0, 1.0], output [0.9712140]

Input [1.0, 0.0], output [0.9721711]

Input [1.0, 1.0], output [0.0075550]

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

Разумеется, описанная выше сеть является очень простой, в качестве более сложного примера можно привести архитектуру так называемой “сверточной сети” (convolutional neural network, CNN). Такая сеть “разбивает” входные данные на фрагменты, каждый из которых в свою очередь выделяет на изображении следующими подсетями какие-то ключевые детали. Подобные сети используются например, в задачах распознавания образов, в частности, рукописного текста.

Фото с сайта https://www.kernix.com

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

И еще один философский вопрос, который может возникнуть при рассмотрении нейронных сетей - возможен ли искусственный интеллект, сможет ли компьютер мыслить. Ответ не так прост как кажется. С физической точки зрения - ответ однозначно “да”. По отдельности нейроны нашего мозга состоят из весьма простых компонентов, и нет никакого принципиального запрета смоделировать их работу на компьютере. С алгоритмической точки зрения - узнать как работает самосознание, пока что никто не смог, данный вопрос еще открыт, как для математиков, так и для физиологов. Таким образом, запрограммировать на компьютере процесс мышления пока невозможно, но лишь потому, что собственно принцип еще неизвестен. Впрочем, учитывая скорость развития современных технологий, можно предположить что это лишь вопрос времени, причем возможно не очень дальнего. Хорошо это или нет - вопрос другой, выходящий за рамки данного текста. Ну и для понимания сложности данной задачи - для симуляции лишь 1% человеческого мозга (а это 1.7млрд нервных клеток и 10.4трлн синапсов) в течении 1 секунды, в 2013 году понадобилось 40 минут расчетов на суперкомпьютере из 82000 процессоров.

Что касается нейронных сетей, рассмотренных выше (MLP, CNN), то разумеется, никаким сознанием они обладать не могут, такие сети могут лишь научиться выполнять какую-то одну, пусть и весьма сложную, задачу. Тут еще важно не путать два принципиально разных момента - можно создать “симулятор чата”, загрузив в компьютер сотни тысяч фраз и ответов на них, и даже человек не сможет отличить такого “собеседника” от живого (что-то похожее работает например, в Siri), но полноценным интеллектом с собственным сознанием (есть даже термин “strong AI”) это не является.

20. Нейронные сети - распознавание текста

Рассмотрим практический пример - распознавание выше написанной нейронной сетью рукописного текста. Для тренировки сети воспользуемся имеющейся в открытом доступе базой MNIST, содержащей 60000 черно-белых рукописных изображений цифр размером 28x28пкс.

Выглядят изображения примерно так:

Массив изображений каждой цифры хранится в виде чисел float в диапазоне от 0 до 1, длина массива равна 28х28 = 784 значения. Кстати, при выводе массива в консоль форма цифры вполне узнаваема:

(картинка соответствует числу “5”)

Всего таких чисел, как было сказано, 60000, что вполне хватит для тренировки нашей сети.

Воспользуемся уже готовым классом MLP, изменим лишь некоторые параметры:

Число входов будет равно 28х28 = 784, т.к. на вход нейросети будет подаваться массив целиком.

На выходе нейросеть должна давать значения от 0 до 9, однако мы не можем получить значение > 1, так что воспользуемся другим путем: нейросеть будет иметь 10 выходов, где “1” будет соответствовать указанной цифре. Т.е. для числа “0” мы получим [1,0,0,0,0,0,0,0,0,0], для “1” [0,1,0,0,0,0,0,0,0,0], и так далее.

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

В итоге, мы имеем такую структуру сети:

Рассмотрим подробнее программу на языке Python.

Тренировка сети

Создаем нейронную сеть и подготавливаем 2 массива тестовых данных (исходные и целевых значения). Массив “digits” загружается из зараннее подготовленного текстового файла базы MNIST. Для каждой цифры содержится исходный массив в 784 цифры, и правильное значение числа, которое и будет использоваться для тренировки.

mlp = MLP(n_in=28*28, n_hidden=48, n_out=10)

print "1. Загрузка данных"

with open("digitsMnist.txt", 'rb') as fp:

digits = cPickle.load(fp)

print "2. Подготовка данных"

batch = 50

inputs = []

targets = []

for p in range(batch):

data = digits[p]["data"]

resulVal = digits[p]["result"]

# Конвертация целевого числа в массив: “3” => [0,0,0,1,0,0,0,0,0,0]

result_flat = [0.0]*10

result_flat[resulVal] = 1.0

inputs.append(data)

targets.append(result_flat)

Собственно тренировка ничем по сути не отличается от предыдущего примера с функцией XOR. Т.к. процесс идет несколько часов, были также добавлены функции сохранения и чтения состояния сети, чтобы процесс можно было прервать и затем продолжить. Прервать тренировку можно в любой момент нажатием Ctrl+C. Для удобства визуализации процесса выводится суммарная ошибка по всем тестируемым цифрам.

print "3. Обучение"

mlp.load()

try:

N = 10000

for p in range(N):

errSum = 0.0

for d in range(batch):

mlp.forwardPass(inputs[d])

mlp.backPass(inputs[d], targets[d])

errSum += mlp.calcError(inputs[d], targets[d])

print "Step", p, "error", errSum

except KeyboardInterrupt:

pass

mlp.save()

Всего было проведено около 50000 итераций на блоке размером 50 цифр, весь массив в 60000 цифр не использовался. Обучение сети заняло порядка 2х часов на компьютере с Core i7. Разумеется, этот проект учебный, и программа никак не оптимизирована по быстродействию. С использованием специальных фреймворков типа Keras тренировка такой сети занимает не более 5 минут.

Предыдущая главаГлава 8 из 11Следующая глава