Распознавание рукописного текста. Нейронные сети.Python

В этой статье мы напишем простую нейронную сеть(персептрон) для распознавания рукописного текса. Это скорей практикум по реализации конкретной сети, мы совсем поверхностно коснемся теории. План работы:

  • Немного теории.
  • Пишем сеть.
  • Обучение сети, обратное распространение ошибки, машинное обучение.
  • Итог.

Теория.

Совсем без теории конечно не обойтись и начнем мы с нейрона.

Все нейронные сети состоят из нейронов. А если конкретней из математической модели работы настоящего нейрона. Суть этой модели в том что есть много входов ( X ) по которым поступают какие то данные ( на примет 1 или 0 ). У каждого сигнала\входа есть вес (сила сигнала W) . Все эти данные обрабатываются внутри нейрона, так называемая функция активации нейрона f(x) . И нейрон падает обработанные данные на выход.

Из этих кубиков собираются слои : входной, скрытый , выходной. Собственно на входной слой подаються сигналы, далее они передаться на скрытый слой ( их может быть и несколько) . В скрытом слое происходит сама работа всей сети. Ну и по аналоги со скрытого слоя данные поступают на выходной. На этих выходных данных и строиться результат работы. Классификация объектов , распознавание или вероятность чего то. Важно помнить, не одна нейронная сеть не дает 100% точности так же как и мозг человека.

Пишем код.

Мы будем писать Персептрон, простая модель в который сигналы передаются с лева на право (от входного к выходному слою) и все нейроны соединены друг с другом. То есть каждый нейров входного слоя соединен выходами со всеми нейронами скрытого слоя и т.д.

Первым делом нам надо задать число нейронов в входном,выходном и скрытом слое . Еще нужно задать коэффициент обучения (с какой скоростью будет обучаться сеть от 0 до 1) нам он потребуется позже.

def init_net():
    input_nodes = 784
    print('Введите число скрытых нейронов: ')
    hidden_nodes = int(input())
    out_nodes = 10
    print('Введите скорость обучения(0.5): ')
    lern_node = float(input())
    return input_nodes, hidden_nodes, out_nodes, lern_node

Теперь создадим саму сеть. Мы создадим все связи между слоями, если точнее — силу этих связей. Для этого мы будем использовать матрицы ( двухмерные массивы) которые будут хранить эти связи. Вы же помните что каждый нейрон связен с каждым нейроном следующего слоя ( например если в скрытом слое 100 нейронов, то для каждого нейрона входного слоя будет иметься 1х100 связей и т.д.)

import numpy
def creat_net(input_nodes, hidden_nodes, out_nodes,):
    # сознание массивов. -0.5 вычитаем что бы получить диапазон -0.5 +0.5 для весов
    input_hidden_w = (numpy.random.rand(hidden_nodes, input_nodes) - 0.5)
    hidden_out_w = (numpy.random.rand(out_nodes, hidden_nodes) - 0.5)
    return input_hidden_w, hidden_out_w

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

numpy.random.rand() — создает массив заданной формы и заполняет его случайными значениями от 0 до 1.

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

Сигмооида — это гладкая монотонная возрастающая нелинейная функция, имеющая форму буквы «S», которая часто применяется для «сглаживания» значений некоторой величины. Формула : f(x) = 1 / (1+e ** -x )

В нейронных сетях применяют не только эту функцию, а например : Гиперболический тангенс , Ступенчатая функция , Гауссова функция.
import scipy.special #  библиотека scipy.special содержит сигмоиду expit()
def fun_active(x):
    return scipy.special.expit(x)

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

Тут очень важный момент, почему матрицы ? На этой картинке простоя модель

А теперь представим эти данные в виде матрицы :

def query(input_hidden_w, hidden_out_w, inputs_list):
    # преобразовать список входных значений
    # в двухмерный массив
    inputs_sig = numpy.array(inputs_list, ndmin=2).T
    hidden_inputs = numpy.dot(input_hidden_w, inputs_sig)# умножение матриц
    hidden_out = fun_active(hidden_inputs)  # вычисляем выходной сигнал скрытого слоя
    # умножение матриц выходи в веса для выходного слоя
    final_inputs = numpy.dot(hidden_out_w, hidden_out)
    final_out = fun_active(final_inputs)
    return final_out

T — означает транспонированная матрица, не буду вдаваться в математику. Вы можете посмотреть что это значит в интернете.

Машинное обучение.

У нас есть каркас. Но наша сеть глупа как только что родившийся ребенок. И теперь нам надо ее обучить распознавать рукописный текст. Первым делам для этого нужны входные данные( много ) а так же проверочные выходные данные с которыми мы будем сверяться в ходе обучения.

Мы возьмем готовую базу MNIST (сокращение от «Modified National Institute of Standards and Technology») — объёмная база данных образцов рукописного написания цифр. База данных является стандартом, предложенным Национальным институтом стандартов и технологий США с целью калибрации и сопоставления методов распознавания изображений с помощью машинного обучения в первую очередь на основе нейронных сетей. Качаем с нашего сайта.

Смысл обучения нейронной сети сводиться к коррекции W весов связей( силы сигнала) между нейронами. Первым делом мы вычисляем ошибку выходных данных : e1 = t1 — o1 где t1 проверочное значение для нейрона выходного слоя, o1 — фактическое значение. Что бы посчитать ошибку для нейронов скрытого слоя мы воспользуемся методом обратного распространения ошибки . Суть проста : Ошибка1 (первого нейрона скрытого слоя) = e1 (выходной) * w1.1 + e2(второй выходной нейрона) * w1.2(вес связи от него)

Формула для матриц
def treyn(targget_list,input_list, input_hidden_w, hidden_out_w, lern_node):
    #Прогоняем данные через сеть
    targgets = numpy.array(targget_list, ndmin=2).T
    inputs_sig = numpy.array(input_list, ndmin=2).T
    hidden_inputs = numpy.dot(input_hidden_w, inputs_sig)
    hidden_out = fun_active(hidden_inputs)
    final_inputs = numpy.dot(hidden_out_w, hidden_out)
    final_out = fun_active(final_inputs)
    #Рассчитываем ошибку выходного слоя
    out_errors = targgets - final_out
    #Рассчитываем ошибку скрытого слоя
    hidden_errors = numpy.dot(hidden_out_w.T, out_errors)

Ошибки нейронов мы посчитали, а на сколько же и как нам менять веса связей ? Не вдаваясь в подробности, будем использовать метод Градиентного спуска. То есть коррекция = lern_nodes * e выходного слой * сигмоида выходного слоя * (1 — сигмоида выходного слоя) * сигмоида скрытого слоя

# Обновление весов связей
hidden_out_w += lern_node * numpy.dot((out_errors * final_out*(1 - final_out)), numpy.transpose(hidden_out))
input_hidden_w += lern_node * numpy.dot((hidden_errors * hidden_out*(1-hidden_out)), numpy.transpose(inputs_sig))

Обучающие данные.

Нас с вами интересует два файл :

  • mnist_train.csv
  • mnist_test.csv

Данные предоставлены в удобной текстовой форме, разделенные запятой. Тренировочный набор содержит около 60.000 промаркированных образцов, тестовый поменьше — около 10.000 .

  • Первое значение это маркер от 0-9 , какая цифра изображена.
  • Далее следует пиксельный массив 28х28 . Всего 784 значения от 0 до 255.

Прежде чем использовать эти данные для обучения и проверки нейронной сети, нам нужно подготовить их для работы с функцией активации. Что бы данные оставались в оптимальном диапазоне для нее. Для нас будет оптимально значение от 0,01 до 1.00 . Почему не 0 ? Нулевое значение может помешать нам обновлять весовые коэффициенты .

data_file = open('mnist_train.csv','r')
trening_list = data_file.readlines()
data_file.close()
for record in trening_list:
получить список значений, используя символы запятой (1,1) в качестве разделителей
all_values = record.split(',')
#масштабировать и сместить входные значения
# numpy.asfarray(a,dtype=float64'>>)  Возвращает массив,преобразованный в тип float. 
inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
# Создаем массив из 10 элементов и в нужном элемент (считанный из первого знака базы данных) записываем проверочное значение. 
targets = numpy.zeros(10) + 0.01
targets[int(all_values[0])] =0.99

Наша нейросеть будет классифицировать изображения по 10 признакам , цифра от 0 до 9 . Выходные значения опять же будут в рамках нашей функции активации, фактически сигмоида не может достичь 1 , по этому мы записываем 0.99 .

Немного все сумбурно получилось. Используя эту базу данных мы будем подавать на каждый входной нейрон одно значение пикселя картинки. То есть входной слой будет у нас матрица 28х28 (784 нейрона) . А выходной слой будет содержать 10 нейронов- классификаторов. Самый интерес вопрос : сколько нейронов должно быть в скрытом слое ? Если их будет мало, сеть будет не способна распознать цифры. Если очень много сеть просто запомнит обучающие данные(зазубрит) что скажется на точности распознания. Попробуем остановиться на 100 скрытых нейронах.

input_nodes, hidden_nodes, out_nodes, lern_node = init_net()
input_hidden_w, hidden_out_w = creat_net(input_nodes, hidden_nodes, out_nodes)

data_file = open('mnist_train.csv','r')
trening_list = data_file.readlines()
data_file.close()

for record in trening_list:

    all_values = record.split(',')
#масштабировать и сместить входные значения
# numpy.asfarray(a,dtype=float64'>>)  Возвращает массив,преобразованный в тип float.
    inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
# Создаем массив из 10 элементов и в нужном элемент (считанный из первого знака базы данных) записываем проверочное значение.
    targets = numpy.zeros(10) + 0.01
    targets[int(all_values[0])] = 0.99
    hidden_out_w, input_hidden_w = treyn(targets, inputs, input_hidden_w, hidden_out_w, lern_node)

data_file = open('mnist_test.csv','r')
test_list = data_file.readlines()
data_file.close()
test = []
for record in test_list:
    all_values = record.split(',')
    inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
    out_session = query(input_hidden_w, hidden_out_w, inputs)
    if int(all_values[0]) == numpy.argmax(out_session):
    #Если ответ верный записывает 1 в список
        test.append(1)
    else:
        test.append(0)
test = numpy.asarray(test)
# Сумму делим на размер массива и вычисляем эффективность сети
print('Эффективность сети % =', (test.sum()/test.size)*100)

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

Код полностью.

import numpy
import scipy.special
# pip install scipy
def init_net():
    input_nodes = 784
    print('Введите число скрытых нейронов: ')
    hidden_nodes = int(input())
    out_nodes = 10
    print('Введите скорость обучения(0.5): ')
    lern_node = float(input())
    return input_nodes, hidden_nodes, out_nodes, lern_node
def creat_net(input_nodes, hidden_nodes, out_nodes,):
    # сознание массивов. -0.5 вычитаем что бы получить диапазон -0.5 +0.5 для весов
    input_hidden_w = (numpy.random.rand(hidden_nodes, input_nodes) - 0.5)
    hidden_out_w = (numpy.random.rand(out_nodes, hidden_nodes) - 0.5)
    return input_hidden_w, hidden_out_w

def fun_active(x):
    return scipy.special.expit(x)

def query(input_hidden_w, hidden_out_w, inputs_list):
    # преобразовать список входных значений
    # в двухмерный массив
    inputs_sig = numpy.array(inputs_list, ndmin=2).T

    hidden_inputs = numpy.dot(input_hidden_w, inputs_sig)  # умножение матриц
    hidden_out = fun_active(hidden_inputs)  # вычисляем выходной сигнал скрытого слоя
    # умножение матриц выходи в веса для выходного слоя
    final_inputs = numpy.dot(hidden_out_w, hidden_out)
    final_out = fun_active(final_inputs)

    return final_out

def treyn(targget_list,input_list, input_hidden_w, hidden_out_w, lern_node):
    #Прогоняем данные через сеть
    targgets = numpy.array(targget_list, ndmin=2).T
    inputs_sig = numpy.array(input_list, ndmin=2).T
    hidden_inputs = numpy.dot(input_hidden_w, inputs_sig)
    hidden_out = fun_active(hidden_inputs)
    final_inputs = numpy.dot(hidden_out_w, hidden_out)
    final_out = fun_active(final_inputs)
    #Рассчитываем ошибку выходного слоя
    out_errors = targgets - final_out
    #Рассчитываем ошибку скрытого слоя
    hidden_errors = numpy.dot(hidden_out_w.T, out_errors)
    # Обновление весов связей
    hidden_out_w += lern_node * numpy.dot((out_errors * final_out * (1 - final_out)), numpy.transpose(hidden_out))
    input_hidden_w += lern_node * numpy.dot((hidden_errors * hidden_out * (1 - hidden_out)),numpy.transpose(inputs_sig))
    return hidden_out_w, input_hidden_w

def test_set(hidden_out_w, input_hidden_w):
    data_file = open('mnist_train.csv', 'r')
    trening_list = data_file.readlines()
    data_file.close()

    for record in trening_list:
        all_values = record.split(',')
        # масштабировать и сместить входные значения
        # numpy.asfarray(a,dtype=float64'>>)  Возвращает массив,преобразованный в тип float.
        inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
        # Создаем массив из 10 элементов и в нужном элемент (считанный из первого знака базы данных) записываем проверочное значение.
        targets = numpy.zeros(10) + 0.01
        targets[int(all_values[0])] = 0.99
        hidden_out_w, input_hidden_w = treyn(targets, inputs, input_hidden_w, hidden_out_w, lern_node)

    data_file = open('mnist_test.csv', 'r')
    test_list = data_file.readlines()
    data_file.close()
    test = []
    for record in test_list:
        all_values = record.split(',')
        inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
        out_session = query(input_hidden_w, hidden_out_w, inputs)
        if int(all_values[0]) == numpy.argmax(out_session):
            test.append(1)
        else:
            test.append(0)
    print(len(test))
    test = numpy.asarray(test)
    print('Эфективность сети % =', (test.sum() / test.size) * 100)
    return hidden_out_w, input_hidden_w
input_nodes, hidden_nodes, out_nodes, lern_node = init_net()
input_hidden_w, hidden_out_w = creat_net(input_nodes, hidden_nodes, out_nodes)
for i in range (5):
    print('Test #', i+1)
    hidden_out_w, input_hidden_w = test_set(hidden_out_w, input_hidden_w)