На
этом занятии мы посмотрим на построение архитектур нейронных сетей под новым
углом зрения – с позиции функционального API пакета Keras. Давайте
вначале разберемся, что это такое. Предположим, что мы хотим создать следующую
модель НС:
И
мы уже знаем, что это можно реализовать с помощью класса Sequential, о котором
мы говорили на предыдущем занятии. Но эту же самую архитектуру можно описать
подобно графам –через связи между слоями. Именно эта идея и положена в основу
функционального описания модели.
Давайте
посмотрим, как это делается.В начале программы подключим необходимые модули:
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
import tensorflow as tf
from tensorflow.keras.layers import Dense, Input, Conv2D, MaxPooling2D, Flatten
from tensorflow import keras
from tensorflow.keras.datasets import cifar10
import matplotlib.pyplot as plt
И
установим зерно датчика случайных чисел в 1, чтобы обеспечить воспроизводимость
результатов:
Затем,
сформируем первыедва слоя так, как это мы обычно делали:
input = keras.Input(shape=(32, 32, 3))
x = Conv2D(32, 3, activation='relu')
А,
далее, свяжем их между собой, следующим образом:
Эта
строчка и есть элемент функционального API пакета Keras. Но как это
работает? Все очень просто. Смотрите, объект input – это тензор специального
вида, имеющий типKerasTensor:
<class
'tensorflow.python.keras.engine.keras_tensor.KerasTensor'>
То
есть, это объект, представляющий слой, а не конкретные данные. Далее, так как
каждый слой в Keras является
функтором (реализует магический метод __call__ и может быть
вызван как функция), то при подаче ему на вход другого слоя, он делает не
вычисления, а формирует связку с указанным слоем. Вот и все.
Конечно,
эту связь можно описать короче, в виде:
x = Conv2D(32, 3, activation='relu')(input)
Здесь
мы сразу создаем объект и выполняем связывание со слоем input. В итоге, вся
структура нейронной сети может быть описана в виде:
input = keras.Input(shape=(32, 32, 3))
x = layers.Conv2D(32, 3, activation='relu')(input)
x = layers.MaxPooling2D(2, padding='same')(x)
x = layers.Conv2D(64, 3, activation='relu')(x)
x = layers.MaxPooling2D(2, padding='same')(x)
x = layers.Flatten()(x)
x = layers.Dense(256, activation='relu')(x)
x = layers.Dropout(0.5)(x)
output = layers.Dense(10, activation='softmax')(x)
Но
это еще не модель. Конечно, мы можем использовать сеть в таком виде, подавать
на вход числовые данные и получать выходные значения, но нам бы хотелось
получить полноценную модель, чтобы средствами Keras обучать ее и
производить оценку эффективности. Для этого достаточно воспользоваться классом Model, о котором мы с
вами уже говорили на предыдущих занятиях, и указать входы и выходы:
model = keras.Model(inputs=input, outputs=output)
Все,
модель сформирована с использованием функционального подхода и готова к
использованию. Чтобы убедиться, что структура сети верная, выведем ее с помощью
метода:
Как
видите, все было сделано верно. Возможно, у вас возникнет вопрос: а зачем было
все так усложнять? Почему бы не воспользоваться классом Sequential? Это было бы
проще. Все верно. Для последовательных структур так и следует поступать.
Однако, на практике относительно часто встречаются и другие архитектуры,
которые не удается описать последовательными моделями. Либо, сделать это
сложнее. В таких ситуациях, как раз, и используется функциональный подход к
проектированию архитектур сетей.
Обучение сверточной нейронной сети
Давайте
теперь обучим полученную модель сверточной НС для классификации изображений БД CIFAR-10. Сразу
отмечу, что современные нейросети достигают здесь точности около 96,5%, а
человек – всего 94%. Посмотрим, что получится у нас.
В
этой БД имеется 50 000 полноцветных изображений, размером 32x32 пикселов в
обучающей выборке и 10 000 таких же полноцветных изображений в тестовой
выборке. Все изображения разбиты на 10 классов (см. рисунок). Наша задача
отнести предъявленный образец нужному классу.
Вначале,
как всегда, загрузим набор данных и стандартизуем их:
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
x_train = x_train / 255
x_test = x_test / 255
y_train = keras.utils.to_categorical(y_train, 10)
y_test = keras.utils.to_categorical(y_test, 10)
Затем, укажем оптимизатор и функцию потерь:
model.compile(optimizer='adam',
loss='categorical_crossentropy',
metrics=['accuracy'])
После этого, запустим процесс обучения:
model.fit(x_train, y_train, batch_size=64, epochs=20, validation_split=0.2)
В
конце программы выполним оценку качества работы модели на тестовой выборке:
print(model.evaluate(x_test, y_test) )
После
запуска программы увидим качество распознавания на обучающей выборке в 87%, а
на тестовой в 72%, что в целом неплохо, учитывая сложность задачи и простоту
архитектуры НС. Предлагаю вам в качестве практики попытаться улучшить качество
распознавания изображений БД CIFAR-10.
Реализация сверточных слоев в Tensorflow
Так
как этот курс посвящен не только Keras, но и Tensorflow, то я покажу
пример реализации сверточных слоев на уровне Tensorflow. Делается
относительно просто, но несколько сложнее, чем в Keras. Вначале
определим вспомогательный класс слоя:
class TfConv2D(tf.Module):
def __init__(self, kernel=(3, 3), channels=1, strides=(2, 2), padding='SAME', activate="relu"):
super().__init__()
self.kernel = kernel
self.channels = channels
self.strides = strides
self.padding = padding
self.activate = activate
self.fl_init = False
def __call__(self, x):
if not self.fl_init:
self.w = tf.random.truncated_normal((*self.kernel, x.shape[-1], self.channels), stddev=0.1)
self.b = tf.zeros([self.channels], dtype=tf.float32)
self.w = tf.Variable(self.w)
self.b = tf.Variable(self.b)
self.fl_init = True
y = tf.nn.conv2d(x, self.w, strides=(1, *self.strides, 1), padding=self.padding)
if self.activate == "relu":
return tf.nn.relu(y)
elif self.activate == "softmax":
return tf.nn.softmax(y)
return y
Я
его взял из одного из предыдущих занятий и полносвязный слой заменил
сверточным. Логика работы здесь следующая. Для сверток нужно задать размер
фильтра (kernel), число
выходных каналов (channels), смещение фильтров в плоскости входных
каналов (strides) и режим
формирования выходных каналов (padding). То есть, мы прописываем все те
же параметры, что и в стандартном сверточном слое.
Затем,
при первом вызове магического метода __call__() формируем
наборы весовых коэффициентов размерностью:
[kernel_x,kernel_y,
input_channels, output_channels]
И
еще тензор коэффициентов для смещений (biases) размером:
[output_channels]
После
этого обращаемся к ветке tf.nn и вызываем
функцию conv2d для применения
фильтров к входному сигналу x. К полученному выходному тензору y применяем
функцию активации и, таким образом, формируем выход сверточного слоя.
Использовать
этот класс можно, следующим образом:
layer1 = TfConv2D((3, 3), 32)
y = layer1(tf.expand_dims(x_test[0], axis=0))
print(y.shape)
Применяя
его к первому входному изображению, получаем на выходе 32 канала размерностью
16x 16 отсчетов:
(1,
16, 16, 32)
Далее,
можно применить операцию MaxPooling, например, так:
y = tf.nn.max_pool2d(y, ksize=(1, 2, 2, 1), strides=(1, 2, 2, 1), padding="SAME")
print(y.shape)
Сформируется
тензор yразмерностью:
(1,
8, 8, 32)
По
аналогии можно реализовать и другие типы сверточных слоев. Затем, сформировать
модель сети и обучить так, как мы это делали на первых занятиях.
Этот
пример хорошо показывает, как Keras заметно облегчает описание архитектур
нейронных сетей, а также их обучение.
Модели в функциональном API
Любую
модель в Keras можно
воспринимать как функциональный элемент и добавлять его в общую структуру сети,
используя функциональный подход. В качестве примера давайте опишем архитектуру
простого автоэнкодера, используя две независимые модели: модель кодера и модель
декодера:
Если
вы не знаете, что такое автоэнкодер, для чего он нужен и как работает, то
смотрите занятие по этой теме в курсе «Нейронные сети»:
Курс по нейронным сетям
Вначале
опишем модель кодера, используя сверточные слои:
enc_input = Input(shape=(28, 28, 1))
x = Conv2D(32, 3, activation='relu')(enc_input)
x = MaxPooling2D(2, padding='same')(x)
x = Conv2D(64, 3, activation='relu')(x)
x = MaxPooling2D(2, padding='same')(x)
x = Flatten()(x)
enc_output = Dense(8, activation='linear')(x)
encoder = keras.Model(enc_input, enc_output, name="encoder")
А, затем, модельдекодера:
dec_input = keras.Input(shape=(8,), name="encoded_img")
x = Dense(7 * 7 * 8, activation='relu')(dec_input)
x = keras.layers.Reshape((7, 7, 8))(x)
x = Conv2DTranspose(64, 5, strides=(2, 2), activation="relu", padding='same')(x)
x = keras.layers.BatchNormalization()(x)
x = Conv2DTranspose(32, 5, strides=(2, 2), activation="linear", padding='same')(x)
x = keras.layers.BatchNormalization()(x)
dec_output = Conv2DTranspose(1, 3, activation="sigmoid", padding='same')(x)
decoder = keras.Model(dec_input, dec_output, name="decoder")
Теперь,
используя модели encoder и decoder можно на их
основе сформировать общую архитектуру автоэнкодера. Воспользуемся для этого
функциональным подходом и будем рассматривать каждую модель как один единый
элемент (объект):
autoencoder_input = Input(shape=(28, 28, 1), name="img")
x = encoder(autoencoder_input)
autoencoder_output = decoder(x)
autoencoder = keras.Model(autoencoder_input, autoencoder_output, name="autoencoder")
Я
здесь специально создал еще один входной слой, чтобы он был уникальным для
автоэнкодера. Далее, связываем его с моделью кодера, а кодер с моделью
декодера. Затем, из полученной структуры слоев и моделей формируем единую
модель.
Причем,
обратите внимание, в модели автоэнкодера не создаются копии архитектур кодера и
декодера, а используются существующие. Это значит, что при обучении
автоэнкодера мы будем автоматически обучать и модели кодера и декодера.
Давайте
выполним этот шаг для БД изображений цифр MNIST. Вначале
подготовим обучающую выборку:
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train = x_train.astype("float32") / 255.0
x_test = x_test.astype("float32") / 255.0
y_train = keras.utils.to_categorical(y_train, 10)
y_test = keras.utils.to_categorical(y_test, 10)
Установим
оптимизатор Adamдля автоэнкодера
и потребуем минимум квадрата ошибки рассогласования между входным и выходным
изображениями:
autoencoder.compile(optimizer='adam', loss='mean_squared_error')
После
этого можно запускать сам процесс обучения (одной эпохи вполне достаточно для
этой задачи):
autoencoder.fit(x_train, x_train, batch_size=32, epochs=1)
Все,
модель обучилась, а это значит, что мы также обучили кодер и декодер. Давайте в
этом убедимся. Пропустим первое тестовое изображение через кодер, а его
выходной сигнал подадим на декодер:
h = encoder.predict(tf.expand_dims(x_test[0], axis=0))
img = decoder.predict(h)
В
результате должны получить изображение похожее на входное. Для этого отобразим
исходное изображение и восстановленное:
plt.subplot(121)
plt.imshow(x_test[0], cmap='gray')
plt.subplot(122)
plt.imshow(img.squeeze(), cmap='gray')
plt.show()
Как
видим, результаты получились достаточно близкими. Это показывает, что кодер и
декодер действительно обучились через автоэнкодер.
На
следующем занятии мы продолжим эту тему и поближе познакомимся с принципом
архитектуры ResNet, а также
проблемами, связанными с обучением глубоких сетей.