Список работ

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

Содержание

Введение

Рассматривается сверточная нейронная сеть (СНС) на Python, реализованная средствами библиотеки TensorFlow, доступ которой выполнен при помощи оболочки Keras.
Обучение и тестирование СНС выполняется на наборе данных 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 он отображается в следующую таблицу:

[[  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0]
 [  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  0  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
# Так, метка 6 будет преобразована в вектор [0. 0. 0. 0. 0. 0. 1. 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.]

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

Архитектура СНС показана на рис. 3 и описана в табл. 1.

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

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

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

Тип слояРазмер ядра
(окна)
ШагФорма выхода
Input(None, 28, 28, 1)
Conv / ReLU(5, 5)(1, 1)(None, 28, 28, 32)
MaxPool(2, 2)(2, 2)(None, 14, 14, 32)
Conv / ReLU(5, 5)(1, 1)(None, 10, 10, 64)
MaxPool(2, 2)(2, 2)(None, 5, 5, 64)
Flatten(None, 1600)
FC / ReLU(None, 1024)
FC / Linear(None, 16)
FC / Softmax(None, 10)

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

Последний (полносвязный) слой НС определяет вероятности принадлежности изображения к классам в зависимости от значений, выделенных на предыдущем слое признаков.
Сеть, получая на входе изображение, извлекает вектор из 16 признаков, по значениям которых изображение относится к одному из десяти классов.
Дополнительно может быть задан и Dropout-слой, на котором нейронов исключается из сети (рис. 4).

Dropout

Рис. 4. До и после слоя 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 (рис. 5).

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

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

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

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

Pooling

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

Реализация

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

from mnist import MNIST
import numpy as np
import keras
from keras.models import Sequential
from keras.layers import Dense, Dropout, Flatten
from keras.layers import Conv2D, MaxPooling2D
from keras import backend as K
#from keras.datasets import mnist
#
# Задание затравки датчика случайных чисел обеспечит повторяемость результата
np.random.seed(348)
#
# Параметры модели
# Число экземпляров данных обучающей выборки между пересчетами весовых коэффициентов
# Чем меньше batch_size, тем дольше процесс обучения
batch_size = 128
# Число классов (по числу цифр)
num_classes = 10
# Число эпох обучения
epochs = 2
#
# Читаем в списки 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
# Так, метка 6 будет преобразована в вектор [0. 0. 0. 0. 0. 0. 1. 0. 0. 0.]
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.]
#
print('Форма x_train:', x_train.shape) # Напечатает: Форма x_train: (60000, 28, 28, 1)
print(x_train.shape[0], 'обучающих образов') # Напечатает: 60000 обучающих образов
print(x_test.shape[0], 'тестовых образов') # Напечатает: 10000 тестовых образов
#
# Создаем модель сверточной нейронной сети
model = Sequential()
model.add(Conv2D(32, kernel_size = (5, 5), strides = (1, 1), padding = 'same',
               activation = 'relu', input_shape = input_shape))
print('Input shape: ', model.input_shape) # Input shape: (None, 28, 28, 1)
print('Conv2D_32: ', model.output_shape) # Conv2D_32: (None, 28, 28, 32)
model.add(MaxPooling2D(pool_size = (2, 2), strides = (2, 2), padding = 'same'))
print('MaxPooling2D_1: ', model.output_shape) # MaxPooling2D_1: (None, 14, 14, 32)
model.add(Conv2D(64, kernel_size = (5, 5), strides = (1, 1), activation = 'relu'))
print('Conv2D_64: ', model.output_shape) # Conv2D_64: (None, 10, 10, 64)
model.add(MaxPooling2D(pool_size = (2, 2), strides = (2, 2)))
print('MaxPooling2D_2: ', model.output_shape) # MaxPooling2D_2: (None, 5, 5, 64)
#model.add(Dropout(0.25))
#print('Dropout_0.25: ', model.output_shape) # Dropout_0.25: (None, 5, 5, 64)
model.add(Flatten())
print('Flatten: ', model.output_shape) # (None, 1600)
model.add(Dense(1024, activation = 'relu'))
print('Dense_1024: ', model.output_shape) # (None, 1024)
model.add(Dense(16, activation = 'linear'))
print('Dense_16: ', model.output_shape) # (None, 16)
#model.add(Dropout(0.5))
#print('Dropout_0.5: ', model.output_shape) # (None, 16)
model.add(Dense(num_classes, activation = 'softmax'))
print('Dense_10: ', model.output_shape) # (None, 10)
model.compile(loss = keras.losses.categorical_crossentropy,
              optimizer = keras.optimizers.Adadelta(),
              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])
#
# После двух эпох обучения
#
# При наличии Dropout-слоев:
# Потери при тестировании: 0.03456792705629323
# Точность при тестировании: 0.99
#
# Без Dropout-слоев:
# Потери при тестировании: 0.026056741140922532
# Точность при тестировании: 0.9911
# После шести эпох обучения с одним (вторым) Dropout-слоем:
#
Потери при тестировании: 0.027503584279406617
Точность при тестировании: 0.9932

Заключение

Две эпохи обучения на MNIST рассмотренной СНС без Dropout-слоев показали на тестовой выборке точность 0.9911.
Шесть эпох обучения с одним Dropout-слоем, расположенным непосредственно перед последним, классифицирующем слоем, обеспечили на тестовой выборке точность 0.9932.
Лучший результат по точности, приводимый в [1], равен 0.9977. Он достигается, прежде всего, за счет усложнения архитектуры сети и применения более совершенной функции потерь.

Литература

  1. Yann LeCun, Corinna Cortes, Christopher J.C. Burges. The MNIST database of handwritten digits. [Электронный ресурс] URL: http://yann.lecun.com/exdb/mnist/ (дата обращения: 27.05.2018).
  2. Николенко С., Кадурин А., Архангельская Е. Глубокое обучение. – СПб.: Питер, 2018. – 480 с.
  3. Keras-team/keras. [Электронный ресурс] URL: https://github.com/keras-team/keras/blob/master/examples/mnist_cnn.py (дата обращения: 27.05.2018).

Список работ

Рейтинг@Mail.ru