Список работ

Сверточная нейронная сеть на Python

Содержание

Введение

Рассматривается сверточная нейронная сеть (СНС) на Python, реализованная средствами библиотеки TensorFlow, доступ которой выполнен при помощи оболочки Keras.
После обучения сеть способна классифицировать изображения рукописных цифр, выполненные в оттенках серого цвета и имеющие размер 28*28 пикселей.
Обучение и тестирование СНС выполняется на наборе данных MNIST, загрузка которого обеспечивается средствами TensorFlow. Так же набор можно загрузить и средствами Keras. В первом случае набор должен быть предварительно скачен, а во втором он скачивается при начале загрузки, то есть должен быть обеспечен доступ к интернету.
Скачать MNIST можно, например, на [1].
Для доступа к MNIST необходимо выполнить установить библиотеку python-mnist. Установка библиотек выполняется в командном окне, которое в Windows открывается после нажатия Win + X.

<path>/Scripts/pip3 install python-mnist

Познакомится со сверточными сетями можно, например, прочитав [2].

Описание MNIST и его загрузка в программу

MNIST содержит образы рукописных цифр (рис. 1).

MNIST

Рис. 1. Примеры рукописных цифр набора данных MNIST

Каждый образ имеет размер 28*28 пикселей и выполнен в оттенках серого цвета. Показанных на рис. 1 границ образа в его описании нет, на рис. 1 они приведены для наглядности.
Для представления образа используются 28*28 = 784 цифры из диапазона [0, 55]. Например, вариант рукописного числа 5 представлен следующим вектором цифр:

[0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 3 18 18 18 126 136 175 26 166 255 247 127 0 0 0 0 0 0 0 0 0 0 0 0 30 36 94 154 170 253 253 253 253 253 225 172 253 242 195 64 0 0 0 0 0 0 0 0 0 0 0 49 238 253 253 253 253 253 253 253 253 251 93 82 82 56 39 0 0 0 0 0 0 0 0 0 0 0 0 18 219 253 253 253 253 253 198 182 247 241 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 80 156 107 253 253 205 11 0 43 154 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 14 1 154 253 90 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 139 253 190 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 11 190 253 70 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 35 241 225 160 108 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 81 240 253 253 119 25 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 45 186 253 253 150 27 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 16 93 252 253 187 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 249 253 249 64 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 46 130 183 253 253 207 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 39 148 229 253 253 253 250 182 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 24 114 221 253 253 253 253 201 78 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 23 66 213 253 253 253 253 198 81 2 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 18 171 219 253 253 253 253 195 80 9 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 55 172 226 253 253 253 253 244 133 11 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 136 253 253 253 212 135 132 16 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

На вход СНС подается массив размера 28*28. Для вышеприведенной версии числа 5 он отображается в следующую таблицу:

Таблица 1. Рукописная версия числа 5

[[  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  3 18 18 18126136175 26166255247127  0  0  0  0]
 [  0  0  0  0  0  0  0  0 30 36 94154170253253253253253225172253242195 64  0  0  0  0]
 [  0  0  0  0  0  0  0 49238253253253253253253253253251 93 82 82 56 39  0  0  0  0  0]
 [  0  0  0  0  0  0  0 18219253253253253253198182247241  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0 80156107253253205 11  0 43154  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0 14  115425390  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0139253190  2  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0 11190253 70  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0 35241225160108  1  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0 81240253253119 25  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0 45186253253150 27  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0 16 93252253187  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0249253249 64  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0 46130183253253207  2  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0 39148229253253253250182  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0 24114221253253253253201 78  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0 23 66213253253253253198 81  2  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0 18171219253253253253195 80  9  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0 55172226253253253253244133 11  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0136253253253212135132 16  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]]

Растровое представление образа, масштабированное на 100%, показано на рис. 2.

Число 5

Рис. 2. Растровое представление рассматриваемого рукописного числа 5

Его можно получить, употребив следующий MaxScript-код:

-- Создаем растровый образ
btmp = bitmap 28 28 color:white
-- Состав файла 5.txt описан в табл. 1
fNm = "G:/100byte/python/MNIST_NN/5.txt"
fs = openFile fNm mode:"r"
for y = 0 to 27 do (
    s = readLine fs
    s = substituteString s "[" ""
    s = substituteString s "]" ""
    for x = 0 to 27 do (
        while s[1] == " " or s[1] == "    " do s = substring s 2 -1
        k = 1
        while s[k] > " " and k < s.count do k += 1
        v = substring s 1 (k - 1) as integer
        v = 255 - v
        setPixels btmp [x, y] #((color v v v))
        s = substring s k -1
    )
)
close fs
display btmp

Чтение и преобразование MNIST обеспечивается следующим кодом (на примере обучающей выборки):

mndata = MNIST('G:/python/MNIST/MNIST_data/')
# Разрешаем чтение архивированных данных
mndata.gz = True
# Обучающая выборка (данные и метки); imagesTrain и labelsTrain – это списки
imagesTrain, labelsTrain = mndata.load_training()
# Преобразование списков imagesTrain и labelsTrain в одномерные массивы
# Каждый элемент массива - это вектор размера 28*28 с целочисленными данными в диапазоне [0, 255]
x_train = np.asarray(imagesTrain)
y_train = np.asarray(labelsTrain)
print(x_train[0]) # Напечатает 784 цифры, представляющих вариант рукописного числа 5
print(y_train[0]) # Напечатает: 5
#
# Меняем форму входных данных (обучающих и тестовых)
x_train = x_train.reshape(x_train.shape[0], 28, 28, 1)
#
# Преобразование целочисленных данных в float32; данные лежат в диапазоне [0.0, 255.0]
x_train = x_train.astype('float32')
print(x_train[0, :, :, 0]) # Напечатает массив размера 28*28
#
# Приведение к диапазону [0.0, 1.0]
x_train /= 255
#
# Преобразование метки из диапазона [0, 9] в двоичный вектор размера 10
# Так, метка 5 будет преобразована в вектор [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
y_train = keras.utils.to_categorical(y_train, 10) # 10 – число классов
print(y_train[0]) # Напечатает: [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]

Архитектура сети

Программируемая ниже СНС имеет те же типы слоев, что и известная сеть LeNet-5 [3] (рис. 3).

LeNet-5

Рис. 3. Архитектура LeNet-5

СНС отличается от LeNet формами выхода слоев, функциями активации и механизмом фильтрации данных на сверточных слоях.
Архитектура СНС на показана на рис. 4 и описана в табл. 2.

Архитектура СНС

Рис. 4. Архитектура СНС

Таблица 2. Описание архитектуры СНС

Тип слояФорма ядра
(окна)
ШагФорма выходаЧисло
обучаемых
весов
Conv / ReLU(5, 5)(1, 1)(None, 28, 28, 32)832
MaxPool(2, 2)(2, 2)(None, 14, 14, 32)0
Conv / ReLU(5, 5)(1, 1)(None, 10, 10, 64)51'264
MaxPool(2, 2)(2, 2)(None, 5, 5, 64)0
FC / ReLU(None, 1024)1'639'424
FC / Linear(None, 16)16'400
FC / Softmax(None, 10)170

Используются слои следующих типов:

Сеть содержит два сверточных слоя, два слоя подвыборки и три полносвязных слоя.
Операция свёртки, выполняемая на сверточных слоях, заключается в вычислении скалярных произведений входного массива признаков и ядра свёртки (массива, называемого также фильтром) при различных положениях последнего: ядро свертки позиционируется с заданным шагом на входном массиве признаков. В используемой СНС ядро свёртки – это массив формы (2, 2).
На слоях подвыборки осуществляется уплотнение признаков в результате замены группы признаков, охватываемых окном (массивом) подвыборки, одним признаком этой группы (в используемой СНС берется признак с максимальным значением). Окно подвыборки передвигается с заданным шагом по входному массиву признаков. В используемой СНС окно подвыборки – это массив формы (2, 2). Шаг перемещения окна также задается в виде массива формы (2, 2).
На входе СНС вектор из 784 признаков преобразуется в массив формы (28, 28). Каждый элемент массива – это значение соответствующего пикселя изображения рукописной цифры.
На первом сверточном слое обучаются 32 фильтров, на втором – 64. Выходные данные каждого сверточного слоя фильтруются функцией ReLU и передаются на слой подвыборки; окно и шаг подвыборки имеют форму (2, 2).
Два сверточных слоя позволяют «выразить» взаимосвязь между пикселями изображения, расположенными далеко друг от друга.
На первом сверточном слое обучаются 32 фильтра, на втором – 64. Выходные данные каждого сверточного слоя фильтруются функцией ReLU и передаются на слой подвыборки; окно и шаг подвыборки имеют форму (2, 2).
Слои подвыборки снижают размерность обрабатываемых сетью данных.
Три полносвязных слоя с последовательно уменьшающимся числом нейронов совмещают выделенные сверточными слоями признаки и выдают ответ [2].
Последний (полносвязный) слой СНС определяет вероятности принадлежности изображения к классам в зависимости от значений, выделенных на предыдущем, также полносвязном, слое признаков.
Передача массива признаков от второго слоя подвыборки первому полносвязному слою предваряется операцией Flatten, выполняющей преобразование многомерного выхода предшествующего слоя в одномерный; на выходе Flatten массив формы (None, 1600).
Сеть, получая на входе изображение, извлекает вектор из 16 признаков, по значениям которых изображение относится к одному из десяти классов.
Дополнительно может быть задан и Dropout-слой, на котором нейронов исключается из сети (рис. 5).

Dropout

Рис. 5. До и после слоя Dropout

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

Flatten – преобразует многомерный выход предшествующего слоя в одномерный, например:

model = Sequential()
model.add(Conv2D(64, 3, 3,
                 border_mode = 'same',
                 input_shape = (3, 32, 32)))
# Теперь: model.output_shape = (None, 64, 32, 32)
model.add(Flatten())
# Теперь: model.output_shape = (None, 65536), где 65536 = 64 * 32 * 32

Свертку входного вектора A фильтром (F) можно проиллюстрировать следующим образом: фильтр F скользит по входному вектору А, каждой позиции фильтра отвечает элемент выходного вектора B, значение которого равно скалярному произведению F и области вектора A, закрываемой фильтром F (рис. 6).

Свертка B = A * F

Рис. 6. Свертка B = A * F с шагом 1, в частности, -5 = 3 * 1 + 7 * (-2) + 6

Свертку двумерного массива двумерным фильтром с шагом 1 иллюстрирует рис. 7 (заимствован из [4]).

Свертка с шагом 1

Рис. 7. Свертка с шагом 1

Фильтр показан на рис. 8.

Фильтр

Рис. 8. Фильтр свертки, выполняемой на рис. 7

Одной из задач обучения является поиск наилучших фильтров.

Операция ReLU применяется к каждому элементу полученному после свертки массива, заменяя отрицательные значения на нулевые (рис. 9).

ReLU

Рис. 9. ReLU

На pooling-слое (слое подвыборки) уменьшается размер массива данных за счет объединения его смежных ячеек: объединяемые ячейки заменяются одной; значение в новой ячейке определяется по значениям прежних ячеек по заданному правилу.
Так, в случае функции максимума (MaxPooling) новая ячейка будет содержать максимальное значение объединяемых ячеек.
Выбор объединяемых ячеек осуществляется путем наложения на массив заданного фильтра – массива той же формы, что и массив данных, но меньшего размера.
Первое подмножество объединяемых ячеек выделяется фильтром, начиная с левого верхнего угла массива данных. Следующее положение фильтра зависит от заданного шага.
Процесс объединения с шагом (2, 2) иллюстрирует рис. 10.

Pooling

Рис. 10. Объединение с функцией максимума, фильтром 2 * 2 и шагом (2, 2)

Графическая иллюстрация операций свертки, ReLU и подвыборки приведена на рис. 11 (заимствован из [5]).

Conv + ReLU + Pooling

Рис. 11. Иллюстрация операций свертки, ReLU и подвыборки

Реализация с комментариями и выводом

Код построен на основе программ, приведенных в [2 и 6].
В качестве функции потерь берется перекрестная энтропии – categorical_crossentropy.
Метод оптимизации функции потерь – Adam.
Прочие сведения о сети приведены выше, а также в сопутствующем комментарии.

from mnist import MNIST
import numpy as np
import keras
import time
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K
import sys # Для sys.exit()
#
#from keras.datasets import mnist
#
# Задание затравки датчика случайных чисел обеспечит повторяемость результата
np.random.seed(348)
#
# Параметры модели
# Число экземпляров данных обучающей выборки между пересчетами весовых коэффициентов
# Чем меньше batch_size, тем дольше процесс обучения
batch_size = 256
# Число классов (по числу цифр)
num_classes = 10
# Число эпох обучения
epochs = 5
#
# Читаем в списки imagesTrain, labelsTrain, imagesTest и labelsTest данные и метки
# из ранее скаченных файлов - набора данных MNIST
# Данные - это преставление числа (метки) на рисунке размера 28*28 пикселей
# Метка - это число из диапазона [0, 9]
# В каждом пикселе представлен оттенок серрого цвета. Он задается числом из диапазона [0, 255]
mndata = MNIST('G:/python/MNIST/MNIST_data/')
# Разрешаем чтение архивированных данных
mndata.gz = True
# Обучающая выборка (данные и метки)
imagesTrain, labelsTrain = mndata.load_training()
# Тестовая выборка (данные и метки)
imagesTest, labelsTest = mndata.load_testing()
# Преобразование списков в одномерные массивы
# Каждый элемент массива - это вектор размера 28*28 с целочисленными данными в диапазоне [0, 255]
x_train = np.asarray(imagesTrain)
y_train = np.asarray(labelsTrain)
x_test = np.asarray(imagesTest)
y_test = np.asarray(labelsTest)
print(x_train[0]) # Напечатает 784 цифры, представляющих вариант рукописного числа 5
print(y_train[0]) # Напечатает: 5
#
# Данные можно ввести, используя класс mnist, определенный в keras.datasets, но это медленнее
#(x_train, y_train), (x_test, y_test) = mnist.load_data()
#
# Размер входного образа
img_rows, img_cols = 28, 28
#
# Меняем форму входных данных (обучающих и тестовых)
if K.image_data_format() == 'channels_first':
    x_train = x_train.reshape(x_train.shape[0], 1, img_rows, img_cols)
    x_test = x_test.reshape(x_test.shape[0], 1, img_rows, img_cols)
    input_shape = (1, img_rows, img_cols)
else: # channels_last
    x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1)
    x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
    input_shape = (img_rows, img_cols, 1)
# Реально имеем channels_last
#
# Преобразование целочисленных данных в float32; данные лежат в диапазоне [0.0, 255.0]
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
print(x_train[0, :, :, 0]) # Напечатает массив размера 28*28
#
# Приведение к диапазону [0.0, 1.0]
x_train /= 255
x_test /= 255
#
# Преобразование метки - числа из диапазона [0, 9] в двоичный вектор размера 10
# Так, метка 5 (соответствует цифре 5) будет преобразована в вектор [0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
print(y_train[0]) # Напечатает: 5
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
print(y_train[0]) # Напечатает: [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]
#sys.exit()
#
print('Форма x_train:', x_train.shape) # Напечатает: Форма x_train: (60000, 28, 28, 1)
print(x_train.shape[0], 'обучающих образов') # Напечатает: 60000 обучающих образов
print(x_test.shape[0], 'тестовых образов') # Напечатает: 10000 тестовых образов
#
start_time = time.time()
#
# Создаем модель сверточной нейронной сети
model = Sequential()
model.add(Conv2D(32, kernel_size = (5, 5), strides = (1, 1), padding = 'same',
                 activation = 'relu', input_shape = input_shape))
trainable_count = int(np.sum([K.count_params(p) for p in set(model.trainable_weights)]))
non_trainable_count = int(np.sum([K.count_params(p) for p in set(model.non_trainable_weights)]))
trainable_countConv2D_32 = trainable_count
non_trainable_countConv2D_32 = non_trainable_count
print('Input shape: ', model.input_shape) # Input shape: (None, 28, 28, 1)
print('Conv2D_32. Форма выхода: ', model.output_shape) # Conv2D_32: (None, 28, 28, 32)
print('Conv2D_32. Число обучаемых весов: ', trainable_countConv2D_32) # 832
print('Conv2D_32. Число необучаемых весов: ', non_trainable_countConv2D_32) #
#
model.add(MaxPooling2D(pool_size = (2, 2), strides = (2, 2), padding = 'same'))
trainable_count = int(np.sum([K.count_params(p) for p in set(model.trainable_weights)]))
non_trainable_count = int(np.sum([K.count_params(p) for p in set(model.non_trainable_weights)]))
trainable_countMaxPooling2D_1 = trainable_count - trainable_countConv2D_32
non_trainable_countMaxPooling2D_1 = non_trainable_count - non_trainable_countConv2D_32
print('MaxPooling2D_1. Форма выхода: ', model.output_shape) # MaxPooling2D_1: (None, 14, 14, 32)
print('MaxPooling2D_1. Число обучаемых весов: ', trainable_countMaxPooling2D_1) # 0
print('MaxPooling2D_1. Число необучаемых весов: ', non_trainable_countMaxPooling2D_1) # 0
#
model.add(Conv2D(64, kernel_size = (5, 5), strides = (1, 1), activation = 'relu'))
trainable_count = int(np.sum([K.count_params(p) for p in set(model.trainable_weights)]))
non_trainable_count = int(np.sum([K.count_params(p) for p in set(model.non_trainable_weights)]))
trainable_countConv2D_64 = trainable_count - trainable_countConv2D_32 \
                           - trainable_countMaxPooling2D_1
non_trainable_countConv2D_64 = non_trainable_count - non_trainable_countConv2D_32 \
                               - non_trainable_countMaxPooling2D_1
print('Conv2D_64. Форма выхода: ', model.output_shape) # Conv2D_64: (None, 10, 10, 64)
print('Conv2D_64. Число обучаемых весов: ', trainable_countConv2D_64) # 51264
print('Conv2D_64. Число необучаемых весов: ', non_trainable_countConv2D_64) # 0
#
model.add(MaxPooling2D(pool_size = (2, 2), strides = (2, 2)))
trainable_count = int(np.sum([K.count_params(p) for p in set(model.trainable_weights)]))
non_trainable_count = int(np.sum([K.count_params(p) for p in set(model.non_trainable_weights)]))
trainable_countMaxPooling2D_2 = trainable_count - trainable_countConv2D_32 \
                                - trainable_countMaxPooling2D_1 - trainable_countConv2D_64
non_trainable_countMaxPooling2D_2 = non_trainable_count - non_trainable_countConv2D_32 \
                                - non_trainable_countMaxPooling2D_1 - non_trainable_countConv2D_64
print('MaxPooling2D_2. Форма выхода: ', model.output_shape) # MaxPooling2D_2: (None, 5, 5, 64)
print('MaxPooling2D_2. Число обучаемых весов: ', trainable_countMaxPooling2D_2) # 0
print('MaxPooling2D_2. Число необучаемых весов: ', non_trainable_countMaxPooling2D_2) # 0
#
#model.add(Dropout(0.25))
#print('Dropout_0.25: ', model.output_shape) # Dropout_0.25: (None, 5, 5, 64)
#
model.add(Flatten())
trainable_count = int(np.sum([K.count_params(p) for p in set(model.trainable_weights)]))
non_trainable_count = int(np.sum([K.count_params(p) for p in set(model.non_trainable_weights)]))
trainableFlatten = trainable_count - trainable_countConv2D_32 \
                   - trainable_countMaxPooling2D_1 - trainable_countConv2D_64 \
                   - trainable_countMaxPooling2D_2
non_trainableFlatten = non_trainable_count - non_trainable_countConv2D_32 \
                   - non_trainable_countMaxPooling2D_1 - non_trainable_countConv2D_64 \
                   - non_trainable_countMaxPooling2D_2
print('Flatten. Форма выхода: ', model.output_shape) # (None, 1600)
print('Flatten. Число обучаемых весов: ', trainableFlatten) # 0
print('Flatten. Число необучаемых весов: ', non_trainableFlatten) # 0
#
model.add(Dense(1024, activation = 'relu'))
trainable_count = int(np.sum([K.count_params(p) for p in set(model.trainable_weights)]))
non_trainable_count = int(np.sum([K.count_params(p) for p in set(model.non_trainable_weights)]))
trainableDense_1024 = trainable_count - trainable_countConv2D_32 \
                      - trainable_countMaxPooling2D_1 - trainable_countConv2D_64 \
                      - trainable_countMaxPooling2D_2 - trainableFlatten
non_trainableDense_1024 = non_trainable_count - non_trainable_countConv2D_32 \
                      - non_trainable_countMaxPooling2D_1 - non_trainable_countConv2D_64 \
                      - non_trainable_countMaxPooling2D_2 - non_trainableFlatten
print('Dense_1024. Форма выхода: ', model.output_shape) # (None, 1024)
print('Dense_1024. Число обучаемых весов: ', trainableDense_1024) # 1639424
print('Dense_1024. Число необучаемых весов: ', non_trainableDense_1024) # 0
#
model.add(Dense(16, activation = 'linear'))
trainable_count = int(np.sum([K.count_params(p) for p in set(model.trainable_weights)]))
non_trainable_count = int(np.sum([K.count_params(p) for p in set(model.non_trainable_weights)]))
trainableDense_16 = trainable_count - trainable_countConv2D_32 \
                    - trainable_countMaxPooling2D_1 - trainable_countConv2D_64 \
                    - trainable_countMaxPooling2D_2 - trainableFlatten \
                    - trainableDense_1024
non_trainableDense_16 = non_trainable_count - non_trainable_countConv2D_32 \
                    - non_trainable_countMaxPooling2D_1 - non_trainable_countConv2D_64 \
                    - non_trainable_countMaxPooling2D_2 - non_trainableFlatten \
                    - non_trainableDense_1024
print('Dense_16. Форма выхода: ', model.output_shape) # (None, 16)
print('Dense_16. Число обучаемых весов: ', trainableDense_16) # 16400
print('Dense_16. Число необучаемых весов: ', non_trainableDense_16) # 0
#
#model.add(Dropout(0.5))
#print('Dropout_0.5: ', model.output_shape) # (None, 16)
#
model.add(Dense(num_classes, activation = 'softmax'))
trainable_count = int(np.sum([K.count_params(p) for p in set(model.trainable_weights)]))
non_trainable_count = int(np.sum([K.count_params(p) for p in set(model.non_trainable_weights)]))
trainableDense_10 = trainable_count - trainable_countConv2D_32 \
                    - trainable_countMaxPooling2D_1 - trainable_countConv2D_64 \
                    - trainable_countMaxPooling2D_2 - trainableFlatten \
                    - trainableDense_1024 - trainableDense_16
non_trainableDense_10 = non_trainable_count - non_trainable_countConv2D_32 \
                    - non_trainable_countMaxPooling2D_1 - non_trainable_countConv2D_64 \
                    - non_trainable_countMaxPooling2D_2 - non_trainableFlatten \
                    - non_trainableDense_1024 - non_trainableDense_16
print('Dense_10. Форма выхода: ', model.output_shape) # (None, 10)
print('Dense_10. Число обучаемых весов: ', trainableDense_10) # 170
print('Dense_10. Число необучаемых весов: ', non_trainableDense_10) # 0
#
trainable_count = int(np.sum([K.count_params(p) for p in set(model.trainable_weights)]))
non_trainable_count = int(np.sum([K.count_params(p) for p in set(model.non_trainable_weights)]))
print('Всего обучаемых весов: ', trainable_count) # 1708090
print('Всего необучаемых весов: ', non_trainable_count) # 0
#
#sys.exit()
#
model.compile(loss = keras.losses.categorical_crossentropy,
              optimizer = keras.optimizers.Adam(),
              metrics = ['accuracy'])
#              optimizer = keras.optimizers.Adadelta(),
#
# Обучение
model.fit(x_train, y_train,
          batch_size = batch_size,
          epochs = epochs,
          verbose = 1,
          validation_data = (x_test, y_test))
#
print('Число эпох: ', epochs)
print('batch_size = ', batch_size)
#print('Нет Dense 16')
print('Есть Dense 16')
print('Время обучения: ', (time.time() - start_time))
#
# Проверка
start_time = time.time()
score = model.evaluate(x_test, y_test, verbose = 0)
#
# Вывод потерь и точности
print('Потери при тестировании: ', score[0])
print('Точность при тестировании:', score[1])
# Время тестирования
print('Время тестирования: ', (time.time() - start_time))

Реализация без комментария и вывода

from mnist import MNIST
import numpy as np
import keras
import time
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K
np.random.seed(348)
batch_size = 256
num_classes = 10
epochs = 5
mndata = MNIST('G:/python/MNIST/MNIST_data/')
mndata.gz = True
imagesTrain, labelsTrain = mndata.load_training()
imagesTest, labelsTest = mndata.load_testing()
x_train = np.asarray(imagesTrain)
y_train = np.asarray(labelsTrain)
x_test = np.asarray(imagesTest)
y_test = np.asarray(labelsTest)
img_rows, img_cols = 28, 28
x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1)
x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
input_shape = (img_rows, img_cols, 1)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
model = Sequential()
model.add(Conv2D(32, kernel_size = (5, 5), strides = (1, 1), padding = 'same', activation = 'relu', input_shape = input_shape))
model.add(MaxPooling2D(pool_size = (2, 2), strides = (2, 2), padding = 'same'))
model.add(Conv2D(64, kernel_size = (5, 5), strides = (1, 1), activation = 'relu'))
model.add(MaxPooling2D(pool_size = (2, 2), strides = (2, 2)))
model.add(Flatten())
model.add(Dense(1024, activation = 'relu'))
model.add(Dense(16, activation = 'linear'))
model.add(Dense(num_classes, activation = 'softmax'))
trainable_count = int(np.sum([K.count_params(p) for p in set(model.trainable_weights)]))
model.compile(loss = keras.losses.categorical_crossentropy, optimizer = keras.optimizers.Adam(), metrics = ['accuracy'])
model.fit(x_train, y_train, batch_size = batch_size, epochs = epochs, verbose = 1, validation_data = (x_test, y_test))
score = model.evaluate(x_test, y_test, verbose = 0)
print('Потери при тестировании: ', score[0])
print('Точность при тестировании:', score[1])

Результаты и зависимость результата обучения от числа эпох

Лучший результат по точности в задаче классификации изображений рукописных цифр на MNIST, обнаруженный в [1], равен 0.9977.
На рассматриваемой сети удалось получить точность при тестировании на MNIST, равную 0.9925 (6 эпох, batch_size = 128, оптимизатор Adadelta, есть предпоследний полносвязный слой с 16-ю нейронами).
При замере зависиимости результата от числа эпох использованы СНС двух следующих архитектур:

Во всех измерениях при обучении использованы:

Замечание. При употреблении categorical_crossentropy метка изображения должна быть преобразована в двоичный вектор размера 10.

# Преобразование метки – числа из диапазона [0, 9] в двоичный вектор размера 10
# Так, метка 5 (соответствует цифре 5) будет преобразована в вектор [0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
print(y_train[0]) # Напечатает: 5
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
print(y_train[0]) # Напечатает: [0. 0. 0. 0. 0. 1. 0. 0. 0. 0.]

Результаты измерений приведены в табл. 3.

Таблица 3. Зависимость результата обучения от числа эпох

Число эпохНет FC-16Есть FC-16
Время обучения, сПотериТочностьВремя обучения, сПотериТочность
15010.047680.98435020.049790.9843
210100.036370.988110170.029810.9904
315190.025530.991315150.024620.9915
420280.028950.990220110.023910.9920
525610.030380.989225570.027350.9916

Несколько лучше выглядят результаты при наличии полносвязного слоя с 16-ю нейронами перед последним классифицирующем слоем: точность 99.20% (4-я эпоха) против 99.13%, полученной после трех эпох обучения без слоя FC-16.

Реализация с формами выхода LeNet-5

from mnist import MNIST
import numpy as np
import keras
import time
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K
np.random.seed(348)
batch_size = 256
num_classes = 10
epochs = 5
mndata = MNIST('G:/python/MNIST/MNIST_data/')
mndata.gz = True
imagesTrain, labelsTrain = mndata.load_training()
imagesTest, labelsTest = mndata.load_testing()
x_train = np.asarray(imagesTrain)
y_train = np.asarray(labelsTrain)
x_test = np.asarray(imagesTest)
y_test = np.asarray(labelsTest)
img_rows, img_cols = 28, 28
x_train = x_train.reshape(x_train.shape[0], img_rows, img_cols, 1)
x_test = x_test.reshape(x_test.shape[0], img_rows, img_cols, 1)
input_shape = (img_rows, img_cols, 1)
x_train = x_train.astype('float32')
x_test = x_test.astype('float32')
x_train /= 255
x_test /= 255
y_train = keras.utils.to_categorical(y_train, num_classes)
y_test = keras.utils.to_categorical(y_test, num_classes)
model = Sequential()
model.add(Conv2D(6, kernel_size = (5, 5), strides = (1, 1), padding = 'same', activation = 'relu', input_shape = input_shape))
model.add(MaxPooling2D(pool_size = (2, 2), strides = (2, 2), padding = 'same'))
model.add(Conv2D(16, kernel_size = (5, 5), strides = (1, 1), activation = 'relu'))
model.add(MaxPooling2D(pool_size = (2, 2), strides = (2, 2)))
model.add(Flatten())
model.add(Dense(120, activation = 'relu'))
model.add(Dense(84, activation = 'linear'))
model.add(Dense(num_classes, activation = 'softmax'))
trainable_count = int(np.sum([K.count_params(p) for p in set(model.trainable_weights)]))
model.compile(loss = keras.losses.categorical_crossentropy, optimizer = keras.optimizers.Adam(), metrics = ['accuracy'])
model.fit(x_train, y_train, batch_size = batch_size, epochs = epochs, verbose = 1, validation_data = (x_test, y_test))
score = model.evaluate(x_test, y_test, verbose = 0)
print('Потери при тестировании: ', score[0])
print('Точность при тестировании:', score[1])

Результаты обучения этой сети приведены в табл. 4.

Таблица 4. Результаты обучения СНС с формами выхода LeNet-5

Число эпохПотериТочность
30.05240.9832
50.04220.9876

Результаты по понятной причине уступают предыдущим: число обучаемых весов этой сети равно 61'706, что существенно меньше числа обучаемых весов прежней СНС, равного 1'708'090.

Литература

  1. Yann LeCun, Corinna Cortes, Christopher J.C. Burges. The MNIST database of handwritten digits. [Электронный ресурс] URL: http://yann.lecun.com/exdb/mnist/ (дата обращения: 29.06.2018).
  2. Николенко С., Кадурин А., Архангельская Е. Глубокое обучение. – СПб.: Питер, 2018. – 480 с.
  3. Gradient-Based Learning Applied to Document Recognition / Y. LeCun et al. // Proc. IEEE, 1998, vol. 86, no. 11. – P. 2278–2324.
  4. Shafkat I. Intuitively Understanding Convolutions for Deep Learning. [Электронный ресурс] URL:https://towardsdatascience.com/intuitively-understanding-convolutions-for-deep-learning-1f6f42faee1 (дата обращения: 29.06.2018).
  5. Сверточная нейронная сеть, часть 1: структура, топология, функции активации и обучающее множество. [Электронный ресурс] URL:https://habr.com/post/348000/ (дата обращения: 29.06.2018).
  6. Keras-team/keras. [Электронный ресурс] URL: https://github.com/keras-team/keras/blob/master/examples/mnist_cnn.py (дата обращения: 29.06.2018).

Список работ

Рейтинг@Mail.ru